MDL-62820 question import: properly escape output
[moodle.git] / mod / lesson / format.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * format.php  - Default format class for file imports/exports. Doesn't do
20  * everything on it's own -- it needs to be extended.
21  *
22  * Included by import.ph
23  *
24  * @package mod_lesson
25  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  **/
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Import files embedded into answer or response
33  *
34  * @param string $field nfield name (answer or response)
35  * @param array $data imported data
36  * @param object $answer answer object
37  * @param int $contextid
38  **/
39 function lesson_import_question_files($field, $data, $answer, $contextid) {
40     global $DB;
41     if (!isset($data['itemid'])) {
42         return;
43     }
44     $text = file_save_draft_area_files($data['itemid'],
45             $contextid, 'mod_lesson', 'page_' . $field . 's', $answer->id,
46             array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
47             $answer->$field);
49     $DB->set_field("lesson_answers", $field, $text, array("id" => $answer->id));
50 }
52 /**
53  * Given some question info and some data about the the answers
54  * this function parses, organises and saves the question
55  *
56  * This is only used when IMPORTING questions and is only called
57  * from format.php
58  * Lifted from mod/quiz/lib.php -
59  *    1. all reference to oldanswers removed
60  *    2. all reference to quiz_multichoice table removed
61  *    3. In shortanswer questions usecase is store in the qoption field
62  *    4. In numeric questions store the range as two answers
63  *    5. truefalse options are ignored
64  *    6. For multichoice questions with more than one answer the qoption field is true
65  *
66  * @param object $question Contains question data like question, type and answers.
67  * @param object $lesson
68  * @param int $contextid
69  * @return object Returns $result->error or $result->notice.
70  **/
71 function lesson_save_question_options($question, $lesson, $contextid) {
72     global $DB;
74     // These lines are required to ensure that all page types have
75     // been loaded for the following switch
76     if (!($lesson instanceof lesson)) {
77         $lesson = new lesson($lesson);
78     }
79     $manager = lesson_page_type_manager::get($lesson);
81     $timenow = time();
82     $result = new stdClass();
84     // Default answer to avoid code duplication.
85     $defaultanswer = new stdClass();
86     $defaultanswer->lessonid   = $question->lessonid;
87     $defaultanswer->pageid = $question->id;
88     $defaultanswer->timecreated   = $timenow;
89     $defaultanswer->answerformat = FORMAT_HTML;
90     $defaultanswer->jumpto = LESSON_THISPAGE;
91     $defaultanswer->grade = 0;
92     $defaultanswer->score = 0;
94     switch ($question->qtype) {
95         case LESSON_PAGE_SHORTANSWER:
97             $answers = array();
98             $maxfraction = -1;
100             // Insert all the new answers
101             foreach ($question->answer as $key => $dataanswer) {
102                 if ($dataanswer != "") {
103                     $answer = clone($defaultanswer);
104                     if ($question->fraction[$key] >=0.5) {
105                         $answer->jumpto = LESSON_NEXTPAGE;
106                         $answer->score = 1;
107                     }
108                     $answer->grade = round($question->fraction[$key] * 100);
109                     $answer->answer   = $dataanswer;
110                     $answer->response = $question->feedback[$key]['text'];
111                     $answer->responseformat = $question->feedback[$key]['format'];
112                     $answer->id = $DB->insert_record("lesson_answers", $answer);
113                     lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
114                     $answers[] = $answer->id;
115                     if ($question->fraction[$key] > $maxfraction) {
116                         $maxfraction = $question->fraction[$key];
117                     }
118                 }
119             }
122             /// Perform sanity checks on fractional grades
123             if ($maxfraction != 1) {
124                 $maxfraction = $maxfraction * 100;
125                 $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
126                 return $result;
127             }
128             break;
130         case LESSON_PAGE_NUMERICAL:   // Note similarities to shortanswer.
132             $answers = array();
133             $maxfraction = -1;
136             // for each answer store the pair of min and max values even if they are the same
137             foreach ($question->answer as $key => $dataanswer) {
138                 if ($dataanswer != "") {
139                     $answer = clone($defaultanswer);
140                     if ($question->fraction[$key] >= 0.5) {
141                         $answer->jumpto = LESSON_NEXTPAGE;
142                         $answer->score = 1;
143                     }
144                     $answer->grade = round($question->fraction[$key] * 100);
145                     $min = $question->answer[$key] - $question->tolerance[$key];
146                     $max = $question->answer[$key] + $question->tolerance[$key];
147                     $answer->answer   = $min.":".$max;
148                     $answer->response = $question->feedback[$key]['text'];
149                     $answer->responseformat = $question->feedback[$key]['format'];
150                     $answer->id = $DB->insert_record("lesson_answers", $answer);
151                     lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
153                     $answers[] = $answer->id;
154                     if ($question->fraction[$key] > $maxfraction) {
155                         $maxfraction = $question->fraction[$key];
156                     }
157                 }
158             }
160             /// Perform sanity checks on fractional grades
161             if ($maxfraction != 1) {
162                 $maxfraction = $maxfraction * 100;
163                 $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
164                 return $result;
165             }
166         break;
169         case LESSON_PAGE_TRUEFALSE:
171             // In lesson the correct answer always come first, as it was the case
172             // in question bank exports years ago.
173             $answer = clone($defaultanswer);
174             $answer->grade = 100;
175             $answer->jumpto = LESSON_NEXTPAGE;
176             $answer->score = 1;
177             if ($question->correctanswer) {
178                 $answer->answer = get_string("true", "lesson");
179                 if (isset($question->feedbacktrue)) {
180                     $answer->response = $question->feedbacktrue['text'];
181                     $answer->responseformat = $question->feedbacktrue['format'];
182                     $answer->id = $DB->insert_record("lesson_answers", $answer);
183                     lesson_import_question_files('response', $question->feedbacktrue, $answer, $contextid);
184                 }
185             } else {
186                 $answer->answer = get_string("false", "lesson");
187                 if (isset($question->feedbackfalse)) {
188                     $answer->response = $question->feedbackfalse['text'];
189                     $answer->responseformat = $question->feedbackfalse['format'];
190                     $answer->id = $DB->insert_record("lesson_answers", $answer);
191                     lesson_import_question_files('response', $question->feedbackfalse, $answer, $contextid);
192                 }
193             }
195             // Now the wrong answer.
196             $answer = clone($defaultanswer);
197             if ($question->correctanswer) {
198                 $answer->answer = get_string("false", "lesson");
199                 if (isset($question->feedbackfalse)) {
200                     $answer->response = $question->feedbackfalse['text'];
201                     $answer->responseformat = $question->feedbackfalse['format'];
202                     $answer->id = $DB->insert_record("lesson_answers", $answer);
203                     lesson_import_question_files('response', $question->feedbackfalse, $answer, $contextid);
204                 }
205             } else {
206                 $answer->answer = get_string("true", "lesson");
207                 if (isset($question->feedbacktrue)) {
208                     $answer->response = $question->feedbacktrue['text'];
209                     $answer->responseformat = $question->feedbacktrue['format'];
210                     $answer->id = $DB->insert_record("lesson_answers", $answer);
211                     lesson_import_question_files('response', $question->feedbacktrue, $answer, $contextid);
212                 }
213             }
215           break;
217         case LESSON_PAGE_MULTICHOICE:
219             $totalfraction = 0;
220             $maxfraction = -1;
222             $answers = array();
224             // Insert all the new answers
225             foreach ($question->answer as $key => $dataanswer) {
226                 if ($dataanswer != "") {
227                     $answer = clone($defaultanswer);
228                     $answer->grade = round($question->fraction[$key] * 100);
230                     if ($question->single) {
231                         if ($answer->grade > 50) {
232                             $answer->jumpto = LESSON_NEXTPAGE;
233                             $answer->score = 1;
234                         }
235                     } else {
236                         // If multi answer allowed, any answer with fraction > 0 is considered correct.
237                         if ($question->fraction[$key] > 0) {
238                             $answer->jumpto = LESSON_NEXTPAGE;
239                             $answer->score = 1;
240                         }
241                     }
242                     $answer->answer   = $dataanswer['text'];
243                     $answer->answerformat   = $dataanswer['format'];
244                     $answer->response = $question->feedback[$key]['text'];
245                     $answer->responseformat = $question->feedback[$key]['format'];
246                     $answer->id = $DB->insert_record("lesson_answers", $answer);
247                     lesson_import_question_files('answer', $dataanswer, $answer, $contextid);
248                     lesson_import_question_files('response', $question->feedback[$key], $answer, $contextid);
250                     // for Sanity checks
251                     if ($question->fraction[$key] > 0) {
252                         $totalfraction += $question->fraction[$key];
253                     }
254                     if ($question->fraction[$key] > $maxfraction) {
255                         $maxfraction = $question->fraction[$key];
256                     }
257                 }
258             }
260             /// Perform sanity checks on fractional grades
261             if ($question->single) {
262                 if ($maxfraction != 1) {
263                     $maxfraction = $maxfraction * 100;
264                     $result->notice = get_string("fractionsnomax", "lesson", $maxfraction);
265                     return $result;
266                 }
267             } else {
268                 $totalfraction = round($totalfraction,2);
269                 if ($totalfraction != 1) {
270                     $totalfraction = $totalfraction * 100;
271                     $result->notice = get_string("fractionsaddwrong", "lesson", $totalfraction);
272                     return $result;
273                 }
274             }
275         break;
277         case LESSON_PAGE_MATCHING:
279             $subquestions = array();
281             // The first answer should always be the correct answer
282             $correctanswer = clone($defaultanswer);
283             $correctanswer->answer = get_string('thatsthecorrectanswer', 'lesson');
284             $correctanswer->jumpto = LESSON_NEXTPAGE;
285             $correctanswer->score = 1;
286             $DB->insert_record("lesson_answers", $correctanswer);
288             // The second answer should always be the wrong answer
289             $wronganswer = clone($defaultanswer);
290             $wronganswer->answer = get_string('thatsthewronganswer', 'lesson');
291             $DB->insert_record("lesson_answers", $wronganswer);
293             $i = 0;
294             // Insert all the new question+answer pairs
295             foreach ($question->subquestions as $key => $questiontext) {
296                 $answertext = $question->subanswers[$key];
297                 if (!empty($questiontext) and !empty($answertext)) {
298                     $answer = clone($defaultanswer);
299                     $answer->answer = $questiontext['text'];
300                     $answer->answerformat   = $questiontext['format'];
301                     $answer->response   = $answertext;
302                     if ($i == 0) {
303                         // first answer contains the correct answer jump
304                         $answer->jumpto = LESSON_NEXTPAGE;
305                     }
306                     $answer->id = $DB->insert_record("lesson_answers", $answer);
307                     lesson_import_question_files('answer', $questiontext, $answer, $contextid);
308                     $subquestions[] = $answer->id;
309                     $i++;
310                 }
311             }
313             if (count($subquestions) < 3) {
314                 $result->notice = get_string("notenoughsubquestions", "lesson");
315                 return $result;
316             }
317             break;
319         case LESSON_PAGE_ESSAY:
320             $answer = new stdClass();
321             $answer->lessonid = $question->lessonid;
322             $answer->pageid = $question->id;
323             $answer->timecreated = $timenow;
324             $answer->answer = null;
325             $answer->answerformat = FORMAT_MOODLE;
326             $answer->grade = 0;
327             $answer->score = 1;
328             $answer->jumpto = LESSON_NEXTPAGE;
329             $answer->response = null;
330             $answer->responseformat = FORMAT_MOODLE;
331             $answer->id = $DB->insert_record("lesson_answers", $answer);
332         break;
333         default:
334             $result->error = "Unsupported question type ($question->qtype)!";
335             return $result;
336     }
337     return true;
341 class qformat_default {
343     var $displayerrors = true;
344     var $category = null;
345     var $questionids = array();
346     protected $importcontext = null;
347     var $qtypeconvert = array('numerical'   => LESSON_PAGE_NUMERICAL,
348                                'multichoice' => LESSON_PAGE_MULTICHOICE,
349                                'truefalse'   => LESSON_PAGE_TRUEFALSE,
350                                'shortanswer' => LESSON_PAGE_SHORTANSWER,
351                                'match'       => LESSON_PAGE_MATCHING,
352                                'essay'       => LESSON_PAGE_ESSAY
353                               );
355     // Importing functions
356     function provide_import() {
357         return false;
358     }
360     function set_importcontext($context) {
361         $this->importcontext = $context;
362     }
364     /**
365      * Handle parsing error
366      *
367      * @param string $message information about error
368      * @param string $text imported text that triggered the error
369      * @param string $questionname imported question name
370      */
371     protected function error($message, $text='', $questionname='') {
372         $importerrorquestion = get_string('importerrorquestion', 'question');
374         echo "<div class=\"importerror\">\n";
375         echo "<strong>$importerrorquestion $questionname</strong>";
376         if (!empty($text)) {
377             $text = s($text);
378             echo "<blockquote>$text</blockquote>\n";
379         }
380         echo "<strong>$message</strong>\n";
381         echo "</div>";
382     }
384     /**
385      * Import for questiontype plugins
386      * @param mixed $data The segment of data containing the question
387      * @param object $question processed (so far) by standard import code if appropriate
388      * @param object $extra mixed any additional format specific data that may be passed by the format
389      * @param string $qtypehint hint about a question type from format
390      * @return object question object suitable for save_options() or false if cannot handle
391      */
392     public function try_importing_using_qtypes($data, $question = null, $extra = null,
393             $qtypehint = '') {
395         return false;
396     }
398     function importpreprocess() {
399         // Does any pre-processing that may be desired
400         return true;
401     }
403     function importprocess($filename, $lesson, $pageid) {
404         global $DB, $OUTPUT;
406     /// Processes a given file.  There's probably little need to change this
407         $timenow = time();
409         if (! $lines = $this->readdata($filename)) {
410             echo $OUTPUT->notification("File could not be read, or was empty");
411             return false;
412         }
414         if (! $questions = $this->readquestions($lines)) {   // Extract all the questions
415             echo $OUTPUT->notification("There are no questions in this file!");
416             return false;
417         }
419         //Avoid category as question type
420         echo $OUTPUT->notification(get_string('importcount', 'lesson',
421                 $this->count_questions($questions)), 'notifysuccess');
423         $count = 0;
424         $addquestionontop = false;
425         if ($pageid == 0) {
426             $addquestionontop = true;
427             $updatelessonpage = $DB->get_record('lesson_pages', array('lessonid' => $lesson->id, 'prevpageid' => 0));
428         } else {
429             $updatelessonpage = $DB->get_record('lesson_pages', array('lessonid' => $lesson->id, 'id' => $pageid));
430         }
432         $unsupportedquestions = 0;
434         foreach ($questions as $question) {   // Process and store each question
435             switch ($question->qtype) {
436                 //TODO: Bad way to bypass category in data... Quickfix for MDL-27964
437                 case 'category':
438                     break;
439                 // the good ones
440                 case 'shortanswer' :
441                 case 'numerical' :
442                 case 'truefalse' :
443                 case 'multichoice' :
444                 case 'match' :
445                 case 'essay' :
446                     $count++;
448                     //Show nice formated question in one line.
449                     echo "<hr><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
451                     $newpage = new stdClass;
452                     $newpage->lessonid = $lesson->id;
453                     $newpage->qtype = $this->qtypeconvert[$question->qtype];
454                     switch ($question->qtype) {
455                         case 'shortanswer' :
456                             if (isset($question->usecase)) {
457                                 $newpage->qoption = $question->usecase;
458                             }
459                             break;
460                         case 'multichoice' :
461                             if (isset($question->single)) {
462                                 $newpage->qoption = !$question->single;
463                             }
464                             break;
465                     }
466                     $newpage->timecreated = $timenow;
467                     if ($question->name != $question->questiontext) {
468                         $newpage->title = $question->name;
469                     } else {
470                         $newpage->title = "Page $count";
471                     }
472                     $newpage->contents = $question->questiontext;
473                     $newpage->contentsformat = isset($question->questionformat) ? $question->questionformat : FORMAT_HTML;
475                     // set up page links
476                     if ($pageid) {
477                         // the new page follows on from this page
478                         if (!$page = $DB->get_record("lesson_pages", array("id" => $pageid))) {
479                             print_error('invalidpageid', 'lesson');
480                         }
481                         $newpage->prevpageid = $pageid;
482                         $newpage->nextpageid = $page->nextpageid;
483                         // insert the page and reset $pageid
484                         $newpageid = $DB->insert_record("lesson_pages", $newpage);
485                         // update the linked list
486                         $DB->set_field("lesson_pages", "nextpageid", $newpageid, array("id" => $pageid));
487                     } else {
488                         // new page is the first page
489                         // get the existing (first) page (if any)
490                         $params = array ("lessonid" => $lesson->id, "prevpageid" => 0);
491                         if (!$page = $DB->get_record_select("lesson_pages", "lessonid = :lessonid AND prevpageid = :prevpageid", $params)) {
492                             // there are no existing pages
493                             $newpage->prevpageid = 0; // this is a first page
494                             $newpage->nextpageid = 0; // this is the only page
495                             $newpageid = $DB->insert_record("lesson_pages", $newpage);
496                         } else {
497                             // there are existing pages put this at the start
498                             $newpage->prevpageid = 0; // this is a first page
499                             $newpage->nextpageid = $page->id;
500                             $newpageid = $DB->insert_record("lesson_pages", $newpage);
501                             // update the linked list
502                             $DB->set_field("lesson_pages", "prevpageid", $newpageid, array("id" => $page->id));
503                         }
504                     }
506                     // reset $pageid and put the page ID in $question, used in save_question_option()
507                     $pageid = $newpageid;
508                     $question->id = $newpageid;
510                     $this->questionids[] = $question->id;
512                     // Import images in question text.
513                     if (isset($question->questiontextitemid)) {
514                         $questiontext = file_save_draft_area_files($question->questiontextitemid,
515                                 $this->importcontext->id, 'mod_lesson', 'page_contents', $newpageid,
516                                 null , $question->questiontext);
517                         // Update content with recoded urls.
518                         $DB->set_field("lesson_pages", "contents", $questiontext, array("id" => $newpageid));
519                     }
521                     // Now to save all the answers and type-specific options
523                     $question->lessonid = $lesson->id; // needed for foreign key
524                     $question->qtype = $this->qtypeconvert[$question->qtype];
525                     $result = lesson_save_question_options($question, $lesson, $this->importcontext->id);
527                     if (!empty($result->error)) {
528                         echo $OUTPUT->notification($result->error);
529                         return false;
530                     }
532                     if (!empty($result->notice)) {
533                         echo $OUTPUT->notification($result->notice);
534                         return true;
535                     }
536                     break;
537             // the Bad ones
538                 default :
539                     $unsupportedquestions++;
540                     break;
541             }
542         }
543         // Update the prev links if there were existing pages.
544         if (!empty($updatelessonpage)) {
545             if ($addquestionontop) {
546                 $DB->set_field("lesson_pages", "prevpageid", $pageid, array("id" => $updatelessonpage->id));
547             } else {
548                 $DB->set_field("lesson_pages", "prevpageid", $pageid, array("id" => $updatelessonpage->nextpageid));
549             }
550         }
551         if ($unsupportedquestions) {
552             echo $OUTPUT->notification(get_string('unknownqtypesnotimported', 'lesson', $unsupportedquestions));
553         }
554         return true;
555     }
557     /**
558      * Count all non-category questions in the questions array.
559      *
560      * @param array questions An array of question objects.
561      * @return int The count.
562      *
563      */
564     protected function count_questions($questions) {
565         $count = 0;
566         if (!is_array($questions)) {
567             return $count;
568         }
569         foreach ($questions as $question) {
570             if (!is_object($question) || !isset($question->qtype) ||
571                     ($question->qtype == 'category')) {
572                 continue;
573             }
574             $count++;
575         }
576         return $count;
577     }
579     function readdata($filename) {
580     /// Returns complete file with an array, one item per line
582         if (is_readable($filename)) {
583             $filearray = file($filename);
585             /// Check for Macintosh OS line returns (ie file on one line), and fix
586             if (preg_match("/\r/", $filearray[0]) AND !preg_match("/\n/", $filearray[0])) {
587                 return explode("\r", $filearray[0]);
588             } else {
589                 return $filearray;
590             }
591         }
592         return false;
593     }
595     protected function readquestions($lines) {
596     /// Parses an array of lines into an array of questions,
597     /// where each item is a question object as defined by
598     /// readquestion().   Questions are defined as anything
599     /// between blank lines.
601         $questions = array();
602         $currentquestion = array();
604         foreach ($lines as $line) {
605             $line = trim($line);
606             if (empty($line)) {
607                 if (!empty($currentquestion)) {
608                     if ($question = $this->readquestion($currentquestion)) {
609                         $questions[] = $question;
610                     }
611                     $currentquestion = array();
612                 }
613             } else {
614                 $currentquestion[] = $line;
615             }
616         }
618         if (!empty($currentquestion)) {  // There may be a final question
619             if ($question = $this->readquestion($currentquestion)) {
620                 $questions[] = $question;
621             }
622         }
624         return $questions;
625     }
628     protected function readquestion($lines) {
629     /// Given an array of lines known to define a question in
630     /// this format, this function converts it into a question
631     /// object suitable for processing and insertion into Moodle.
633         // We should never get there unless the qformat plugin is broken.
634         throw new coding_exception('Question format plugin is missing important code: readquestion.');
636         return null;
637     }
639     /**
640      * Construct a reasonable default question name, based on the start of the question text.
641      * @param string $questiontext the question text.
642      * @param string $default default question name to use if the constructed one comes out blank.
643      * @return string a reasonable question name.
644      */
645     public function create_default_question_name($questiontext, $default) {
646         $name = $this->clean_question_name(shorten_text($questiontext, 80));
647         if ($name) {
648             return $name;
649         } else {
650             return $default;
651         }
652     }
654     /**
655      * Ensure that a question name does not contain anything nasty, and will fit in the DB field.
656      * @param string $name the raw question name.
657      * @return string a safe question name.
658      */
659     public function clean_question_name($name) {
660         $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does.
661         $name = trim($name);
662         $trimlength = 251;
663         while (core_text::strlen($name) > 255 && $trimlength > 0) {
664             $name = shorten_text($name, $trimlength);
665             $trimlength -= 10;
666         }
667         return $name;
668     }
670     /**
671      * return an "empty" question
672      * Somewhere to specify question parameters that are not handled
673      * by import but are required db fields.
674      * This should not be overridden.
675      * @return object default question
676      */
677     protected function defaultquestion() {
678         global $CFG;
679         static $defaultshuffleanswers = null;
680         if (is_null($defaultshuffleanswers)) {
681             $defaultshuffleanswers = get_config('quiz', 'shuffleanswers');
682         }
684         $question = new stdClass();
685         $question->shuffleanswers = $defaultshuffleanswers;
686         $question->defaultmark = 1;
687         $question->image = "";
688         $question->usecase = 0;
689         $question->multiplier = array();
690         $question->questiontextformat = FORMAT_MOODLE;
691         $question->generalfeedback = '';
692         $question->generalfeedbackformat = FORMAT_MOODLE;
693         $question->correctfeedback = '';
694         $question->partiallycorrectfeedback = '';
695         $question->incorrectfeedback = '';
696         $question->answernumbering = 'abc';
697         $question->penalty = 0.3333333;
698         $question->length = 1;
699         $question->qoption = 0;
700         $question->layout = 1;
702         // this option in case the questiontypes class wants
703         // to know where the data came from
704         $question->export_process = true;
705         $question->import_process = true;
707         return $question;
708     }
710     function importpostprocess() {
711         /// Does any post-processing that may be desired
712         /// Argument is a simple array of question ids that
713         /// have just been added.
714         return true;
715     }
717     /**
718      * Convert the question text to plain text, so it can safely be displayed
719      * during import to let the user see roughly what is going on.
720      */
721     protected function format_question_text($question) {
722         $formatoptions = new stdClass();
723         $formatoptions->noclean = true;
724         // The html_to_text call strips out all URLs, but format_text complains
725         // if it finds @@PLUGINFILE@@ tokens. So, we need to replace
726         // @@PLUGINFILE@@ with a real URL, but it doesn't matter what.
727         // We use http://example.com/.
728         $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $question->questiontext);
729         return s(html_to_text(format_text($text,
730                 $question->questiontextformat, $formatoptions), 0, false));
731     }
733     /**
734      * Since the lesson module tries to re-use the question bank import classes in
735      * a crazy way, this is necessary to stop things breaking.
736      */
737     protected function add_blank_combined_feedback($question) {
738         return $question;
739     }
743 /**
744  * Since the lesson module tries to re-use the question bank import classes in
745  * a crazy way, this is necessary to stop things breaking. This should be exactly
746  * the same as the class defined in question/format.php.
747  */
748 class qformat_based_on_xml extends qformat_default {
749     /**
750      * A lot of imported files contain unwanted entities.
751      * This method tries to clean up all known problems.
752      * @param string str string to correct
753      * @return string the corrected string
754      */
755     public function cleaninput($str) {
757         $html_code_list = array(
758             "&#039;" => "'",
759             "&#8217;" => "'",
760             "&#8220;" => "\"",
761             "&#8221;" => "\"",
762             "&#8211;" => "-",
763             "&#8212;" => "-",
764         );
765         $str = strtr($str, $html_code_list);
766         // Use core_text entities_to_utf8 function to convert only numerical entities.
767         $str = core_text::entities_to_utf8($str, false);
768         return $str;
769     }
771     /**
772      * Return the array moodle is expecting
773      * for an HTML text. No processing is done on $text.
774      * qformat classes that want to process $text
775      * for instance to import external images files
776      * and recode urls in $text must overwrite this method.
777      * @param array $text some HTML text string
778      * @return array with keys text, format and files.
779      */
780     public function text_field($text) {
781         return array(
782             'text' => trim($text),
783             'format' => FORMAT_HTML,
784             'files' => array(),
785         );
786     }
788     /**
789      * Return the value of a node, given a path to the node
790      * if it doesn't exist return the default value.
791      * @param array xml data to read
792      * @param array path path to node expressed as array
793      * @param mixed default
794      * @param bool istext process as text
795      * @param string error if set value must exist, return false and issue message if not
796      * @return mixed value
797      */
798     public function getpath($xml, $path, $default, $istext=false, $error='') {
799         foreach ($path as $index) {
800             if (!isset($xml[$index])) {
801                 if (!empty($error)) {
802                     $this->error($error);
803                     return false;
804                 } else {
805                     return $default;
806                 }
807             }
809             $xml = $xml[$index];
810         }
812         if ($istext) {
813             if (!is_string($xml)) {
814                 $this->error(get_string('invalidxml', 'qformat_xml'));
815             }
816             $xml = trim($xml);
817         }
819         return $xml;
820     }