Loading config/application.config.php +1 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ $modules = [ 'Atelier', 'Application', 'Personnel', 'Qr', // 'UnicaenVerification', ]; Loading module/Application/view/application/qr/scan.html 0 → 100644 +57 −0 Original line number Diff line number Diff line <video id="preview" autoplay playsinline style="width: 320px; height:auto; border:1px solid #ccc;"></video> <div id="status">Initialisation caméra…</div> <script type="module"> import { BrowserMultiFormatReader } from "https://cdn.jsdelivr.net/npm/@zxing/library@0.20.0/+esm"; const statusEl = document.getElementById('status'); const videoEl = document.getElementById('preview'); const codeReader = new BrowserMultiFormatReader(); async function start() { try { // Choix auto de la caméra arrière si dispo (smartphones) const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter(d => d.kind === 'videoinput'); const backCam = videoInputs.find(d => /back|rear|environment/i.test(d.label)) || videoInputs[0]; const constraints = backCam ? { video: { deviceId: backCam.deviceId } } : { video: { facingMode: { ideal: "environment" } } }; const stream = await navigator.mediaDevices.getUserMedia(constraints); videoEl.srcObject = stream; statusEl.textContent = "Recherche du QR code…"; // Décodage continu depuis la webcam await codeReader.decodeFromVideoDevice(backCam?.deviceId ?? null, videoEl, async (result, err) => { if (result) { statusEl.textContent = "QR détecté : " + result.getText(); // (Optionnel) envoyer à PHP try { await fetch('/qr/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payload: result.getText() }) }); } catch (e) { /* ignore réseau */ } // Si vous voulez stopper après 1 lecture : // codeReader.reset(); // stream.getTracks().forEach(t => t.stop()); } else if (err && !(err instanceof ZXing.NotFoundException)) { // Erreurs non « pas trouvé » console.warn(err); } }); } catch (e) { statusEl.textContent = "Erreur caméra : " + (e.message || e); } } if (navigator.mediaDevices?.getUserMedia) { start(); } else { statusEl.textContent = "getUserMedia non supporté par ce navigateur."; } </script> module/Qr/Module.php 0 → 100644 +56 −0 Original line number Diff line number Diff line <?php namespace Qr; use Laminas\Config\Factory as ConfigFactory; use Laminas\Http\Request as HttpRequest; use Laminas\Mvc\ModuleRouteListener; use Laminas\Mvc\MvcEvent; use Laminas\Stdlib\ArrayUtils; use Laminas\Stdlib\Glob; class Module { public function onBootstrap(MvcEvent $e) { $e->getApplication()->getServiceManager()->get('translator'); $eventManager = $e->getApplication()->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); /* Active un layout spécial si la requête est de type AJAX. Valable pour TOUS les modules de l'application. */ $eventManager->getSharedManager()->attach('Laminas\Mvc\Controller\AbstractActionController', 'dispatch', function (MvcEvent $e) { $request = $e->getRequest(); if ($request instanceof HttpRequest && $request->isXmlHttpRequest()) { $e->getTarget()->layout('layout/ajax.phtml'); } } ); } public function getConfig() { $configInit = [ __DIR__ . '/config/module.config.php' ]; $configFiles = ArrayUtils::merge( $configInit, Glob::glob(__DIR__ . '/config/merged/{,*.}{config}.php', Glob::GLOB_BRACE) ); return ConfigFactory::fromFiles($configFiles); } public function getAutoloaderConfig() { return array( 'Laminas\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/', ), ), ); } } module/Qr/config/module.config.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php use Doctrine\ORM\Mapping\Driver\XmlDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Laminas\Router\Http\Literal; use Qr\Controller\ScanController; use UnicaenPrivilege\Guard\PrivilegeController; return array( 'doctrine' => [ 'driver' => [ 'orm_default' => [ 'class' => MappingDriverChain::class, 'drivers' => [ 'Personnel\Entity\Db' => 'orm_default_xml_driver', ], ], 'orm_default_xml_driver' => [ 'class' => XmlDriver::class, 'cache' => 'apc', 'paths' => [ __DIR__ . '/../src/Entity/Db/Mapping', ], ], ], 'cache' => [ 'apc' => [ 'namespace' => 'MCP__' . __NAMESPACE__, ], ], ], 'bjyauthorize' => [ 'guards' => [ PrivilegeController::class => [ [ 'controller' => ScanController::class, 'action' => [ 'index', 'index-catalogue' ], 'roles' => 'guest' ], ], ], ], 'router' => [ 'routes' => [ 'qr-scan' => [ 'type' => Literal::class, 'options' => [ 'route' => '/qr/scan', 'defaults' => [ 'controller' => ScanController::class, 'action' => 'index', ], ], ], 'qr-callback' => [ 'type' => Literal::class, 'options' => [ 'route' => '/qr/callback', 'defaults' => [ 'controller' => ScanController::class, 'action' => 'callback', ], ], 'may_terminate' => true, 'child_routes' => [], ], ], ], 'service_manager' => [ 'factories' => [ ], ], 'form_elements' => [ 'factories' => [ ], ], 'hydrators' => [ 'invokables' => [ ], ], 'controllers' => [ 'factories' => [ ScanController::class => function ($container) { return new ScanController(); }, ], ], 'view_helpers' => [ 'invokables' => [ ], ], 'view_manager' => [ 'template_path_stack' => [ __DIR__ . '/../view', ], 'strategies' => [ 'ViewJsonStrategy', // pour renvoyer du JSON facilement ], ], ); module/Qr/src/Controller/ScanController.php 0 → 100644 +59 −0 Original line number Diff line number Diff line <?php namespace Qr\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Laminas\View\Model\JsonModel; class ScanController extends AbstractActionController { /** * GET /qr/scan * Affiche la page avec le flux webcam + décodage client. */ public function indexAction() { // Tu peux passer des variables à la vue si besoin : return new ViewModel([ 'callbackUrl' => $this->url()->fromRoute('qr-callback'), ]); } /** * POST /qr/callback * Reçoit le payload décodé côté navigateur et renvoie un JSON. */ public function callbackAction() { $request = $this->getRequest(); if (! $request->isPost()) { return new JsonModel(['ok' => false, 'error' => 'Méthode non autorisée'], 405); } // Récupération JSON brute $raw = $request->getContent(); $data = json_decode($raw, true); if (!is_array($data)) { return new JsonModel(['ok' => false, 'error' => 'JSON invalide']); } $payload = $data['payload'] ?? null; if (!$payload || !is_string($payload)) { return new JsonModel(['ok' => false, 'error' => 'payload manquant']); } // 👉 ICI : traite le contenu du QR (lookup DB, validation billet, etc.) // Exemple : valider un ticket "EVT-XXXX" $valid = (bool) preg_match('/^EVT-[A-Z0-9]{4,}$/', $payload); // Tu peux logguer / enregistrer en base, etc. // $this->someService->handleQrPayload($payload); return new JsonModel([ 'ok' => true, 'valid' => $valid, 'payload' => $payload, ]); } } Loading
config/application.config.php +1 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ $modules = [ 'Atelier', 'Application', 'Personnel', 'Qr', // 'UnicaenVerification', ]; Loading
module/Application/view/application/qr/scan.html 0 → 100644 +57 −0 Original line number Diff line number Diff line <video id="preview" autoplay playsinline style="width: 320px; height:auto; border:1px solid #ccc;"></video> <div id="status">Initialisation caméra…</div> <script type="module"> import { BrowserMultiFormatReader } from "https://cdn.jsdelivr.net/npm/@zxing/library@0.20.0/+esm"; const statusEl = document.getElementById('status'); const videoEl = document.getElementById('preview'); const codeReader = new BrowserMultiFormatReader(); async function start() { try { // Choix auto de la caméra arrière si dispo (smartphones) const devices = await navigator.mediaDevices.enumerateDevices(); const videoInputs = devices.filter(d => d.kind === 'videoinput'); const backCam = videoInputs.find(d => /back|rear|environment/i.test(d.label)) || videoInputs[0]; const constraints = backCam ? { video: { deviceId: backCam.deviceId } } : { video: { facingMode: { ideal: "environment" } } }; const stream = await navigator.mediaDevices.getUserMedia(constraints); videoEl.srcObject = stream; statusEl.textContent = "Recherche du QR code…"; // Décodage continu depuis la webcam await codeReader.decodeFromVideoDevice(backCam?.deviceId ?? null, videoEl, async (result, err) => { if (result) { statusEl.textContent = "QR détecté : " + result.getText(); // (Optionnel) envoyer à PHP try { await fetch('/qr/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payload: result.getText() }) }); } catch (e) { /* ignore réseau */ } // Si vous voulez stopper après 1 lecture : // codeReader.reset(); // stream.getTracks().forEach(t => t.stop()); } else if (err && !(err instanceof ZXing.NotFoundException)) { // Erreurs non « pas trouvé » console.warn(err); } }); } catch (e) { statusEl.textContent = "Erreur caméra : " + (e.message || e); } } if (navigator.mediaDevices?.getUserMedia) { start(); } else { statusEl.textContent = "getUserMedia non supporté par ce navigateur."; } </script>
module/Qr/Module.php 0 → 100644 +56 −0 Original line number Diff line number Diff line <?php namespace Qr; use Laminas\Config\Factory as ConfigFactory; use Laminas\Http\Request as HttpRequest; use Laminas\Mvc\ModuleRouteListener; use Laminas\Mvc\MvcEvent; use Laminas\Stdlib\ArrayUtils; use Laminas\Stdlib\Glob; class Module { public function onBootstrap(MvcEvent $e) { $e->getApplication()->getServiceManager()->get('translator'); $eventManager = $e->getApplication()->getEventManager(); $moduleRouteListener = new ModuleRouteListener(); $moduleRouteListener->attach($eventManager); /* Active un layout spécial si la requête est de type AJAX. Valable pour TOUS les modules de l'application. */ $eventManager->getSharedManager()->attach('Laminas\Mvc\Controller\AbstractActionController', 'dispatch', function (MvcEvent $e) { $request = $e->getRequest(); if ($request instanceof HttpRequest && $request->isXmlHttpRequest()) { $e->getTarget()->layout('layout/ajax.phtml'); } } ); } public function getConfig() { $configInit = [ __DIR__ . '/config/module.config.php' ]; $configFiles = ArrayUtils::merge( $configInit, Glob::glob(__DIR__ . '/config/merged/{,*.}{config}.php', Glob::GLOB_BRACE) ); return ConfigFactory::fromFiles($configFiles); } public function getAutoloaderConfig() { return array( 'Laminas\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/', ), ), ); } }
module/Qr/config/module.config.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php use Doctrine\ORM\Mapping\Driver\XmlDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Laminas\Router\Http\Literal; use Qr\Controller\ScanController; use UnicaenPrivilege\Guard\PrivilegeController; return array( 'doctrine' => [ 'driver' => [ 'orm_default' => [ 'class' => MappingDriverChain::class, 'drivers' => [ 'Personnel\Entity\Db' => 'orm_default_xml_driver', ], ], 'orm_default_xml_driver' => [ 'class' => XmlDriver::class, 'cache' => 'apc', 'paths' => [ __DIR__ . '/../src/Entity/Db/Mapping', ], ], ], 'cache' => [ 'apc' => [ 'namespace' => 'MCP__' . __NAMESPACE__, ], ], ], 'bjyauthorize' => [ 'guards' => [ PrivilegeController::class => [ [ 'controller' => ScanController::class, 'action' => [ 'index', 'index-catalogue' ], 'roles' => 'guest' ], ], ], ], 'router' => [ 'routes' => [ 'qr-scan' => [ 'type' => Literal::class, 'options' => [ 'route' => '/qr/scan', 'defaults' => [ 'controller' => ScanController::class, 'action' => 'index', ], ], ], 'qr-callback' => [ 'type' => Literal::class, 'options' => [ 'route' => '/qr/callback', 'defaults' => [ 'controller' => ScanController::class, 'action' => 'callback', ], ], 'may_terminate' => true, 'child_routes' => [], ], ], ], 'service_manager' => [ 'factories' => [ ], ], 'form_elements' => [ 'factories' => [ ], ], 'hydrators' => [ 'invokables' => [ ], ], 'controllers' => [ 'factories' => [ ScanController::class => function ($container) { return new ScanController(); }, ], ], 'view_helpers' => [ 'invokables' => [ ], ], 'view_manager' => [ 'template_path_stack' => [ __DIR__ . '/../view', ], 'strategies' => [ 'ViewJsonStrategy', // pour renvoyer du JSON facilement ], ], );
module/Qr/src/Controller/ScanController.php 0 → 100644 +59 −0 Original line number Diff line number Diff line <?php namespace Qr\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Laminas\View\Model\JsonModel; class ScanController extends AbstractActionController { /** * GET /qr/scan * Affiche la page avec le flux webcam + décodage client. */ public function indexAction() { // Tu peux passer des variables à la vue si besoin : return new ViewModel([ 'callbackUrl' => $this->url()->fromRoute('qr-callback'), ]); } /** * POST /qr/callback * Reçoit le payload décodé côté navigateur et renvoie un JSON. */ public function callbackAction() { $request = $this->getRequest(); if (! $request->isPost()) { return new JsonModel(['ok' => false, 'error' => 'Méthode non autorisée'], 405); } // Récupération JSON brute $raw = $request->getContent(); $data = json_decode($raw, true); if (!is_array($data)) { return new JsonModel(['ok' => false, 'error' => 'JSON invalide']); } $payload = $data['payload'] ?? null; if (!$payload || !is_string($payload)) { return new JsonModel(['ok' => false, 'error' => 'payload manquant']); } // 👉 ICI : traite le contenu du QR (lookup DB, validation billet, etc.) // Exemple : valider un ticket "EVT-XXXX" $valid = (bool) preg_match('/^EVT-[A-Z0-9]{4,}$/', $payload); // Tu peux logguer / enregistrer en base, etc. // $this->someService->handleQrPayload($payload); return new JsonModel([ 'ok' => true, 'valid' => $valid, 'payload' => $payload, ]); } }