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.

DOMSCORM2004.php 44KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. <?php
  2. namespace Logipro\Bundle\SCORMBundle\LearningModels;
  3. class DOMSCORM2004 extends DOMSCORM
  4. {
  5. const SCORM_2004 = 'SCORM_2004';
  6. /**
  7. * *
  8. * @param string $XMLManifest
  9. */
  10. public function __construct(string $XMLManifest)
  11. {
  12. parent::__construct($XMLManifest);
  13. }
  14. protected function loadNamespaces(): void
  15. {
  16. SCORMTools::loadNamespacesSCORM2004($this->xpath);
  17. }
  18. /**
  19. * durée estimé par l'auteur du cours
  20. * @return array
  21. * * [duration] => P[yY][mM][dD][T[hH][nM][s[.s]S]] (ex. PT1H30M)
  22. * * [description] => Texte ecrit par l'auteur du module (ex.: Durée estimée de la formation)
  23. */
  24. public function getTypicalLearningTime()
  25. {
  26. $tlt = $this->manifest->getElementsByTagName("typicalLearningTime");
  27. $tltNode = $tlt->item(0);
  28. $children = $tltNode->childNodes;
  29. $result = array();
  30. foreach ($children as $node) {
  31. switch ($node->nodeName) {
  32. case 'duration':
  33. case 'description': // IMPORTANT TODO: ce champ peut etre multilangue dans le manifest
  34. $result[$node->nodeName] = $node->nodeValue;
  35. break;
  36. }
  37. }
  38. return $result;
  39. }
  40. /**
  41. * table des matières d'une organization
  42. *
  43. * @param string $organizationId identifiant de l'organization
  44. * @return array
  45. */
  46. public function getTableOfContents(string $organizationId = null)
  47. {
  48. $result = array();
  49. $tlt = $this->manifest->getElementsByTagName("organizations");
  50. $organizations = $tlt->item(0);
  51. if ($organizationId == null) { // alors ce sera l'organization par defaut designée par le manifest
  52. $default = $organizations->attributes->getNamedItem("default")->nodeValue;
  53. } else {
  54. $default = $organizationId;
  55. }
  56. foreach ($organizations->childNodes as $organization) {
  57. if ($organization->nodeName == 'organization') { // on ne veut pas d'autre chose qu'une organization
  58. $identifier = $organization->attributes->getNamedItem("identifier")->nodeValue;
  59. if (($default == $identifier) || ('mm-' . $default == $identifier)) { // ici expliquer pourquoi il faut ajouter 'mm-'
  60. // on parcours les items de l'organization
  61. $result = $this->organization2array($organization->childNodes);
  62. }
  63. }
  64. }
  65. return $result;
  66. }
  67. /**
  68. * construit le tableau arborescence de l'organization.
  69. * @param DOMNodeList $itemList
  70. * @return array
  71. * * [itemId]
  72. * * [itemId][title]
  73. * * [itemId][isvisible]
  74. * * [itemId][ressourceRef]
  75. * * [itemId][itemId] array()
  76. */
  77. private function organization2array(\DOMNodeList $itemList, array $organization = null)
  78. {
  79. if ($organization == null) {
  80. $organization = array();
  81. }
  82. foreach ($itemList as $itemNode) {
  83. if ($itemNode->nodeName == 'title') {
  84. $organization['title'] = $itemNode->nodeValue;
  85. }
  86. if ($itemNode->nodeName == 'adlcp:prerequisites') {
  87. $organization['adlcp:prerequisites'] = $itemNode->nodeValue;
  88. }
  89. if ($itemNode->nodeName == 'item') { // et voila la partie récursive
  90. $itemId = $itemNode->attributes->getNamedItem("identifier")->nodeValue;
  91. $organization[$itemId] = array();
  92. foreach ($itemNode->attributes as $node) {
  93. switch ($node->nodeName) {
  94. case 'identifierref':
  95. case 'isvisible':
  96. default:
  97. $organization[$itemId][$node->nodeName] = $node->nodeValue;
  98. break;
  99. }
  100. }
  101. $organization[$itemId] = $this->organization2array($itemNode->childNodes, $organization[$itemId]);
  102. }
  103. }
  104. return $organization;
  105. }
  106. /**
  107. * organization par défaut dans un paquet
  108. *
  109. * @return string identifiant de l'organization par default
  110. */
  111. public function getDefaultOrganization(): string
  112. {
  113. $tlt = $this->manifest->getElementsByTagName("organizations");
  114. $organizations = $tlt->item(0);
  115. $default = $organizations->attributes->getNamedItem("default")->nodeValue;
  116. return $default;
  117. }
  118. /**
  119. * retrouve l'ancetre commun de 2 items. La racine absolu des items sera l'identifiant de l'organization
  120. *
  121. * @param string $item1 1er item descendant commun
  122. * @param string $item2 2eme item descendant commun
  123. * @return string ancetre commun ou null si pas trouvé
  124. */
  125. public function findCommonAncestor(string $item1, string $item2): ?string
  126. {
  127. // algo : on remonte un noeud-item jusqu'à la racine pour former une branche, puis on fait pareil pour le second
  128. // et on stoppe lorsque le noeud observé est egal à un noeud de la branche.
  129. // si pas de resultat ca veut dire qu'on n'observe des arbres-organization differente ou qu'il y a un pb dans la structure
  130. // de l'arbre (mauvais nom de noeud)
  131. $branche = array();
  132. //creation de la branche pour l'item1
  133. $currentNode = $this->getItemByIdentifier($item1);
  134. do {
  135. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  136. $branche[$current] = true;
  137. $currentNode = $currentNode->parentNode;
  138. } while (($currentNode->nodeName == 'item') || ($currentNode->nodeName == 'organization'));
  139. //remontee de la branche de l'item2 jusqu'à trouver
  140. $currentNode = $this->getItemByIdentifier($item2);
  141. $result = null; // initialisation du resultat à la valeur pas trouvée
  142. do {
  143. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  144. if (isset($branche[$current])) {
  145. $result = $current;
  146. }
  147. $branche[$current] = 1;
  148. $currentNode = $currentNode->parentNode;
  149. } while (($result == null) && (($currentNode->nodeName == 'item') || ($currentNode->nodeName == 'organization')));
  150. return $result;
  151. }
  152. /**
  153. * trouve la Node par son "identifier" dans l'ensemble des "item" et "organization"
  154. *
  155. * @param string $id
  156. * @return \DOMNode
  157. */
  158. private function getElementById(string $id): ?\DOMNode
  159. {
  160. return $this->xpath->query("//*[name()='item' or name()='organization'][@identifier='$id']")->item(0);
  161. }
  162. /**
  163. * chemin sous forme de tableau associatif (dont la clef est l'identifiant de l'item/organization
  164. * dont le 1er element est l'item de depart et le dernier est l'item d'arrivé
  165. *
  166. * @param string $itemStart
  167. * @param string $itemEnd
  168. * @param boolean $withItemStart
  169. * @param boolean $withItemEnd
  170. * @return array|null liste des items constituant le path (tableau dont la valeur est un identifiant d'item/organization)
  171. */
  172. public function getDirectPath(
  173. string $itemStart,
  174. string $itemEnd,
  175. bool $withItemStart = true,
  176. bool $withItemEnd = true
  177. ): ?array {
  178. // algo: on remonte les 2 branches jusqu'a la racine ou jusqu'a trouvé l'autre
  179. $findEnd = false; // indicateur du fait que l'on est trouvé la fin
  180. $branche = array();
  181. $currentNode = $this->getElementById($itemStart);
  182. do {
  183. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  184. if ((($withItemStart == true) || ($current != $itemStart)) && (($withItemEnd == true) || ($current != $itemEnd))) {
  185. $branche[] = $current;
  186. }
  187. if ($current == $itemEnd) {
  188. $findEnd = true;
  189. }
  190. $currentNode = $currentNode->parentNode;
  191. } while (($findEnd == false) && (($currentNode->nodeName == 'item') || ($currentNode->nodeName == 'organization')));
  192. if ($findEnd == true) {
  193. return $branche;
  194. }
  195. //sinon parcours avec demarrage par l'autre noeud
  196. $findStart = false; // indicateur du fait que l'on est trouvé la fin
  197. $branche = array();
  198. $currentNode = $this->getElementById($itemEnd);
  199. do {
  200. $current = $currentNode->attributes->getNamedItem("identifier")->nodeValue;
  201. if ((($withItemStart == true) || ($current != $itemStart)) && (($withItemEnd == true) || ($current != $itemEnd))) {
  202. $branche[] = $current;
  203. }
  204. if ($current == $itemStart) {
  205. $findStart = true;
  206. }
  207. $currentNode = $currentNode->parentNode;
  208. } while (($findStart == false) && (($currentNode->nodeName == 'item') || ($currentNode->nodeName == 'organization')));
  209. if ($findStart == false) {
  210. return null; // aucun resultat
  211. }
  212. $branche = array_reverse($branche);
  213. return $branche;
  214. }
  215. /**
  216. * renvoi l'item/organization parent
  217. *
  218. * @param string $item
  219. * @return string|null item parent (null si c'est le parent de l'organization qui est demandé)
  220. */
  221. public function getParent(string $item): ?string
  222. {
  223. $itemNode = $this->getElementById($item);
  224. if ($itemNode) {
  225. $parent = $itemNode->parentNode;
  226. if (($parent == null) || ($itemNode->nodeName == 'organization')) {
  227. return null;
  228. }
  229. return $parent->attributes->getNamedItem("identifier")->nodeValue;
  230. }
  231. return null;
  232. }
  233. /**
  234. * renvoi un tableau associatif dont les clefs sont les items
  235. * et qui correspond à un applatissage de l'arborescence
  236. *
  237. * @param string $item item/organization racine de l'arbre (ou sous arbre)
  238. * @return array itemroot =>0, itemfirstchild => 1, itemfirstchildofchild => 2, ...
  239. */
  240. public function getFlatTree(string $item): array
  241. {
  242. return $this->getFlatTreeRecursive($item); // cela pour bloquer toutes tentative de bricoler le 2eme parametre
  243. }
  244. private function getFlatTreeRecursive(string $item, array &$output = null): array
  245. {
  246. $itemNode = $this->getElementById($item);
  247. $current = $itemNode->attributes->getNamedItem("identifier")->nodeValue;
  248. if ($output == null) {
  249. $output = array();
  250. }
  251. $output[$current] = sizeof($output);
  252. $enfantsNodeList = $itemNode->childNodes;
  253. for ($i = 0; $i < $enfantsNodeList->count(); $i++) {
  254. $enfant = $enfantsNodeList->item($i);
  255. if ($enfant->nodeName == 'item') { // on fait bien attentino de ne traiter que les items...
  256. $enfantIdentifier = $enfant->attributes->getNamedItem("identifier")->nodeValue;
  257. $output = $this->getFlatTreeRecursive($enfantIdentifier, $output);
  258. }
  259. }
  260. return $output;
  261. }
  262. /**
  263. * tableau associatif des items enfant de l'item/organization passé en entrée
  264. * Les items sont stockés sous la forme de leur identifiant.
  265. *
  266. * @param string $item
  267. * @return array tableau des items enfants
  268. */
  269. public function getChildren(string $item): array
  270. {
  271. $children = array();
  272. $itemNode = $this->getElementById($item);
  273. $childNodeList = $itemNode->childNodes;
  274. for ($i = 0; $i < $childNodeList->length; $i++) {
  275. $childNode = $childNodeList->item($i);
  276. if ($childNode->nodeName == "item") {
  277. $child = $childNode->attributes->getNamedItem("identifier")->nodeValue;
  278. $children[] = $child;
  279. }
  280. }
  281. return $children;
  282. }
  283. /**
  284. * donne la valeur de l'attribut d'un tag à partir de son xpath
  285. * si le tag existe et si l'attribut n'existe pas c'est la valeur par défaut qui est retourné (fonctionnement
  286. * classique de xpath->query car schemaValidateSource a été lancée)
  287. * si l'attribut n'a pas de valeur par défaut : null
  288. * si le tag n'existe pas : null
  289. * attention les query sont complexifié car le namespace par défaut a été défini ( => pas possible de faire
  290. * query(//item) pour tout les tags du namespace de base; faire query(//*name="item))
  291. * Exemples :
  292. * ( '//*[name()="item" or name()="organization"][@identifier="T-01b"]/imsss:sequencing/imsss:controlMode' ,'flow' )
  293. *
  294. * @param string $item Par exemples (valeurs/defaut):
  295. * * imsss:deliveryControls.tracked
  296. * @param string $path chemin de l'attribut
  297. * @return string valeur de l'attribut sous format chaine de caracteres => un cast sera necessaire
  298. * pour les attributs numérique
  299. */
  300. protected function getAttributeByPath(string $path, string $attribute): ?string
  301. {
  302. $value = null; // valeur de l'attribut
  303. $nodeList = $this->xpath->query($path);
  304. if ($nodeList->length == 1) {
  305. $node = $nodeList->item(0);
  306. $node = $node->attributes->getNamedItem($attribute);
  307. if ($node != null) {
  308. $value = $node->nodeValue;
  309. }
  310. }
  311. return $value;
  312. }
  313. /**
  314. * renvoie la valeur d'un attribut d'un tag descendant d'une sequencing
  315. * si le tag n'est pas présent effectue quand meme une recherche
  316. * si le tag est présent renvoie la valeur de l'attribut ou sa valeur par défaut
  317. *
  318. * scrute l'attribut dans le tag sequencingCollection/sequencing référencé par item/sequencing.IDRef s'il existe.
  319. * c'est ce tag qui est renvoyé en priorité.
  320. *
  321. * Exemples de couples chemin attribut :
  322. * * 'imsss:objectives/imsss:primaryObjective', 'satisfiedByMeasure'
  323. * * 'imsss:rollupRules/imsss:rollupRule/imsss:rollupConditions/imsss:rollupCondition', 'condition'
  324. *
  325. * @param string $tagPath chemin du tag
  326. * @param string $attributeName nom de l'attribut
  327. * @param string $item pour lequel on examine le sequencing
  328. * @return string|null meme une valeur numérique sera une chaine (Ex. "true" (et pas (bool)1 ))
  329. */
  330. public function getSequencingAttribute(string $tagPath, string $attributeName, string $item = null): ?string
  331. {
  332. $value = null;
  333. if ($item != null) {
  334. // on tente de de récupérer la valeur dans sequencingCollection
  335. $itemNode = $this->getItemByIdentifier($item);
  336. $listItemNodes = $this->xpath->query('imsss:sequencing', $itemNode);
  337. if ($listItemNodes->length > 0) {
  338. $sequencingNode = $listItemNodes->item(0);
  339. $attributeNode = $sequencingNode->getAttributeNode('IDRef');
  340. if ($attributeNode != null) {
  341. $value = $this->getSequencingAttribute(
  342. 'imsss:sequencing[@ID="' . $attributeNode->nodeValue . '"]/' . $tagPath,
  343. $attributeName
  344. );
  345. if ($value != null) { //on tient la valeur
  346. return $value;
  347. }
  348. }
  349. }
  350. if ($value == null) { // dernière tentative pour renvoyer une valeur par défaut
  351. $value = SCORMTools::getAttributeDefaultValue($attributeName);
  352. }
  353. // sinon on récupère dans la sequence de l'item
  354. $query = '//*[name()="item" or name()="organization"][@identifier="' . $item . '"]/imsss:sequencing/' . $tagPath;
  355. } else {
  356. $query = '//imsss:sequencingCollection/' . $tagPath;
  357. $nodeList = $this->xpath->query($query);
  358. }
  359. $nodeList = $this->xpath->query($query);
  360. if ($nodeList->length == 1) {
  361. $node = $nodeList->item(0);
  362. $attributeNode = $node->attributes->getNamedItem($attributeName);
  363. if ($attributeNode != null) {
  364. $value = $attributeNode->nodeValue;
  365. }
  366. }
  367. return $value;
  368. }
  369. /**
  370. * renvoi le contenu d'un element (tag) d'un item ou la valeur d'un attribut
  371. * de l'élement si le nom de l'attribut est spécifié
  372. *
  373. * Exemple:
  374. * <organization>
  375. * <item identifier="ITEM3" identifierref="RESOURCE3" isvisible="true">
  376. * <title>Content 1</title>
  377. * <adlcp:completionThreshold completedByMeasure = “true” minProgressMeasure = “0.75” />
  378. * </item>
  379. * </organization>
  380. *
  381. * getItemElementContent('ITEM3','adlcp:completionThreshold','completedByMeasure') renvoi true
  382. * getItemElementContent('ITEM3','*[name()="title"]') renvoi 'Content 1'
  383. *
  384. * Autre exemple:
  385. * <metadata>
  386. * <adlcp:location>activities/activity1MD.xml</adlcp:location>
  387. * </metadata>
  388. *
  389. * getItemElementContent('ITEM???','*[name()="metadata"]/adlcp:location') renvoi 'activities/activity1MD.xml'
  390. *
  391. * @param string $item
  392. * @param string $path
  393. * @param string $attributeName
  394. * @return string|null
  395. */
  396. public function getItemElementContent(string $item, string $path, string $attributeName = null): ?string
  397. {
  398. $content = null;
  399. $query = '//*[name()="item" or name()="organization"][@identifier="' . $item . '"]/' . $path;
  400. $nodeList = $this->xpath->query($query);
  401. if ($nodeList->length == 1) {
  402. $node = $nodeList->item(0);
  403. if ($attributeName == null) {
  404. $content = $node->nodeValue;
  405. } else {
  406. $content = $node->hasAttribute($attributeName) ? $node->getAttribute($attributeName) : SCORMTools::getAttributeDefaultValue($attributeName);
  407. }
  408. } elseif ($nodeList->length > 1) {
  409. throw new \Exception("Element $path should have only 1 (or none) value, not " . $nodeList->length);
  410. } else {
  411. if ($attributeName != null) {
  412. $content = SCORMTools::getAttributeDefaultValue($attributeName);
  413. }
  414. }
  415. return $content;
  416. }
  417. public function getDataMap(string $item): ?array
  418. {
  419. $maps = array();
  420. $query = '//*[name()="item" or name()="organization"][@identifier="' . $item . '"]/adlcp:data/adlcp:map';
  421. $nodeList = $this->xpath->query($query);
  422. for ($i = 0; $i < $nodeList->length; $i++) {
  423. $node = $nodeList->item($i);
  424. $targetID = $node->attributes->getNamedItem('targetID')->nodeValue;
  425. $maps[] = array();
  426. $maps[$i]['targetID'] = $targetID;
  427. $maps[$i]['readSharedData'] = $node->attributes->getNamedItem('readSharedData')->nodeValue ??
  428. SCORMTools::getAttributeDefaultValue('readSharedData');
  429. $maps[$i]['writeSharedData'] = $node->attributes->getNamedItem('writeSharedData')->nodeValue ??
  430. SCORMTools::getAttributeDefaultValue('writeSharedData');
  431. }
  432. return $maps;
  433. }
  434. /**
  435. * renvoie les regles de rollup sous forme d'un tableau
  436. * Exemple :
  437. * 0 => [
  438. * 'childActivitySet' => 'all',
  439. * 'minimumCount' => '0',
  440. * 'minimumPercent' => '0',
  441. * 'action' => 'satisfied',
  442. * 'conditionCombination' => 'any',
  443. * 'conditions' => [
  444. * 0 => [
  445. * 'operator' => 'noOp',
  446. * 'condition' => 'completed'
  447. * ]
  448. * ]
  449. * ] ]
  450. * @param string $item
  451. * @return array
  452. */
  453. public function getRollupRules(string $item): array
  454. {
  455. $rules = array();
  456. // on commence par recuperer (si elles existent) les valeurs sur le sequencing collection
  457. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  458. if ($refSeqCol != null) {
  459. $query = 'imsss:sequencingCollection/imsss:sequencing[@ID="' . $refSeqCol . '"]/imsss:rollupRules/descendant::*';
  460. $listNodes = $this->xpath->query($query);
  461. $this->rollupNodesToArray($listNodes, $rules);
  462. }
  463. // recuperation des rules de la sequence de l'item
  464. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  465. $item . '"]/imsss:sequencing/imsss:rollupRules/descendant::*';
  466. $listNodes = $this->xpath->query($query);
  467. $this->rollupNodesToArray($listNodes, $rules);
  468. return $rules;
  469. }
  470. private function rollupNodesToArray(\DOMNodeList $listNodes, array &$rules): void
  471. {
  472. $indice = -1; // indices du plus haut niveau du tableau de rule...
  473. // dans cette boucle on joue sur le fait que les tag sont ordonnés hiérarcchiquement (mere avant fille)
  474. for ($i = 0; $i < $listNodes->length; $i++) {
  475. $node = $listNodes->item($i);
  476. switch ($node->nodeName) {
  477. case 'imsss:rollupRule':
  478. $indice++;
  479. $rules[$indice] = array();
  480. // si pas de valeur trouvée, il faut donner la valeur par défaut
  481. $rules[$indice]['childActivitySet'] = $node->hasAttribute('childActivitySet') ? $node->getAttribute('childActivitySet') : SCORMTools::getAttributeDefaultValue('childActivitySet');
  482. $rules[$indice]['minimumCount'] = $node->hasAttribute('minimumCount') ? $node->getAttribute('minimumCount') : SCORMTools::getAttributeDefaultValue('minimumCount');
  483. $rules[$indice]['minimumPercent'] = $node->hasAttribute('minimumPercent') ? $node->getAttribute('minimumPercent') : SCORMTools::getAttributeDefaultValue('minimumPercent');
  484. break;
  485. case 'imsss:rollupConditions':
  486. $rules[$indice]['conditionCombination'] = $node->hasAttribute('conditionCombination') ? $node->getAttribute('conditionCombination') : SCORMTools::getAttributeDefaultValue('imsss:rollupConditions@conditionCombination');
  487. break;
  488. case 'imsss:rollupCondition':
  489. $theAttributes = array();
  490. // il faut initialiser certains attributs avec la valeur par défaut
  491. $theAttributes['operator'] = SCORMTools::getAttributeDefaultValue('imsss:rollupCondition@operator');
  492. for ($j = 0; $j < $node->attributes->length; $j++) {
  493. $nodeAttribute = $node->attributes->item($j);
  494. $theAttributes[$nodeAttribute->nodeName] = $nodeAttribute->nodeValue;
  495. }
  496. $rules[$indice]['conditions'][] = $theAttributes;
  497. break;
  498. case 'imsss:rollupAction':
  499. $rules[$indice]['action'] = $node->getAttribute('action');
  500. break;
  501. }
  502. }
  503. }
  504. /**
  505. * renvoi les règles de sequencage sous forme d'un tableau
  506. *
  507. * Exemple de retour:
  508. *
  509. * 0 => [
  510. * 'conditionCombination' => 'any',
  511. * 'conditions' => [
  512. * 0 => [
  513. * 'operator' => 'not',
  514. * 'referencedObjective' => 'previous_sco_satisfied',
  515. * 'condition' => 'satisfied'
  516. * ],
  517. * 1 => [
  518. * 'operator' => 'not',
  519. * 'referencedObjective' => 'previous_sco_satisfied',
  520. * 'condition' => 'objectiveStatusKnown'
  521. * ]
  522. * ],
  523. * 'action' => 'disabled'
  524. * ]
  525. * @param string $item
  526. * @return array
  527. */
  528. public function getSequencingRules(string $item): array
  529. {
  530. $rules1 = array();
  531. $rules2 = array();
  532. $defaultObjectif = $this->getPrimaryObjective($item)['objective'] ?? null;
  533. // on commence par recuperer (si elles existent) les valeurs sur le sequencing collection
  534. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  535. if ($refSeqCol != null) {
  536. $query = 'imsss:sequencingCollection/imsss:sequencing[@ID="' . $refSeqCol . '"]/imsss:sequencingRules/descendant::*';
  537. $listNodes = $this->xpath->query($query);
  538. $this->nodesToArray($listNodes, $rules1, $defaultObjectif);
  539. }
  540. // recuperation des rules de la sequence de l'item
  541. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  542. $item . '"]/imsss:sequencing/imsss:sequencingRules/descendant::*';
  543. $listNodes = $this->xpath->query($query);
  544. $this->nodesToArray($listNodes, $rules2, $defaultObjectif);
  545. $rules = array_merge($rules2, $rules1);
  546. return $rules;
  547. }
  548. /**
  549. * transforme une liste de node ordonnées en tableau associatif
  550. * ayant le format commode pour le traitement
  551. *
  552. * node ordonnée: dans le sens de lecture de découverte des nodes dans le manifest.
  553. * l'algo simmplicisme (parcours d'une boucle) est bati sur ce principe.
  554. * (voir exemple de format dans la fonction 'public' appelante)
  555. *
  556. * @param \DOMNodeList $listNodes
  557. * @param array $rules
  558. * @param string $defaultObjectif (default : null) si presente ajoute la clef referencedObjective avec cette valeur
  559. * @return void
  560. */
  561. private function nodesToArray(\DOMNodeList $listNodes, array &$rules, string $defaultObjectif = null): void
  562. {
  563. $indice = -1; // indices du plus haut niveau du tableau de rule...
  564. // dans cette boucle on joue sur le fait que les tag sont ordonnés hiérarcchiquement (mere avant fille)
  565. for ($i = 0; $i < $listNodes->length; $i++) {
  566. $node = $listNodes->item($i);
  567. switch ($node->nodeName) {
  568. case 'imsss:preConditionRule':
  569. case 'imsss:exitConditionRule':
  570. case 'imsss:postConditionRule':
  571. $indice++;
  572. $rules[$indice] = array();
  573. break;
  574. case 'imsss:ruleCondition':
  575. $theAttributes = array();
  576. // il faut initialiser certains attributs avec la valeur par défaut
  577. $theAttributes['operator'] = SCORMTools::getAttributeDefaultValue('imsss:ruleCondition@operator');
  578. for ($j = 0; $j < $node->attributes->length; $j++) {
  579. $nodeAttribute = $node->attributes->item($j);
  580. $theAttributes[$nodeAttribute->nodeName] = $nodeAttribute->nodeValue;
  581. }
  582. if (!isset($theAttributes['referencedObjective']) && ($defaultObjectif != null)) { // il doit toujours y avoir un objectif associé, si absent alors...
  583. $theAttributes['referencedObjective'] = $defaultObjectif;
  584. }
  585. $rules[$indice]['conditions'][] = $theAttributes;
  586. break;
  587. case 'imsss:ruleConditions':
  588. $rules[$indice]['conditionCombination'] = $node->hasAttribute('conditionCombination') ? $node->getAttribute('conditionCombination') : SCORMTools::getAttributeDefaultValue('imsss:ruleConditions@conditionCombination');
  589. break;
  590. case 'imsss:ruleAction':
  591. $rules[$indice]['action'] = $node->getAttribute('action');
  592. break;
  593. }
  594. }
  595. }
  596. /**
  597. * renvoie la valeur d'un attribut. Appel getAttributesByName
  598. * et renvoie le seul élement trouvé, s'il y en a plusieurs renvoi null.
  599. *
  600. * @param string $item
  601. * @param string $attributeName
  602. * @return string|null
  603. */
  604. public function getAttributeByName(string $item, string $attributeName): ?string
  605. {
  606. $value = null;
  607. $values = $this->getAttributesByName($item, $attributeName);
  608. if (sizeof($values) == 1) {
  609. $value = $values[0];
  610. }
  611. return $value;
  612. }
  613. /**
  614. * renvoie les valeurs des attributs d'un meme nom.
  615. * scrute tous les attributs de tous les tags descendants de l'item courrant
  616. * y compris l'item courrant SAUF les items descendants.
  617. * ATTENTION: ne suit pas les references
  618. *
  619. * @param string $item nom de l'item racine
  620. * @param string $attributeName nom de l'attribut
  621. * @return array tableau de string contenant les valeurs des attributs dans l'ordre de découverte
  622. */
  623. public function getAttributesByName(string $item, string $attributeName): array
  624. {
  625. $values = array();
  626. $itemNode = $this->getItemByIdentifier($item); // la recherche sse fait au niveau de l'item
  627. // on recherche la presence de l'attribut dans tous les tags descendant sauf les items enfants
  628. $query = 'descendant::*[name() != "item" and name() != "organization" and @' . $attributeName . '] | self::*[@' . $attributeName . ']';
  629. $nodeList = $this->xpath->query($query, $itemNode);
  630. for ($i = 0; $i < $nodeList->length; $i++) {
  631. $node = $nodeList->item($i);
  632. $value = $node->getAttribute($attributeName);
  633. $values[] = $value;
  634. }
  635. if (empty($values)) {
  636. $values[] = SCORMTools::getAttributeDefaultValue($attributeName);
  637. }
  638. return $values;
  639. }
  640. /**
  641. * renvoi la liste des objectif (objectiveID) d'un item
  642. * la forme d'un objective est le triplet (identifier (item),objectiveID, isPrimary)
  643. *
  644. * @param string $item
  645. * @param string $objectiveID (facultatif) l'identifiant d'un objectif
  646. * @return array de tableaux d'"objective"s
  647. */
  648. public function getObjectives(string $item, string $objectiveID = null): array
  649. {
  650. $ids = array();
  651. $equalObjective = '';
  652. if ($objectiveID != null) {
  653. $equalObjective = "[@objectiveID='$objectiveID']";
  654. }
  655. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  656. $query2 = '';
  657. if ($refSeqCol != null) {
  658. $query2 = " | //imsss:sequencingCollection/imsss:sequencing[@ID='$refSeqCol']/imsss:objectives/child::*$equalObjective";
  659. }
  660. // recuperation des rules de la sequence de l'item
  661. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  662. $item . "\"]/imsss:sequencing/imsss:objectives/child::*$equalObjective" . $query2;
  663. $listNodes = $this->xpath->query($query);
  664. for ($i = 0; $i < $listNodes->length; $i++) {
  665. $minNormalizedMeasure = $this->xpath->query('imsss:minNormalizedMeasure', $listNodes->item($i))->item(0)->nodeValue ??
  666. SCORMTools::getAttributeDefaultValue('minNormalizedMeasure');
  667. $ids[] = $this->constructObjective(
  668. $item,
  669. $listNodes->item($i)->getAttribute('objectiveID'),
  670. $listNodes->item($i)->nodeName,
  671. $minNormalizedMeasure
  672. );
  673. }
  674. return $ids;
  675. }
  676. /**
  677. * recupere le triplet definnissant un PrimaryObjective l'attribut imsss:primaryObjective
  678. * la forme d'un objective est le triplet (identifier (item),objectiveID, isPrimary)
  679. * @param string $item
  680. * @return array|null
  681. */
  682. public function getPrimaryObjective(string $item): ?array
  683. {
  684. $o = null;
  685. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  686. $item . '"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective';
  687. $listNodes = $this->xpath->query($query);
  688. if ($listNodes->length == 0) { // tentative de recherche l'objectif primaire dans la sequencingcollection
  689. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  690. if ($refSeqCol != null) {
  691. $query = '//imsss:sequencingCollection/imsss:sequencing[@ID="' . $refSeqCol .
  692. '"]/imsss:objectives/descendant::*[@objectiveID]';
  693. $listNodes = $this->xpath->query($query);
  694. }
  695. }
  696. if ($listNodes->length == 1) {
  697. $minNormalizedMeasure = $this->xpath->query('imsss:minNormalizedMeasure', $listNodes->item(0))->item(0)->nodeValue ??
  698. SCORMTools::getAttributeDefaultValue('minNormalizedMeasure');
  699. $o = $this->constructObjective(
  700. $item,
  701. $listNodes->item(0)->getAttribute('objectiveID'),
  702. $listNodes->item(0)->nodeName,
  703. $minNormalizedMeasure
  704. );
  705. }
  706. return $o;
  707. }
  708. /**
  709. * un bon carcan pour construire à l'identique le tableau (on evite la Class spécifique)
  710. *
  711. * @param string $item
  712. * @param string $objectiveID
  713. * @param string $objectiveTagName
  714. * @return void
  715. */
  716. private function constructObjective(string $item, string $objectiveID, string $objectiveTagName, string $minNormalizedMeasure)
  717. {
  718. return array(
  719. 'item' => $item,
  720. 'objective' => $objectiveID,
  721. 'isPrimary' => ($objectiveTagName == 'imsss:primaryObjective') ? true : false,
  722. 'minNormalizedMeasure' => $minNormalizedMeasure
  723. );
  724. }
  725. /**
  726. * recupere la valeur du tag minNormalizedMeasure
  727. *
  728. * @param string $item
  729. * @param string $objectiveID identifiant de l'ojectif contenant le tag
  730. * @return string|null
  731. */
  732. public function getMinNormalizeMeasure(string $item, string $objectiveID): ?string
  733. {
  734. // recupere l'objectif
  735. $objectives = $this->getObjectives($item, $objectiveID); // il ne peut y avoir qu'un seul objectif
  736. if (sizeof($objectives) != 1) {
  737. return null;
  738. }
  739. return $objectives[0]['minNormalizedMeasure'];
  740. }
  741. /**
  742. * En preambule:
  743. * tous les objectives tag : "objective" porteurs
  744. * d'une mapInfo ont obligatoirement un objectiveID associé (CAM5-26 )
  745. * les tags "primaryObjective" n'ont pas pas forcement un objectiveID
  746. * Un ims.mapinfo peut avoir un complement adl (lien via attribut targetObjectiveID)
  747. * un adl.mapinfo peut ne pas etre lié à un ims.mapinfo, dans ce cas c'est un mapinfo en soi (et
  748. * non un complement)
  749. *
  750. * @param string $item
  751. * @param string $objectiveID peut etre chaine vide si et seulement $isPrimary = true
  752. * @param boolean $isPrimary
  753. * @return array|null
  754. */
  755. public function getMapInfos(string $item, string $objectiveID, bool $isPrimary): ?array
  756. {
  757. $mapInfos = array(); // initialisation du tableau de retour
  758. if (($objectiveID == '') && ($isPrimary == false)) {
  759. throw new \Exception('SCORM 2004: objectiveID cannot be void for a non primary mapInfo tag. Item :' . $item);
  760. }
  761. $mapInfo = $this->initNullMapinfo();
  762. $targetObjectiveIDs = array(); // tableau local pour mémoriser ces IDs, utile ppour l'algo
  763. // 1) récupération des nodes mapinfo ims
  764. if ($isPrimary == false) {
  765. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  766. $item . '"]/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="' . $objectiveID . '"]/imsss:mapInfo | ' .
  767. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="' . $objectiveID . '"]/imsss:mapInfo';
  768. } else { // quand c'est un primaire, il n'y a pas d'objectiveID
  769. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  770. $query2 = '';
  771. if ($refSeqCol != null) {
  772. $query2 = " | //imsss:sequencingCollection/imsss:sequencing[@ID='$refSeqCol']/imsss:objectives/imsss:primaryObjective/imsss:mapInfo";
  773. }
  774. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  775. $item . '"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective/imsss:mapInfo' . $query2;
  776. }
  777. $imsNodes = $this->xpath->query($query); // recupération des nodes map infos
  778. // 2) recuperation des complements de l'adl
  779. for ($i = 0; $i < $imsNodes->length; $i++) {
  780. $mapInfo = $this->initNullMapinfo();
  781. $attributes = $imsNodes->item($i)->attributes;
  782. for ($j = 0; $j < $attributes->length; $j++) {
  783. $a = $attributes->item($j);
  784. $mapInfo[$a->nodeName] = $a->nodeValue;
  785. }
  786. // recuperation complement
  787. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  788. $item . '"]/imsss:sequencing/adlseq:objectives/child::*/adlseq:mapInfo[@targetObjectiveID="' . $mapInfo['targetObjectiveID'] . '"] | ' .
  789. '//imsss:sequencingCollection/adlseq:objectives/child::*/adlseq:mapInfo[@targetObjectiveID="' . $mapInfo['targetObjectiveID'] . '"]';
  790. $adlNodes = $this->xpath->query($query); // il ne devrait y avoir qu'un seul complement max
  791. if ($adlNodes->length > 1) { // on ne sait pas gérer le cas de plusieurs mapinfo pour 1 objectif
  792. throw new \Exception('SCORM 2004: Several complement mapInfos adlseq tags found for item ' . $item);
  793. }
  794. if ($adlNodes->length == 1) {
  795. $attributes = $adlNodes->item(0)->attributes;
  796. for ($j = 0; $j < $attributes->length; $j++) {
  797. $a = $attributes->item($j);
  798. $mapInfo[$a->nodeName] = $a->nodeValue;
  799. }
  800. }
  801. $mapInfo = $this->completeMapinfoWithDefaults($mapInfo);
  802. $mapInfos[] = $mapInfo;
  803. $targetObjectiveIDs[$mapInfo['targetObjectiveID']] = true; // on cree une entrée dans le tableau
  804. }
  805. //3) recupération des mapinfo de l'adl qui sont isolés (pas de lien via targetObjectiveID)
  806. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  807. $item . '"]/imsss:sequencing/adlseq:objectives/*[@objectiveID="' . $objectiveID . '"]/adlseq:mapInfo | ' .
  808. '//imsss:sequencingCollection/imsss:sequencing/adlseq:objectives/*[@objectiveID="' . $objectiveID . '"]/adlseq:mapInfo';
  809. $adlNodes = $this->xpath->query($query);
  810. // on parcours la liste en creant un nouveau mapinfo lorsque targetObjectiveID n'est pas une clef
  811. for ($i = 0; $i < $adlNodes->length; $i++) {
  812. $attributes = $adlNodes->item($i)->attributes;
  813. $targetObjectiveID = $attributes->getNamedItem('targetObjectiveID')->nodeValue;
  814. if (!isset($targetObjectiveIDs[$targetObjectiveID])) { // creation d'un nouveau mapinfo, si iniexsitance
  815. $mapInfo = $this->initNullMapinfo();
  816. for ($j = 0; $j < $attributes->length; $j++) {
  817. $a = $attributes->item($j);
  818. $mapInfo[$a->nodeName] = $a->nodeValue;
  819. }
  820. $mapInfo = $this->completeMapinfoWithDefaults($mapInfo);
  821. $mapInfos[] = $mapInfo;
  822. }
  823. }
  824. return $mapInfos;
  825. }
  826. /**
  827. * on remplit un tableau mapinfo avec les valeur par défaut
  828. * (attention : les clefs non spécifiées ne sont pas gérées)
  829. *
  830. * @param array $mapInfo
  831. * @return array
  832. */
  833. private function completeMapinfoWithDefaults(array $mapInfo): array
  834. {
  835. foreach ($mapInfo as $key => $value) {
  836. if ($value == null) {
  837. $mapInfo[$key] = SCORMTools::getAttributeDefaultValue($key);
  838. }
  839. }
  840. return $mapInfo;
  841. }
  842. /**
  843. * creation d'un tableau ayant toutes les clef nommées pour un mapinfo
  844. * avec des valeurs null
  845. *
  846. * @return array
  847. */
  848. private function initNullMapinfo(): array
  849. {
  850. return array( // tous les attributs que nous souhaitons retourner obligatoirement...
  851. 'targetObjectiveID' => null, // ims
  852. 'readSatisfiedStatus' => null,
  853. 'readNormalizedMeasure' => null,
  854. 'writeSatisfiedStatus' => null,
  855. 'writeNormalizedMeasure' => null,
  856. 'readRawScore' => null, //adl
  857. 'readMinScore' => null,
  858. 'readMaxScore' => null,
  859. 'readCompletionStatus' => null,
  860. 'readProgressMeasure' => null,
  861. 'writeRawScore' => null,
  862. 'writeMinScore' => null,
  863. 'writeMaxScore' => null,
  864. 'writeCompletionStatus' => null,
  865. 'writeProgressMeasure' => null
  866. );
  867. }
  868. /**
  869. * renvoie l'attribut satisfiedByMeasure accroché à un "(primary)Objective"
  870. *
  871. * @param string $item
  872. * @param string $objectiveID
  873. * @param boolean $isPrimary
  874. * @return string
  875. */
  876. public function getSatisfiedByMeasure(string $item, string $objectiveID, bool $isPrimary): string
  877. {
  878. $value = null;
  879. if ($isPrimary == false) {
  880. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  881. $item . '"]/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="' . $objectiveID . '"] | ' .
  882. '//imsss:sequencingCollection/imsss:sequencing/imsss:objectives/imsss:objective[@objectiveID="' . $objectiveID . '"]';
  883. } else { // en cas de primary on ne s'occupe pas de la valeur de l'objectiveID (qui pourrait etre "")
  884. $refSeqCol = $this->getAttributeByName($item, 'IDRef');
  885. $query2 = '';
  886. if ($refSeqCol != null) {
  887. $query2 = ' | imsss:sequencingCollection/imsss:sequencing[@ID="' . $refSeqCol . '"]/imsss:objectives/imsss:primaryObjective';
  888. }
  889. $query = '//*[name()="item" or name()="organization"][@identifier="' .
  890. $item . '"]/imsss:sequencing/imsss:objectives/imsss:primaryObjective' . $query2;
  891. }
  892. $listNodes = $this->xpath->query($query);
  893. if ($listNodes->length == 1) {
  894. $value = $listNodes->item(0)->getAttribute('satisfiedByMeasure');
  895. }
  896. $value = $value ?? SCORMTools::getAttributeDefaultValue('satisfiedByMeasure');
  897. return $value;
  898. }
  899. /**
  900. * attribut de la balise organization 'adlseq:objectivesGlobalToSystem' (valeur par défaut : true)
  901. *
  902. * @param string $identifier identifient eventuelle de l'organization (si absent organisation par defaut)
  903. * @return string
  904. */
  905. public function getIsGlobalToSystem(string $identifier = null): string
  906. {
  907. $value = null;
  908. if ($identifier != null) {
  909. $query = '//*[name()="organization"][@identifier="' . $identifier . '"]';
  910. } else {
  911. $query = '//*[name()="organization"][1]'; //selection du &er element
  912. }
  913. $listNodes = $this->xpath->query($query);
  914. if ($listNodes->length == 1) {
  915. $value = $listNodes->item(0)->getAttribute('adlseq:objectivesGlobalToSystem');
  916. }
  917. $value = $value ?? SCORMTools::getAttributeDefaultValue('adlseq:objectivesGlobalToSystem');
  918. return $value;
  919. }
  920. /**
  921. * Retourne le standard du paquet associé au DOM
  922. * Méthode surchargée
  923. *
  924. * @return string
  925. */
  926. public function getStandard()
  927. {
  928. return self::SCORM_2004;
  929. }
  930. /**
  931. * recupere la reference associé à un item
  932. *
  933. * @param string $item
  934. * @return string
  935. */
  936. public function getHref(string $item): string
  937. {
  938. $idhref = $this->getAttributeByName($item, 'identifierref');
  939. // y a t'il une base (cad un repertoire avant la reference)
  940. $href = $this->getAttributeByPath("//*[name()='resource'][@identifier='$idhref']", 'base');
  941. if ($href == null) {
  942. $href = "";
  943. }
  944. $href .= $this->getAttributeByPath("//*[name()='resource'][@identifier='$idhref']", 'href');
  945. $parameters = $this->getAttributeByName($item, 'parameters');
  946. // pour ajouter les parametres, verifions que...
  947. if ($parameters != null) { // ... tout d'abord qu'il y a bien des parametres à ajouter
  948. // on detecte le '?' qui caractérise la présence des paramètres
  949. if (strpos($href, '?') !== false) {
  950. $parameters = str_replace('?', '&', $parameters);
  951. }
  952. $href .= $parameters;
  953. }
  954. return $href;
  955. }
  956. /**
  957. * le titre d'un item (ou d'une organization)
  958. *
  959. * @param string $item
  960. * @return string le titre
  961. */
  962. public function getTitle(string $item = null): string
  963. {
  964. if ($item == null) {
  965. return parent::getTitle();
  966. }
  967. $query = '//*[name()="item" or name()="organization"][@identifier="' . $item . '"]/*[name()="title"]';
  968. $listNodes = $this->xpath->query($query);
  969. $result = "";
  970. if ($listNodes->length == 1) {
  971. $result = $listNodes->item(0)->textContent;
  972. }
  973. return $result;
  974. }
  975. /**
  976. * tableau des valeurs HideLMSUI d'un item
  977. *
  978. * @param string $item
  979. * @return array
  980. */
  981. public function getHideLMSUI(string $item): array
  982. {
  983. $hideLMSUI = array();
  984. $query = '//*[name()="item" or name()="organization"][@identifier="' . $item . '"]/adlnav:presentation/adlnav:navigationInterface/adlnav:hideLMSUI';
  985. $listNodes = $this->xpath->query($query);
  986. for ($i = 0; $i < $listNodes->length; $i++) {
  987. $hideLMSUI[] = $listNodes->item($i)->textContent;
  988. }
  989. return $hideLMSUI;
  990. }
  991. }