exercise_import.inc.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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. //2. Create question
  153. $question = new Ims2Question();
  154. $question->type = $question_array['type'];
  155. if (empty($question->type)) {
  156. // If the type was not provided, assume this is a multiple choice, unique answer type (the most basic)
  157. $question->type = MCUA;
  158. }
  159. $question->setAnswer();
  160. $description = '';
  161. $question->updateTitle(formatText(strip_tags($question_array['title'])));
  162. if (isset($question_array['category'])) {
  163. $category = formatText(strip_tags($question_array['category']));
  164. if (!empty($category)) {
  165. $categoryId = TestCategory::get_category_id_for_title(
  166. $category,
  167. $courseId
  168. );
  169. if (empty($categoryId)) {
  170. $cat = new TestCategory();
  171. $cat->name = $category;
  172. $cat->description = '';
  173. $categoryId = $cat->save($courseId);
  174. if ($categoryId) {
  175. $question->category = $categoryId;
  176. }
  177. } else {
  178. $question->category = $categoryId;
  179. }
  180. }
  181. }
  182. if (!empty($question_array['description'])) {
  183. $description .= $question_array['description'];
  184. }
  185. $question->updateDescription($description);
  186. $question->save($exercise);
  187. $last_question_id = $question->selectId();
  188. //3. Create answer
  189. $answer = new Answer($last_question_id);
  190. $answerList = $question_array['answer'];
  191. $answer->new_nbrAnswers = count($answerList);
  192. $totalCorrectWeight = 0;
  193. $j = 1;
  194. $matchAnswerIds = [];
  195. if (!empty($answerList)) {
  196. foreach ($answerList as $key => $answers) {
  197. if (preg_match('/_/', $key)) {
  198. $split = explode('_', $key);
  199. $i = $split[1];
  200. } else {
  201. $i = $j;
  202. $j++;
  203. $matchAnswerIds[$key] = $j;
  204. }
  205. // Answer
  206. $answer->new_answer[$i] = isset($answers['value']) ? formatText($answers['value']) : '';
  207. // Comment
  208. $answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
  209. // Position
  210. $answer->new_position[$i] = $i;
  211. // Correct answers
  212. if (in_array($key, $question_array['correct_answers'])) {
  213. $answer->new_correct[$i] = 1;
  214. } else {
  215. $answer->new_correct[$i] = 0;
  216. }
  217. $answer->new_weighting[$i] = 0;
  218. if (isset($question_array['weighting'][$key])) {
  219. $answer->new_weighting[$i] = $question_array['weighting'][$key];
  220. }
  221. if ($answer->new_correct[$i]) {
  222. $totalCorrectWeight += $answer->new_weighting[$i];
  223. }
  224. }
  225. }
  226. if ($question->type == FREE_ANSWER) {
  227. $totalCorrectWeight = $question_array['weighting'][0];
  228. }
  229. $question->updateWeighting($totalCorrectWeight);
  230. $question->save($exercise);
  231. $answer->save();
  232. }
  233. // delete the temp dir where the exercise was unzipped
  234. my_delete($baseWorkDir.$uploadPath);
  235. return $last_exercise_id;
  236. }
  237. return false;
  238. }
  239. /**
  240. * We assume the file charset is UTF8.
  241. */
  242. function formatText($text)
  243. {
  244. return api_html_entity_decode($text);
  245. }
  246. /**
  247. * Parses a given XML file and fills global arrays with the elements.
  248. *
  249. * @param string $exercisePath
  250. * @param string $file
  251. * @param string $questionFile
  252. *
  253. * @return bool
  254. */
  255. function qti_parse_file($exercisePath, $file, $questionFile)
  256. {
  257. global $record_item_body;
  258. global $questionTempDir;
  259. $questionTempDir = $exercisePath.'/'.$file.'/';
  260. $questionFilePath = $questionTempDir.$questionFile;
  261. if (!($fp = fopen($questionFilePath, 'r'))) {
  262. Display::addFlash(Display::return_message(get_lang('Error opening question\'s XML file'), 'error'));
  263. return false;
  264. }
  265. $data = fread($fp, filesize($questionFilePath));
  266. //close file
  267. fclose($fp);
  268. //parse XML question file
  269. //$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
  270. $data = ChamiloApi::stripGivenTags($data, ['p', 'front']);
  271. $qtiVersion = [];
  272. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion);
  273. $qtiMainVersion = 2; //by default, assume QTI version 2
  274. if ($match) {
  275. $qtiMainVersion = $qtiVersion[1];
  276. }
  277. //used global variable start values declaration:
  278. $record_item_body = false;
  279. if ($qtiMainVersion != 2) {
  280. Display::addFlash(
  281. Display::return_message(
  282. get_lang('UnsupportedQtiVersion'),
  283. 'error'
  284. )
  285. );
  286. return false;
  287. }
  288. parseQti2($data);
  289. return true;
  290. }
  291. /**
  292. * Function used to parser a QTI2 xml file.
  293. *
  294. * @param string $xmlData
  295. */
  296. function parseQti2($xmlData)
  297. {
  298. global $exerciseInfo;
  299. global $questionTempDir;
  300. global $resourcesLinks;
  301. $crawler = new Crawler($xmlData);
  302. $nodes = $crawler->filter('*');
  303. $currentQuestionIdent = '';
  304. $currentAnswerId = '';
  305. $currentQuestionItemBody = '';
  306. $cardinality = '';
  307. $nonHTMLTagToAvoid = [
  308. "simpleChoice",
  309. "choiceInteraction",
  310. "inlineChoiceInteraction",
  311. "inlineChoice",
  312. "soMPLEMATCHSET",
  313. "simpleAssociableChoice",
  314. "textEntryInteraction",
  315. "feedbackInline",
  316. "matchInteraction",
  317. 'extendedTextInteraction',
  318. "itemBody",
  319. "br",
  320. "img",
  321. ];
  322. $currentMatchSet = null;
  323. /** @var DOMElement $node */
  324. foreach ($nodes as $node) {
  325. if ('#text' === $node->nodeName) {
  326. continue;
  327. }
  328. switch ($node->nodeName) {
  329. case 'assessmentItem':
  330. $currentQuestionIdent = $node->getAttribute('identifier');
  331. $exerciseInfo['question'][$currentQuestionIdent] = [
  332. 'answer' => [],
  333. 'correct_answers' => [],
  334. 'title' => $node->getAttribute('title'),
  335. 'category' => $node->getAttribute('category'),
  336. 'type' => '',
  337. 'tempdir' => $questionTempDir,
  338. ];
  339. break;
  340. case 'section':
  341. $title = $node->getAttribute('title');
  342. if (!empty($title)) {
  343. $exerciseInfo['name'] = $title;
  344. }
  345. break;
  346. case 'responseDeclaration':
  347. if ('multiple' === $node->getAttribute('cardinality')) {
  348. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MCMA;
  349. $cardinality = 'multiple';
  350. }
  351. if ('single' === $node->getAttribute('cardinality')) {
  352. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MCUA;
  353. $cardinality = 'single';
  354. }
  355. $currentAnswerId = $node->getAttribute('identifier');
  356. break;
  357. case 'inlineChoiceInteraction':
  358. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
  359. $exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'LISTBOX_FILL';
  360. $currentAnswerId = $node->getAttribute('responseIdentifier');
  361. break;
  362. case 'inlineChoice':
  363. $answerIdentifier = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
  364. if ($node->getAttribute('identifier') == $answerIdentifier) {
  365. $currentQuestionItemBody = str_replace(
  366. "**claroline_start**".$currentAnswerId."**claroline_end**",
  367. "[".$node->nodeValue."]",
  368. $currentQuestionItemBody
  369. );
  370. } else {
  371. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'])) {
  372. $exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'] = [];
  373. }
  374. $exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'][] = $node->nodeValue;
  375. }
  376. break;
  377. case 'textEntryInteraction':
  378. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
  379. $exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'TEXTFIELD_FILL';
  380. $exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
  381. break;
  382. case 'matchInteraction':
  383. $exerciseInfo['question'][$currentQuestionIdent]['type'] = MATCHING;
  384. break;
  385. case 'extendedTextInteraction':
  386. $exerciseInfo['question'][$currentQuestionIdent]['type'] = FREE_ANSWER;
  387. $exerciseInfo['question'][$currentQuestionIdent]['description'] = $node->nodeValue;
  388. break;
  389. case 'simpleMatchSet':
  390. if (!isset($currentMatchSet)) {
  391. $currentMatchSet = 1;
  392. } else {
  393. $currentMatchSet++;
  394. }
  395. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet] = [];
  396. break;
  397. case 'simpleAssociableChoice':
  398. $currentAssociableChoice = $node->getAttribute('identifier');
  399. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet][$currentAssociableChoice] = trim($node->nodeValue);
  400. break;
  401. case 'simpleChoice':
  402. $currentAnswerId = $node->getAttribute('identifier');
  403. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId])) {
  404. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId] = [];
  405. }
  406. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'])) {
  407. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] = trim(
  408. $node->nodeValue
  409. );
  410. } else {
  411. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] .= ''
  412. .trim($node->nodeValue);
  413. }
  414. break;
  415. case 'mapEntry':
  416. if (in_array($node->parentNode->nodeName, ['mapping', 'mapEntry'])) {
  417. $answer_id = $node->getAttribute('mapKey');
  418. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['weighting'])) {
  419. $exerciseInfo['question'][$currentQuestionIdent]['weighting'] = [];
  420. }
  421. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][$answer_id] = $node->getAttribute(
  422. 'mappedValue'
  423. );
  424. }
  425. break;
  426. case 'mapping':
  427. $defaultValue = $node->getAttribute('defaultValue');
  428. if (!empty($defaultValue)) {
  429. $exerciseInfo['question'][$currentQuestionIdent]['default_weighting'] = $defaultValue;
  430. }
  431. // no break ?
  432. case 'itemBody':
  433. $nodeValue = $node->nodeValue;
  434. $currentQuestionItemBody = '';
  435. /** @var DOMElement $childNode */
  436. foreach ($node->childNodes as $childNode) {
  437. if ('#text' === $childNode->nodeName) {
  438. continue;
  439. }
  440. if (!in_array($childNode->nodeName, $nonHTMLTagToAvoid)) {
  441. $currentQuestionItemBody .= '<'.$childNode->nodeName;
  442. if ($childNode->attributes) {
  443. foreach ($childNode->attributes as $attribute) {
  444. $currentQuestionItemBody .= ' '.$attribute->nodeName.'="'.$attribute->nodeValue.'"';
  445. }
  446. }
  447. $currentQuestionItemBody .= '>'
  448. .$childNode->nodeValue
  449. .'</'.$node->nodeName.'>';
  450. continue;
  451. }
  452. if ('inlineChoiceInteraction' === $childNode->nodeName) {
  453. $currentQuestionItemBody .= "**claroline_start**"
  454. .$childNode->attr('responseIdentifier')
  455. ."**claroline_end**";
  456. continue;
  457. }
  458. if ('textEntryInteraction' === $childNode->nodeName) {
  459. $correct_answer_value = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
  460. $currentQuestionItemBody .= "[".$correct_answer_value."]";
  461. continue;
  462. }
  463. if ('br' === $childNode->nodeName) {
  464. $currentQuestionItemBody .= '<br>';
  465. }
  466. }
  467. // Replace relative links by links to the documents in the course
  468. // $resourcesLinks is only defined by qtiProcessManifest()
  469. if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
  470. foreach ($resourcesLinks['manifest'] as $key => $value) {
  471. $nodeValue = preg_replace('|'.$value.'|', $resourcesLinks['web'][$key], $nodeValue);
  472. }
  473. }
  474. $currentQuestionItemBody .= $node->firstChild->nodeValue;
  475. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FIB) {
  476. $exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
  477. } else {
  478. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FREE_ANSWER) {
  479. $currentQuestionItemBody = trim($currentQuestionItemBody);
  480. if (!empty($currentQuestionItemBody)) {
  481. $exerciseInfo['question'][$currentQuestionIdent]['description'] = $currentQuestionItemBody;
  482. }
  483. } else {
  484. $exerciseInfo['question'][$currentQuestionIdent]['statement'] = $currentQuestionItemBody;
  485. }
  486. }
  487. break;
  488. case 'img':
  489. $exerciseInfo['question'][$currentQuestionIdent]['attached_file_url'] = $node->getAttribute('src');
  490. break;
  491. case 'order':
  492. $orderType = $node->getAttribute('order_type');
  493. if (!empty($orderType)) {
  494. $exerciseInfo['order_type'] = $orderType;
  495. }
  496. break;
  497. case 'feedbackInline':
  498. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'])) {
  499. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId] = trim(
  500. $node->nodeValue
  501. );
  502. } else {
  503. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] .= ''
  504. .trim(
  505. $node->nodeValue
  506. );
  507. }
  508. break;
  509. case 'value':
  510. if ('correctResponse' === $node->parentNode->nodeName) {
  511. $nodeValue = trim($node->nodeValue);
  512. if ('single' === $cardinality) {
  513. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$nodeValue] = $nodeValue;
  514. } else {
  515. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][] = $nodeValue;
  516. }
  517. }
  518. if ('outcomeDeclaration' === $node->parentNode->parentNode->nodeName) {
  519. $nodeValue = trim($node->nodeValue);
  520. if (!empty($nodeValue)) {
  521. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][0] = $nodeValue;
  522. }
  523. }
  524. break;
  525. case 'mattext':
  526. if ('flow_mat' === $node->parentNode->parentNode->nodeName &&
  527. ('presentation_material' === $node->parentNode->parentNode->parentNode->nodeName ||
  528. 'section' === $node->parentNode->parentNode->parentNode->nodeName
  529. )
  530. ) {
  531. $nodeValue = trim($node->nodeValue);
  532. if (!empty($nodeValue)) {
  533. $exerciseInfo['description'] = $node->nodeValue;
  534. }
  535. }
  536. break;
  537. }
  538. }
  539. }
  540. /**
  541. * Check if a given file is an IMS/QTI question bank file.
  542. *
  543. * @param string $filePath The absolute filepath
  544. *
  545. * @return bool Whether it is an IMS/QTI question bank or not
  546. */
  547. function isQtiQuestionBank($filePath)
  548. {
  549. $data = file_get_contents($filePath);
  550. if (!empty($data)) {
  551. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
  552. // @todo allow other types
  553. //$match2 = preg_match('/imsqti_v(\d)p(\d)/', $data);
  554. if ($match) {
  555. return true;
  556. }
  557. }
  558. return false;
  559. }
  560. /**
  561. * Check if a given file is an IMS/QTI manifest file (listing of extra files).
  562. *
  563. * @param string $filePath The absolute filepath
  564. *
  565. * @return bool Whether it is an IMS/QTI manifest file or not
  566. */
  567. function isQtiManifest($filePath)
  568. {
  569. $data = file_get_contents($filePath);
  570. if (!empty($data)) {
  571. $match = preg_match('/imsccv(\d)p(\d)/', $data);
  572. if ($match) {
  573. return true;
  574. }
  575. }
  576. return false;
  577. }
  578. /**
  579. * Processes an IMS/QTI manifest file: store links to new files
  580. * to be able to transform them into the questions text.
  581. *
  582. * @param string $filePath The absolute filepath
  583. * @param array $links List of filepaths changes
  584. *
  585. * @return bool
  586. */
  587. function qtiProcessManifest($filePath)
  588. {
  589. $xml = simplexml_load_file($filePath);
  590. $course = api_get_course_info();
  591. $sessionId = api_get_session_id();
  592. $courseDir = $course['path'];
  593. $sysPath = api_get_path(SYS_COURSE_PATH);
  594. $exercisesSysPath = $sysPath.$courseDir.'/document/';
  595. $webPath = api_get_path(WEB_CODE_PATH);
  596. $exercisesWebPath = $webPath.'document/document.php?'.api_get_cidreq().'&action=download&id=';
  597. $links = [
  598. 'manifest' => [],
  599. 'system' => [],
  600. 'web' => [],
  601. ];
  602. $tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
  603. $countResources = count($xml->resources->resource->file);
  604. for ($i = 0; $i < $countResources; $i++) {
  605. $file = $xml->resources->resource->file[$i];
  606. $href = '';
  607. foreach ($file->attributes() as $key => $value) {
  608. if ($key == 'href') {
  609. if (substr($value, -3, 3) != 'xml') {
  610. $href = $value;
  611. }
  612. }
  613. }
  614. if (!empty($href)) {
  615. $links['manifest'][] = (string) $href;
  616. $links['system'][] = $exercisesSysPath.strtolower($href);
  617. $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
  618. $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
  619. $sql = "SELECT iid FROM $tableDocuments
  620. WHERE
  621. c_id = ".$course['real_id']." AND
  622. session_id = $sessionId AND
  623. path = '/".$specialHref."'";
  624. $result = Database::query($sql);
  625. $documentId = 0;
  626. while ($row = Database::fetch_assoc($result)) {
  627. $documentId = $row['iid'];
  628. }
  629. $links['web'][] = $exercisesWebPath.$documentId;
  630. }
  631. }
  632. return $links;
  633. }