MoodleImport.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. <?php
  2. /* For licensing terms, see /license.txt */
  3. /**
  4. * Class MoodleImport
  5. *
  6. * @author José Loguercio <jose.loguercio@beeznest.com>,
  7. * @author Julio Montoya <gugli100@gmail.com>
  8. *
  9. * @package chamilo.library
  10. */
  11. class MoodleImport
  12. {
  13. /**
  14. * Import moodle file
  15. *
  16. * @param resource $uploadedFile *.* mbz file moodle course backup
  17. * @return bool
  18. */
  19. public function import($uploadedFile)
  20. {
  21. $file = $uploadedFile['tmp_name'];
  22. if (is_file($file) && is_readable($file)) {
  23. $package = new PclZip($file);
  24. $packageContent = $package->listContent();
  25. $mainFileKey = 0;
  26. foreach ($packageContent as $index => $value) {
  27. if ($value['filename'] == 'moodle_backup.xml') {
  28. $mainFileKey = $index;
  29. break;
  30. }
  31. }
  32. if (!$mainFileKey) {
  33. Display::addFlash(
  34. Display::return_message(
  35. get_lang('FailedToImportThisIsNotAMoodleFile'),
  36. 'error'
  37. )
  38. );
  39. }
  40. $folder = api_get_unique_id();
  41. $destinationDir = api_get_path(SYS_ARCHIVE_PATH) . $folder;
  42. $coursePath = api_get_course_path();
  43. $sessionId = api_get_session_id();
  44. $groupId = api_get_group_id();
  45. $documentPath = api_get_path(SYS_COURSE_PATH) . $coursePath . '/document';
  46. $courseInfo = api_get_course_info();
  47. mkdir($destinationDir, api_get_permissions_for_new_directories(), true);
  48. create_unexisting_directory(
  49. $courseInfo,
  50. api_get_user_id(),
  51. $sessionId,
  52. $groupId,
  53. null,
  54. $documentPath,
  55. '/moodle',
  56. 'Moodle Docs',
  57. 0
  58. );
  59. $package->extract(
  60. PCLZIP_OPT_PATH,
  61. $destinationDir
  62. );
  63. // This process will upload all question resource files
  64. $filesXml = @file_get_contents($destinationDir.'/files.xml');
  65. $mainFileModuleValues = $this->getAllQuestionFiles($filesXml);
  66. $currentResourceFilePath = $destinationDir.'/files/';
  67. $importedFiles = [];
  68. foreach ($mainFileModuleValues as $fileInfo) {
  69. $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
  70. foreach (new RecursiveIteratorIterator($dirs) as $file) {
  71. if (is_file($file) && strpos($file, $fileInfo['contenthash']) !== false) {
  72. $files = [];
  73. $files['file']['name'] = $fileInfo['filename'];
  74. $files['file']['tmp_name'] = $file->getPathname();
  75. $files['file']['type'] = $fileInfo['mimetype'];
  76. $files['file']['error'] = 0;
  77. $files['file']['size'] = $fileInfo['filesize'];
  78. $files['file']['from_file'] = true;
  79. $files['file']['move_file'] = true;
  80. $_POST['language'] = $courseInfo['language'];
  81. $_POST['moodle_import'] = true;
  82. $data = DocumentManager::upload_document(
  83. $files,
  84. '/moodle',
  85. isset($fileInfo['title']) ? $fileInfo['title'] : pathinfo($fileInfo['filename'], PATHINFO_FILENAME),
  86. '',
  87. null,
  88. null,
  89. true,
  90. true,
  91. 'file',
  92. // This is to validate spaces as hyphens
  93. false
  94. );
  95. if ($data) {
  96. $importedFiles[$fileInfo['filename']] = basename($data['path']);
  97. }
  98. }
  99. }
  100. }
  101. $xml = @file_get_contents($destinationDir.'/moodle_backup.xml');
  102. $doc = new DOMDocument();
  103. $res = @$doc->loadXML($xml);
  104. if ($res) {
  105. $activities = $doc->getElementsByTagName('activity');
  106. foreach ($activities as $activity) {
  107. if ($activity->childNodes->length) {
  108. $currentItem = [];
  109. foreach ($activity->childNodes as $item) {
  110. $currentItem[$item->nodeName] = $item->nodeValue;
  111. }
  112. $moduleName = isset($currentItem['modulename']) ? $currentItem['modulename'] : false;
  113. switch ($moduleName) {
  114. case 'forum':
  115. require_once '../forum/forumfunction.inc.php';
  116. $catForumValues = [];
  117. // Read the current forum module xml.
  118. $moduleDir = $currentItem['directory'];
  119. $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
  120. $moduleValues = $this->readForumModule($moduleXml);
  121. // Create a Forum category based on Moodle forum type.
  122. $catForumValues['forum_category_title'] = $moduleValues['type'];
  123. $catForumValues['forum_category_comment'] = '';
  124. $catId = store_forumcategory($catForumValues, $courseInfo, false);
  125. $forumValues = [];
  126. $forumValues['forum_title'] = $moduleValues['name'];
  127. $forumValues['forum_image'] = '';
  128. $forumValues['forum_comment'] = $moduleValues['intro'];
  129. $forumValues['forum_category'] = $catId;
  130. $forumValues['moderated'] = 0;
  131. store_forum($forumValues, $courseInfo);
  132. break;
  133. case 'quiz':
  134. // Read the current quiz module xml.
  135. // The quiz case is the very complicate process of all the import.
  136. // Please if you want to review the script, try to see the readingXML functions.
  137. // The readingXML functions in this clases do all the mayor work here.
  138. $moduleDir = $currentItem['directory'];
  139. $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
  140. $questionsXml = @file_get_contents($destinationDir.'/questions.xml');
  141. $moduleValues = $this->readQuizModule($moduleXml);
  142. // At this point we got all the prepared resources from Moodle file
  143. // $moduleValues variable contains all the necesary info to the quiz import
  144. // var_dump($moduleValues); // <-- uncomment this to see the final array
  145. $exercise = new Exercise($courseInfo['real_id']);
  146. $title = Exercise::format_title_variable($moduleValues['name']);
  147. $exercise->updateTitle($title);
  148. $exercise->updateDescription($moduleValues['intro']);
  149. $exercise->updateAttempts($moduleValues['attempts_number']);
  150. $exercise->updateFeedbackType(0);
  151. // Match shuffle question with chamilo
  152. switch ($moduleValues['shufflequestions']) {
  153. case '0':
  154. $exercise->setRandom(0);
  155. break;
  156. case '1':
  157. $exercise->setRandom(-1);
  158. break;
  159. default:
  160. $exercise->setRandom(0);
  161. }
  162. $exercise->updateRandomAnswers($moduleValues['shuffleanswers']);
  163. // @todo divide to minutes
  164. $exercise->updateExpiredTime($moduleValues['timelimit']);
  165. if ($moduleValues['questionsperpage'] == 1) {
  166. $exercise->updateType(2);
  167. } else {
  168. $exercise->updateType(1);
  169. }
  170. // Create the new Quiz
  171. $exercise->save();
  172. // Ok, we got the Quiz and create it, now its time to add the Questions
  173. foreach ($moduleValues['question_instances'] as $index => $question) {
  174. $questionsValues = $this->readMainQuestionsXml($questionsXml, $question['questionid']);
  175. $moduleValues['question_instances'][$index] = $questionsValues;
  176. // Set Question Type from Moodle XML element <qtype>
  177. $qType = $moduleValues['question_instances'][$index]['qtype'];
  178. // Add the matched chamilo question type to the array
  179. $moduleValues['question_instances'][$index]['chamilo_qtype'] = $this->matchMoodleChamiloQuestionTypes($qType);
  180. $questionInstance = Question::getInstance($moduleValues['question_instances'][$index]['chamilo_qtype']);
  181. if ($questionInstance) {
  182. $questionInstance->updateTitle($moduleValues['question_instances'][$index]['name']);
  183. $questionText = $moduleValues['question_instances'][$index]['questiontext'];
  184. // Replace the path from @@PLUGINFILE@@ to a correct chamilo path
  185. $questionText = str_replace(
  186. '@@PLUGINFILE@@',
  187. '/courses/'.$coursePath.'/document/moodle',
  188. $questionText
  189. );
  190. if ($importedFiles) {
  191. foreach ($importedFiles as $old => $new) {
  192. $questionText = str_replace($old, $new, $questionText);
  193. }
  194. }
  195. $questionInstance->updateDescription($questionText);
  196. $questionInstance->updateLevel(1);
  197. $questionInstance->updateCategory(0);
  198. //Save normal question if NOT media
  199. if ($questionInstance->type != MEDIA_QUESTION) {
  200. $questionInstance->save($exercise->id);
  201. // modify the exercise
  202. $exercise->addToList($questionInstance->id);
  203. $exercise->update_question_positions();
  204. }
  205. $questionList = $moduleValues['question_instances'][$index]['plugin_qtype_'.$qType.'_question'];
  206. $currentQuestion = $moduleValues['question_instances'][$index];
  207. $this->processAnswers(
  208. $questionList,
  209. $qType,
  210. $questionInstance,
  211. $currentQuestion,
  212. $importedFiles
  213. );
  214. }
  215. }
  216. break;
  217. case 'resource':
  218. // Read the current resource module xml.
  219. $moduleDir = $currentItem['directory'];
  220. $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
  221. $filesXml = @file_get_contents($destinationDir.'/files.xml');
  222. $moduleValues = $this->readResourceModule($moduleXml);
  223. $mainFileModuleValues = $this->readMainFilesXml($filesXml, $moduleValues['contextid']);
  224. $fileInfo = array_merge($moduleValues, $mainFileModuleValues, $currentItem);
  225. $currentResourceFilePath = $destinationDir.'/files/';
  226. $dirs = new RecursiveDirectoryIterator($currentResourceFilePath);
  227. foreach (new RecursiveIteratorIterator($dirs) as $file) {
  228. if (is_file($file) && strpos($file, $fileInfo['contenthash']) !== false) {
  229. $files = [];
  230. $files['file']['name'] = $fileInfo['filename'];
  231. $files['file']['tmp_name'] = $file->getPathname();
  232. $files['file']['type'] = $fileInfo['mimetype'];
  233. $files['file']['error'] = 0;
  234. $files['file']['size'] = $fileInfo['filesize'];
  235. $files['file']['from_file'] = true;
  236. $files['file']['move_file'] = true;
  237. $_POST['language'] = $courseInfo['language'];
  238. $_POST['moodle_import'] = true;
  239. DocumentManager::upload_document(
  240. $files,
  241. '/moodle',
  242. $fileInfo['title'],
  243. '',
  244. null,
  245. null,
  246. true,
  247. true
  248. );
  249. }
  250. }
  251. break;
  252. case 'url':
  253. // Read the current url module xml.
  254. $moduleDir = $currentItem['directory'];
  255. $moduleXml = @file_get_contents($destinationDir.'/'.$moduleDir.'/'.$moduleName.'.xml');
  256. $moduleValues = $this->readUrlModule($moduleXml);
  257. $_POST['title'] = $moduleValues['name'];
  258. $_POST['url'] = $moduleValues['externalurl'];
  259. $_POST['description'] = $moduleValues['intro'];
  260. $_POST['category_id'] = 0;
  261. $_POST['target'] = '_blank';
  262. Link::addlinkcategory("link");
  263. break;
  264. }
  265. }
  266. }
  267. } else {
  268. removeDir($destinationDir);
  269. return false;
  270. }
  271. } else {
  272. return false;
  273. }
  274. removeDir($destinationDir);
  275. return $packageContent[$mainFileKey];
  276. }
  277. /**
  278. * Read and validate the forum module XML
  279. *
  280. * @param resource $moduleXml XML file
  281. * @return mixed | array if is a valid xml file, false otherwise
  282. */
  283. public function readForumModule($moduleXml)
  284. {
  285. $moduleDoc = new DOMDocument();
  286. $moduleRes = @$moduleDoc->loadXML($moduleXml);
  287. if ($moduleRes) {
  288. $activities = $moduleDoc->getElementsByTagName('forum');
  289. $currentItem = [];
  290. foreach ($activities as $activity) {
  291. if ($activity->childNodes->length) {
  292. foreach ($activity->childNodes as $item) {
  293. $currentItem[$item->nodeName] = $item->nodeValue;
  294. }
  295. }
  296. }
  297. return $currentItem;
  298. }
  299. return false;
  300. }
  301. /**
  302. * Read and validate the resource module XML
  303. *
  304. * @param resource $moduleXml XML file
  305. * @return mixed | array if is a valid xml file, false otherwise
  306. */
  307. public function readResourceModule($moduleXml)
  308. {
  309. $moduleDoc = new DOMDocument();
  310. $moduleRes = @$moduleDoc->loadXML($moduleXml);
  311. if ($moduleRes) {
  312. $activities = $moduleDoc->getElementsByTagName('resource');
  313. $mainActivity = $moduleDoc->getElementsByTagName('activity');
  314. $contextId = $mainActivity->item(0)->getAttribute('contextid');
  315. $currentItem = [];
  316. foreach ($activities as $activity) {
  317. if ($activity->childNodes->length) {
  318. foreach ($activity->childNodes as $item) {
  319. $currentItem[$item->nodeName] = $item->nodeValue;
  320. }
  321. }
  322. }
  323. $currentItem['contextid'] = $contextId;
  324. return $currentItem;
  325. }
  326. return false;
  327. }
  328. /**
  329. * Read and validate the url module XML
  330. *
  331. * @param resource $moduleXml XML file
  332. * @return mixed | array if is a valid xml file, false otherwise
  333. */
  334. public function readUrlModule($moduleXml)
  335. {
  336. $moduleDoc = new DOMDocument();
  337. $moduleRes = @$moduleDoc->loadXML($moduleXml);
  338. if ($moduleRes) {
  339. $activities = $moduleDoc->getElementsByTagName('url');
  340. $currentItem = [];
  341. foreach ($activities as $activity) {
  342. if ($activity->childNodes->length) {
  343. foreach ($activity->childNodes as $item) {
  344. $currentItem[$item->nodeName] = $item->nodeValue;
  345. }
  346. }
  347. }
  348. return $currentItem;
  349. }
  350. return false;
  351. }
  352. /**
  353. * Read and validate the quiz module XML
  354. *
  355. * @param resource $moduleXml XML file
  356. * @return mixed | array if is a valid xml file, false otherwise
  357. */
  358. public function readQuizModule($moduleXml)
  359. {
  360. $moduleDoc = new DOMDocument();
  361. $moduleRes = @$moduleDoc->loadXML($moduleXml);
  362. if ($moduleRes) {
  363. $activities = $moduleDoc->getElementsByTagName('quiz');
  364. $currentItem = [];
  365. foreach ($activities as $activity) {
  366. if ($activity->childNodes->length) {
  367. foreach ($activity->childNodes as $item) {
  368. $currentItem[$item->nodeName] = $item->nodeValue;
  369. }
  370. }
  371. }
  372. $questions = $moduleDoc->getElementsByTagName('question_instance');
  373. $questionList = [];
  374. $counter = 0;
  375. foreach ($questions as $question) {
  376. if ($question->childNodes->length) {
  377. foreach ($question->childNodes as $item) {
  378. $questionList[$counter][$item->nodeName] = $item->nodeValue;
  379. }
  380. $counter++;
  381. }
  382. }
  383. $currentItem['question_instances'] = $questionList;
  384. return $currentItem;
  385. }
  386. return false;
  387. }
  388. /**
  389. * Search the current file resource in main Files XML
  390. *
  391. * @param resource $filesXml XML file
  392. * @param int $contextId
  393. * @return mixed | array if is a valid xml file, false otherwise
  394. */
  395. public function readMainFilesXml($filesXml, $contextId)
  396. {
  397. $moduleDoc = new DOMDocument();
  398. $moduleRes = @$moduleDoc->loadXML($filesXml);
  399. if ($moduleRes) {
  400. $activities = $moduleDoc->getElementsByTagName('file');
  401. $currentItem = [];
  402. foreach ($activities as $activity) {
  403. if ($activity->childNodes->length) {
  404. $isThisItemThatIWant = false;
  405. foreach ($activity->childNodes as $item) {
  406. if (!$isThisItemThatIWant && $item->nodeName == 'contenthash') {
  407. $currentItem['contenthash'] = $item->nodeValue;
  408. }
  409. if ($item->nodeName == 'contextid' && intval($item->nodeValue) == intval($contextId) && !$isThisItemThatIWant) {
  410. $isThisItemThatIWant = true;
  411. continue;
  412. }
  413. if ($isThisItemThatIWant && $item->nodeName == 'filename') {
  414. $currentItem['filename'] = $item->nodeValue;
  415. }
  416. if ($isThisItemThatIWant && $item->nodeName == 'filesize') {
  417. $currentItem['filesize'] = $item->nodeValue;
  418. }
  419. if ($isThisItemThatIWant && $item->nodeName == 'mimetype' && $item->nodeValue == 'document/unknown') {
  420. break;
  421. }
  422. if ($isThisItemThatIWant && $item->nodeName == 'mimetype' && $item->nodeValue !== 'document/unknown') {
  423. $currentItem['mimetype'] = $item->nodeValue;
  424. break 2;
  425. }
  426. }
  427. }
  428. }
  429. return $currentItem;
  430. }
  431. return false;
  432. }
  433. /**
  434. * Search the current question resource in main Questions XML
  435. *
  436. * @param resource $questionsXml XML file
  437. * @param int $questionId
  438. * @return mixed | array if is a valid xml file, false otherwise
  439. */
  440. public function readMainQuestionsXml($questionsXml, $questionId)
  441. {
  442. $moduleDoc = new DOMDocument();
  443. $moduleRes = @$moduleDoc->loadXML($questionsXml);
  444. if ($moduleRes) {
  445. $questions = $moduleDoc->getElementsByTagName('question');
  446. $currentItem = [];
  447. foreach ($questions as $question) {
  448. if (intval($question->getAttribute('id')) == $questionId) {
  449. if ($question->childNodes->length) {
  450. $currentItem['questionid'] = $questionId;
  451. $questionType = '';
  452. foreach ($question->childNodes as $item) {
  453. $currentItem[$item->nodeName] = $item->nodeValue;
  454. if ($item->nodeName == 'qtype') {
  455. $questionType = $item->nodeValue;
  456. }
  457. if ($item->nodeName == 'plugin_qtype_'.$questionType.'_question') {
  458. $answer = $item->getElementsByTagName($this->getQuestionTypeAnswersTag($questionType));
  459. $currentItem['plugin_qtype_'.$questionType.'_question'] = [];
  460. for ($i = 0; $i <= $answer->length - 1; $i++) {
  461. $currentItem['plugin_qtype_'.$questionType.'_question'][$i]['answerid'] = $answer->item($i)->getAttribute('id');
  462. foreach ($answer->item($i)->childNodes as $properties) {
  463. $currentItem['plugin_qtype_'.$questionType.'_question'][$i][$properties->nodeName] = $properties->nodeValue;
  464. }
  465. }
  466. $typeValues = $item->getElementsByTagName($this->getQuestionTypeOptionsTag($questionType));
  467. for ($i = 0; $i <= $typeValues->length - 1; $i++) {
  468. foreach ($typeValues->item($i)->childNodes as $properties) {
  469. $currentItem[$questionType.'_values'][$properties->nodeName] = $properties->nodeValue;
  470. if ($properties->nodeName == 'sequence') {
  471. $sequence = $properties->nodeValue;
  472. $sequenceIds = explode(',', $sequence);
  473. foreach ($sequenceIds as $qId) {
  474. $questionMatch = $this->readMainQuestionsXml($questionsXml, $qId);
  475. $currentItem['plugin_qtype_'.$questionType.'_question'][] = $questionMatch;
  476. }
  477. }
  478. }
  479. }
  480. }
  481. }
  482. }
  483. }
  484. }
  485. $this->traverseArray($currentItem, ['#text', 'question_hints', 'tags']);
  486. return $currentItem;
  487. }
  488. return false;
  489. }
  490. /**
  491. * return the correct question type options tag
  492. *
  493. * @param string $questionType name
  494. * @return string question type tag
  495. */
  496. public function getQuestionTypeOptionsTag($questionType)
  497. {
  498. switch ($questionType) {
  499. case 'match':
  500. case 'ddmatch':
  501. return 'matchoptions';
  502. break;
  503. default:
  504. return $questionType;
  505. break;
  506. }
  507. }
  508. /**
  509. * return the correct question type answers tag
  510. *
  511. * @param string $questionType name
  512. * @return string question type tag
  513. */
  514. public function getQuestionTypeAnswersTag($questionType)
  515. {
  516. switch ($questionType) {
  517. case 'match':
  518. case 'ddmatch':
  519. return 'match';
  520. break;
  521. default:
  522. return 'answer';
  523. break;
  524. }
  525. }
  526. /**
  527. *
  528. * @param string $moodleQuestionType
  529. * @return integer Chamilo question type
  530. */
  531. public function matchMoodleChamiloQuestionTypes($moodleQuestionType)
  532. {
  533. switch ($moodleQuestionType) {
  534. case 'multichoice':
  535. return UNIQUE_ANSWER;
  536. break;
  537. case 'multianswer':
  538. case 'shortanswer':
  539. case 'match':
  540. return FILL_IN_BLANKS;
  541. break;
  542. case 'essay':
  543. return FREE_ANSWER;
  544. break;
  545. case 'truefalse':
  546. return UNIQUE_ANSWER_NO_OPTION;
  547. break;
  548. }
  549. }
  550. /**
  551. * Fix moodle files that contains spaces
  552. * @param array $importedFiles
  553. * @param string $text
  554. * @return mixed
  555. */
  556. public function fixPathInText($importedFiles, &$text)
  557. {
  558. if ($importedFiles) {
  559. foreach ($importedFiles as $old => $new) {
  560. // Ofaj fix moodle file names
  561. // In some questions moodle text contains file with name like:
  562. // Bild%20Check-In-Formular%20Ausfu%CC%88llen.jpg"
  563. // rawurlencode function transforms '' (whitespace) to %20 and so on
  564. $text = str_replace(rawurlencode($old), $new, $text);
  565. }
  566. }
  567. return $text;
  568. }
  569. /**
  570. * Process Moodle Answers to Chamilo
  571. *
  572. * @param array $questionList
  573. * @param string $questionType
  574. * @param object $questionInstance Question/Answer instance
  575. * @param array $currentQuestion
  576. * @param array $importedFiles
  577. * @return integer db response
  578. */
  579. public function processAnswers($questionList, $questionType, $questionInstance, $currentQuestion, $importedFiles)
  580. {
  581. switch ($questionType) {
  582. case 'multichoice':
  583. $objAnswer = new Answer($questionInstance->id);
  584. $questionWeighting = 0;
  585. foreach ($questionList as $slot => $answer) {
  586. $this->processUniqueAnswer(
  587. $objAnswer,
  588. $answer,
  589. $slot + 1,
  590. $questionWeighting,
  591. $importedFiles
  592. );
  593. }
  594. // saves the answers into the data base
  595. $objAnswer->save();
  596. // sets the total weighting of the question
  597. $questionInstance->updateWeighting($questionWeighting);
  598. $questionInstance->save();
  599. return true;
  600. break;
  601. case 'multianswer':
  602. $objAnswer = new Answer($questionInstance->id);
  603. $coursePath = api_get_course_path();
  604. $placeholder = str_replace(
  605. '@@PLUGINFILE@@',
  606. '/courses/'.$coursePath.'/document/moodle',
  607. $currentQuestion['questiontext']
  608. );
  609. $optionsValues = [];
  610. foreach ($questionList as $slot => $subQuestion) {
  611. $qtype = $subQuestion['qtype'];
  612. $optionsValues[] = $this->processFillBlanks(
  613. $objAnswer,
  614. $qtype,
  615. $subQuestion['plugin_qtype_'.$qtype.'_question'],
  616. $placeholder,
  617. $slot + 1,
  618. $importedFiles
  619. );
  620. }
  621. $answerOptionsWeight = '::';
  622. $answerOptionsSize = '';
  623. $questionWeighting = 0;
  624. foreach ($optionsValues as $index => $value) {
  625. $questionWeighting += $value['weight'];
  626. $answerOptionsWeight .= $value['weight'].',';
  627. $answerOptionsSize .= $value['size'].',';
  628. }
  629. $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
  630. $answerOptionsSize = substr($answerOptionsSize, 0, -1);
  631. $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
  632. $placeholder = $placeholder.PHP_EOL.$answerOptions;
  633. // This is a minor trick to clean the question description that in a multianswer is the main placeholder
  634. $questionInstance->updateDescription('');
  635. // sets the total weighting of the question
  636. $questionInstance->updateWeighting($questionWeighting);
  637. $questionInstance->save();
  638. $this->fixPathInText($importedFiles, $placeholder);
  639. // saves the answers into the data base
  640. $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
  641. $objAnswer->save();
  642. return true;
  643. case 'match':
  644. $objAnswer = new Answer($questionInstance->id);
  645. $placeholder = '';
  646. $optionsValues = $this->processFillBlanks(
  647. $objAnswer,
  648. 'match',
  649. $questionList,
  650. $placeholder,
  651. 0,
  652. $importedFiles
  653. );
  654. $answerOptionsWeight = '::';
  655. $answerOptionsSize = '';
  656. $questionWeighting = 0;
  657. foreach ($optionsValues as $index => $value) {
  658. $questionWeighting += $value['weight'];
  659. $answerOptionsWeight .= $value['weight'].',';
  660. $answerOptionsSize .= $value['size'].',';
  661. }
  662. $answerOptionsWeight = substr($answerOptionsWeight, 0, -1);
  663. $answerOptionsSize = substr($answerOptionsSize, 0, -1);
  664. $answerOptions = $answerOptionsWeight.':'.$answerOptionsSize.':0@';
  665. $placeholder = $placeholder.PHP_EOL.$answerOptions;
  666. // sets the total weighting of the question
  667. $questionInstance->updateWeighting($questionWeighting);
  668. $questionInstance->save();
  669. // saves the answers into the data base
  670. $this->fixPathInText($importedFiles, $placeholder);
  671. $objAnswer->createAnswer($placeholder, 0, '', 0, 1);
  672. $objAnswer->save();
  673. return true;
  674. break;
  675. case 'shortanswer':
  676. case 'ddmatch':
  677. $questionWeighting = $currentQuestion['defaultmark'];
  678. $questionInstance->updateWeighting($questionWeighting);
  679. $questionInstance->updateDescription(get_lang('ThisQuestionIsNotSupportedYet'));
  680. $questionInstance->save();
  681. return false;
  682. break;
  683. case 'essay':
  684. $questionWeighting = $currentQuestion['defaultmark'];
  685. $questionInstance->updateWeighting($questionWeighting);
  686. $questionInstance->save();
  687. return true;
  688. break;
  689. case 'truefalse':
  690. $objAnswer = new Answer($questionInstance->id);
  691. $questionWeighting = 0;
  692. foreach ($questionList as $slot => $answer) {
  693. $this->processTrueFalse(
  694. $objAnswer,
  695. $answer,
  696. $slot + 1,
  697. $questionWeighting,
  698. $importedFiles
  699. );
  700. }
  701. // saves the answers into the data base
  702. $objAnswer->save();
  703. // sets the total weighting of the question
  704. $questionInstance->updateWeighting($questionWeighting);
  705. $questionInstance->save();
  706. return false;
  707. break;
  708. default:
  709. return false;
  710. break;
  711. }
  712. }
  713. /**
  714. * Process Chamilo Unique Answer
  715. *
  716. * @param object $objAnswer
  717. * @param array $answerValues
  718. * @param integer $position
  719. * @param integer $questionWeighting
  720. * @param array $importedFiles
  721. * @return integer db response
  722. */
  723. public function processUniqueAnswer($objAnswer, $answerValues, $position, &$questionWeighting, $importedFiles)
  724. {
  725. $correct = intval($answerValues['fraction']) ? intval($answerValues['fraction']) : 0;
  726. $answer = $answerValues['answertext'];
  727. $comment = $answerValues['feedback'];
  728. $weighting = $answerValues['fraction'];
  729. $weighting = abs($weighting);
  730. if ($weighting > 0) {
  731. $questionWeighting += $weighting;
  732. }
  733. $goodAnswer = $correct ? true : false;
  734. $this->fixPathInText($importedFiles, $answer);
  735. $objAnswer->createAnswer(
  736. $answer,
  737. $goodAnswer,
  738. $comment,
  739. $weighting,
  740. $position,
  741. null,
  742. null,
  743. ''
  744. );
  745. }
  746. /**
  747. * Process Chamilo True False
  748. *
  749. * @param object $objAnswer
  750. * @param array $answerValues
  751. * @param integer $position
  752. * @param integer $questionWeighting
  753. * @param array $importedFiles
  754. *
  755. * @return integer db response
  756. */
  757. public function processTrueFalse($objAnswer, $answerValues, $position, &$questionWeighting, $importedFiles)
  758. {
  759. $correct = intval($answerValues['fraction']) ? intval($answerValues['fraction']) : 0;
  760. $answer = $answerValues['answertext'];
  761. $comment = $answerValues['feedback'];
  762. $weighting = $answerValues['fraction'];
  763. $weighting = abs($weighting);
  764. if ($weighting > 0) {
  765. $questionWeighting += $weighting;
  766. }
  767. $goodAnswer = $correct ? true : false;
  768. $this->fixPathInText($importedFiles, $answer);
  769. $objAnswer->createAnswer(
  770. $answer,
  771. $goodAnswer,
  772. $comment,
  773. $weighting,
  774. $position,
  775. null,
  776. null,
  777. ''
  778. );
  779. }
  780. /**
  781. * Process Chamilo FillBlanks
  782. *
  783. * @param object $objAnswer
  784. * @param array $questionType
  785. * @param array $answerValues
  786. * @param string $placeholder
  787. * @param integer $position
  788. * @param array $importedFiles
  789. * @return integer db response
  790. *
  791. */
  792. public function processFillBlanks($objAnswer, $questionType, $answerValues, &$placeholder, $position, $importedFiles)
  793. {
  794. $coursePath = api_get_course_path();
  795. switch ($questionType) {
  796. case 'multichoice':
  797. $optionsValues = [];
  798. $correctAnswer = '';
  799. $othersAnswer = '';
  800. foreach ($answerValues as $answer) {
  801. $correct = intval($answer['fraction']);
  802. if ($correct) {
  803. $correctAnswer .= $answer['answertext'].'|';
  804. $optionsValues['weight'] = $answer['fraction'];
  805. $optionsValues['size'] = '200';
  806. } else {
  807. $othersAnswer .= $answer['answertext'].'|';
  808. }
  809. }
  810. $currentAnswers = $correctAnswer.$othersAnswer;
  811. $currentAnswers = '['.substr($currentAnswers, 0, -1).']';
  812. $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
  813. return $optionsValues;
  814. break;
  815. case 'shortanswer':
  816. $optionsValues = [];
  817. $correctAnswer = '';
  818. foreach ($answerValues as $answer) {
  819. $correct = intval($answer['fraction']);
  820. if ($correct) {
  821. $correctAnswer .= $answer['answertext'];
  822. $optionsValues['weight'] = $answer['fraction'];
  823. $optionsValues['size'] = '200';
  824. }
  825. }
  826. $currentAnswers = '['.$correctAnswer.']';
  827. $placeholder = str_replace("{#$position}", $currentAnswers, $placeholder);
  828. return $optionsValues;
  829. break;
  830. case 'match':
  831. $answers = [];
  832. // Here first we need to extract all the possible answers
  833. foreach ($answerValues as $slot => $answer) {
  834. $answers[$slot] = $answer['answertext'];
  835. }
  836. // Now we set the order of the values matching the correct answer and set it to the first element
  837. $optionsValues = [];
  838. foreach ($answerValues as $slot => $answer) {
  839. $correctAnswer = '';
  840. $othersAnswers = '';
  841. $correctAnswer .= $answer['answertext'].'|';
  842. foreach ($answers as $other) {
  843. if ($other !== $answer['answertext']) {
  844. $othersAnswers .= $other.'|';
  845. }
  846. }
  847. $optionsValues[$slot]['weight'] = 1;
  848. $optionsValues[$slot]['size'] = '200';
  849. $currentAnswers = $correctAnswer.$othersAnswers;
  850. $currentAnswers = '['.substr($currentAnswers, 0, -1).'] ';
  851. $answer['questiontext'] = str_replace(
  852. '@@PLUGINFILE@@',
  853. '/courses/'.$coursePath.'/document/moodle',
  854. $answer['questiontext']
  855. );
  856. $placeholder .= '<p> ' . strip_tags($answer['questiontext']).' '.$currentAnswers . ' </p>';
  857. }
  858. return $optionsValues;
  859. break;
  860. default:
  861. return false;
  862. break;
  863. }
  864. }
  865. /**
  866. * get All files associated with a question
  867. *
  868. * @param $filesXml
  869. * @return array
  870. */
  871. public function getAllQuestionFiles($filesXml)
  872. {
  873. $moduleDoc = new DOMDocument();
  874. $moduleRes = @$moduleDoc->loadXML($filesXml);
  875. $allFiles = [];
  876. if ($moduleRes) {
  877. $activities = $moduleDoc->getElementsByTagName('file');
  878. foreach ($activities as $activity) {
  879. $currentItem = [];
  880. $thisIsAnInvalidItem = false;
  881. if ($activity->childNodes->length) {
  882. foreach ($activity->childNodes as $item) {
  883. if ($item->nodeName == 'component' && $item->nodeValue == 'mod_resource') {
  884. $thisIsAnInvalidItem = true;
  885. }
  886. if ($item->nodeName == 'contenthash') {
  887. $currentItem['contenthash'] = $item->nodeValue;
  888. }
  889. if ($item->nodeName == 'filename') {
  890. $currentItem['filename'] = $item->nodeValue;
  891. }
  892. if ($item->nodeName == 'filesize') {
  893. $currentItem['filesize'] = $item->nodeValue;
  894. }
  895. if ($item->nodeName == 'mimetype' && $item->nodeValue == 'document/unknown') {
  896. $thisIsAnInvalidItem = true;
  897. }
  898. if ($item->nodeName == 'mimetype' && $item->nodeValue !== 'document/unknown') {
  899. $currentItem['mimetype'] = $item->nodeValue;
  900. }
  901. }
  902. }
  903. if (!$thisIsAnInvalidItem) {
  904. $allFiles[] = $currentItem;
  905. }
  906. }
  907. }
  908. return $allFiles;
  909. }
  910. /**
  911. * Litle utility to delete the unuseful tags
  912. *
  913. * @param $array
  914. * @param $keys
  915. */
  916. public function traverseArray(&$array, $keys)
  917. {
  918. foreach ($array as $key => &$value) {
  919. if (is_array($value)) {
  920. $this->traverseArray($value, $keys);
  921. } else {
  922. if (in_array($key, $keys)) {
  923. unset($array[$key]);
  924. }
  925. }
  926. }
  927. }
  928. }