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. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'])) {
  410. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] = trim(
  411. $node->nodeValue
  412. );
  413. } else {
  414. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] .= ''
  415. .trim($node->nodeValue);
  416. }
  417. break;
  418. case 'mapEntry':
  419. if (in_array($node->parentNode->nodeName, ['mapping', 'mapEntry'])) {
  420. $answer_id = $node->getAttribute('mapKey');
  421. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['weighting'])) {
  422. $exerciseInfo['question'][$currentQuestionIdent]['weighting'] = [];
  423. }
  424. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][$answer_id] = $node->getAttribute(
  425. 'mappedValue'
  426. );
  427. }
  428. break;
  429. case 'mapping':
  430. $defaultValue = $node->getAttribute('defaultValue');
  431. if (!empty($defaultValue)) {
  432. $exerciseInfo['question'][$currentQuestionIdent]['default_weighting'] = $defaultValue;
  433. }
  434. // no break ?
  435. case 'itemBody':
  436. $nodeValue = $node->nodeValue;
  437. $currentQuestionItemBody = '';
  438. /** @var DOMElement $childNode */
  439. foreach ($node->childNodes as $childNode) {
  440. if ('#text' === $childNode->nodeName) {
  441. continue;
  442. }
  443. if (!in_array($childNode->nodeName, $nonHTMLTagToAvoid)) {
  444. $currentQuestionItemBody .= '<'.$childNode->nodeName;
  445. if ($childNode->attributes) {
  446. foreach ($childNode->attributes as $attribute) {
  447. $currentQuestionItemBody .= ' '.$attribute->nodeName.'="'.$attribute->nodeValue.'"';
  448. }
  449. }
  450. $currentQuestionItemBody .= '>'
  451. .$childNode->nodeValue
  452. .'</'.$node->nodeName.'>';
  453. continue;
  454. }
  455. if ('inlineChoiceInteraction' === $childNode->nodeName) {
  456. $currentQuestionItemBody .= "**claroline_start**"
  457. .$childNode->attr('responseIdentifier')
  458. ."**claroline_end**";
  459. continue;
  460. }
  461. if ('textEntryInteraction' === $childNode->nodeName) {
  462. $correct_answer_value = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
  463. $currentQuestionItemBody .= "[".$correct_answer_value."]";
  464. continue;
  465. }
  466. if ('br' === $childNode->nodeName) {
  467. $currentQuestionItemBody .= '<br>';
  468. }
  469. }
  470. // Replace relative links by links to the documents in the course
  471. // $resourcesLinks is only defined by qtiProcessManifest()
  472. if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
  473. foreach ($resourcesLinks['manifest'] as $key => $value) {
  474. $nodeValue = preg_replace('|'.$value.'|', $resourcesLinks['web'][$key], $nodeValue);
  475. }
  476. }
  477. $currentQuestionItemBody .= $node->firstChild->nodeValue;
  478. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FIB) {
  479. $exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
  480. } else {
  481. if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FREE_ANSWER) {
  482. $currentQuestionItemBody = trim($currentQuestionItemBody);
  483. if (!empty($currentQuestionItemBody)) {
  484. $exerciseInfo['question'][$currentQuestionIdent]['description'] = $currentQuestionItemBody;
  485. }
  486. } else {
  487. $exerciseInfo['question'][$currentQuestionIdent]['statement'] = $currentQuestionItemBody;
  488. }
  489. }
  490. break;
  491. case 'img':
  492. $exerciseInfo['question'][$currentQuestionIdent]['attached_file_url'] = $node->getAttribute('src');
  493. break;
  494. case 'order':
  495. $orderType = $node->getAttribute('order_type');
  496. if (!empty($orderType)) {
  497. $exerciseInfo['order_type'] = $orderType;
  498. }
  499. break;
  500. case 'feedbackInline':
  501. if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'])) {
  502. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId] = trim(
  503. $node->nodeValue
  504. );
  505. } else {
  506. $exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] .= ''
  507. .trim(
  508. $node->nodeValue
  509. );
  510. }
  511. break;
  512. case 'value':
  513. if ('correctResponse' === $node->parentNode->nodeName) {
  514. $nodeValue = trim($node->nodeValue);
  515. if ('single' === $cardinality) {
  516. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$nodeValue] = $nodeValue;
  517. } else {
  518. $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][] = $nodeValue;
  519. }
  520. }
  521. if ('outcomeDeclaration' === $node->parentNode->parentNode->nodeName) {
  522. $nodeValue = trim($node->nodeValue);
  523. if (!empty($nodeValue)) {
  524. $exerciseInfo['question'][$currentQuestionIdent]['weighting'][0] = $nodeValue;
  525. }
  526. }
  527. break;
  528. case 'mattext':
  529. if ('flow_mat' === $node->parentNode->parentNode->nodeName &&
  530. ('presentation_material' === $node->parentNode->parentNode->parentNode->nodeName ||
  531. 'section' === $node->parentNode->parentNode->parentNode->nodeName
  532. )
  533. ) {
  534. $nodeValue = trim($node->nodeValue);
  535. if (!empty($nodeValue)) {
  536. $exerciseInfo['description'] = $node->nodeValue;
  537. }
  538. }
  539. break;
  540. }
  541. }
  542. }
  543. /**
  544. * Check if a given file is an IMS/QTI question bank file.
  545. *
  546. * @param string $filePath The absolute filepath
  547. *
  548. * @return bool Whether it is an IMS/QTI question bank or not
  549. */
  550. function isQtiQuestionBank($filePath)
  551. {
  552. $data = file_get_contents($filePath);
  553. if (!empty($data)) {
  554. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
  555. // @todo allow other types
  556. //$match2 = preg_match('/imsqti_v(\d)p(\d)/', $data);
  557. if ($match) {
  558. return true;
  559. }
  560. }
  561. return false;
  562. }
  563. /**
  564. * Check if a given file is an IMS/QTI manifest file (listing of extra files).
  565. *
  566. * @param string $filePath The absolute filepath
  567. *
  568. * @return bool Whether it is an IMS/QTI manifest file or not
  569. */
  570. function isQtiManifest($filePath)
  571. {
  572. $data = file_get_contents($filePath);
  573. if (!empty($data)) {
  574. $match = preg_match('/imsccv(\d)p(\d)/', $data);
  575. if ($match) {
  576. return true;
  577. }
  578. }
  579. return false;
  580. }
  581. /**
  582. * Processes an IMS/QTI manifest file: store links to new files
  583. * to be able to transform them into the questions text.
  584. *
  585. * @param string $filePath The absolute filepath
  586. * @param array $links List of filepaths changes
  587. *
  588. * @return bool
  589. */
  590. function qtiProcessManifest($filePath)
  591. {
  592. $xml = simplexml_load_file($filePath);
  593. $course = api_get_course_info();
  594. $sessionId = api_get_session_id();
  595. $courseDir = $course['path'];
  596. $sysPath = api_get_path(SYS_COURSE_PATH);
  597. $exercisesSysPath = $sysPath.$courseDir.'/document/';
  598. $webPath = api_get_path(WEB_CODE_PATH);
  599. $exercisesWebPath = $webPath.'document/document.php?'.api_get_cidreq().'&action=download&id=';
  600. $links = [
  601. 'manifest' => [],
  602. 'system' => [],
  603. 'web' => [],
  604. ];
  605. $tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
  606. $countResources = count($xml->resources->resource->file);
  607. for ($i = 0; $i < $countResources; $i++) {
  608. $file = $xml->resources->resource->file[$i];
  609. $href = '';
  610. foreach ($file->attributes() as $key => $value) {
  611. if ($key == 'href') {
  612. if (substr($value, -3, 3) != 'xml') {
  613. $href = $value;
  614. }
  615. }
  616. }
  617. if (!empty($href)) {
  618. $links['manifest'][] = (string) $href;
  619. $links['system'][] = $exercisesSysPath.strtolower($href);
  620. $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
  621. $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
  622. $sql = "SELECT iid FROM $tableDocuments
  623. WHERE
  624. c_id = ".$course['real_id']." AND
  625. session_id = $sessionId AND
  626. path = '/".$specialHref."'";
  627. $result = Database::query($sql);
  628. $documentId = 0;
  629. while ($row = Database::fetch_assoc($result)) {
  630. $documentId = $row['iid'];
  631. }
  632. $links['web'][] = $exercisesWebPath.$documentId;
  633. }
  634. }
  635. return $links;
  636. }