exercise_import.inc.php 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
  5. * @package chamilo.exercise
  6. * @author claro team <cvs@claroline.net>
  7. * @author Guillaume Lederer <guillaume@claroline.net>
  8. * @author Yannick Warnier <yannick.warnier@beeznest.com>
  9. */
  10. use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
  11. /**
  12. * Unzip the exercise in the temp folder
  13. * @param string The path of the temporary directory where the exercise was uploaded and unzipped
  14. * @param string
  15. * @param string $baseWorkDir
  16. * @param string $uploadPath
  17. * @return bool
  18. */
  19. function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
  20. {
  21. $_course = api_get_course_info();
  22. $_user = api_get_user_info();
  23. //Check if the file is valid (not to big and exists)
  24. if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
  25. // upload failed
  26. return false;
  27. }
  28. if (preg_match('/.zip$/i', $_FILES['userFile']['name']) &&
  29. handle_uploaded_document(
  30. $_course,
  31. $_FILES['userFile'],
  32. $baseWorkDir,
  33. $uploadPath,
  34. $_user['user_id'],
  35. 0,
  36. null,
  37. 1,
  38. null,
  39. null,
  40. null,
  41. null,
  42. null,
  43. false
  44. )
  45. ) {
  46. return true;
  47. }
  48. return false;
  49. }
  50. /**
  51. * Imports an exercise in QTI format if the XML structure can be found in it
  52. * @param array $file
  53. * @return string|array as a backlog of what was really imported, and error or debug messages to display
  54. */
  55. function import_exercise($file)
  56. {
  57. global $exercise_info;
  58. global $element_pile;
  59. global $non_HTML_tag_to_avoid;
  60. global $record_item_body;
  61. // used to specify the question directory where files could be found in relation in any question
  62. global $questionTempDir;
  63. global $resourcesLinks;
  64. $baseWorkDir = api_get_path(SYS_ARCHIVE_PATH) . 'qti2';
  65. if (!is_dir($baseWorkDir)) {
  66. mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
  67. }
  68. $uploadPath = '/';
  69. // set some default values for the new exercise
  70. $exercise_info = array();
  71. $exercise_info['name'] = preg_replace('/.zip$/i', '', $file);
  72. $exercise_info['question'] = array();
  73. $element_pile = array();
  74. // create parser and array to retrieve info from manifest
  75. $element_pile = array(); //pile to known the depth in which we are
  76. //$module_info = array (); //array to store the info we need
  77. // if file is not a .zip, then we cancel all
  78. if (!preg_match('/.zip$/i', $file)) {
  79. return 'UplZipCorrupt';
  80. }
  81. // unzip the uploaded file in a tmp directory
  82. if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
  83. return 'UplZipCorrupt';
  84. }
  85. // find the different manifests for each question and parse them.
  86. $exerciseHandle = opendir($baseWorkDir);
  87. //$question_number = 0;
  88. $file_found = false;
  89. $operation = false;
  90. $result = false;
  91. $filePath = null;
  92. $resourcesLinks = array();
  93. // parse every subdirectory to search xml question files and other assets to be imported
  94. // The assets-related code is a bit fragile as it has to deal with files renamed by Chamilo and it only works if
  95. // the imsmanifest.xml file is read.
  96. while (false !== ($file = readdir($exerciseHandle))) {
  97. if (is_dir($baseWorkDir . '/' . $file) && $file != "." && $file != "..") {
  98. // Find each manifest for each question repository found
  99. $questionHandle = opendir($baseWorkDir . '/' . $file);
  100. // Only analyse one level of subdirectory - no recursivity here
  101. while (false !== ($questionFile = readdir($questionHandle))) {
  102. if (preg_match('/.xml$/i', $questionFile)) {
  103. $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file . '/' . $questionFile);
  104. if ($isQti) {
  105. $result = qti_parse_file($baseWorkDir, $file, $questionFile);
  106. $filePath = $baseWorkDir . $file;
  107. $file_found = true;
  108. } else {
  109. $isManifest = isQtiManifest($baseWorkDir . '/' . $file . '/' . $questionFile);
  110. if ($isManifest) {
  111. $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file . '/' . $questionFile);
  112. }
  113. }
  114. }
  115. }
  116. } elseif (preg_match('/.xml$/i', $file)) {
  117. $isQti = isQtiQuestionBank($baseWorkDir . '/' . $file);
  118. if ($isQti) {
  119. $result = qti_parse_file($baseWorkDir, '', $file);
  120. $filePath = $baseWorkDir . '/' . $file;
  121. $file_found = true;
  122. } else {
  123. $isManifest = isQtiManifest($baseWorkDir . '/' . $file);
  124. if ($isManifest) {
  125. $resourcesLinks = qtiProcessManifest($baseWorkDir . '/' . $file);
  126. }
  127. }
  128. }
  129. }
  130. if (!$file_found) {
  131. return 'NoXMLFileFoundInTheZip';
  132. }
  133. if ($result == false) {
  134. return false;
  135. }
  136. $doc = new DOMDocument();
  137. $doc->load($filePath);
  138. // 1. Create exercise.
  139. $exercise = new Exercise();
  140. $exercise->exercise = $exercise_info['name'];
  141. if (!empty($exercise_info['description'])) {
  142. $exercise->updateDescription($exercise_info['description']);
  143. }
  144. $exercise->save();
  145. $last_exercise_id = $exercise->selectId();
  146. if (!empty($last_exercise_id)) {
  147. // For each question found...
  148. foreach ($exercise_info['question'] as $question_array) {
  149. //2. Create question
  150. $question = new Ims2Question();
  151. $question->type = $question_array['type'];
  152. if (empty($question->type)) {
  153. // If the type was not provided, assume this is a multiple choice, unique answer type (the most basic)
  154. $question->type = MCUA;
  155. }
  156. $question->setAnswer();
  157. $description = '';
  158. if (strlen($question_array['title']) < 50) {
  159. $question->updateTitle(formatText(strip_tags($question_array['title'])) . '...');
  160. } else {
  161. $question->updateTitle(formatText(substr(strip_tags($question_array['title']), 0, 50)));
  162. $description .= $question_array['title'];
  163. }
  164. if (!empty($question_array['description'])) {
  165. $description .= $question_array['description'];
  166. }
  167. $question->updateDescription($description);
  168. $question->save($last_exercise_id);
  169. $last_question_id = $question->selectId();
  170. //3. Create answer
  171. $answer = new Answer($last_question_id);
  172. $answer->new_nbrAnswers = count($question_array['answer']);
  173. $totalCorrectWeight = 0;
  174. $j = 1;
  175. $matchAnswerIds = array();
  176. foreach ($question_array['answer'] as $key => $answers) {
  177. if (preg_match('/_/', $key)) {
  178. $split = explode('_', $key);
  179. $i = $split[1];
  180. } else {
  181. $i = $j;
  182. $j++;
  183. $matchAnswerIds[$key] = $j;
  184. }
  185. // Answer
  186. $answer->new_answer[$i] = formatText($answers['value']);
  187. // Comment
  188. $answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
  189. // Position
  190. $answer->new_position[$i] = $i;
  191. // Correct answers
  192. if (in_array($key, $question_array['correct_answers'])) {
  193. $answer->new_correct[$i] = 1;
  194. } else {
  195. $answer->new_correct[$i] = 0;
  196. }
  197. $answer->new_weighting[$i] = $question_array['weighting'][$key];
  198. if ($answer->new_correct[$i]) {
  199. $totalCorrectWeight += $answer->new_weighting[$i];
  200. }
  201. }
  202. $question->updateWeighting($totalCorrectWeight);
  203. $question->save($last_exercise_id);
  204. $answer->save();
  205. }
  206. // delete the temp dir where the exercise was unzipped
  207. my_delete($baseWorkDir . $uploadPath);
  208. return $last_exercise_id;
  209. }
  210. return false;
  211. }
  212. /**
  213. * We assume the file charset is UTF8
  214. **/
  215. function formatText($text)
  216. {
  217. return api_html_entity_decode($text);
  218. }
  219. /**
  220. * Parses a given XML file and fills global arrays with the elements
  221. * @param string $exercisePath
  222. * @param string $file
  223. * @param string $questionFile
  224. * @return bool
  225. */
  226. function qti_parse_file($exercisePath, $file, $questionFile)
  227. {
  228. global $non_HTML_tag_to_avoid;
  229. global $record_item_body;
  230. global $questionTempDir;
  231. $questionTempDir = $exercisePath . '/' . $file . '/';
  232. $questionFilePath = $questionTempDir . $questionFile;
  233. if (!($fp = fopen($questionFilePath, 'r'))) {
  234. Display::addFlash(Display::return_message(get_lang('Error opening question\'s XML file'), 'error'));
  235. return false;
  236. } else {
  237. $data = fread($fp, filesize($questionFilePath));
  238. }
  239. //parse XML question file
  240. //$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
  241. $data = ChamiloApi::stripGivenTags($data, array('p', 'front'));
  242. $qtiVersion = array();
  243. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion);
  244. $qtiMainVersion = 2; //by default, assume QTI version 2
  245. if ($match) {
  246. $qtiMainVersion = $qtiVersion[1];
  247. }
  248. //used global variable start values declaration :
  249. $record_item_body = false;
  250. $non_HTML_tag_to_avoid = array(
  251. "SIMPLECHOICE",
  252. "CHOICEINTERACTION",
  253. "INLINECHOICEINTERACTION",
  254. "INLINECHOICE",
  255. "SIMPLEMATCHSET",
  256. "SIMPLEASSOCIABLECHOICE",
  257. "TEXTENTRYINTERACTION",
  258. "FEEDBACKINLINE",
  259. "MATCHINTERACTION",
  260. "ITEMBODY",
  261. "BR",
  262. "IMG"
  263. );
  264. $question_format_supported = true;
  265. $xml_parser = xml_parser_create();
  266. xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, false);
  267. if ($qtiMainVersion == 1) {
  268. xml_set_element_handler($xml_parser, 'startElementQti1', 'endElementQti1');
  269. xml_set_character_data_handler($xml_parser, 'elementDataQti1');
  270. } else {
  271. xml_set_element_handler($xml_parser, 'startElementQti2', 'endElementQti2');
  272. xml_set_character_data_handler($xml_parser, 'elementDataQti2');
  273. }
  274. if (!xml_parse($xml_parser, $data, feof($fp))) {
  275. // if reading of the xml file in not successful :
  276. // set errorFound, set error msg, break while statement
  277. $error = xml_get_error_code();
  278. Display::addFlash(
  279. Display::return_message(
  280. get_lang('Error reading XML file') . sprintf('[%d:%d]', xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser)),
  281. 'error'
  282. )
  283. );
  284. return false;
  285. }
  286. //close file
  287. fclose($fp);
  288. if (!$question_format_supported) {
  289. Display::addFlash(
  290. Display::return_message(
  291. get_lang(
  292. 'Unknown question format in file %file',
  293. array(
  294. '%file' => $questionFile,
  295. )
  296. ),
  297. 'error'
  298. )
  299. );
  300. return false;
  301. }
  302. return true;
  303. }
  304. /**
  305. * Function used by the SAX xml parser when the parser meets a opening tag
  306. *
  307. * @param object $parser xml parser created with "xml_parser_create()"
  308. * @param string $name name of the element
  309. * @param array $attributes
  310. */
  311. function startElementQti2($parser, $name, $attributes)
  312. {
  313. global $element_pile;
  314. global $exercise_info;
  315. global $current_question_ident;
  316. global $current_answer_id;
  317. global $current_match_set;
  318. global $currentAssociableChoice;
  319. global $current_question_item_body;
  320. global $record_item_body;
  321. global $non_HTML_tag_to_avoid;
  322. global $current_inlinechoice_id;
  323. global $cardinality;
  324. global $questionTempDir;
  325. array_push($element_pile, $name);
  326. $current_element = end($element_pile);
  327. if (sizeof($element_pile) >= 2) {
  328. $parent_element = $element_pile[sizeof($element_pile) - 2];
  329. } else {
  330. $parent_element = "";
  331. }
  332. if ($record_item_body) {
  333. if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
  334. $current_question_item_body .= "<" . $name;
  335. foreach ($attributes as $attribute_name => $attribute_value) {
  336. $current_question_item_body .= " " . $attribute_name . "=\"" . $attribute_value . "\"";
  337. }
  338. $current_question_item_body .= ">";
  339. } else {
  340. //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
  341. //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
  342. if ($current_element == 'INLINECHOICEINTERACTION') {
  343. $current_question_item_body .= "**claroline_start**" . $attributes['RESPONSEIDENTIFIER'] . "**claroline_end**";
  344. }
  345. if ($current_element == 'TEXTENTRYINTERACTION') {
  346. $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  347. $current_question_item_body .= "[" . $correct_answer_value . "]";
  348. }
  349. if ($current_element == 'BR') {
  350. $current_question_item_body .= "<br />";
  351. }
  352. }
  353. }
  354. switch ($current_element) {
  355. case 'ASSESSMENTITEM':
  356. //retrieve current question
  357. $current_question_ident = $attributes['IDENTIFIER'];
  358. $exercise_info['question'][$current_question_ident] = array();
  359. $exercise_info['question'][$current_question_ident]['answer'] = array();
  360. $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
  361. $exercise_info['question'][$current_question_ident]['title'] = $attributes['TITLE'];
  362. $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
  363. break;
  364. case 'SECTION':
  365. //retrieve exercise name
  366. if (isset($attributes['TITLE']) && !empty($attributes['TITLE'])) {
  367. $exercise_info['name'] = $attributes['TITLE'];
  368. }
  369. break;
  370. case 'RESPONSEDECLARATION':
  371. // Retrieve question type
  372. if ("multiple" == $attributes['CARDINALITY']) {
  373. $exercise_info['question'][$current_question_ident]['type'] = MCMA;
  374. $cardinality = 'multiple';
  375. }
  376. if ("single" == $attributes['CARDINALITY']) {
  377. $exercise_info['question'][$current_question_ident]['type'] = MCUA;
  378. $cardinality = 'single';
  379. }
  380. //needed for FIB
  381. $current_answer_id = $attributes['IDENTIFIER'];
  382. break;
  383. case 'INLINECHOICEINTERACTION':
  384. $exercise_info['question'][$current_question_ident]['type'] = FIB;
  385. $exercise_info['question'][$current_question_ident]['subtype'] = 'LISTBOX_FILL';
  386. $current_answer_id = $attributes['RESPONSEIDENTIFIER'];
  387. break;
  388. case 'INLINECHOICE':
  389. $current_inlinechoice_id = $attributes['IDENTIFIER'];
  390. break;
  391. case 'TEXTENTRYINTERACTION':
  392. $exercise_info['question'][$current_question_ident]['type'] = FIB;
  393. $exercise_info['question'][$current_question_ident]['subtype'] = 'TEXTFIELD_FILL';
  394. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  395. //replace claroline tags
  396. break;
  397. case 'MATCHINTERACTION':
  398. $exercise_info['question'][$current_question_ident]['type'] = MATCHING;
  399. break;
  400. case 'SIMPLEMATCHSET':
  401. if (!isset($current_match_set)) {
  402. $current_match_set = 1;
  403. } else {
  404. $current_match_set++;
  405. }
  406. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set] = array();
  407. break;
  408. case 'SIMPLEASSOCIABLECHOICE':
  409. $currentAssociableChoice = $attributes['IDENTIFIER'];
  410. break;
  411. //retrieve answers id for MCUA and MCMA questions
  412. case 'SIMPLECHOICE':
  413. $current_answer_id = $attributes['IDENTIFIER'];
  414. if (!isset($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id])) {
  415. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = array();
  416. }
  417. break;
  418. case 'MAPENTRY':
  419. if ($parent_element == "MAPPING") {
  420. $answer_id = $attributes['MAPKEY'];
  421. if (!isset ($exercise_info['question'][$current_question_ident]['weighting'])) {
  422. $exercise_info['question'][$current_question_ident]['weighting'] = array();
  423. }
  424. $exercise_info['question'][$current_question_ident]['weighting'][$answer_id] = $attributes['MAPPEDVALUE'];
  425. }
  426. break;
  427. case 'MAPPING':
  428. if (isset ($attributes['DEFAULTVALUE'])) {
  429. $exercise_info['question'][$current_question_ident]['default_weighting'] = $attributes['DEFAULTVALUE'];
  430. }
  431. case 'ITEMBODY':
  432. $record_item_body = true;
  433. $current_question_item_body = '';
  434. break;
  435. case 'IMG':
  436. $exercise_info['question'][$current_question_ident]['attached_file_url'] = $attributes['SRC'];
  437. break;
  438. }
  439. }
  440. /**
  441. * Function used by the SAX xml parser when the parser meets a closing tag
  442. *
  443. * @param $parser xml parser created with "xml_parser_create()"
  444. * @param $name name of the element
  445. */
  446. function endElementQti2($parser, $name)
  447. {
  448. global $element_pile;
  449. global $exercise_info;
  450. global $current_question_ident;
  451. global $record_item_body;
  452. global $current_question_item_body;
  453. global $non_HTML_tag_to_avoid;
  454. global $cardinality;
  455. array_push($element_pile, $name);
  456. $current_element = end($element_pile);
  457. if (sizeof($element_pile) >= 2) {
  458. $parent_element = $element_pile[sizeof($element_pile) - 2];
  459. } else {
  460. $parent_element = "";
  461. }
  462. if (sizeof($element_pile) >= 3) {
  463. $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
  464. } else {
  465. $grand_parent_element = "";
  466. }
  467. if (sizeof($element_pile) >= 4) {
  468. $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
  469. } else {
  470. $great_grand_parent_element = "";
  471. }
  472. //treat the record of the full content of itembody tag :
  473. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  474. $current_question_item_body .= "</" . $name . ">";
  475. }
  476. switch ($name) {
  477. case 'ITEMBODY':
  478. $record_item_body = false;
  479. if ($exercise_info['question'][$current_question_ident]['type'] == FIB) {
  480. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  481. } else {
  482. $exercise_info['question'][$current_question_ident]['statement'] = $current_question_item_body;
  483. }
  484. break;
  485. }
  486. array_pop($element_pile);
  487. }
  488. /**
  489. * @param $parser
  490. * @param $data
  491. */
  492. function elementDataQti2($parser, $data)
  493. {
  494. global $element_pile;
  495. global $exercise_info;
  496. global $current_question_ident;
  497. global $current_answer_id;
  498. global $current_match_set;
  499. global $currentAssociableChoice;
  500. global $current_question_item_body;
  501. global $record_item_body;
  502. global $non_HTML_tag_to_avoid;
  503. global $current_inlinechoice_id;
  504. global $cardinality;
  505. global $resourcesLinks;
  506. $current_element = end($element_pile);
  507. if (sizeof($element_pile) >= 2) {
  508. $parent_element = $element_pile[sizeof($element_pile) - 2];
  509. } else {
  510. $parent_element = "";
  511. }
  512. if (sizeof($element_pile) >= 3) {
  513. $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
  514. } else {
  515. $grand_parent_element = "";
  516. }
  517. if (sizeof($element_pile) >= 4) {
  518. $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
  519. } else {
  520. $great_grand_parent_element = "";
  521. }
  522. //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
  523. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  524. $current_question_item_body .= $data;
  525. }
  526. switch ($current_element) {
  527. case 'SIMPLECHOICE':
  528. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'])) {
  529. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] = trim($data);
  530. } else {
  531. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] .= '' . trim($data);
  532. }
  533. break;
  534. case 'FEEDBACKINLINE':
  535. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'])) {
  536. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] = trim($data);
  537. } else {
  538. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] .= ' ' . trim($data);
  539. }
  540. break;
  541. case 'SIMPLEASSOCIABLECHOICE':
  542. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set][$currentAssociableChoice] = trim($data);
  543. break;
  544. case 'VALUE':
  545. if ($parent_element == "CORRECTRESPONSE") {
  546. if ($cardinality == "single") {
  547. $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id] = $data;
  548. } else {
  549. $exercise_info['question'][$current_question_ident]['correct_answers'][] = $data;
  550. }
  551. }
  552. break;
  553. case 'ITEMBODY':
  554. // Replace relative links by links to the documents in the course
  555. // $resourcesLinks is only defined by qtiProcessManifest()
  556. if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
  557. foreach ($resourcesLinks['manifest'] as $key => $value) {
  558. $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data);
  559. }
  560. }
  561. $current_question_item_body .= $data;
  562. break;
  563. case 'INLINECHOICE':
  564. // if this is the right answer, then we must replace the claroline tags in the FIB text bye the answer between "[" and "]" :
  565. $answer_identifier = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  566. if ($current_inlinechoice_id == $answer_identifier) {
  567. $current_question_item_body = str_replace(
  568. "**claroline_start**" . $current_answer_id . "**claroline_end**",
  569. "[" . $data . "]",
  570. $current_question_item_body
  571. );
  572. } else {
  573. if (!isset ($exercise_info['question'][$current_question_ident]['wrong_answers'])) {
  574. $exercise_info['question'][$current_question_ident]['wrong_answers'] = array();
  575. }
  576. $exercise_info['question'][$current_question_ident]['wrong_answers'][] = $data;
  577. }
  578. break;
  579. case 'MATTEXT':
  580. if ($grand_parent_element == 'FLOW_MAT' && ($great_grand_parent_element == 'PRESENTATION_MATERIAL' || $great_grand_parent_element == 'SECTION')) {
  581. if (!empty(trim($data))) {
  582. $exercise_info['description'] = $data;
  583. }
  584. }
  585. }
  586. }
  587. /**
  588. * Function used by the SAX xml parser when the parser meets a opening tag for QTI1
  589. *
  590. * @param object $parser xml parser created with "xml_parser_create()"
  591. * @param string $name name of the element
  592. * @param array $attributes
  593. */
  594. function startElementQti1($parser, $name, $attributes)
  595. {
  596. global $element_pile;
  597. global $exercise_info;
  598. global $current_question_ident;
  599. global $current_answer_id;
  600. global $current_match_set;
  601. global $currentAssociableChoice;
  602. global $current_question_item_body;
  603. global $record_item_body;
  604. global $non_HTML_tag_to_avoid;
  605. global $current_inlinechoice_id;
  606. global $cardinality;
  607. global $questionTempDir;
  608. global $lastLabelFieldName;
  609. global $lastLabelFieldValue;
  610. array_push($element_pile, $name);
  611. $current_element = end($element_pile);
  612. if (sizeof($element_pile) >= 2) {
  613. $parent_element = $element_pile[sizeof($element_pile) - 2];
  614. } else {
  615. $parent_element = "";
  616. }
  617. if (sizeof($element_pile) >= 3) {
  618. $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
  619. } else {
  620. $grand_parent_element = "";
  621. }
  622. if (sizeof($element_pile) >= 4) {
  623. $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
  624. } else {
  625. $great_grand_parent_element = "";
  626. }
  627. if ($record_item_body) {
  628. if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
  629. $current_question_item_body .= "<" . $name;
  630. foreach ($attributes as $attribute_name => $attribute_value) {
  631. $current_question_item_body .= " " . $attribute_name . "=\"" . $attribute_value . "\"";
  632. }
  633. $current_question_item_body .= ">";
  634. } else {
  635. //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
  636. //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
  637. if ($current_element == 'INLINECHOICEINTERACTION') {
  638. $current_question_item_body .= "**claroline_start**" . $attributes['RESPONSEIDENTIFIER'] . "**claroline_end**";
  639. }
  640. if ($current_element == 'TEXTENTRYINTERACTION') {
  641. $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  642. $current_question_item_body .= "[" . $correct_answer_value . "]";
  643. }
  644. if ($current_element == 'BR') {
  645. $current_question_item_body .= "<br />";
  646. }
  647. }
  648. }
  649. switch ($current_element) {
  650. case 'ASSESSMENT':
  651. // This is the assessment element: we don't care, we just want questions
  652. if (!empty($attributes['TITLE'])) {
  653. $exercise_info['name'] = $attributes['TITLE'];
  654. }
  655. break;
  656. case 'ITEM':
  657. //retrieve current question
  658. $current_question_ident = $attributes['IDENT'];
  659. $exercise_info['question'][$current_question_ident] = array();
  660. $exercise_info['question'][$current_question_ident]['answer'] = array();
  661. $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
  662. $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
  663. break;
  664. case 'SECTION':
  665. break;
  666. case 'RESPONSE_LID':
  667. // Retrieve question type
  668. if ("multiple" == strtolower($attributes['RCARDINALITY'])) {
  669. $cardinality = 'multiple';
  670. }
  671. if ("single" == strtolower($attributes['RCARDINALITY'])) {
  672. $cardinality = 'single';
  673. }
  674. //needed for FIB
  675. $current_answer_id = $attributes['IDENT'];
  676. $current_question_item_body = '';
  677. break;
  678. case 'RENDER_CHOICE';
  679. break;
  680. case 'RESPONSE_LABEL':
  681. if (!empty($attributes['IDENT'])) {
  682. $current_answer_id = $attributes['IDENT'];
  683. //set the placeholder for the answer to come (in endElementQti1)
  684. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = '';
  685. }
  686. break;
  687. case 'DECVAR':
  688. if ($parent_element == 'OUTCOMES' && $grand_parent_element == 'RESPROCESSING') {
  689. // The following attributes are available
  690. //$attributes['VARTYPE'];
  691. //$attributes['DEFAULTVAL'];
  692. //$attributes['MINVALUE'];
  693. //$attributes['MAXVALUE'];
  694. }
  695. break;
  696. case 'VAREQUAL':
  697. if ($parent_element == 'CONDITIONVAR' && $grand_parent_element == 'RESPCONDITION') {
  698. // The following attributes are available
  699. //$attributes['RESPIDENT']
  700. }
  701. break;
  702. case 'SETVAR':
  703. if ($parent_element == 'RESPCONDITION') {
  704. // The following attributes are available
  705. //$attributes['ACTION']
  706. }
  707. break;
  708. case 'IMG':
  709. break;
  710. case 'MATTEXT':
  711. if ($parent_element == 'MATERIAL') {
  712. if ($grand_parent_element == 'PRESENTATION') {
  713. $exercise_info['question'][$current_question_ident]['title'] = $current_question_item_body;
  714. }
  715. }
  716. }
  717. }
  718. /**
  719. * Function used by the SAX xml parser when the parser meets a closing tag for QTI1
  720. *
  721. * @param object $parser xml parser created with "xml_parser_create()"
  722. * @param string $name name of the element
  723. * @param array $attributes The element attributes
  724. */
  725. function endElementQti1($parser, $name, $attributes)
  726. {
  727. global $element_pile;
  728. global $exercise_info;
  729. global $current_question_ident;
  730. global $record_item_body;
  731. global $current_question_item_body;
  732. global $non_HTML_tag_to_avoid;
  733. global $cardinality;
  734. global $lastLabelFieldName;
  735. global $lastLabelFieldValue;
  736. global $resourcesLinks;
  737. $current_element = end($element_pile);
  738. if (sizeof($element_pile) >= 2) {
  739. $parent_element = $element_pile[sizeof($element_pile) - 2];
  740. } else {
  741. $parent_element = "";
  742. }
  743. if (sizeof($element_pile) >= 3) {
  744. $grand_parent_element = $element_pile[sizeof($element_pile) - 3];
  745. } else {
  746. $grand_parent_element = "";
  747. }
  748. if (sizeof($element_pile) >= 4) {
  749. $great_grand_parent_element = $element_pile[sizeof($element_pile) - 4];
  750. } else {
  751. $great_grand_parent_element = "";
  752. }
  753. //treat the record of the full content of itembody tag :
  754. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  755. $current_question_item_body .= "</" . $name . ">";
  756. }
  757. switch ($name) {
  758. case 'MATTEXT':
  759. if ($parent_element == 'MATERIAL') {
  760. // For some reason an item in a hierarchy <item><presentation><material><mattext> doesn't seem to
  761. // catch the grandfather 'presentation', so we check for 'item' as a patch (great-grand-father)
  762. if ($grand_parent_element == 'PRESENTATION' OR $grand_parent_element == 'ITEM') {
  763. $exercise_info['question'][$current_question_ident]['title'] = $current_question_item_body;
  764. $current_question_item_body = '';
  765. } elseif ($grand_parent_element == 'RESPONSE_LABEL') {
  766. $last = '';
  767. foreach ($exercise_info['question'][$current_question_ident]['answer'] as $key => $value) {
  768. $last = $key;
  769. }
  770. $exercise_info['question'][$current_question_ident]['answer'][$last]['value'] = $current_question_item_body;
  771. $current_question_item_body = '';
  772. }
  773. }
  774. case 'RESPONSE_LID':
  775. // Retrieve question type
  776. if (!isset($exercise_info['question'][$current_question_ident]['type'])) {
  777. if ("multiple" == strtolower($attributes['RCARDINALITY'])) {
  778. $exercise_info['question'][$current_question_ident]['type'] = MCMA;
  779. }
  780. if ("single" == strtolower($attributes['RCARDINALITY'])) {
  781. $exercise_info['question'][$current_question_ident]['type'] = MCUA;
  782. }
  783. }
  784. $current_question_item_body = '';
  785. //needed for FIB
  786. $current_answer_id = $attributes['IDENT'];
  787. break;
  788. case 'ITEMMETADATA':
  789. $current_question_item_body = '';
  790. break;
  791. }
  792. array_pop($element_pile);
  793. }
  794. /**
  795. * QTI1 element parser
  796. * @param $parser
  797. * @param $data
  798. */
  799. function elementDataQti1($parser, $data)
  800. {
  801. global $element_pile;
  802. global $exercise_info;
  803. global $current_question_ident;
  804. global $current_answer_id;
  805. global $current_match_set;
  806. global $currentAssociableChoice;
  807. global $current_question_item_body;
  808. global $record_item_body;
  809. global $non_HTML_tag_to_avoid;
  810. global $current_inlinechoice_id;
  811. global $cardinality;
  812. global $lastLabelFieldName;
  813. global $lastLabelFieldValue;
  814. global $resourcesLinks;
  815. $current_element = end($element_pile);
  816. if (sizeof($element_pile) >= 2) {
  817. $parent_element = $element_pile[sizeof($element_pile) - 2];
  818. } else {
  819. $parent_element = "";
  820. }
  821. //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
  822. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  823. $current_question_item_body .= $data;
  824. }
  825. switch ($current_element) {
  826. case 'FIELDLABEL':
  827. if (!empty($data)) {
  828. $lastLabelFieldName = $current_element;
  829. $lastLabelFieldValue = $data;
  830. }
  831. case 'FIELDENTRY':
  832. $current_question_item_body = $data;
  833. switch ($lastLabelFieldValue) {
  834. case 'cc_profile':
  835. // The following values might be proprietary in MATRIX software. No specific reference
  836. // in QTI doc: http://www.imsglobal.org/question/qtiv1p2/imsqti_asi_infov1p2.html#1415855
  837. switch ($data) {
  838. case 'cc.true_false.v0p1':
  839. //this is a true-false question (translated to multiple choice in Chamilo because true-false comes with "I don't know")
  840. $exercise_info['question'][$current_question_ident]['type'] = MCUA;
  841. break;
  842. case 'cc.multiple_choice.v0p1':
  843. //this is a multiple choice (unique answer) question
  844. $exercise_info['question'][$current_question_ident]['type'] = MCUA;
  845. break;
  846. case 'cc.multiple_response.v0p1':
  847. //this is a multiple choice (unique answer) question
  848. $exercise_info['question'][$current_question_ident]['type'] = MCMA;
  849. break;
  850. }
  851. break;
  852. case 'cc_weighting':
  853. //defines the total weight of the question
  854. $exercise_info['question'][$current_question_ident]['default_weighting'] = $lastLabelFieldValue;
  855. break;
  856. case 'assessment_question_identifierref':
  857. //placeholder - not used yet
  858. // Possible values are not defined by qti v1.2
  859. break;
  860. }
  861. break;
  862. case 'MATTEXT':
  863. // Replace relative links by links to the documents in the course
  864. // $resourcesLinks is only defined by qtiProcessManifest()
  865. if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
  866. foreach ($resourcesLinks['manifest'] as $key=>$value) {
  867. $data = preg_replace('|' . $value . '|', $resourcesLinks['web'][$key], $data);
  868. }
  869. }
  870. if (!empty($current_question_item_body)) {
  871. $current_question_item_body .= $data;
  872. } else {
  873. $current_question_item_body = $data;
  874. }
  875. break;
  876. case 'VAREQUAL':
  877. $lastLabelFieldName = 'VAREQUAL';
  878. $lastLabelFieldValue = $data;
  879. break;
  880. case 'SETVAR':
  881. if ($parent_element == 'RESPCONDITION') {
  882. // The following attributes are available
  883. //$attributes['ACTION']
  884. $exercise_info['question'][$current_question_ident]['correct_answers'][] = $lastLabelFieldValue;
  885. $exercise_info['question'][$current_question_ident]['weighting'][$lastLabelFieldValue] = $data;
  886. }
  887. break;
  888. }
  889. }
  890. /**
  891. * Check if a given file is an IMS/QTI question bank file
  892. * @param string $filePath The absolute filepath
  893. * @return bool Whether it is an IMS/QTI question bank or not
  894. */
  895. function isQtiQuestionBank($filePath)
  896. {
  897. $data = file_get_contents($filePath);
  898. if (!empty($data)) {
  899. $match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
  900. if ($match) {
  901. return true;
  902. }
  903. }
  904. return false;
  905. }
  906. /**
  907. * Check if a given file is an IMS/QTI manifest file (listing of extra files)
  908. * @param string $filePath The absolute filepath
  909. * @return bool Whether it is an IMS/QTI manifest file or not
  910. */
  911. function isQtiManifest($filePath)
  912. {
  913. $data = file_get_contents($filePath);
  914. if (!empty($data)) {
  915. $match = preg_match('/imsccv(\d)p(\d)/', $data);
  916. if ($match) {
  917. return true;
  918. }
  919. }
  920. return false;
  921. }
  922. /**
  923. * Processes an IMS/QTI manifest file: store links to new files to be able to transform them into the questions text
  924. * @param string $filePath The absolute filepath
  925. * @param array $links List of filepaths changes
  926. * @return bool
  927. */
  928. function qtiProcessManifest($filePath)
  929. {
  930. $xml = simplexml_load_file($filePath);
  931. $course = api_get_course_info();
  932. $sessionId = api_get_session_id();
  933. $courseDir = $course['path'];
  934. $sysPath = api_get_path(SYS_COURSE_PATH);
  935. $exercisesSysPath = $sysPath . $courseDir . '/document/';
  936. $webPath = api_get_path(WEB_CODE_PATH);
  937. $exercisesWebPath = $webPath . 'document/document.php?' . api_get_cidreq() . '&action=download&id=';
  938. $links = array(
  939. 'manifest' => array(),
  940. 'system' => array(),
  941. 'web' => array(),
  942. );
  943. $tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
  944. $countResources = count($xml->resources->resource->file);
  945. for ($i=0; $i < $countResources; $i++) {
  946. $file = $xml->resources->resource->file[$i];
  947. $href = '';
  948. foreach ($file->attributes() as $key => $value) {
  949. if ($key == 'href') {
  950. if (substr($value, -3, 3) != 'xml') {
  951. $href = $value;
  952. }
  953. }
  954. }
  955. if (!empty($href)) {
  956. $links['manifest'][] = (string) $href;
  957. $links['system'][] = $exercisesSysPath . strtolower($href);
  958. $specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
  959. $specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
  960. $sql = "SELECT iid FROM " . $tableDocuments . " WHERE c_id = " . $course['real_id'] . " AND session_id = $sessionId AND path = '/" . $specialHref . "'";
  961. $result = Database::query($sql);
  962. $documentId = 0;
  963. while ($row = Database::fetch_assoc($result)) {
  964. $documentId = $row['iid'];
  965. }
  966. $links['web'][] = $exercisesWebPath . $documentId;
  967. }
  968. }
  969. return $links;
  970. }