exercise_import.inc.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
  4. use Symfony\Component\DomCrawler\Crawler;
  5. /**
  6. * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
  7. *
  8. * @package chamilo.exercise
  9. *
  10. * @author claro team <cvs@claroline.net>
  11. * @author Guillaume Lederer <guillaume@claroline.net>
  12. * @author Yannick Warnier <yannick.warnier@beeznest.com>
  13. */
  14. /**
  15. * Unzip the exercise in the temp folder.
  16. *
  17. * @param string $baseWorkDir The path of the temporary directory where the exercise was uploaded and unzipped
  18. * @param string $uploadPath
  19. *
  20. * @return bool
  21. */
  22. function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
  23. {
  24. $_course = api_get_course_info();
  25. $_user = api_get_user_info();
  26. //Check if the file is valid (not to big and exists)
  27. if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
  28. // upload failed
  29. return false;
  30. }
  31. if (preg_match('/.zip$/i', $_FILES['userFile']['name'])) {
  32. $result = handle_uploaded_document(
  33. $_course,
  34. $_FILES['userFile'],
  35. $baseWorkDir,
  36. $uploadPath,
  37. $_user['user_id'],
  38. 0,
  39. null,
  40. 1,
  41. null,
  42. null,
  43. true,
  44. null,
  45. null,
  46. false
  47. );
  48. return $result;
  49. }
  50. return false;
  51. }
  52. /**
  53. * Imports an exercise in QTI format if the XML structure can be found in it.
  54. *
  55. * @param array $file
  56. *
  57. * @return string|array as a backlog of what was really imported, and error or debug messages to display
  58. */
  59. function import_exercise($file)
  60. {
  61. global $exerciseInfo;
  62. global $resourcesLinks;
  63. $baseWorkDir = api_get_path(SYS_ARCHIVE_PATH).'qti2/';
  64. if (!is_dir($baseWorkDir)) {
  65. mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
  66. }
  67. $uploadPath = api_get_unique_id().'/';
  68. if (!is_dir($baseWorkDir.$uploadPath)) {
  69. mkdir($baseWorkDir.$uploadPath, api_get_permissions_for_new_directories(), true);
  70. }
  71. // set some default values for the new exercise
  72. $exerciseInfo = [];
  73. $exerciseInfo['name'] = preg_replace('/.zip$/i', '', $file);
  74. $exerciseInfo['question'] = [];
  75. // if file is not a .zip, then we cancel all
  76. if (!preg_match('/.zip$/i', $file)) {
  77. return 'UplZipCorrupt';
  78. }
  79. // unzip the uploaded file in a tmp directory
  80. if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
  81. return 'UplZipCorrupt';
  82. }
  83. $baseWorkDir = $baseWorkDir.$uploadPath;
  84. // find the different manifests for each question and parse them.
  85. $exerciseHandle = opendir($baseWorkDir);
  86. $fileFound = false;
  87. $result = false;
  88. $filePath = null;
  89. $resourcesLinks = [];
  90. // parse every subdirectory to search xml question files and other assets to be imported
  91. // The assets-related code is a bit fragile as it has to deal with files renamed by Chamilo and it only works if
  92. // the imsmanifest.xml file is read.
  93. while (false !== ($file = readdir($exerciseHandle))) {
  94. if (is_dir($baseWorkDir.'/'.$file) && $file != "." && $file != "..") {
  95. // Find each manifest for each question repository found
  96. $questionHandle = opendir($baseWorkDir.'/'.$file);
  97. // Only analyse one level of subdirectory - no recursivity here
  98. while (false !== ($questionFile = readdir($questionHandle))) {
  99. if (preg_match('/.xml$/i', $questionFile)) {
  100. $isQti = isQtiQuestionBank($baseWorkDir.'/'.$file.'/'.$questionFile);
  101. if ($isQti) {
  102. $result = qti_parse_file($baseWorkDir, $file, $questionFile);
  103. $filePath = $baseWorkDir.$file;
  104. $fileFound = true;
  105. } else {
  106. $isManifest = isQtiManifest($baseWorkDir.'/'.$file.'/'.$questionFile);
  107. if ($isManifest) {
  108. $resourcesLinks = qtiProcessManifest($baseWorkDir.'/'.$file.'/'.$questionFile);
  109. }
  110. }
  111. }
  112. }
  113. } elseif (preg_match('/.xml$/i', $file)) {
  114. $isQti = isQtiQuestionBank($baseWorkDir.'/'.$file);
  115. if ($isQti) {
  116. $result = qti_parse_file($baseWorkDir, '', $file);
  117. $filePath = $baseWorkDir.'/'.$file;
  118. $fileFound = true;
  119. } else {
  120. $isManifest = isQtiManifest($baseWorkDir.'/'.$file);
  121. if ($isManifest) {
  122. $resourcesLinks = qtiProcessManifest($baseWorkDir.'/'.$file);
  123. }
  124. }
  125. }
  126. }
  127. if (!$fileFound) {
  128. return 'NoXMLFileFoundInTheZip';
  129. }
  130. if ($result == false) {
  131. return false;
  132. }
  133. // 1. Create exercise.
  134. $exercise = new Exercise();
  135. $exercise->exercise = $exerciseInfo['name'];
  136. // Random QTI support
  137. if (isset($exerciseInfo['order_type'])) {
  138. if ($exerciseInfo['order_type'] == 'Random') {
  139. $exercise->setQuestionSelectionType(2);
  140. $exercise->random = -1;
  141. }
  142. }
  143. if (!empty($exerciseInfo['description'])) {
  144. $exercise->updateDescription(formatText(strip_tags($exerciseInfo['description'])));
  145. }
  146. $exercise->save();
  147. $last_exercise_id = $exercise->selectId();
  148. $courseId = api_get_course_int_id();
  149. if (!empty($last_exercise_id)) {
  150. // For each question found...
  151. foreach ($exerciseInfo['question'] as $question_array) {
  152. if (!in_array($question_array['type'], [UNIQUE_ANSWER, MULTIPLE_ANSWER, FREE_ANSWER])) {
  153. continue;
  154. }
  155. //2. Create question
  156. $question = new Ims2Question();
  157. $question->type = $question_array['type'];
  158. if (empty($question->type)) {
  159. // If the type was not provided, assume this is a multiple choice, unique answer type (the most basic)
  160. $question->type = MCUA;
  161. }
  162. $question->setAnswer();
  163. $description = '';
  164. $question->updateTitle(formatText(strip_tags($question_array['title'])));
  165. if (isset($question_array['category'])) {
  166. $category = formatText(strip_tags($question_array['category']));
  167. if (!empty($category)) {
  168. $categoryId = TestCategory::get_category_id_for_title(
  169. $category,
  170. $courseId
  171. );
  172. if (empty($categoryId)) {
  173. $cat = new TestCategory();
  174. $cat->name = $category;
  175. $cat->description = '';
  176. $categoryId = $cat->save($courseId);
  177. if ($categoryId) {
  178. $question->category = $categoryId;
  179. }
  180. } else {
  181. $question->category = $categoryId;
  182. }
  183. }
  184. }
  185. if (!empty($question_array['description'])) {
  186. $description .= $question_array['description'];
  187. }
  188. $question->updateDescription($description);
  189. $question->save($exercise);
  190. $last_question_id = $question->selectId();
  191. //3. Create answer
  192. $answer = new Answer($last_question_id);
  193. $answerList = $question_array['answer'];
  194. $answer->new_nbrAnswers = count($answerList);
  195. $totalCorrectWeight = 0;
  196. $j = 1;
  197. $matchAnswerIds = [];
  198. if (!empty($answerList)) {
  199. foreach ($answerList as $key => $answers) {
  200. if (preg_match('/_/', $key)) {
  201. $split = explode('_', $key);
  202. $i = $split[1];
  203. } else {
  204. $i = $j;
  205. $j++;
  206. $matchAnswerIds[$key] = $j;
  207. }
  208. // Answer
  209. $answer->new_answer[$i] = isset($answers['value']) ? formatText($answers['value']) : '';
  210. // Comment
  211. $answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
  212. // Position
  213. $answer->new_position[$i] = $i;
  214. // Correct answers
  215. if (in_array($key, $question_array['correct_answers'])) {
  216. $answer->new_correct[$i] = 1;
  217. } else {
  218. $answer->new_correct[$i] = 0;
  219. }
  220. $answer->new_weighting[$i] = 0;
  221. if (isset($question_array['weighting'][$key])) {
  222. $answer->new_weighting[$i] = $question_array['weighting'][$key];
  223. }
  224. if ($answer->new_correct[$i]) {
  225. $totalCorrectWeight += $answer->new_weighting[$i];
  226. }
  227. }
  228. }
  229. if ($question->type == FREE_ANSWER) {
  230. $totalCorrectWeight = $question_array['weighting'][0];
  231. }
  232. $question->updateWeighting($totalCorrectWeight);
  233. $question->save($exercise);
  234. $answer->save();
  235. }
  236. // delete the temp dir where the exercise was unzipped
  237. my_delete($baseWorkDir.$uploadPath);
  238. return $last_exercise_id;
  239. }
  240. return false;
  241. }
  242. /**
  243. * We assume the file charset is UTF8.
  244. */
  245. function formatText($text)
  246. {
  247. return api_html_entity_decode($text);
  248. }
  249. /**
  250. * Parses a given XML file and fills global arrays with the elements.
  251. *
  252. * @param string $exercisePath
  253. * @param string $file
  254. * @param string $questionFile
  255. *
  256. * @return bool
  257. */
  258. function qti_parse_file($exercisePath, $file, $questionFile)
  259. {
  260. global $record_item_body;
  261. global $questionTempDir;
  262. $questionTempDir = $exercisePath.'/'.$file.'/';
  263. $questionFilePath = $questionTempDir.$questionFile;
  264. if (!($fp = fopen($questionFilePath, 'r'))) {
  265. Display::addFlash(Display::return_message(get_lang('Error opening question\'s XML file'), 'error'));
  266. return false;
  267. }
  268. $data = fread($fp, filesize($questionFilePath));
  269. //close file
  270. fclose($fp);
  271. //parse XML question file
  272. //$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
  273. $data = ChamiloApi::stripGivenTags($data, ['p', 'front']);
  274. $qtiVersion = [];
  275. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion);
  276. $qtiMainVersion = 2; //by default, assume QTI version 2
  277. if ($match) {
  278. $qtiMainVersion = $qtiVersion[1];
  279. }
  280. //used global variable start values declaration:
  281. $record_item_body = false;
  282. if ($qtiMainVersion != 2) {
  283. Display::addFlash(
  284. Display::return_message(
  285. get_lang('UnsupportedQtiVersion'),
  286. 'error'
  287. )
  288. );
  289. return false;
  290. }
  291. parseQti2($data);
  292. return true;
  293. }
  294. /**
  295. * Function used to parser a QTI2 xml file.
  296. *
  297. * @param string $xmlData
  298. */
  299. function parseQti2($xmlData)
  300. {
  301. global $exerciseInfo;
  302. global $questionTempDir;
  303. global $resourcesLinks;
  304. $crawler = new Crawler($xmlData);
  305. $nodes = $crawler->filter('*');
  306. $currentQuestionIdent = '';
  307. $currentAnswerId = '';
  308. $currentQuestionItemBody = '';
  309. $cardinality = '';
  310. $nonHTMLTagToAvoid = [
  311. 'simpleChoice',
  312. 'choiceInteraction',
  313. 'inlineChoiceInteraction',
  314. 'inlineChoice',
  315. 'soMPLEMATCHSET',
  316. 'simpleAssociableChoice',
  317. 'textEntryInteraction',
  318. 'feedbackInline',
  319. 'matchInteraction',
  320. 'extendedTextInteraction',
  321. 'itemBody',
  322. 'br',
  323. 'img',
  324. ];
  325. $currentMatchSet = null;
  326. /** @var DOMElement $node */
  327. foreach ($nodes as $node) {
  328. if ('#text' === $node->nodeName) {
  329. continue;
  330. }
  331. switch ($node->nodeName) {
  332. case 'assessmentItem':
  333. $currentQuestionIdent = $node->getAttribute('identifier');
  334. $exerciseInfo['question'][$currentQuestionIdent] = [
  335. 'answer' => [],
  336. 'correct_answers' => [],
  337. 'title' => $node->getAttribute('title'),
  338. 'category' => $node->getAttribute('category'),
  339. 'type' => '',
  340. 'tempdir' => $questionTempDir,
  341. ];
  342. break;
  343. case 'section':
  344. $title = $node->getAttribute('title');
  345. if (!empty($title)) {
  346. $exerciseInfo['name'] = $title;
  347. }
  348. break;
  349. case 'responseDeclaration':
  350. if ('multiple' === $node->getAttribute('cardinality')) {
  351. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MCMA;
  352. $cardinality = 'multiple';
  353. }
  354. if ('single' === $node->getAttribute('cardinality')) {
  355. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MCUA;
  356. $cardinality = 'single';
  357. }
  358. $currentAnswerId = $node->getAttribute('identifier');
  359. break;
  360. case 'inlineChoiceInteraction':
  361. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
  362. $exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'LISTBOX_FILL';
  363. $currentAnswerId = $node->getAttribute('responseIdentifier');
  364. break;
  365. case 'inlineChoice':
  366. $answerIdentifier = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
  367. if ($node->getAttribute('identifier') == $answerIdentifier) {
  368. $currentQuestionItemBody = str_replace(
  369. "**claroline_start**".$currentAnswerId."**claroline_end**",
  370. "[".$node->nodeValue."]",
  371. $currentQuestionItemBody
  372. );
  373. } else {
  374. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'])) {
  375. $exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'] = [];
  376. }
  377. $exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'][] = $node->nodeValue;
  378. }
  379. break;
  380. case 'textEntryInteraction':
  381. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
  382. $exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'TEXTFIELD_FILL';
  383. $exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
  384. break;
  385. case 'matchInteraction':
  386. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MATCHING;
  387. break;
  388. case 'extendedTextInteraction':
  389. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FREE_ANSWER;
  390. $exerciseInfo['question'][$currentQuestionIdent]['description'] = $node->nodeValue;
  391. break;
  392. case 'simpleMatchSet':
  393. if (!isset($currentMatchSet)) {
  394. $currentMatchSet = 1;
  395. } else {
  396. $currentMatchSet++;
  397. }
  398. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet] = [];
  399. break;
  400. case 'simpleAssociableChoice':
  401. $currentAssociableChoice = $node->getAttribute('identifier');
  402. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet][$currentAssociableChoice] = trim($node->nodeValue);
  403. break;
  404. case 'simpleChoice':
  405. $currentAnswerId = $node->getAttribute('identifier');
  406. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId])) {
  407. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId] = [];
  408. }
  409. //$simpleChoiceValue = $node->nodeValue;
  410. $simpleChoiceValue = '';
  411. /** @var DOMElement $childNode */
  412. foreach ($node->childNodes as $childNode) {
  413. if ('feedbackInline' === $childNode->nodeName) {
  414. continue;
  415. }
  416. $simpleChoiceValue .= $childNode->nodeValue;
  417. }
  418. $simpleChoiceValue = trim($simpleChoiceValue);
  419. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'])) {
  420. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] = $simpleChoiceValue;
  421. } else {
  422. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] .= $simpleChoiceValue;
  423. }
  424. break;
  425. case 'mapEntry':
  426. if (in_array($node->parentNode->nodeName, ['mapping', 'mapEntry'])) {
  427. $answer_id = $node->getAttribute('mapKey');
  428. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['weighting'])) {
  429. $exerciseInfo['question'][$currentQuestionIdent]['weighting'] = [];
  430. }
  431. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][$answer_id] = $node->getAttribute(
  432. 'mappedValue'
  433. );
  434. }
  435. break;
  436. case 'mapping':
  437. $defaultValue = $node->getAttribute('defaultValue');
  438. if (!empty($defaultValue)) {
  439. $exerciseInfo['question'][$currentQuestionIdent]['default_weighting'] = $defaultValue;
  440. }
  441. // no break ?
  442. case 'itemBody':
  443. $nodeValue = $node->nodeValue;
  444. $currentQuestionItemBody = '';
  445. /** @var DOMElement $childNode */
  446. foreach ($node->childNodes as $childNode) {
  447. if ('#text' === $childNode->nodeName) {
  448. continue;
  449. }
  450. if (!in_array($childNode->nodeName, $nonHTMLTagToAvoid)) {
  451. $currentQuestionItemBody .= '<'.$childNode->nodeName;
  452. if ($childNode->attributes) {
  453. foreach ($childNode->attributes as $attribute) {
  454. $currentQuestionItemBody .= ' '.$attribute->nodeName.'="'.$attribute->nodeValue.'"';
  455. }
  456. }
  457. $currentQuestionItemBody .= '>'.$childNode->nodeValue.'</'.$node->nodeName.'>';
  458. continue;
  459. }
  460. if ('inlineChoiceInteraction' === $childNode->nodeName) {
  461. $currentQuestionItemBody .= "**claroline_start**".$childNode->attr('responseIdentifier')
  462. ."**claroline_end**";
  463. continue;
  464. }
  465. if ('textEntryInteraction' === $childNode->nodeName) {
  466. $correct_answer_value = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
  467. $currentQuestionItemBody .= "[".$correct_answer_value."]";
  468. continue;
  469. }
  470. if ('br' === $childNode->nodeName) {
  471. $currentQuestionItemBody .= '<br>';
  472. }
  473. }
  474. // Replace relative links by links to the documents in the course
  475. // $resourcesLinks is only defined by qtiProcessManifest()
  476. if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
  477. foreach ($resourcesLinks['manifest'] as $key => $value) {
  478. $nodeValue = preg_replace('|'.$value.'|', $resourcesLinks['web'][$key], $nodeValue);
  479. }
  480. }
  481. $currentQuestionItemBody .= $node->firstChild->nodeValue;
  482. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FIB) {
  483. $exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
  484. } else {
  485. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FREE_ANSWER) {
  486. $currentQuestionItemBody = trim($currentQuestionItemBody);
  487. if (!empty($currentQuestionItemBody)) {
  488. $exerciseInfo['question'][$currentQuestionIdent]['description'] = $currentQuestionItemBody;
  489. }
  490. } else {
  491. $exerciseInfo['question'][$currentQuestionIdent]['statement'] = $currentQuestionItemBody;
  492. }
  493. }
  494. break;
  495. case 'img':
  496. $exerciseInfo['question'][$currentQuestionIdent]['attached_file_url'] = $node->getAttribute('src');
  497. break;
  498. case 'order':
  499. $orderType = $node->getAttribute('order_type');
  500. if (!empty($orderType)) {
  501. $exerciseInfo['order_type'] = $orderType;
  502. }
  503. break;
  504. case 'feedbackInline':
  505. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'])) {
  506. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] = trim(
  507. $node->nodeValue
  508. );
  509. } else {
  510. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] .= trim(
  511. $node->nodeValue
  512. );
  513. }
  514. break;
  515. case 'value':
  516. if ('correctResponse' === $node->parentNode->nodeName) {
  517. $nodeValue = trim($node->nodeValue);
  518. if ('single' === $cardinality) {
  519. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$nodeValue] = $nodeValue;
  520. } else {
  521. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][] = $nodeValue;
  522. }
  523. }
  524. if ('outcomeDeclaration' === $node->parentNode->parentNode->nodeName) {
  525. $nodeValue = trim($node->nodeValue);
  526. if (!empty($nodeValue)) {
  527. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][0] = $nodeValue;
  528. }
  529. }
  530. break;
  531. case 'mattext':
  532. if ('flow_mat' === $node->parentNode->parentNode->nodeName &&
  533. ('presentation_material' === $node->parentNode->parentNode->parentNode->nodeName ||
  534. 'section' === $node->parentNode->parentNode->parentNode->nodeName
  535. )
  536. ) {
  537. $nodeValue = trim($node->nodeValue);
  538. if (!empty($nodeValue)) {
  539. $exerciseInfo['description'] = $node->nodeValue;
  540. }
  541. }
  542. break;
  543. }
  544. }
  545. }
  546. /**
  547. * Check if a given file is an IMS/QTI question bank file.
  548. *
  549. * @param string $filePath The absolute filepath
  550. *
  551. * @return bool Whether it is an IMS/QTI question bank or not
  552. */
  553. function isQtiQuestionBank($filePath)
  554. {
  555. $data = file_get_contents($filePath);
  556. if (!empty($data)) {
  557. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
  558. // @todo allow other types
  559. //$match2 = preg_match('/imsqti_v(\d)p(\d)/', $data);
  560. if ($match) {
  561. return true;
  562. }
  563. }
  564. return false;
  565. }
  566. /**
  567. * Check if a given file is an IMS/QTI manifest file (listing of extra files).
  568. *
  569. * @param string $filePath The absolute filepath
  570. *
  571. * @return bool Whether it is an IMS/QTI manifest file or not
  572. */
  573. function isQtiManifest($filePath)
  574. {
  575. $data = file_get_contents($filePath);
  576. if (!empty($data)) {
  577. $match = preg_match('/imsccv(\d)p(\d)/', $data);
  578. if ($match) {
  579. return true;
  580. }
  581. }
  582. return false;
  583. }
  584. /**
  585. * Processes an IMS/QTI manifest file: store links to new files
  586. * to be able to transform them into the questions text.
  587. *
  588. * @param string $filePath The absolute filepath
  589. * @param array $links List of filepaths changes
  590. *
  591. * @return bool
  592. */
  593. function qtiProcessManifest($filePath)
  594. {
  595. $xml = simplexml_load_file($filePath);
  596. $course = api_get_course_info();
  597. $sessionId = api_get_session_id();
  598. $courseDir = $course['path'];
  599. $sysPath = api_get_path(SYS_COURSE_PATH);
  600. $exercisesSysPath = $sysPath.$courseDir.'/document/';
  601. $webPath = api_get_path(WEB_CODE_PATH);
  602. $exercisesWebPath = $webPath.'document/document.php?'.api_get_cidreq().'&action=download&id=';
  603. $links = [
  604. 'manifest' => [],
  605. 'system' => [],
  606. 'web' => [],
  607. ];
  608. $tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
  609. $countResources = count($xml->resources->resource->file);
  610. for ($i = 0; $i < $countResources; $i++) {
  611. $file = $xml->resources->resource->file[$i];
  612. $href = '';
  613. foreach ($file->attributes() as $key => $value) {
  614. if ($key == 'href') {
  615. if (substr($value, -3, 3) != 'xml') {
  616. $href = $value;
  617. }
  618. }
  619. }
  620. if (!empty($href)) {
  621. $links['manifest'][] = (string) $href;
  622. $links['system'][] = $exercisesSysPath.strtolower($href);
  623. $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
  624. $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
  625. $sql = "SELECT iid FROM $tableDocuments
  626. WHERE
  627. c_id = ".$course['real_id']." AND
  628. session_id = $sessionId AND
  629. path = '/".$specialHref."'";
  630. $result = Database::query($sql);
  631. $documentId = 0;
  632. while ($row = Database::fetch_assoc($result)) {
  633. $documentId = $row['iid'];
  634. }
  635. $links['web'][] = $exercisesWebPath.$documentId;
  636. }
  637. }
  638. return $links;
  639. }