diff --git a/migrations/Version20241121135658.php b/migrations/Version20241121135658.php new file mode 100644 index 00000000..5de1714d --- /dev/null +++ b/migrations/Version20241121135658.php @@ -0,0 +1,29 @@ +addSql('ALTER TABLE kobo_device ADD device_id VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX kobo_device_id ON kobo_device (device_id);'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX kobo_device_id ON kobo_device;'); + $this->addSql('ALTER TABLE kobo_device DROP device_id;'); + } +} diff --git a/migrations/Version20241121142239.php b/migrations/Version20241121142239.php new file mode 100644 index 00000000..ab4a8767 --- /dev/null +++ b/migrations/Version20241121142239.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE kobo_device ADD model VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE kobo_device DROP model'); + } +} diff --git a/src/Controller/Kobo/KoboAnalyticsController.php b/src/Controller/Kobo/KoboAnalyticsController.php index 5886453b..fe2105d6 100644 --- a/src/Controller/Kobo/KoboAnalyticsController.php +++ b/src/Controller/Kobo/KoboAnalyticsController.php @@ -2,8 +2,10 @@ namespace App\Controller\Kobo; +use App\Entity\KoboDevice; use App\Kobo\Proxy\KoboProxyConfiguration; use App\Kobo\Proxy\KoboStoreProxy; +use App\Repository\KoboDeviceRepository; use GuzzleHttp\Exception\GuzzleException; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -16,8 +18,12 @@ #[Route('/kobo/{accessKey}', name: 'kobo')] class KoboAnalyticsController extends AbstractController { - public function __construct(protected KoboProxyConfiguration $koboProxyConfiguration, protected KoboStoreProxy $koboStoreProxy, protected LoggerInterface $logger) - { + public function __construct( + protected KoboProxyConfiguration $koboProxyConfiguration, + protected KoboStoreProxy $koboStoreProxy, + protected KoboDeviceRepository $koboDeviceRepository, + protected LoggerInterface $logger + ) { } /** @@ -46,8 +52,17 @@ public function analyticsTests(Request $request): Response * @throws GuzzleException */ #[Route('/v1/analytics/event', methods: ['POST'])] - public function analyticsEvent(Request $request): Response + public function analyticsEvent(Request $request, KoboDevice $kobo): Response { + // Save the device_id and model + if ($request->headers->has(KoboDevice::KOBO_DEVICE_ID_HEADER)) { + $kobo->setDeviceId($request->headers->get(KoboDevice::KOBO_DEVICE_ID_HEADER)); + if ($request->headers->has(KoboDevice::KOBO_DEVICE_MODEL)) { + $kobo->setModel($request->headers->get(KoboDevice::KOBO_DEVICE_MODEL)); + } + $this->koboDeviceRepository->save($kobo); + } + $content = $request->getContent(); $json = trim($content) === '' ? [] : (array) json_decode($content, true, 512, JSON_THROW_ON_ERROR); $this->logger->debug('Analytics event received', ['body' => $json]); diff --git a/src/Entity/KoboDevice.php b/src/Entity/KoboDevice.php index a69ad4e7..13a221ae 100644 --- a/src/Entity/KoboDevice.php +++ b/src/Entity/KoboDevice.php @@ -11,10 +11,14 @@ #[ORM\Entity(repositoryClass: KoboDeviceRepository::class)] #[ORM\UniqueConstraint(name: 'kobo_access_key', columns: ['access_key'])] +#[ORM\Index(columns: ['device_id'], name: 'kobo_device_id')] +#[ORM\Index(columns: ['access_key'], name: 'kobo_access_key')] #[UniqueEntity(fields: ['accessKey'])] class KoboDevice { use RandomGeneratorTrait; + public const KOBO_DEVICE_ID_HEADER = 'X-Kobo-Deviceid'; + public const KOBO_DEVICE_MODEL = 'X-Kobo-Devicemodel'; #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -34,6 +38,11 @@ class KoboDevice #[Assert\Regex(pattern: '/^[a-f0-9]+$/', message: 'Need to be Hexadecimal')] private ?string $accessKey = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $deviceId = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $model = null; /** * @var Collection */ @@ -162,4 +171,28 @@ public function removeShelf(Shelf $shelf): void { $this->shelves->removeElement($shelf); } + + public function getDeviceId(): ?string + { + return $this->deviceId; + } + + public function setDeviceId(?string $deviceId): self + { + $this->deviceId = $deviceId; + + return $this; + } + + public function getModel(): ?string + { + return $this->model; + } + + public function setModel(?string $model): self + { + $this->model = $model; + + return $this; + } } diff --git a/src/Form/KoboType.php b/src/Form/KoboType.php index 50e7a1c5..fc238419 100644 --- a/src/Form/KoboType.php +++ b/src/Form/KoboType.php @@ -23,6 +23,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder ->add('name') ->add('accessKey') + ->add('deviceId', null, [ + 'label' => 'Device ID', + 'disabled' => true, + 'required' => false, + ]) + ->add('model', null, [ + 'label' => 'Model', + 'disabled' => true, + 'required' => false, + ]) ->add('forceSync', null, [ 'label' => 'Force Sync', 'required' => false, diff --git a/src/Repository/KoboDeviceRepository.php b/src/Repository/KoboDeviceRepository.php index ebf8e6fb..ad897409 100644 --- a/src/Repository/KoboDeviceRepository.php +++ b/src/Repository/KoboDeviceRepository.php @@ -50,4 +50,10 @@ public function findAllByUser(?UserInterface $user = null): array return $result; } + + public function save(KoboDevice $kobo): void + { + $this->_em->persist($kobo); + $this->_em->flush(); + } } diff --git a/templates/kobodevice_user/index.html.twig b/templates/kobodevice_user/index.html.twig index c9a9987b..ff45dc61 100644 --- a/templates/kobodevice_user/index.html.twig +++ b/templates/kobodevice_user/index.html.twig @@ -14,7 +14,11 @@ {% for kobo in kobos %} - {{ kobo.name }} + {{ kobo.name }} + {% if kobo.model is not empty %} +
{{ kobo.model }} + {% endif %} + {{ kobo.accessKey }} {% if is_granted('EDIT', kobo) %} diff --git a/tests/Controller/Kobo/KoboAnalyticsControllerTest.php b/tests/Controller/Kobo/KoboAnalyticsControllerTest.php index 9fe91a8e..61bdfa0a 100644 --- a/tests/Controller/Kobo/KoboAnalyticsControllerTest.php +++ b/tests/Controller/Kobo/KoboAnalyticsControllerTest.php @@ -3,9 +3,13 @@ namespace App\Tests\Controller\Kobo; +use App\Entity\KoboDevice; + class KoboAnalyticsControllerTest extends AbstractKoboControllerTest { const SERIAL_NUMBER = 'N9413679432456'; + const DEVICE_ID = '2a92bba197b1e0574a3f7d29cb2b05b399ab0d197e6b1aa230bfb75a920b14e7c'; + const MODEL = 'Kobo Libra H2O'; const APP_VERSION = '4.38.21908'; public function testPostEvent(): void @@ -442,9 +446,18 @@ public function testPostEvent(): void ]; $client = static::getClient(); $client?->setServerParameter('HTTP_CONNECTION', 'keep-alive'); + $server = [ + 'HTTP_'.KoboDevice::KOBO_DEVICE_ID_HEADER => self::DEVICE_ID, + 'HTTP_'.KoboDevice::KOBO_DEVICE_MODEL => self::MODEL, + ]; $client?->request('POST', '/kobo/'.$this->accessKey.'/v1/analytics/event', [ 'json' => $body, - ]); + ], [], $server); + + // Make sure we define Kobo's device_id and model + $kobo = $this->getKoboDevice(true); + self::assertSame(self::DEVICE_ID,$kobo->getDeviceId()); + self::assertSame(self::MODEL,$kobo->getModel()); self::assertResponseIsSuccessful(); self::assertResponseHeaderSame('Connection', 'keep-alive');