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.

SCORMTools.php 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. namespace Logipro\Bundle\SCORMBundle\LearningModels;
  3. class SCORMTools
  4. {
  5. const ERR_FAILED = 0;
  6. const ERR_SUCCESS = 1;
  7. /**
  8. * memoire cahche des valeurs par defaut des attributes des tags (utilisé par getAttributeDefaultValue)
  9. *
  10. * @var array
  11. */
  12. private static $attributes = null;
  13. /**
  14. * memoire cache des nomtags avec pour clef le nom de l'attribut (utilisé par getAttributeDefaultValue)
  15. * les homonymes sont séparés par des virgules
  16. *
  17. * @var array
  18. */
  19. private static $tagnames = null;
  20. public static function analyseManifest(string $XMLManifest) : array
  21. {
  22. $analyse = array(
  23. 'error' => null,
  24. 'report' => array(
  25. 'errors' => array(),
  26. 'warns' => array(),
  27. 'fatals' => array()
  28. ),
  29. 'version' => null,
  30. 'organizations' => array(),
  31. 'items' => array()
  32. );
  33. $dom = new \DOMDocument();
  34. // Enable user error handling
  35. libxml_use_internal_errors(true);
  36. // verification de la teneur du xml : peut il passer loadXML
  37. // c'est compliqué de rattrapper l'erreur de loadXML (cf https://www.php.net/manual/en/domdocument.loadxml.php),
  38. // c'est pourquoi il a fallut créer 2 méthode statics spéciales XMLLoader et handleXmlError
  39. set_error_handler(array(__CLASS__,'handleXmlError')); // on reaffecte l'interception des erreurs
  40. try {
  41. $dom->loadXml($XMLManifest);
  42. } catch (\DOMException $e) {
  43. $analyse['error'] = self::ERR_FAILED;
  44. $analyse['report']['errors'][] = $e->getMessage();
  45. } finally {
  46. SCORMTools::libxmlDisplayErrors($analyse);
  47. restore_error_handler(); // erreur ou pas il faut absolument remettre la gestion des erreurs comme avant
  48. }
  49. libxml_clear_errors();
  50. $racine = __DIR__.'/SCORM2004/xsd/4th';
  51. // verification pour chacun des fichier xsd
  52. $source = SCORMTools::getSchemaSource();
  53. $isValide = $dom->schemaValidateSource($source);
  54. SCORMTools::libxmlDisplayErrors($analyse);
  55. return $analyse;
  56. for ($i=0; $i < sizeof($filenames); $i++) {
  57. $filename = $racine.'/'.$filenames[$i];
  58. }
  59. $isValide = $dom->schemaValidate($filename);
  60. if ($isValide == false) {
  61. $analyse['error'] = self::ERR_FAILED;
  62. SCORMTools::libxmlDisplayErrors($analyse);
  63. }
  64. if ($analyse['error'] == null) {
  65. $analyse['error'] = self::ERR_SUCCESS;
  66. }
  67. return $analyse;
  68. }
  69. /**
  70. * construction du fichier xsd permettant d'évaluer le manifest.
  71. * Si un paquet est fournit en entrée ce sont ses xsd qui sont inclus, sinon ce sont les xsd
  72. * par defaut
  73. *
  74. * @param string $packageZipName
  75. * @return string
  76. */
  77. public static function getSchemaSource(string $packageZipName = null, string $SCORMVersion = null) : string
  78. {
  79. $files = array(); // clef nom du fichier, valeur namespace (tableau à contrsuire)
  80. if ($packageZipName == null) { // si pas de pacque alors on prend des xsd stocké dans le bundle
  81. $files['imscp_v1p1.xsd']='http://www.imsglobal.org/xsd/imscp_v1p1';
  82. $files['adlcp_v1p3.xsd']='http://www.adlnet.org/xsd/adlcp_v1p3';
  83. $files['adlseq_v1p3.xsd']='http://www.adlnet.org/xsd/adlseq_v1p3';
  84. $files['adlnav_v1p3.xsd']='http://www.adlnet.org/xsd/adlnav_v1p3';
  85. $files['imsss_v1p0.xsd']='http://www.imsglobal.org/xsd/imss';
  86. } else { // recuperation des xsd dans le paquet
  87. $zip = new \ZipArchive();
  88. $zip->open($packageZipName);
  89. $XMLmanifest = $zip->getFromName('imsmanifest.xml');
  90. $doc = new \DOMDocument();
  91. $doc->loadXML($XMLmanifest);
  92. $schemas = $doc->getElementsByTagName("manifest")->item(0)->attributes->getNamedItem('schemaLocation')->nodeValue;
  93. // TO DO...
  94. }
  95. // preparation de la chaine $imports contenant les liens vers les paquets
  96. $path = __DIR__.'/SCORM2004/xsd/4th';
  97. $imports = '';
  98. foreach ($files as $filename => $ns) {
  99. $filename = "$path/$filename";
  100. $imports .= " <xsd:import namespace=\"$ns\" schemaLocation=\"$filename\" />" . PHP_EOL;
  101. }
  102. //preparation de la chaine de retour
  103. $source = '<?xml version="1.0" encoding="utf-8" ?>
  104. <xsd:schema xmlns="http://symfony.com/schema"
  105. xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  106. targetNamespace="http://symfony.com/schema"
  107. elementFormDefault="qualified">
  108. <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
  109. '.$imports.'
  110. </xsd:schema>';
  111. return $source;
  112. }
  113. /**
  114. * renvoi le contenu d'un manifest (SCOMR20044th) de base contenant toutes les balises
  115. * mais aucun attribut (peut eter utile pour retrouver les valeurs par défaut des attributs)
  116. * Attention : les espaces de noms ne sont pas défini; un traitement supplémentaire et donc nécessaires
  117. * pour accrocher les DTD et autres xls.
  118. * IMPORTANT: aucune balise ne doit etre en double car les algos sont batis sur ce principe
  119. *
  120. * @return string le manifest de base
  121. */
  122. protected static function getBasicManifest(string $SCORMVersion = null) : string
  123. {
  124. return <<<EOT
  125. <?xml version = "1.0" standalone = "no"?>
  126. <manifest identifier = "LMSTestPackage_T-01b" version = "1.1.1"
  127. xmlns = "http://www.imsglobal.org/xsd/imscp_v1p1"
  128. xmlns:adlcp = "http://www.adlnet.org/xsd/adlcp_v1p3"
  129. xmlns:adlseq = "http://www.adlnet.org/xsd/adlseq_v1p3"
  130. xmlns:adlnav = "http://www.adlnet.org/xsd/adlnav_v1p3"
  131. xmlns:imsss = "http://www.imsglobal.org/xsd/imsss"
  132. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  133. <metadata>
  134. <schema>ADL SCORM</schema>
  135. <schemaversion>2004 4th Edition</schemaversion>
  136. </metadata>
  137. <organizations >
  138. <organization identifier="organizationIdentifier" adlseq:objectivesGlobalToSystem="true" adlcp:sharedDataGlobalToSystem="true">
  139. <title>organization title</title>
  140. <item identifier="itemIdentifier">
  141. <title>item title</title>
  142. <adlnav:presentation>
  143. <adlnav:navigationInterface>
  144. <adlnav:hideLMSUI>continue</adlnav:hideLMSUI>
  145. <adlnav:hideLMSUI>previous</adlnav:hideLMSUI>
  146. <adlnav:hideLMSUI>suspendAll</adlnav:hideLMSUI>
  147. </adlnav:navigationInterface>
  148. </adlnav:presentation>
  149. <adlcp:completionThreshold />
  150. <adlcp:dataFromLMS />
  151. <adlcp:data>
  152. <adlcp:map targetID="tID"/>
  153. </adlcp:data>
  154. <imsss:sequencing>
  155. <imsss:objectives>
  156. <imsss:primaryObjective >
  157. <imsss:minNormalizedMeasure>0.6</imsss:minNormalizedMeasure>
  158. <imsss:mapInfo targetObjectiveID="toID" />
  159. </imsss:primaryObjective>
  160. <imsss:objective objectiveID="oID"/>
  161. </imsss:objectives>
  162. <imsss:deliveryControls />
  163. <adlseq:objectives>
  164. <adlseq:objective objectiveID="oID2">
  165. <adlseq:mapInfo targetObjectiveID="toID" />
  166. </adlseq:objective>
  167. </adlseq:objectives>
  168. </imsss:sequencing>
  169. </item>
  170. <imsss:sequencing>
  171. <imsss:controlMode />
  172. <imsss:sequencingRules>
  173. <imsss:exitConditionRule>
  174. <imsss:ruleConditions>
  175. <imsss:ruleCondition condition="always" />
  176. </imsss:ruleConditions>
  177. <imsss:ruleAction action="exit"/>
  178. </imsss:exitConditionRule>
  179. </imsss:sequencingRules>
  180. <imsss:rollupRules>
  181. <imsss:rollupRule >
  182. <imsss:rollupConditions>
  183. <imsss:rollupCondition condition="completed" />
  184. </imsss:rollupConditions>
  185. <imsss:rollupAction action="completed"/>
  186. </imsss:rollupRule>
  187. </imsss:rollupRules>
  188. </imsss:sequencing>
  189. </organization>
  190. </organizations>
  191. <resources>
  192. <resource identifier="ressourceIdentifier" type="" href="">
  193. <file href=""/>
  194. <dependency identifierref="idref"/>
  195. </resource>
  196. </resources>
  197. <imsss:sequencingCollection>
  198. <imsss:sequencing>
  199. <imsss:limitConditions />
  200. <adlseq:rollupConsiderations />
  201. </imsss:sequencing>
  202. </imsss:sequencingCollection>
  203. </manifest>
  204. EOT;
  205. }
  206. /**
  207. * renvoi la valeur par défaut d'un attribut, pour les homonymes d'attribut renvoie un tableau
  208. * associatif, la clef étant le nom du tag, la valeur, celle de l'attribut
  209. * Exemple pour 'operator' :
  210. * ['imsss:ruleCondition' => 'noOp',
  211. * 'imsss:rollupCondition' => 'noOp']
  212. * lorsque le nom de l'attribut est ecrit sous la forme : nomtag/nomattibut la recherche est filtrée
  213. * avec nom tag. Cela permet de resoudre les cas d'homonyme comme est de renvoyer uniuqment la valeur:
  214. * imsss:rollupConditions@conditionCombination
  215. * imsss:ruleConditions@conditionCombination
  216. *
  217. * @param string $attributeName
  218. * @param string $SCORMVersion
  219. * @return string|null|array
  220. */
  221. public static function getAttributeDefaultValue(string $attributeName, string $SCORMVersion = null)
  222. {
  223. $tagName= null; // initialisation, puis eventuelle isolation du tagName
  224. $explosion = explode('@', $attributeName);
  225. if (sizeof($explosion) == 2) {
  226. $tagName = $explosion[0];
  227. $attributeName = $explosion[1]; // nom canonique de l'attribut
  228. }
  229. if (self::$attributes == null) { // preparation du "cache" contenant tous les attributs
  230. //recuperation d'un modele de base contenant tous les tags
  231. $XMLManifest = SCORMTools::getBasicManifest($SCORMVersion);
  232. // preparation du modele DOM du manifest de Base
  233. $manifest = new \DOMDocument();
  234. $manifest->validateOnParse = true;
  235. $manifest->loadXML($XMLManifest);
  236. $xpath = new \DOMXPath($manifest);
  237. $schema = SCORMTools::getSchemaSource(); // schema XML contenant toute la description de coherence
  238. $manifest->schemaValidateSource($schema, LIBXML_SCHEMA_CREATE); // on a accès aux valeurs par défaut
  239. self::$attributes = array();
  240. self::$tagnames = array();
  241. // recuperation de tous les tag du manifest
  242. $query = '//descendant::*';
  243. $nodeList = $xpath->query($query);
  244. for ($i=0; $i<$nodeList->length; $i++) {
  245. $tag = $nodeList->item($i);
  246. for ($j=0; $j<$tag->attributes->length; $j++) {
  247. $unAttribute = $tag->attributes->item($j);
  248. self::$attributes[$tag->nodeName.'@'.$unAttribute->nodeName] = $unAttribute->nodeValue;
  249. if (!isset(self::$tagnames[$unAttribute->nodeName])) {
  250. self::$tagnames[$unAttribute->nodeName] = $tag->nodeName;
  251. } else {
  252. self::$tagnames[$unAttribute->nodeName] .= '|'.$tag->nodeName; // on sépare par des virgules
  253. }
  254. }
  255. }
  256. }
  257. $values = null;
  258. if ($tagName == null) { // alors on retourne le raccourci
  259. if (isset(self::$tagnames[$attributeName])) {
  260. $tags = explode('|', self::$tagnames[$attributeName]);
  261. if (sizeof($tags)>1) {
  262. $values = array();
  263. for ($i=0; $i<sizeof($tags); $i++) {
  264. $values[] = self::$attributes[$tags[$i].'@'.$attributeName];
  265. }
  266. //si tous les valeurs sont identiques, on n'en renverra qu'une seule
  267. if (sizeof(array_count_values($values)) == 1) { // 1 seule ligne signifie aussi 1 seule valeur
  268. $values = current($values);
  269. }
  270. } else {
  271. $values = self::$attributes[self::$tagnames[$attributeName].'@'.$attributeName];
  272. }
  273. }
  274. } else {
  275. $values = self::$attributes[$tagName.'@'.$attributeName];
  276. }
  277. return $values;
  278. }
  279. protected function validatePackage(string $XMLManifest) : bool
  280. {
  281. }
  282. private static function libxmlDisplayErrors(array &$errorList)
  283. {
  284. $errors = libxml_get_errors();
  285. foreach ($errors as $error) {
  286. $description = '';
  287. switch ($error->level) {
  288. case LIBXML_ERR_WARNING:
  289. $level = 'warns';
  290. $description = "Warning $error->code: ";
  291. break;
  292. case LIBXML_ERR_ERROR:
  293. $level = 'errors';
  294. $description = "Error $error->code: ";
  295. break;
  296. case LIBXML_ERR_FATAL:
  297. $level = 'fatals';
  298. $description = "Fatal Error $error->code: ";
  299. $errorList['error'] = self::ERR_FAILED; // seules les fatales renvoie un blocage
  300. break;
  301. }
  302. $description .= $error->message .
  303. "\n Line: $error->line" .
  304. "\n Column: $error->column";
  305. if ($error->file) {
  306. $description .= "\n File: $error->file";
  307. }
  308. $errorList['report'][$level][] = $description;
  309. }
  310. libxml_clear_errors();
  311. }
  312. /**
  313. * mecanisme d'erreur spécial pour loadXML
  314. *
  315. * @param [type] $errno
  316. * @param [type] $errstr
  317. * @param [type] $errfile
  318. * @param [type] $errline
  319. * @return void
  320. */
  321. final public static function handleXmlError($errno, $errstr, $errfile, $errline)
  322. {
  323. if ($errno==E_WARNING && (substr_count($errstr, "DOMDocument::loadXML()") >0)) {
  324. throw new \DOMException($errstr, $errno);
  325. }
  326. return false;
  327. }
  328. }