exercise_import.inc.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <?php
  2. /**
  3. * @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
  4. *
  5. * @license http://www.gnu.org/copyleft/gpl.html (GPL) GENERAL PUBLIC LICENSE
  6. *
  7. * @package chamilo.exercise
  8. *
  9. * @author claro team <cvs@claroline.net>
  10. * @author Guillaume Lederer <guillaume@claroline.net>
  11. */
  12. /**
  13. * function to create a temporary directory (SAME AS IN MODULE ADMIN)
  14. */
  15. function tempdir($dir, $prefix = 'tmp', $mode = 0777)
  16. {
  17. if (substr($dir, -1) != '/') {
  18. $dir .= '/';
  19. }
  20. do {
  21. $path = $dir . $prefix . mt_rand(0, 9999999);
  22. } while (!mkdir($path, $mode));
  23. return $path;
  24. }
  25. /**
  26. * Unzip the exercise in the temp folder
  27. * @param string The path of the temporary directory where the exercise was uploaded and unzipped
  28. * @param string
  29. * @return bool
  30. */
  31. function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
  32. {
  33. $_course = api_get_course_info();
  34. $_user = api_get_user_info();
  35. //Check if the file is valid (not to big and exists)
  36. if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
  37. // upload failed
  38. return false;
  39. }
  40. if (preg_match('/.zip$/i', $_FILES['userFile']['name']) &&
  41. handle_uploaded_document(
  42. $_course,
  43. $_FILES['userFile'],
  44. $baseWorkDir,
  45. $uploadPath,
  46. $_user['user_id'],
  47. 0,
  48. null,
  49. 1
  50. )
  51. ) {
  52. return true;
  53. }
  54. return false;
  55. }
  56. /**
  57. * Imports an exercise in QTI format if the XML structure can be found in it
  58. * @param array $file
  59. * @return an array as a backlog of what was really imported, and error or debug messages to display
  60. */
  61. function import_exercise($file)
  62. {
  63. global $exercise_info;
  64. global $element_pile;
  65. global $non_HTML_tag_to_avoid;
  66. global $record_item_body;
  67. // used to specify the question directory where files could be found in relation in any question
  68. global $questionTempDir;
  69. $archive_path = api_get_path(SYS_ARCHIVE_PATH) . 'qti2';
  70. $baseWorkDir = $archive_path;
  71. if (!is_dir($baseWorkDir)) {
  72. mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
  73. }
  74. $uploadPath = '/';
  75. // set some default values for the new exercise
  76. $exercise_info = array();
  77. $exercise_info['name'] = preg_replace('/.zip$/i', '', $file);
  78. $exercise_info['question'] = array();
  79. $element_pile = array();
  80. // create parser and array to retrieve info from manifest
  81. $element_pile = array(); //pile to known the depth in which we are
  82. //$module_info = array (); //array to store the info we need
  83. // if file is not a .zip, then we cancel all
  84. if (!preg_match('/.zip$/i', $file)) {
  85. return 'UplZipCorrupt';
  86. }
  87. // unzip the uploaded file in a tmp directory
  88. if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
  89. return 'UplZipCorrupt';
  90. }
  91. // find the different manifests for each question and parse them.
  92. $exerciseHandle = opendir($baseWorkDir);
  93. //$question_number = 0;
  94. $file_found = false;
  95. $operation = false;
  96. $result = false;
  97. $filePath = null;
  98. // parse every subdirectory to search xml question files
  99. while (false !== ($file = readdir($exerciseHandle))) {
  100. if (is_dir($baseWorkDir . '/' . $file) && $file != "." && $file != "..") {
  101. // Find each manifest for each question repository found
  102. $questionHandle = opendir($baseWorkDir . '/' . $file);
  103. while (false !== ($questionFile = readdir($questionHandle))) {
  104. if (preg_match('/.xml$/i', $questionFile)) {
  105. $result = parse_file($baseWorkDir, $file, $questionFile);
  106. $filePath = $baseWorkDir . $file;
  107. $file_found = true;
  108. }
  109. }
  110. } elseif (preg_match('/.xml$/i', $file)) {
  111. // Else ignore file
  112. $result = parse_file($baseWorkDir, '', $file);
  113. $filePath = $baseWorkDir . '/' . $file;
  114. $file_found = true;
  115. }
  116. }
  117. if (!$file_found) {
  118. return 'No XML file found in the zip';
  119. }
  120. if ($result == false) {
  121. return false;
  122. }
  123. $doc = new DOMDocument();
  124. $doc->load($filePath);
  125. $encoding = $doc->encoding;
  126. // 1. Create exercise.
  127. $exercise = new Exercise();
  128. $exercise->exercise = $exercise_info['name'];
  129. $exercise->save();
  130. $last_exercise_id = $exercise->selectId();
  131. if (!empty($last_exercise_id)) {
  132. // For each question found...
  133. foreach ($exercise_info['question'] as $question_array) {
  134. //2. Create question
  135. $question = new Ims2Question();
  136. $question->type = $question_array['type'];
  137. $question->setAnswer();
  138. $question->updateTitle(formatText($question_array['title']));
  139. //$question->updateDescription($question_array['title']);
  140. $type = $question->selectType();
  141. $question->type = constant($type);
  142. $question->save($last_exercise_id);
  143. $last_question_id = $question->selectId();
  144. //3. Create answer
  145. $answer = new Answer($last_question_id);
  146. $answer->new_nbrAnswers = count($question_array['answer']);
  147. $totalCorrectWeight = 0;
  148. foreach ($question_array['answer'] as $key => $answers) {
  149. $split = explode('_', $key);
  150. $i = $split[1];
  151. // Answer
  152. $answer->new_answer[$i] = formatText($answers['value']);
  153. // Comment
  154. $answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
  155. // Position
  156. $answer->new_position[$i] = $i;
  157. // Correct answers
  158. if (in_array($key, $question_array['correct_answers'])) {
  159. $answer->new_correct[$i] = 1;
  160. } else {
  161. $answer->new_correct[$i] = 0;
  162. }
  163. $answer->new_weighting[$i] = $question_array['weighting'][$key];
  164. if ($answer->new_correct[$i]) {
  165. $totalCorrectWeight = $answer->new_weighting[$i];
  166. }
  167. }
  168. $question->updateWeighting($totalCorrectWeight);
  169. $question->save($last_exercise_id);
  170. $answer->save();
  171. }
  172. // delete the temp dir where the exercise was unzipped
  173. my_delete($baseWorkDir . $uploadPath);
  174. return $last_exercise_id;
  175. }
  176. return false;
  177. }
  178. /**
  179. * We assume the file charset is UTF8
  180. **/
  181. function formatText($text)
  182. {
  183. return api_html_entity_decode($text);
  184. }
  185. /**
  186. * Parses a given XML file and fills global arrays with the elements
  187. * @param $exercisePath
  188. * @param $file
  189. * @param $questionFile
  190. * @return bool
  191. */
  192. function parse_file($exercisePath, $file, $questionFile)
  193. {
  194. global $non_HTML_tag_to_avoid;
  195. global $record_item_body;
  196. global $questionTempDir;
  197. $questionTempDir = $exercisePath . '/' . $file . '/';
  198. $questionFilePath = $questionTempDir . $questionFile;
  199. if (!($fp = fopen($questionFilePath, 'r'))) {
  200. Display:: display_error_message(get_lang('Error opening question\'s XML file'));
  201. return false;
  202. } else {
  203. $data = fread($fp, filesize($questionFilePath));
  204. }
  205. //parse XML question file
  206. $data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
  207. //used global variable start values declaration :
  208. $record_item_body = false;
  209. $non_HTML_tag_to_avoid = array(
  210. "SIMPLECHOICE",
  211. "CHOICEINTERACTION",
  212. "INLINECHOICEINTERACTION",
  213. "INLINECHOICE",
  214. "SIMPLEMATCHSET",
  215. "SIMPLEASSOCIABLECHOICE",
  216. "TEXTENTRYINTERACTION",
  217. "FEEDBACKINLINE",
  218. "MATCHINTERACTION",
  219. "ITEMBODY",
  220. "BR",
  221. "IMG"
  222. );
  223. $question_format_supported = true;
  224. $xml_parser = xml_parser_create();
  225. xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, false);
  226. xml_set_element_handler($xml_parser, 'startElement', 'endElement');
  227. xml_set_character_data_handler($xml_parser, 'elementData');
  228. if (!xml_parse($xml_parser, $data, feof($fp))) {
  229. // if reading of the xml file in not successful :
  230. // set errorFound, set error msg, break while statement
  231. Display:: display_error_message(get_lang('Error reading XML file'));
  232. return false;
  233. }
  234. //close file
  235. fclose($fp);
  236. if (!$question_format_supported) {
  237. Display:: display_error_message(
  238. get_lang(
  239. 'Unknown question format in file %file',
  240. array(
  241. '%file' => $questionFile
  242. )
  243. )
  244. );
  245. return false;
  246. }
  247. return true;
  248. }
  249. /**
  250. * Function used by the SAX xml parser when the parser meets a opening tag
  251. *
  252. * @param object $parser xml parser created with "xml_parser_create()"
  253. * @param string $name name of the element
  254. * @param array $attributes
  255. */
  256. function startElement($parser, $name, $attributes)
  257. {
  258. global $element_pile;
  259. global $exercise_info;
  260. global $current_question_ident;
  261. global $current_answer_id;
  262. global $current_match_set;
  263. global $currentAssociableChoice;
  264. global $current_question_item_body;
  265. global $record_item_body;
  266. global $non_HTML_tag_to_avoid;
  267. global $current_inlinechoice_id;
  268. global $cardinality;
  269. global $questionTempDir;
  270. array_push($element_pile, $name);
  271. $current_element = end($element_pile);
  272. if (sizeof($element_pile) >= 2) {
  273. $parent_element = $element_pile[sizeof($element_pile) - 2];
  274. } else {
  275. $parent_element = "";
  276. }
  277. if (sizeof($element_pile) >= 3) {
  278. $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
  279. } else {
  280. $grant_parent_element = "";
  281. }
  282. if ($record_item_body) {
  283. if ((!in_array($current_element, $non_HTML_tag_to_avoid))) {
  284. $current_question_item_body .= "<" . $name;
  285. foreach ($attributes as $attribute_name => $attribute_value) {
  286. $current_question_item_body .= " " . $attribute_name . "=\"" . $attribute_value . "\"";
  287. }
  288. $current_question_item_body .= ">";
  289. } else {
  290. //in case of FIB question, we replace the IMS-QTI tag b y the correct answer between "[" "]",
  291. //we first save with claroline tags ,then when the answer will be parsed, the claroline tags will be replaced
  292. if ($current_element == 'INLINECHOICEINTERACTION') {
  293. $current_question_item_body .= "**claroline_start**" . $attributes['RESPONSEIDENTIFIER'] . "**claroline_end**";
  294. }
  295. if ($current_element == 'TEXTENTRYINTERACTION') {
  296. $correct_answer_value = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  297. $current_question_item_body .= "[" . $correct_answer_value . "]";
  298. }
  299. if ($current_element == 'BR') {
  300. $current_question_item_body .= "<br />";
  301. }
  302. }
  303. }
  304. switch ($current_element) {
  305. case 'ASSESSMENTITEM':
  306. //retrieve current question
  307. $current_question_ident = $attributes['IDENTIFIER'];
  308. $exercise_info['question'][$current_question_ident] = array();
  309. $exercise_info['question'][$current_question_ident]['answer'] = array();
  310. $exercise_info['question'][$current_question_ident]['correct_answers'] = array();
  311. $exercise_info['question'][$current_question_ident]['title'] = $attributes['TITLE'];
  312. $exercise_info['question'][$current_question_ident]['tempdir'] = $questionTempDir;
  313. break;
  314. case 'SECTION':
  315. //retrieve exercise name
  316. $exercise_info['name'] = $attributes['TITLE'];
  317. break;
  318. case 'RESPONSEDECLARATION':
  319. // Retrieve question type
  320. if ("multiple" == $attributes['CARDINALITY']) {
  321. $exercise_info['question'][$current_question_ident]['type'] = 'MCMA';
  322. $cardinality = 'multiple';
  323. }
  324. if ("single" == $attributes['CARDINALITY']) {
  325. $exercise_info['question'][$current_question_ident]['type'] = 'MCUA';
  326. $cardinality = 'single';
  327. }
  328. //needed for FIB
  329. $current_answer_id = $attributes['IDENTIFIER'];
  330. break;
  331. case 'INLINECHOICEINTERACTION':
  332. $exercise_info['question'][$current_question_ident]['type'] = 'FIB';
  333. $exercise_info['question'][$current_question_ident]['subtype'] = 'LISTBOX_FILL';
  334. $current_answer_id = $attributes['RESPONSEIDENTIFIER'];
  335. break;
  336. case 'INLINECHOICE':
  337. $current_inlinechoice_id = $attributes['IDENTIFIER'];
  338. break;
  339. case 'TEXTENTRYINTERACTION':
  340. $exercise_info['question'][$current_question_ident]['type'] = 'FIB';
  341. $exercise_info['question'][$current_question_ident]['subtype'] = 'TEXTFIELD_FILL';
  342. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  343. //replace claroline tags
  344. break;
  345. case 'MATCHINTERACTION':
  346. $exercise_info['question'][$current_question_ident]['type'] = 'MATCHING';
  347. break;
  348. case 'SIMPLEMATCHSET':
  349. if (!isset ($current_match_set)) {
  350. $current_match_set = 1;
  351. } else {
  352. $current_match_set++;
  353. }
  354. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set] = array();
  355. break;
  356. case 'SIMPLEASSOCIABLECHOICE':
  357. $currentAssociableChoice = $attributes['IDENTIFIER'];
  358. break;
  359. //retrieve answers id for MCUA and MCMA questions
  360. case 'SIMPLECHOICE':
  361. $current_answer_id = $attributes['IDENTIFIER'];
  362. if (!isset($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id])) {
  363. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id] = array();
  364. }
  365. break;
  366. case 'MAPENTRY':
  367. if ($parent_element == "MAPPING") {
  368. $answer_id = $attributes['MAPKEY'];
  369. if (!isset ($exercise_info['question'][$current_question_ident]['weighting'])) {
  370. $exercise_info['question'][$current_question_ident]['weighting'] = array();
  371. }
  372. $exercise_info['question'][$current_question_ident]['weighting'][$answer_id] = $attributes['MAPPEDVALUE'];
  373. }
  374. break;
  375. case 'MAPPING':
  376. if (isset ($attributes['DEFAULTVALUE'])) {
  377. $exercise_info['question'][$current_question_ident]['default_weighting'] = $attributes['DEFAULTVALUE'];
  378. }
  379. case 'ITEMBODY':
  380. $record_item_body = true;
  381. $current_question_item_body = '';
  382. break;
  383. case 'IMG':
  384. $exercise_info['question'][$current_question_ident]['attached_file_url'] = $attributes['SRC'];
  385. break;
  386. }
  387. }
  388. /**
  389. * Function used by the SAX xml parser when the parser meets a closing tag
  390. *
  391. * @param $parser xml parser created with "xml_parser_create()"
  392. * @param $name name of the element
  393. */
  394. function endElement($parser, $name)
  395. {
  396. global $element_pile;
  397. global $exercise_info;
  398. global $current_question_ident;
  399. global $record_item_body;
  400. global $current_question_item_body;
  401. global $non_HTML_tag_to_avoid;
  402. global $cardinality;
  403. $current_element = end($element_pile);
  404. //treat the record of the full content of itembody tag :
  405. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  406. $current_question_item_body .= "</" . $name . ">";
  407. }
  408. switch ($name) {
  409. case 'ITEMBODY':
  410. $record_item_body = false;
  411. if ($exercise_info['question'][$current_question_ident]['type'] == 'FIB') {
  412. $exercise_info['question'][$current_question_ident]['response_text'] = $current_question_item_body;
  413. } else {
  414. $exercise_info['question'][$current_question_ident]['statement'] = $current_question_item_body;
  415. }
  416. break;
  417. }
  418. array_pop($element_pile);
  419. }
  420. /**
  421. * @param $parser
  422. * @param $data
  423. */
  424. function elementData($parser, $data)
  425. {
  426. global $element_pile;
  427. global $exercise_info;
  428. global $current_question_ident;
  429. global $current_answer_id;
  430. global $current_match_set;
  431. global $currentAssociableChoice;
  432. global $current_question_item_body;
  433. global $record_item_body;
  434. global $non_HTML_tag_to_avoid;
  435. global $current_inlinechoice_id;
  436. global $cardinality;
  437. $current_element = end($element_pile);
  438. if (sizeof($element_pile) >= 2) {
  439. $parent_element = $element_pile[sizeof($element_pile) - 2];
  440. } else {
  441. $parent_element = "";
  442. }
  443. if (sizeof($element_pile) >= 3) {
  444. $grant_parent_element = $element_pile[sizeof($element_pile) - 3];
  445. } else {
  446. $grant_parent_element = "";
  447. }
  448. //treat the record of the full content of itembody tag (needed for question statment and/or FIB text:
  449. if ($record_item_body && (!in_array($current_element, $non_HTML_tag_to_avoid))) {
  450. $current_question_item_body .= $data;
  451. }
  452. switch ($current_element) {
  453. case 'SIMPLECHOICE':
  454. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'])) {
  455. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] = trim($data);
  456. } else {
  457. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['value'] .= '' . trim($data);
  458. }
  459. break;
  460. case 'FEEDBACKINLINE':
  461. if (!isset ($exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'])) {
  462. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] = trim($data);
  463. } else {
  464. $exercise_info['question'][$current_question_ident]['answer'][$current_answer_id]['feedback'] .= ' ' . trim($data);
  465. }
  466. break;
  467. case 'SIMPLEASSOCIABLECHOICE':
  468. $exercise_info['question'][$current_question_ident]['answer'][$current_match_set][$currentAssociableChoice] = trim($data);
  469. break;
  470. case 'VALUE':
  471. if ($parent_element == "CORRECTRESPONSE") {
  472. if ($cardinality == "single") {
  473. $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id] = $data;
  474. } else {
  475. $exercise_info['question'][$current_question_ident]['correct_answers'][] = $data;
  476. }
  477. }
  478. break;
  479. case 'ITEMBODY':
  480. $current_question_item_body .= $data;
  481. break;
  482. case 'INLINECHOICE':
  483. // if this is the right answer, then we must replace the claroline tags in the FIB text bye the answer between "[" and "]" :
  484. $answer_identifier = $exercise_info['question'][$current_question_ident]['correct_answers'][$current_answer_id];
  485. if ($current_inlinechoice_id == $answer_identifier) {
  486. $current_question_item_body = str_replace(
  487. "**claroline_start**" . $current_answer_id . "**claroline_end**",
  488. "[" . $data . "]",
  489. $current_question_item_body
  490. );
  491. } else {
  492. if (!isset ($exercise_info['question'][$current_question_ident]['wrong_answers'])) {
  493. $exercise_info['question'][$current_question_ident]['wrong_answers'] = array();
  494. }
  495. $exercise_info['question'][$current_question_ident]['wrong_answers'][] = $data;
  496. }
  497. break;
  498. }
  499. }