exercise_import.inc.php 41 KB

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