MoodleImport.php 42 KB

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