No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ScormEngine.php 43KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305
  1. <?php
  2. namespace Logipro\Bundle\SCORMBundle\Services;
  3. use Doctrine\Common\Persistence\ManagerRegistry;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Logipro\Bundle\SCORMBundle\Entity\Course;
  6. use Logipro\Bundle\SCORMBundle\Entity\Learner;
  7. use Logipro\Bundle\SCORMBundle\Entity\MapCourseZipfile;
  8. use Logipro\Bundle\SCORMBundle\Entity\Registration;
  9. use Logipro\Bundle\SCORMBundle\Entity\RegistrationAttempt;
  10. use Logipro\Bundle\SCORMBundle\Entity\ZipFile;
  11. use Logipro\Bundle\SCORMBundle\Entity\Scorm2004\Scorm2004Track;
  12. use Logipro\Bundle\SCORMBundle\Entity\Scorm2004\Scorm2004MapTrackItem;
  13. use Logipro\Bundle\SCORMBundle\Entity\Scorm2004\Scorm2004AttemptProgressInformation;
  14. use Logipro\Bundle\SCORMBundle\Entity\Scorm2004\Scorm2004Interaction;
  15. use Logipro\Bundle\SCORMBundle\LearningModels\DOMSCORM12;
  16. use Logipro\Bundle\SCORMBundle\LearningModels\DOMSCORM2004;
  17. use Logipro\Bundle\SCORMBundle\Package\Common\PackageTypeDetector;
  18. use Logipro\Bundle\SCORMBundle\Player\SCORM2004\Scorm2004PlayerLogic;
  19. use Logipro\Bundle\SCORMBundle\Package\PackageValidator;
  20. use Symfony\Component\DependencyInjection\ContainerInterface;
  21. use Symfony\Component\DependencyInjection\ContainerAwareTrait;
  22. use Symfony\Component\DependencyInjection\Reference;
  23. use Symfony\Component\DependencyInjection\ContainerAwareInterface;
  24. use Symfony\Component\HttpFoundation\Cookie;
  25. use Symfony\Component\HttpFoundation\Request;
  26. use Symfony\Component\HttpFoundation\Response;
  27. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  28. /**
  29. * Service de communication du moteur
  30. */
  31. class ScormEngine implements ContainerAwareInterface
  32. {
  33. use ContainerAwareTrait;
  34. protected $packageFolder;
  35. protected $contentFolder;
  36. protected $contentUrlPrefix;
  37. /**
  38. * la variable de session passé automatiquement par le kernel
  39. * du fait de sa déclaration ET de sa présence dans la déclaration de service
  40. * (ici services.xml) Presence de la ligne <argument type="service" id="session"/>
  41. *
  42. * @var SessionInterface
  43. */
  44. protected $session;
  45. /**
  46. * la response est créée ou modifiée pour inserer un cookie dans le header
  47. * lors de la lecture du paquet (ce cookie est uniquement utilisé dans un mecanisme de
  48. * blocage de sécurité)
  49. *
  50. * @var Response
  51. */
  52. private $response;
  53. public function __construct(string $packageFolder, string $contentFolder, string $contentUrlPrefix, SessionInterface $session)
  54. {
  55. $this->packageFolder = $packageFolder;
  56. $this->contentFolder = $contentFolder;
  57. $this->contentUrlPrefix = $contentUrlPrefix;
  58. $this->session = $session;
  59. }
  60. // COURS
  61. /**
  62. * Crée un cours à partir d'un package identifié par un chemin
  63. *
  64. * @param string $courseKey
  65. * @param string $zipPath
  66. * @param bool $analyse
  67. * @param string $versioningRule
  68. *
  69. * @return array
  70. */
  71. public function createCourse(string $courseKey, string $zipPath, bool $analyse = false, string $versioningRule = Course::VERSIONING_RULE_DEFAULT)
  72. {
  73. // Création de la structure de la réponse
  74. $response = array(
  75. 'code' => 200,
  76. 'isSuccessfullyCreated' => false,
  77. 'error' => '',
  78. 'parserErrors' => array(),
  79. 'course' => array(
  80. 'courseKey' => '',
  81. 'maxVersion' => 0,
  82. 'versioningRule' => $versioningRule
  83. ),
  84. 'metadata' => array(
  85. 'isAlreadyExisting' => false,
  86. 'standard' => '',
  87. 'title' => '',
  88. 'duration' => ''
  89. )
  90. );
  91. // Controle sur la clef fournie (elle doit etre non null et non existante en BD)
  92. if (!empty($courseKey)) {
  93. $course = $this->requireCourse($courseKey);
  94. if ($course) {
  95. $response['code'] = 400;
  96. $response['error'] = 'La clef de cours existe déjà. Utiliser \'updateCourse\' pour mettre à jour un cours existant';
  97. return $response;
  98. }
  99. // ZIP
  100. // Test de l'existence du fichier à uploader
  101. if (!file_exists($zipPath)) {
  102. $response['code'] = 400;
  103. $response['error'] = 'Le chemin ne pointe pas vers un paquet valide';
  104. return $response;
  105. }
  106. // Validation du Package
  107. $validation = PackageValidator::validatePackage($zipPath);
  108. if (!$validation['validation']['status']) {
  109. return $response;
  110. }
  111. $response['metadata']['standard'] = $validation['standard'];
  112. $response['metadata']['title'] = $validation['title'];
  113. // Récupération de la signature du zip
  114. $hash = md5_file($zipPath);
  115. // Récupération de l'entity manager
  116. $em = $this->getDoctrine()->getManager();
  117. // Test de l'existence de la signature
  118. $repository = $em->getRepository(ZipFile::class);
  119. $zipFile = $repository->findOneBy(['zipFileprint' => $hash]);
  120. $response['metadata']['isAlreadyExisting'] = !empty($zipFile);
  121. // On s'arrete ici si seule l'analyse du paquet est demandée
  122. if ($analyse) {
  123. return $response;
  124. }
  125. // si le fichier n'existe pas déjà on le crée et on le stock coté moteur
  126. if (empty($zipFile)) {
  127. $response['metadata']['isAlreadyExisting'] = false;
  128. //upload du fichier coté moteur
  129. $uploadDate = new \DateTime('NOW');
  130. $fileName = pathinfo($zipPath)['filename'];
  131. $filePath = $this->packageFolder . '/' . $hash;
  132. if (copy($zipPath, $filePath) === false) {
  133. // Si la copie plante, on retourne une erreur
  134. $response['code'] = 400;
  135. $response['error'] = 'Erreur lors de la copie du fichier zip';
  136. return $response;
  137. }
  138. $endUploadDate = new \DateTime('NOW');
  139. // Création d'un objet permettant de traiter le fichier zip
  140. $zipFile = new ZipFile();
  141. $zipFile->setZipFileName($fileName);
  142. $zipFile->setZipFileprint($hash);
  143. $zipFile->setUploadDate($uploadDate);
  144. $zipFile->setEndUploadDate($endUploadDate);
  145. $zipFile->setStandard($validation['standard']);
  146. $zipFile->setImportStatus(ZipFile::IMPORT_STATUS_PROCESSING);
  147. $em->persist($zipFile);
  148. $em->flush();
  149. // Une fois l'objet ZipFile créé, on l'extrait
  150. $zipFile->extractZipFile($this->packageFolder, $this->contentFolder);
  151. }
  152. // COURS
  153. // Création du cours
  154. $course = new Course();
  155. $course->setCourseKey($courseKey);
  156. $course->setVersionRule($versioningRule);
  157. $em->persist($course);
  158. // Métadonées du cours
  159. $response['course']['courseKey'] = $course->getCourseKey();
  160. $response['course']['maxVersion'] = $course->getMaxVersion();
  161. // Mapping Zip/Cours
  162. $mapping = new MapCourseZipfile();
  163. $mapping->setCourse($course);
  164. $mapping->setZipFile($zipFile);
  165. $mapping->setVersion($response['course']['maxVersion']);
  166. $em->persist($mapping);
  167. $em->flush();
  168. $response['isSuccessfullyCreated'] = true;
  169. }
  170. return $response;
  171. }
  172. /**
  173. * Remplace le package d'un cours
  174. *
  175. * @param string $courseKey
  176. * @param string $zipPath
  177. * @param bool $analyse
  178. * @param string $versioningRule
  179. *
  180. * @return array
  181. */
  182. public function updateCourse(string $courseKey, string $zipPath, bool $analyse = false, string $versioningRule = Course::VERSIONING_RULE_DEFAULT)
  183. {
  184. // Création de la structure de la réponse
  185. $response = array(
  186. 'code' => 200,
  187. 'isSuccessfullyUpdated' => false,
  188. 'error' => '',
  189. 'parserErrors' => array(),
  190. 'course' => array(
  191. 'courseKey' => '',
  192. 'maxVersion' => 0,
  193. 'versioningRule' => $versioningRule
  194. ),
  195. 'metadata' => array(
  196. 'isAlreadyExisting' => false,
  197. 'standard' => '',
  198. 'title' => '',
  199. 'duration' => ''
  200. )
  201. );
  202. // Controle sur la clef fournie (elle doit etre non null et non existante en BD)
  203. if (!empty($courseKey)) {
  204. $course = $this->requireCourse($courseKey);
  205. if (empty($course)) {
  206. $response['code'] = 400;
  207. $response['error'] = 'La clef de cours ne fait référence à aucun cours existant';
  208. return $response;
  209. }
  210. // ZIP
  211. // Test de l'existence du fichier à uploader
  212. if (!file_exists($zipPath)) {
  213. $response['code'] = 400;
  214. $response['error'] = 'le chemin ne pointe pas vers un paquet valide';
  215. return $response;
  216. }
  217. // Validation du Package
  218. $validation = PackageValidator::validatePackage($zipPath);
  219. if (!$validation['validation']['status']) {
  220. return $response;
  221. }
  222. $response['metadata']['standard'] = $validation['standard'];
  223. $response['metadata']['title'] = $validation['title'];
  224. // Récupération de la signature du zip
  225. $hash = md5_file($zipPath);
  226. // Récupération de l'entity manager
  227. $em = $this->getDoctrine()->getManager();
  228. // Test de l'existence de la signature
  229. $repository = $em->getRepository(ZipFile::class);
  230. $zipFile = $repository->findOneBy(['zipFileprint' => $hash]);
  231. $response['metadata']['isAlreadyExisting'] = !empty($zipFile);
  232. // On s'arrete ici si seule l'analyse du paquet est demandée
  233. if ($analyse) {
  234. return $response;
  235. }
  236. // si le fichier n'existe pas déjà on le crée et on le stock coté moteur
  237. if (empty($zipFile)) {
  238. $response['metadata']['isAlreadyExisting'] = false;
  239. //upload du fichier coté moteur
  240. $uploadDate = new \DateTime('NOW');
  241. $fileName = pathinfo($zipPath)['filename'];
  242. $filePath = $this->packageFolder . '/' . $hash;
  243. if (copy($zipPath, $filePath) === false) {
  244. // Si la copie plante, on retourne une erreur
  245. $response['code'] = 400;
  246. $response['error'] = 'Erreur lors de la copie du fichier zip';
  247. return $response;
  248. }
  249. $endUploadDate = new \DateTime('NOW');
  250. // Création d'un objet permettant de traiter le fichier zip
  251. $zipFile = new ZipFile();
  252. $zipFile->setZipFileName($fileName);
  253. $zipFile->setZipFileprint($hash);
  254. $zipFile->setUploadDate($uploadDate);
  255. $zipFile->setEndUploadDate($endUploadDate);
  256. $zipFile->setStandard($validation['standard']);
  257. $zipFile->setImportStatus(ZipFile::IMPORT_STATUS_PROCESSING);
  258. $em->persist($zipFile);
  259. // Une fois l'objet ZipFile créé, on l'extrait
  260. $zipFile->extractZipFile($this->packageFolder, $this->contentFolder);
  261. }
  262. // COURS
  263. // MaJ du cours
  264. $course->setVersionRule($versioningRule);
  265. $newMaxVersion = $course->getMaxVersion();
  266. $newMaxVersion++;
  267. $course->setMaxVersion($newMaxVersion);
  268. $em->persist($course);
  269. // Métadonées du cours
  270. $response['course']['courseKey'] = $course->getCourseKey();
  271. $response['course']['maxVersion'] = $newMaxVersion;
  272. // Mapping Zip/Cours
  273. $mapping = new MapCourseZipfile();
  274. $mapping->setCourse($course);
  275. $mapping->setZipFile($zipFile);
  276. $mapping->setVersion($newMaxVersion);
  277. $em->persist($mapping);
  278. $em->flush();
  279. $response['isSuccessfullyUpdated'] = true;
  280. }
  281. return $response;
  282. }
  283. /**
  284. * Copie un cours à partir d'un cours existant
  285. *
  286. * @param string $orginalCourseKey
  287. * @param string $newCourseKey
  288. * @param bool $analyse
  289. * @param string $versioningRule
  290. *
  291. * @return array
  292. */
  293. public function duplicateCourse(string $orginalCourseKey, string $newCourseKey)
  294. {
  295. // Création de la structure de la réponse
  296. $response = array(
  297. 'code' => 200,
  298. 'isSuccessfullyCreated' => false,
  299. 'error' => '',
  300. 'course' => array(
  301. 'courseKey' => '',
  302. 'maxVersion' => 0,
  303. 'versioningRule' => ''
  304. )
  305. );
  306. // Contrôle que la clef du cours existant est fournie
  307. if (empty($orginalCourseKey)) {
  308. $response['code'] = 400;
  309. $response['error'] = 'La clef d\'un cours existant à dupliquer est requise.';
  310. return $response;
  311. }
  312. // Contrôle que la clef du nouveau cours est fournie
  313. if (empty($newCourseKey)) {
  314. $response['code'] = 400;
  315. $response['error'] = 'La clef du nouveau cours dupliqué est requise.';
  316. return $response;
  317. }
  318. // Contrôle de l'existence du cours à dupliquer
  319. $originalCourse = $this->requireCourse($orginalCourseKey);
  320. if (empty($originalCourse)) {
  321. $response['code'] = 400;
  322. $response['error'] = 'Le cours que vous souhaitez dupliquer est inexistant.';
  323. return $response;
  324. }
  325. // Contrôle de la disponibilité de la clef pour le nouveau cours
  326. $newCourse = $this->requireCourse($newCourseKey);
  327. if ($newCourse) {
  328. $response['code'] = 400;
  329. $response['error'] = 'La clef du nouveau cours existe déjà. Utilser \'updateCourse\' pour mettre à jour un cours existant.';
  330. return $response;
  331. }
  332. // Récupération de l'entity manager
  333. $em = $this->getDoctrine()->getManager();
  334. // COURS
  335. $newCourse = clone $originalCourse;
  336. $newCourse->setCourseKey($newCourseKey);
  337. $newCourse->setMaxVersion(1);
  338. $em->persist($newCourse);
  339. // Récupération de l'objet Zip
  340. $mappingRepository = $em->getRepository(MapCourseZipfile::class);
  341. $originalMapping = $mappingRepository->findOneBy(array('course' => $originalCourse), array('mapCourseZipfileId' => 'desc'));
  342. $zipFile = $originalMapping->getZipFile();
  343. // Mapping Zip/Cours
  344. $mapping = new MapCourseZipfile();
  345. $mapping->setCourse($newCourse);
  346. $mapping->setZipFile($zipFile);
  347. $mapping->setVersion($newCourse->getMaxVersion());
  348. $em->persist($mapping);
  349. $em->flush();
  350. // Réponse
  351. $response['course']['courseKey'] = $newCourse->getCourseKey();
  352. $response['course']['maxVersion'] = $newCourse->getMaxVersion();
  353. $response['course']['versioningRule'] = $newCourse->getVersionRule();
  354. $response['isSuccessfullyCreated'] = true;
  355. return $response;
  356. }
  357. /**
  358. * Remplace la règle de versionning d'un cours
  359. *
  360. * @param string $courseKey
  361. * @param string $versionRule
  362. *
  363. * @return string
  364. */
  365. public function updateCourseVersionRule(string $courseKey, string $versionRule)
  366. {
  367. $em = $this->getDoctrine()->getManager();
  368. $course = $this->requireCourse($courseKey);
  369. if ($course) {
  370. $course->setVersionRule($versionRule);
  371. $em->persist($course);
  372. $em->flush();
  373. return $course->getCourseKey();
  374. }
  375. return null;
  376. }
  377. /**
  378. * Supprime une activité
  379. *
  380. * @return bool
  381. */
  382. public function deleteCourse(string $courseKey)
  383. {
  384. $em = $this->getDoctrine()->getManager();
  385. $course = $this->requireCourse($courseKey);
  386. if ($course) {
  387. $em->remove($course);
  388. $em->flush();
  389. return true;
  390. }
  391. return false;
  392. }
  393. /**
  394. * retourne le numéro de version de l'activité
  395. */
  396. public function getCourseCurrentVersion(string $courseKey)
  397. {
  398. $course = $this->requireCourse($courseKey);
  399. if ($course) {
  400. return $course->getMaxVersion();
  401. }
  402. return null;
  403. }
  404. /**
  405. * retourne la règle de versionnig
  406. */
  407. public function getCourseVersionRule()
  408. {
  409. $course = $this->requireCourse($courseKey);
  410. if ($course) {
  411. return $course->getVersionRule();
  412. }
  413. return null;
  414. }
  415. public function getCourses() : array
  416. {
  417. $em = $this->getDoctrine()->getManager();
  418. $repository = $em->getRepository(Course::class);
  419. $courses = $repository->findAll();
  420. return $courses;
  421. }
  422. // LEARNER
  423. /**
  424. * Création d'un apprenant
  425. *
  426. * @param string $learnerKey
  427. * @param string $familyName
  428. * @param string $givenName
  429. * @return int
  430. */
  431. public function createLearner(string $learnerKey, string $familyName, string $givenName)
  432. {
  433. // Création de la structure de la réponse
  434. $response = array(
  435. 'code' => 400,
  436. 'isAlreadyExisting' => false,
  437. 'error' => '',
  438. 'learner' => array(
  439. 'learnerKey' => '',
  440. 'familyName' => '',
  441. 'givenName' => ''
  442. )
  443. );
  444. if (!empty($learnerKey)) {
  445. $learner = $this->requireLearner($learnerKey);
  446. if (empty($learner)) {
  447. $em = $this->getDoctrine()->getManager();
  448. $learner = new Learner();
  449. $learner->setLearnerKey($learnerKey);
  450. $learner->setFamilyName($familyName);
  451. $learner->setGivenName($givenName);
  452. $em->persist($learner);
  453. $em->flush();
  454. } else {
  455. $response['isAlreadyExisting'] = true;
  456. }
  457. $response['code'] = 200;
  458. $response['learner']['learnerKey'] = $learner->getLearnerKey();
  459. $response['learner']['familyName'] = $learner->getFamilyName();
  460. $response['learner']['givenName'] = $learner->getGivenName();
  461. } else {
  462. $response['error'] = 'La clef d\'identification est necessaire';
  463. }
  464. return $response;
  465. }
  466. /**
  467. * Modification d'un apprenant
  468. *
  469. * @param string $learnerKey
  470. * @param string $familyName
  471. * @param string $givenName
  472. * @return int
  473. */
  474. public function updateLearner(string $learnerKey, string $familyName, string $givenName)
  475. {
  476. if (!empty($learnerKey)) {
  477. $learner = $this->requireLearner($learnerKey);
  478. if ($learner) {
  479. $learner->setFamilyName($familyName);
  480. $learner->setGivenName($givenName);
  481. $em->persist($learner);
  482. $em->flush();
  483. }
  484. }
  485. return $learner->getLearnerKey();
  486. }
  487. /**
  488. * récupération d'un apprenant
  489. *
  490. * @param string $learnerKey
  491. *
  492. * @return Learner
  493. */
  494. public function getLearner(string $learnerKey)
  495. {
  496. $em = $this->getDoctrine()->getManager();
  497. $learner = $this->requireLearner($learnerKey);
  498. return $learner;
  499. }
  500. public function getLearners()
  501. {
  502. $em = $this->getDoctrine()->getManager();
  503. $repository = $em->getRepository(Learner::class);
  504. $learners = $repository->findAll();
  505. return $learners;
  506. }
  507. /**
  508. * supprime un apprenant
  509. *
  510. * @param string $learnerKey
  511. *
  512. * @return bool
  513. */
  514. public function deleteLearner(string $learnerKey)
  515. {
  516. $em = $this->getDoctrine()->getManager();
  517. $learner = $this->requireLearner($learnerKey);
  518. if ($learner) {
  519. $em->remove($learner);
  520. $em->flush();
  521. return true;
  522. }
  523. return false;
  524. }
  525. // REGISTRATION
  526. /**
  527. * création d'une inscription
  528. *
  529. * @param string $registrationKey
  530. * @param string $courseKey
  531. * @param string $learnerKey
  532. *
  533. * @return string
  534. */
  535. public function createRegistration(string $registrationKey, string $courseKey, string $learnerKey)
  536. {
  537. // Création de la structure de la réponse
  538. $response = array(
  539. 'code' => 400,
  540. 'isAlreadyExisting' => false,
  541. 'error' => '',
  542. 'registration' => array(
  543. 'registrationKey' => '',
  544. 'courseKey' => '',
  545. 'learnerKey' => ''
  546. )
  547. );
  548. if (!empty($registrationKey)) {
  549. $registration = $this->requireRegistration($registrationKey);
  550. $course = $this->requireCourse($courseKey);
  551. $learner = $this->requireLearner($learnerKey);
  552. if ($course && $learner) {
  553. if (empty($registration)) {
  554. $em = $this->getDoctrine()->getManager();
  555. $registration = new Registration();
  556. $registration->setRegistrationKey($registrationKey);
  557. $registration->setCourse($course);
  558. $registration->setLearner($learner);
  559. $em->persist($registration);
  560. $em->flush();
  561. } else {
  562. $response['isAlreadyExisting'] = true;
  563. }
  564. $response['code'] = 200;
  565. $response['registration']['registrationKey'] = $registration->getRegistrationKey();
  566. $response['registration']['courseKey'] = $registration->getCourse();
  567. $response['registration']['learnerKey'] = $registration->getLearner();
  568. } else {
  569. $response['error'] = 'Le cours et l\'apprenant doivent exister';
  570. }
  571. } else {
  572. $response['error'] = 'La clef d\'identification est necessaire';
  573. }
  574. return $response;
  575. }
  576. /**
  577. * Renvoie les variables necessaire pour l'affichage du contenu du lecteur
  578. * dans le cadre d'une inscription (pour un apprenant et pour un cours)
  579. *
  580. * @param string $registrationKey
  581. * @param string $data
  582. *
  583. * @return array
  584. */
  585. public function getPlayerVariables(string $registrationKey, string $data = null, Response $response = null) : array
  586. {
  587. $result = array();
  588. // Vérification de l'existance de l'inscription
  589. $registration = $this->requireRegistration($registrationKey);
  590. if (empty($registration)) {
  591. // Erreur : une inscription valide doit être passée
  592. return null;
  593. }
  594. $em = $this->getDoctrine()->getManager();
  595. // Récupération du cours et des infos de version
  596. $course = $registration->getCourse();
  597. $maxVersion = $course->getMaxVersion();
  598. $versionRule = $course->getVersionRule();
  599. // Récupération du registrationAttempt courant
  600. $attemptRepository = $em->getRepository(RegistrationAttempt::class);
  601. $currentAttempt = $attemptRepository->findOneBy(array('registration' => $registration), array('registrationAttemptId' => 'desc'));
  602. $needCreation = empty($currentAttempt);
  603. $currentVersion = $needCreation ? null : $currentAttempt->getCourseVersion();
  604. // Vérification de l'existance d'un essai
  605. // et, si oui, de la présence d'une nouvelle version si la règle de versionning impose un restart
  606. if ($needCreation || ($versionRule == Course::VERSIONING_RULE_RESTART && $currentVersion < $maxVersion)) {
  607. $newAttempt = new RegistrationAttempt();
  608. $newAttempt->setCourseVersion($maxVersion);
  609. $newAttempt->setRegistration($registration);
  610. $em->persist($newAttempt);
  611. $em->flush();
  612. $currentAttempt = $newAttempt;
  613. $currentVersion = $maxVersion;
  614. if (!$needCreation) {
  615. // TODO Indiquer dans la réponse que l'on a effectué une modification de version pour l'apprenan
  616. $data = null;
  617. }
  618. }
  619. // Récupération du zip correspondant et ainsi de la version du standard utilisée
  620. $mapCourseZipRepository = $em->getRepository(MapCourseZipfile::class);
  621. $mapCourseZip = $mapCourseZipRepository->findOneBy(array('course' => $course, 'version' => $currentVersion));
  622. $zipFile = $mapCourseZip->getZipFile();
  623. $standard = $zipFile->getStandard();
  624. // Traitement des données en fonction du standard
  625. $variables = null;
  626. switch ($standard) {
  627. case DOMSCORM2004::SCORM_2004:
  628. $playerLogic = new Scorm2004PlayerLogic();
  629. $playerLogic->setContainer($this->container);
  630. $variables = $playerLogic->getScorm2004PlayerVariables($registrationKey, $zipFile, $currentAttempt, $data);
  631. break;
  632. case DOMSCORM12::SCORM_12:
  633. default:
  634. return null;
  635. }
  636. if ($variables['result'] == 'player') {
  637. // Construction d'un URL protégé
  638. $courseKey = $zipFile->getZipFileprint();
  639. $this->prepareContentAccess($courseKey, $response);
  640. $protection = $this->getBaseUrl($courseKey);
  641. $url = $variables['player']['content'];
  642. $url = $protection . '/' . $url;
  643. $variables['player']['content'] = $url;
  644. } else {
  645. $this->response = new Response();
  646. }
  647. return $variables;
  648. }
  649. /**
  650. * Récupération de la complétion d'un cours
  651. *
  652. * @param string $registrationKey
  653. */
  654. public function getCompletion(string $registrationKey)
  655. {
  656. $em = $this->getDoctrine()->getManager();
  657. // Vérification de l'existance de l'inscription
  658. $registration = $this->requireRegistration($registrationKey);
  659. if (empty($registration)) {
  660. return 0;
  661. }
  662. // Récupération du dernier essai pour cette inscription
  663. $registrationAttemptRepository = $em->getRepository(RegistrationAttempt::class);
  664. $lastAttempt = $registrationAttemptRepository->findOneBy(array('registration' => $registration), array('registrationAttemptId' => 'desc'));
  665. // DETECTER AVANT LE TYPE DE SCORM POUR SAVOIR QUEL STANDARD
  666. // CODE SCORM 2004
  667. // On récupère le track correspondant
  668. $track2004Repository = $em->getRepository(Scorm2004Track::class);
  669. $track2004 = $track2004Repository->findOneBy(array('registrationAttempt' => $lastAttempt));
  670. if (empty($track2004)) {
  671. return 0;
  672. }
  673. // Dans un premier temps on détermine si la complétion peut être remontée
  674. // en se basant sur l'état de complétion de l'ensemble des items terminaux
  675. $mapTrackItemRepository = $em->getRepository(Scorm2004MapTrackItem::class);
  676. $attemptItemRepository = $em->getRepository(Scorm2004AttemptProgressInformation::class);
  677. $mapTrackItems = $mapTrackItemRepository->findBy(array('scorm2004Track' => $track2004));
  678. if (!empty($mapTrackItems)) {
  679. $numCompletedItems = 0;
  680. $numFinalItems = 0;
  681. foreach ($mapTrackItems as $item) {
  682. // Si les items sont terminaux
  683. if ($item->getAvailableOrder() > -1) {
  684. $numFinalItems ++;
  685. $identifier = $item->getItemIdentifier();
  686. $attemptItem = $attemptItemRepository->findOneBy(array('scorm2004Track'=> $track2004, 'item_identifier' => $identifier), array('attempt_id' => 'desc'));
  687. if (!empty($attemptItem)) {
  688. $progressStatus = $attemptItem->getProgressStatus();
  689. $completionStatus = $attemptItem->getCompletionStatus();
  690. if (true == $progressStatus && true == $completionStatus) {
  691. $numCompletedItems ++;
  692. }
  693. }
  694. }
  695. }
  696. if ($numFinalItems != 0) {
  697. $completion = 100.0 * ($numCompletedItems / $numFinalItems);
  698. $completion = round($completion, 2);
  699. // Si on obtient une valeur de complétion elle est retournée
  700. if ($completion > 0) {
  701. return $completion;
  702. }
  703. }
  704. }
  705. // Si aucune complétion n'a été déterminée précédement on controle que l'on aie pas un taux de complétion présent au niveau de l'organisation
  706. $organization = $track2004->getOrganizationId();
  707. if (empty($organization)) {
  708. return 0;
  709. }
  710. $lastOrganizationAttempt = $attemptItemRepository->findOneBy(array('scorm2004Track'=> $track2004, 'item_identifier' => $organization), array('attempt_id' => 'desc'));
  711. if (empty($lastOrganizationAttempt)) {
  712. return 0;
  713. }
  714. $completionAmount = $lastOrganizationAttempt->getCompletionAmount();
  715. if ($completionAmount > 0) {
  716. return round(100.0 * $completionAmount, 2);
  717. }
  718. $progressStatus = $lastOrganizationAttempt->getProgressStatus();
  719. $completionStatus = $lastOrganizationAttempt->getCompletionStatus();
  720. if (true == $progressStatus && true == $completionStatus) {
  721. return 100;
  722. }
  723. return 0;
  724. }
  725. /**
  726. * Récupération du score moyen d'un cours
  727. *
  728. * @param string $registrationKey
  729. */
  730. public function getAverageScore(string $registrationKey)
  731. {
  732. $em = $this->getDoctrine()->getManager();
  733. // Vérification de l'existance de l'inscription
  734. $registration = $this->requireRegistration($registrationKey);
  735. if (empty($registration)) {
  736. return null;
  737. }
  738. // Récupération du dernier essai pour cette inscription
  739. $registrationAttemptRepository = $em->getRepository(RegistrationAttempt::class);
  740. $lastAttempt = $registrationAttemptRepository->findOneBy(array('registration' => $registration), array('registrationAttemptId' => 'desc'));
  741. // DETECTER AVANT LE TYPE DE SCORM POUR SAVOIR QUEL STANDARD
  742. // CODE SCORM 2004
  743. // On récupère le track correspondant
  744. $track2004Repository = $em->getRepository(Scorm2004Track::class);
  745. $track2004 = $track2004Repository->findOneBy(array('registrationAttempt' => $lastAttempt));
  746. if (empty($track2004)) {
  747. return null;
  748. }
  749. $mapTrackItemRepository = $em->getRepository(Scorm2004MapTrackItem::class);
  750. $mapTrackItems = $mapTrackItemRepository->findBy(array('scorm2004Track' => $track2004));
  751. $averageScore = null;
  752. if (!empty($mapTrackItems)) {
  753. $scoreSum = 0;
  754. $scoreNum = 0;
  755. foreach ($mapTrackItems as $item) {
  756. $scoreMax = $item->getScoreMax();
  757. // Si pas de score max, pas de score valide
  758. if ($scoreMax > 0) {
  759. $score = $item->getScoreRaw();
  760. $score = $score * 100 / $scoreMax;
  761. $score = max(min($score, 100), 0);
  762. $scoreNum ++;
  763. $scoreSum += $score;
  764. }
  765. }
  766. if ($scoreNum > 0) {
  767. $averageScore = $scoreSum / $scoreNum;
  768. $averageScore = round($averageScore, 2);
  769. }
  770. }
  771. return $averageScore;
  772. }
  773. /**
  774. * Récupération des interactions d'un cours
  775. * Elles sont rangées par item
  776. *
  777. * @param string $registrationKey
  778. */
  779. public function getInteractions(string $registrationKey)
  780. {
  781. $em = $this->getDoctrine()->getManager();
  782. // Vérification de l'existance de l'inscription
  783. $registration = $this->requireRegistration($registrationKey);
  784. if (empty($registration)) {
  785. return null;
  786. }
  787. // Récupération du dernier essai pour cette inscription
  788. $registrationAttemptRepository = $em->getRepository(RegistrationAttempt::class);
  789. $lastAttempt = $registrationAttemptRepository->findOneBy(array('registration' => $registration), array('registrationAttemptId' => 'desc'));
  790. // DETECTER AVANT LE TYPE DE SCORM POUR SAVOIR QUEL STANDARD
  791. // CODE SCORM 2004
  792. // On récupère le track correspondant
  793. $track2004Repository = $em->getRepository(Scorm2004Track::class);
  794. $track2004 = $track2004Repository->findOneBy(array('registrationAttempt' => $lastAttempt));
  795. if (empty($track2004)) {
  796. return null;
  797. }
  798. $mapTrackItemRepository = $em->getRepository(Scorm2004MapTrackItem::class);
  799. // Récupération des données pour chaque items
  800. $mapTrackItems = $mapTrackItemRepository->findBy(array('scorm2004Track' => $track2004));
  801. $interactions = array();
  802. if (!empty($mapTrackItems)) {
  803. // Récupération du DOM afin d'avoir des infos sur les items
  804. $packageFolder = $this->container->getParameter('logipro_scorm.package_folder');
  805. $domScorm2004 = $lastAttempt->getDOM($packageFolder);
  806. $attemptItemRepository = $em->getRepository(Scorm2004AttemptProgressInformation::class);
  807. $interactionRepository = $em->getRepository(Scorm2004Interaction::class);
  808. // Pour chaque item
  809. foreach ($mapTrackItems as $item) {
  810. // On récupère l'identifiant de l'item
  811. $identifier = $item->getItemIdentifier();
  812. // On récupère le dernier AttemptProgressInfo en cours
  813. $attemptItem = $attemptItemRepository->findOneBy(array('scorm2004Track'=> $track2004, 'item_identifier' => $identifier), array('attempt_id' => 'desc'));
  814. // On récupère les interactions pour cet item et cet Attempt
  815. $itemInteractions = $interactionRepository->findBy(array('scorm2004Track' => $track2004, 'scorm2004AttemptProgressInformation' => $attemptItem, 'itemIdentifier' => $identifier), array('interaction_id' => 'asc'));
  816. // Si il existe des interactions
  817. if (!empty($itemInteractions)) {
  818. $result = array();
  819. // On récupère le nom de l'item
  820. $result['title'] = $domScorm2004->getTitle($identifier);
  821. // On Récupère le Score sur le mapTrackItem
  822. $score = null;
  823. $scoreMax = $item->getScoreMax();
  824. // Si pas de score max, pas de score valide
  825. if ($scoreMax > 0) {
  826. $score = $item->getScoreRaw();
  827. $score = $score * 100 / $scoreMax;
  828. $score = max(min($score, 100), 0);
  829. }
  830. $result['score'] = $score;
  831. // On récupère le Lesson Status (si on peut / voir Dans AI quoi envoyé pour avoir un résultat neutre à l'affichage)
  832. $status = $attemptItem->getSuccessStatus();
  833. $result['lessonStatus'] = $status;
  834. // On ajoute les objets interactions
  835. $result['interactions'] = $itemInteractions;
  836. // On ajoute ce lot d'interaction aux interactions du cours
  837. $interactions[] = $result;
  838. }
  839. }
  840. }
  841. if (count($interactions) > 0) {
  842. return $interactions;
  843. }
  844. return null;
  845. }
  846. /**
  847. * supprime une inscription
  848. *
  849. * @param string $registrationKey
  850. *
  851. * @return string
  852. */
  853. public function deleteRegistration(string $registrationKey)
  854. {
  855. $registration = $this->requireRegistration($registrationKey);
  856. if ($registration) {
  857. $em = $this->getDoctrine()->getManager();
  858. $em->remove($registration);
  859. $em->flush();
  860. return true;
  861. }
  862. return false;
  863. }
  864. /**
  865. * supprime les données d'apprentissage d'un apprenant pour une inscription
  866. *
  867. * @param string $learnerKey
  868. *
  869. * @return bool
  870. */
  871. public function resetRegistraitonData(string $registrationKey)
  872. {
  873. $em = $this->getDoctrine()->getManager();
  874. $trackRepository = $em->getRepository(Registration::class);
  875. $registration = $this->requireRegistration($registrationKey);
  876. if ($registration) {
  877. // Récupération des Registrations Attempts
  878. $attempts = $this->getAttempts($registrationKey);
  879. // Efface les RegistrationAttempts
  880. // Les objets SCORM seront effacés en cascade
  881. foreach ($attempts as $attempt) {
  882. $em->remove($attempt);
  883. }
  884. // Création d'un nouveau RegistrationAttempt
  885. $course = $registration->getCourse();
  886. $attempt = new RegistrationAttempt();
  887. $attempt->setCourseVersion($course->getMaxVersion());
  888. $attempt->setRegistration($registration);
  889. $em->persist($attempt);
  890. $em->flush();
  891. return true;
  892. }
  893. return false;
  894. }
  895. public function getRegistrations()
  896. {
  897. $em = $this->getDoctrine()->getManager();
  898. $repository = $em->getRepository(Registration::class);
  899. $registrations = $repository->findAll();
  900. return $registrations;
  901. }
  902. // ATTEMPT
  903. /**
  904. * Récupération des essais
  905. *
  906. * @param string $registrationKey
  907. *
  908. * @return string
  909. */
  910. public function getAttempts(string $registrationKey)
  911. {
  912. $em = $this->getDoctrine()->getManager();
  913. $registration = $this->requireRegistration($registrationKey);
  914. if ($registration) {
  915. $attempts = $registration->getRegistrationAttempts();
  916. return ($attempts);
  917. }
  918. return null;
  919. }
  920. /**
  921. * Récupération de l'id de l'essai en cours
  922. *
  923. * @param string $registrationKey
  924. *
  925. * @return int
  926. */
  927. public function getCurrentAttempt(string $registrationKey)
  928. {
  929. $em = $this->getDoctrine()->getManager();
  930. $registration = $this->requireRegistration($registrationKey);
  931. if ($registration) {
  932. $attemptRepository = $em->getRepository(RegistrationAttempt::class);
  933. $currentAttempt = $attemptRepository->findCurrentAttemptByRegistration($registration);
  934. return $currentAttempt->registrationAttemptId();
  935. }
  936. return null;
  937. }
  938. /**
  939. * Récupération de la version de l'activité pour l'essai
  940. *
  941. * @param int $attemptId
  942. *
  943. * @return int
  944. */
  945. public function getAttemptCourseVersion(int $attemptId)
  946. {
  947. $em = $this->getDoctrine()->getManager();
  948. $attempt = $this->requireRegistrationAttempt($attemptId);
  949. if ($attempt) {
  950. return $attempt->getCourseVersion();
  951. }
  952. return null;
  953. }
  954. /**
  955. * Récupération des statistiques liées à un essai
  956. *
  957. * @param int $attemptId
  958. *
  959. * @return array
  960. */
  961. public function getAttemptStats(int $attemptId)
  962. {
  963. }
  964. // TOOLS
  965. /**
  966. * Undocumented function
  967. *
  968. * @param string $courseKey
  969. * @param Response $response
  970. * @return void
  971. */
  972. private function prepareContentAccess(string $courseKey, Response $response = null) : void
  973. {
  974. if (!$this->session->has('scormkey')) {
  975. $scormkey = rand();
  976. $this->session->set('scormkey', $scormkey);
  977. } else {
  978. $scormkey = $this->session->get('scormkey');
  979. }
  980. if (null === $response) {
  981. $this->response = new Response();
  982. } else {
  983. $this->response = $response;
  984. }
  985. $pathCookie = $this->contentUrlPrefix."/$scormkey/$courseKey/";
  986. $md5cookiekey = md5($scormkey);
  987. $cookie = new Cookie('SCORMBundle', $md5cookiekey, 0, $pathCookie);
  988. $this->response->headers->setCookie($cookie);
  989. }
  990. /**
  991. * il FAUT utiliser la Response de l'objet ScormEngine pour bénéficier du cookie
  992. * de sécurité (sans cela blocage complet de l'acces à la ressource)
  993. *
  994. * @return Response
  995. */
  996. public function getResponse() : Response
  997. {
  998. return $this->response;
  999. }
  1000. /**
  1001. * renvoi l'url de base permettant d'acceder à un fichier de la ressource
  1002. * Format :
  1003. * /content/nomdupaquet/clefautorisation
  1004. *
  1005. * Example :
  1006. * /content/
  1007. *
  1008. * @return string
  1009. */
  1010. public function getBaseUrl($courseKey) : string
  1011. {
  1012. return $this->contentUrlPrefix.'/'.$this->session->get('scormkey').'/'.$courseKey;
  1013. }
  1014. /**
  1015. *
  1016. * @throws \LogicException
  1017. * @return ManagerRegistry
  1018. */
  1019. protected function getDoctrine(): ManagerRegistry
  1020. {
  1021. if (!$this->container->has('doctrine')) {
  1022. throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
  1023. }
  1024. return $this->container->get('doctrine');
  1025. }
  1026. /**
  1027. * Retourne l'activité
  1028. *
  1029. * @param string $courseKey
  1030. * @return Course
  1031. */
  1032. protected function requireCourse(string $courseKey)
  1033. {
  1034. $em = $this->getDoctrine()->getManager();
  1035. $repository = $em->getRepository(Course::class);
  1036. $course = $repository->findOneBy(array('courseKey' => $courseKey));
  1037. return $course;
  1038. }
  1039. /**
  1040. * Retourne l'apprenant
  1041. *
  1042. * @param string $learnerKey
  1043. * @return Learner
  1044. */
  1045. protected function requireLearner(string $learnerKey)
  1046. {
  1047. $em = $this->getDoctrine()->getManager();
  1048. $repository = $em->getRepository(Learner::class);
  1049. $learner = $repository->findOneBy(array('learnerKey' => $learnerKey));
  1050. return $learner;
  1051. }
  1052. /**
  1053. * Retourne l'inscription
  1054. *
  1055. * @param string $registrationKey
  1056. *
  1057. * @return Registration
  1058. */
  1059. protected function requireRegistration(string $registrationKey)
  1060. {
  1061. $em = $this->getDoctrine()->getManager();
  1062. $repository = $em->getRepository(Registration::class);
  1063. $registration = $repository->findOneBy(array('registrationKey' => $registrationKey));
  1064. return $registration;
  1065. }
  1066. /**
  1067. * Retourne un essais relatif à une inscription
  1068. *
  1069. * @param int $attemptId
  1070. *
  1071. * @return RegistrationAttempt
  1072. */
  1073. protected function requireRegistrationAttempt(int $attemptId)
  1074. {
  1075. $em = $this->getDoctrine()->getManager();
  1076. $repository = $em->getRepository(RegistrationAttempt::class);
  1077. $attempt = $repository->find($attemptId);
  1078. return $attempt;
  1079. }
  1080. }