exercise_import.inc.php 41 KB

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