Merge branch 'MDL-58179-master-fix' of https://github.com/andrewnicols/moodle
[moodle.git] / mod / quiz / backup / moodle2 / restore_quiz_stepslib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * @package    mod_quiz
19  * @subpackage backup-moodle2
20  * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
25 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Structure step to restore one quiz activity
30  *
31  * @copyright  2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
32  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
34 class restore_quiz_activity_structure_step extends restore_questions_activity_structure_step {
36     /**
37      * @var bool tracks whether the quiz contains at least one section. Before
38      * Moodle 2.9 quiz sections did not exist, so if the file being restored
39      * did not contain any, we need to create one in {@link after_execute()}.
40      */
41     protected $sectioncreated = false;
43     /**
44      * @var bool when restoring old quizzes (2.8 or before) this records the
45      * shufflequestionsoption quiz option which has moved to the quiz_sections table.
46      */
47     protected $legacyshufflequestionsoption = false;
49     protected function define_structure() {
51         $paths = array();
52         $userinfo = $this->get_setting_value('userinfo');
54         $quiz = new restore_path_element('quiz', '/activity/quiz');
55         $paths[] = $quiz;
57         // A chance for access subplugings to set up their quiz data.
58         $this->add_subplugin_structure('quizaccess', $quiz);
60         $paths[] = new restore_path_element('quiz_question_instance',
61                 '/activity/quiz/question_instances/question_instance');
62         $paths[] = new restore_path_element('quiz_section', '/activity/quiz/sections/section');
63         $paths[] = new restore_path_element('quiz_feedback', '/activity/quiz/feedbacks/feedback');
64         $paths[] = new restore_path_element('quiz_override', '/activity/quiz/overrides/override');
66         if ($userinfo) {
67             $paths[] = new restore_path_element('quiz_grade', '/activity/quiz/grades/grade');
69             if ($this->task->get_old_moduleversion() > 2011010100) {
70                 // Restoring from a version 2.1 dev or later.
71                 // Process the new-style attempt data.
72                 $quizattempt = new restore_path_element('quiz_attempt',
73                         '/activity/quiz/attempts/attempt');
74                 $paths[] = $quizattempt;
76                 // Add states and sessions.
77                 $this->add_question_usages($quizattempt, $paths);
79                 // A chance for access subplugings to set up their attempt data.
80                 $this->add_subplugin_structure('quizaccess', $quizattempt);
82             } else {
83                 // Restoring from a version 2.0.x+ or earlier.
84                 // Upgrade the legacy attempt data.
85                 $quizattempt = new restore_path_element('quiz_attempt_legacy',
86                         '/activity/quiz/attempts/attempt',
87                         true);
88                 $paths[] = $quizattempt;
89                 $this->add_legacy_question_attempt_data($quizattempt, $paths);
90             }
91         }
93         // Return the paths wrapped into standard activity structure.
94         return $this->prepare_activity_structure($paths);
95     }
97     protected function process_quiz($data) {
98         global $CFG, $DB;
100         $data = (object)$data;
101         $oldid = $data->id;
102         $data->course = $this->get_courseid();
104         // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
105         // See MDL-9367.
107         $data->timeopen = $this->apply_date_offset($data->timeopen);
108         $data->timeclose = $this->apply_date_offset($data->timeclose);
110         if (property_exists($data, 'questions')) {
111             // Needed by {@link process_quiz_attempt_legacy}, in which case it will be present.
112             $this->oldquizlayout = $data->questions;
113         }
115         // The setting quiz->attempts can come both in data->attempts and
116         // data->attempts_number, handle both. MDL-26229.
117         if (isset($data->attempts_number)) {
118             $data->attempts = $data->attempts_number;
119             unset($data->attempts_number);
120         }
122         // The old optionflags and penaltyscheme from 2.0 need to be mapped to
123         // the new preferredbehaviour. See MDL-20636.
124         if (!isset($data->preferredbehaviour)) {
125             if (empty($data->optionflags)) {
126                 $data->preferredbehaviour = 'deferredfeedback';
127             } else if (empty($data->penaltyscheme)) {
128                 $data->preferredbehaviour = 'adaptivenopenalty';
129             } else {
130                 $data->preferredbehaviour = 'adaptive';
131             }
132             unset($data->optionflags);
133             unset($data->penaltyscheme);
134         }
136         // The old review column from 2.0 need to be split into the seven new
137         // review columns. See MDL-20636.
138         if (isset($data->review)) {
139             require_once($CFG->dirroot . '/mod/quiz/locallib.php');
141             if (!defined('QUIZ_OLD_IMMEDIATELY')) {
142                 define('QUIZ_OLD_IMMEDIATELY', 0x3c003f);
143                 define('QUIZ_OLD_OPEN',        0x3c00fc0);
144                 define('QUIZ_OLD_CLOSED',      0x3c03f000);
146                 define('QUIZ_OLD_RESPONSES',        1*0x1041);
147                 define('QUIZ_OLD_SCORES',           2*0x1041);
148                 define('QUIZ_OLD_FEEDBACK',         4*0x1041);
149                 define('QUIZ_OLD_ANSWERS',          8*0x1041);
150                 define('QUIZ_OLD_SOLUTIONS',       16*0x1041);
151                 define('QUIZ_OLD_GENERALFEEDBACK', 32*0x1041);
152                 define('QUIZ_OLD_OVERALLFEEDBACK',  1*0x4440000);
153             }
155             $oldreview = $data->review;
157             $data->reviewattempt =
158                     mod_quiz_display_options::DURING |
159                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ?
160                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
161                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ?
162                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
163                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ?
164                             mod_quiz_display_options::AFTER_CLOSE : 0);
166             $data->reviewcorrectness =
167                     mod_quiz_display_options::DURING |
168                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
169                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
170                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
171                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
172                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
173                             mod_quiz_display_options::AFTER_CLOSE : 0);
175             $data->reviewmarks =
176                     mod_quiz_display_options::DURING |
177                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ?
178                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
179                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ?
180                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
181                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ?
182                             mod_quiz_display_options::AFTER_CLOSE : 0);
184             $data->reviewspecificfeedback =
185                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
186                             mod_quiz_display_options::DURING : 0) |
187                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ?
188                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
189                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ?
190                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
191                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ?
192                             mod_quiz_display_options::AFTER_CLOSE : 0);
194             $data->reviewgeneralfeedback =
195                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
196                             mod_quiz_display_options::DURING : 0) |
197                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ?
198                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
199                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ?
200                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
201                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ?
202                             mod_quiz_display_options::AFTER_CLOSE : 0);
204             $data->reviewrightanswer =
205                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
206                             mod_quiz_display_options::DURING : 0) |
207                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ?
208                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
209                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ?
210                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
211                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ?
212                             mod_quiz_display_options::AFTER_CLOSE : 0);
214             $data->reviewoverallfeedback =
215                     0 |
216                     ($oldreview & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ?
217                             mod_quiz_display_options::IMMEDIATELY_AFTER : 0) |
218                     ($oldreview & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ?
219                             mod_quiz_display_options::LATER_WHILE_OPEN : 0) |
220                     ($oldreview & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ?
221                             mod_quiz_display_options::AFTER_CLOSE : 0);
222         }
224         // The old popup column from from <= 2.1 need to be mapped to
225         // the new browsersecurity. See MDL-29627.
226         if (!isset($data->browsersecurity)) {
227             if (empty($data->popup)) {
228                 $data->browsersecurity = '-';
229             } else if ($data->popup == 1) {
230                 $data->browsersecurity = 'securewindow';
231             } else if ($data->popup == 2) {
232                 $data->browsersecurity = 'safebrowser';
233             } else {
234                 $data->preferredbehaviour = '-';
235             }
236             unset($data->popup);
237         }
239         if (!isset($data->overduehandling)) {
240             $data->overduehandling = get_config('quiz', 'overduehandling');
241         }
243         // Old shufflequestions setting is now stored in quiz sections,
244         // so save it here if necessary so it is available when we need it.
245         $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
247         // Insert the quiz record.
248         $newitemid = $DB->insert_record('quiz', $data);
249         // Immediately after inserting "activity" record, call this.
250         $this->apply_activity_instance($newitemid);
251     }
253     protected function process_quiz_question_instance($data) {
254         global $CFG, $DB;
256         $data = (object)$data;
258         // Backwards compatibility for old field names (MDL-43670).
259         if (!isset($data->questionid) && isset($data->question)) {
260             $data->questionid = $data->question;
261         }
262         if (!isset($data->maxmark) && isset($data->grade)) {
263             $data->maxmark = $data->grade;
264         }
266         if (!property_exists($data, 'slot')) {
267             $page = 1;
268             $slot = 1;
269             foreach (explode(',', $this->oldquizlayout) as $item) {
270                 if ($item == 0) {
271                     $page += 1;
272                     continue;
273                 }
274                 if ($item == $data->questionid) {
275                     $data->slot = $slot;
276                     $data->page = $page;
277                     break;
278                 }
279                 $slot += 1;
280             }
281         }
283         if (!property_exists($data, 'slot')) {
284             // There was a question_instance in the backup file for a question
285             // that was not acutally in the quiz. Drop it.
286             $this->log('question ' . $data->questionid . ' was associated with quiz ' .
287                     $this->get_new_parentid('quiz') . ' but not actually used. ' .
288                     'The instance has been ignored.', backup::LOG_INFO);
289             return;
290         }
292         $data->quizid = $this->get_new_parentid('quiz');
293         $questionmapping = $this->get_mapping('question', $data->questionid);
294         $data->questionid = $questionmapping ? $questionmapping->newitemid : false;
296         if (isset($data->questioncategoryid)) {
297             $data->questioncategoryid = $this->get_mappingid('question_category', $data->questioncategoryid);
298         } else if ($questionmapping && $questionmapping->info->qtype == 'random') {
299             // Backward compatibility for backups created using Moodle 3.4 or earlier.
300             $data->questioncategoryid = $this->get_mappingid('question_category', $questionmapping->parentitemid);
301             $data->includingsubcategories = $questionmapping->info->questiontext ? 1 : 0;
302         }
304         if (isset($data->tags)) {
305             require_once($CFG->dirroot . '/mod/quiz/locallib.php');
307             $tags = quiz_extract_random_question_tags($data->tags, $this->task->is_samesite());
308             $data->tags = quiz_build_random_question_tag_json($tags);
309         }
311         $DB->insert_record('quiz_slots', $data);
312     }
314     protected function process_quiz_section($data) {
315         global $DB;
317         $data = (object) $data;
318         $data->quizid = $this->get_new_parentid('quiz');
319         $newitemid = $DB->insert_record('quiz_sections', $data);
320         $this->sectioncreated = true;
321     }
323     protected function process_quiz_feedback($data) {
324         global $DB;
326         $data = (object)$data;
327         $oldid = $data->id;
329         $data->quizid = $this->get_new_parentid('quiz');
331         $newitemid = $DB->insert_record('quiz_feedback', $data);
332         $this->set_mapping('quiz_feedback', $oldid, $newitemid, true); // Has related files.
333     }
335     protected function process_quiz_override($data) {
336         global $DB;
338         $data = (object)$data;
339         $oldid = $data->id;
341         // Based on userinfo, we'll restore user overides or no.
342         $userinfo = $this->get_setting_value('userinfo');
344         // Skip user overrides if we are not restoring userinfo.
345         if (!$userinfo && !is_null($data->userid)) {
346             return;
347         }
349         $data->quiz = $this->get_new_parentid('quiz');
351         if ($data->userid !== null) {
352             $data->userid = $this->get_mappingid('user', $data->userid);
353         }
355         if ($data->groupid !== null) {
356             $data->groupid = $this->get_mappingid('group', $data->groupid);
357         }
359         $data->timeopen = $this->apply_date_offset($data->timeopen);
360         $data->timeclose = $this->apply_date_offset($data->timeclose);
362         $newitemid = $DB->insert_record('quiz_overrides', $data);
364         // Add mapping, restore of logs needs it.
365         $this->set_mapping('quiz_override', $oldid, $newitemid);
366     }
368     protected function process_quiz_grade($data) {
369         global $DB;
371         $data = (object)$data;
372         $oldid = $data->id;
374         $data->quiz = $this->get_new_parentid('quiz');
376         $data->userid = $this->get_mappingid('user', $data->userid);
377         $data->grade = $data->gradeval;
379         $DB->insert_record('quiz_grades', $data);
380     }
382     protected function process_quiz_attempt($data) {
383         $data = (object)$data;
385         $data->quiz = $this->get_new_parentid('quiz');
386         $data->attempt = $data->attemptnum;
388         $data->userid = $this->get_mappingid('user', $data->userid);
390         if (!empty($data->timecheckstate)) {
391             $data->timecheckstate = $this->apply_date_offset($data->timecheckstate);
392         } else {
393             $data->timecheckstate = 0;
394         }
396         // Deals with up-grading pre-2.3 back-ups to 2.3+.
397         if (!isset($data->state)) {
398             if ($data->timefinish > 0) {
399                 $data->state = 'finished';
400             } else {
401                 $data->state = 'inprogress';
402             }
403         }
405         // The data is actually inserted into the database later in inform_new_usage_id.
406         $this->currentquizattempt = clone($data);
407     }
409     protected function process_quiz_attempt_legacy($data) {
410         global $DB;
412         $this->process_quiz_attempt($data);
414         $quiz = $DB->get_record('quiz', array('id' => $this->get_new_parentid('quiz')));
415         $quiz->oldquestions = $this->oldquizlayout;
416         $this->process_legacy_quiz_attempt_data($data, $quiz);
417     }
419     protected function inform_new_usage_id($newusageid) {
420         global $DB;
422         $data = $this->currentquizattempt;
424         $oldid = $data->id;
425         $data->uniqueid = $newusageid;
427         $newitemid = $DB->insert_record('quiz_attempts', $data);
429         // Save quiz_attempt->id mapping, because logs use it.
430         $this->set_mapping('quiz_attempt', $oldid, $newitemid, false);
431     }
433     protected function after_execute() {
434         global $DB;
436         parent::after_execute();
437         // Add quiz related files, no need to match by itemname (just internally handled context).
438         $this->add_related_files('mod_quiz', 'intro', null);
439         // Add feedback related files, matching by itemname = 'quiz_feedback'.
440         $this->add_related_files('mod_quiz', 'feedback', 'quiz_feedback');
442         if (!$this->sectioncreated) {
443             $DB->insert_record('quiz_sections', array(
444                     'quizid' => $this->get_new_parentid('quiz'),
445                     'firstslot' => 1, 'heading' => '',
446                     'shufflequestions' => $this->legacyshufflequestionsoption));
447         }
448     }