source: moodle/trunk/fuentes/backup/moodle2/restore_stepslib.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 207.7 KB
Line 
1<?php
2
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/>.
17
18/**
19 * Defines various restore steps that will be used by common tasks in restore
20 *
21 * @package     core_backup
22 * @subpackage  moodle2
23 * @category    backup
24 * @copyright   2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
25 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 */
27
28defined('MOODLE_INTERNAL') || die();
29
30/**
31 * delete old directories and conditionally create backup_temp_ids table
32 */
33class restore_create_and_clean_temp_stuff extends restore_execution_step {
34
35    protected function define_execution() {
36        $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
37        // If the table already exists, it's because restore_prechecks have been executed in the same
38        // request (without problems) and it already contains a bunch of preloaded information (users...)
39        // that we aren't going to execute again
40        if ($exists) { // Inform plan about preloaded information
41            $this->task->set_preloaded_information();
42        }
43        // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
44        $itemid = $this->task->get_old_contextid();
45        $newitemid = context_course::instance($this->get_courseid())->id;
46        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
47        // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
48        $itemid = $this->task->get_old_system_contextid();
49        $newitemid = context_system::instance()->id;
50        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
51        // Create the old-course-id to new-course-id mapping, we need that available since the beginning
52        $itemid = $this->task->get_old_courseid();
53        $newitemid = $this->get_courseid();
54        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
55
56    }
57}
58
59/**
60 * delete the temp dir used by backup/restore (conditionally),
61 * delete old directories and drop temp ids table
62 */
63class restore_drop_and_clean_temp_stuff extends restore_execution_step {
64
65    protected function define_execution() {
66        global $CFG;
67        restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
68        $progress = $this->task->get_progress();
69        $progress->start_progress('Deleting backup dir');
70        backup_helper::delete_old_backup_dirs(strtotime('-1 week'), $progress);      // Delete > 1 week old temp dirs.
71        if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
72            backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
73        }
74        $progress->end_progress();
75    }
76}
77
78/**
79 * Restore calculated grade items, grade categories etc
80 */
81class restore_gradebook_structure_step extends restore_structure_step {
82
83    /**
84     * To conditionally decide if this step must be executed
85     * Note the "settings" conditions are evaluated in the
86     * corresponding task. Here we check for other conditions
87     * not being restore settings (files, site settings...)
88     */
89     protected function execute_condition() {
90        global $CFG, $DB;
91
92        if ($this->get_courseid() == SITEID) {
93            return false;
94        }
95
96        // No gradebook info found, don't execute
97        $fullpath = $this->task->get_taskbasepath();
98        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
99        if (!file_exists($fullpath)) {
100            return false;
101        }
102
103        // Some module present in backup file isn't available to restore
104        // in this site, don't execute
105        if ($this->task->is_missing_modules()) {
106            return false;
107        }
108
109        // Some activity has been excluded to be restored, don't execute
110        if ($this->task->is_excluding_activities()) {
111            return false;
112        }
113
114        // There should only be one grade category (the 1 associated with the course itself)
115        // If other categories already exist we're restoring into an existing course.
116        // Restoring categories into a course with an existing category structure is unlikely to go well
117        $category = new stdclass();
118        $category->courseid  = $this->get_courseid();
119        $catcount = $DB->count_records('grade_categories', (array)$category);
120        if ($catcount>1) {
121            return false;
122        }
123
124        // Arrived here, execute the step
125        return true;
126     }
127
128    protected function define_structure() {
129        $paths = array();
130        $userinfo = $this->task->get_setting_value('users');
131
132        $paths[] = new restore_path_element('gradebook', '/gradebook');
133        $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
134        $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
135        if ($userinfo) {
136            $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
137        }
138        $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
139        $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
140
141        return $paths;
142    }
143
144    protected function process_gradebook($data) {
145        // For non-merge restore types:
146        // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
147        $target = $this->get_task()->get_target();
148        if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
149            set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
150        }
151        if (!empty($data['calculations_freeze'])) {
152            if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
153                    $target == backup::TARGET_EXISTING_DELETING) {
154                set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
155            }
156        }
157    }
158
159    protected function process_grade_item($data) {
160        global $DB;
161
162        $data = (object)$data;
163
164        $oldid = $data->id;
165        $data->course = $this->get_courseid();
166
167        $data->courseid = $this->get_courseid();
168
169        if ($data->itemtype=='manual') {
170            // manual grade items store category id in categoryid
171            $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
172            // if mapping failed put in course's grade category
173            if (NULL == $data->categoryid) {
174                $coursecat = grade_category::fetch_course_category($this->get_courseid());
175                $data->categoryid = $coursecat->id;
176            }
177        } else if ($data->itemtype=='course') {
178            // course grade item stores their category id in iteminstance
179            $coursecat = grade_category::fetch_course_category($this->get_courseid());
180            $data->iteminstance = $coursecat->id;
181        } else if ($data->itemtype=='category') {
182            // category grade items store their category id in iteminstance
183            $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
184        } else {
185            throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
186        }
187
188        $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
189        $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
190
191        $data->locktime     = $this->apply_date_offset($data->locktime);
192        $data->timecreated  = $this->apply_date_offset($data->timecreated);
193        $data->timemodified = $this->apply_date_offset($data->timemodified);
194
195        $coursecategory = $newitemid = null;
196        //course grade item should already exist so updating instead of inserting
197        if($data->itemtype=='course') {
198            //get the ID of the already created grade item
199            $gi = new stdclass();
200            $gi->courseid  = $this->get_courseid();
201            $gi->itemtype  = $data->itemtype;
202
203            //need to get the id of the grade_category that was automatically created for the course
204            $category = new stdclass();
205            $category->courseid  = $this->get_courseid();
206            $category->parent  = null;
207            //course category fullname starts out as ? but may be edited
208            //$category->fullname  = '?';
209            $coursecategory = $DB->get_record('grade_categories', (array)$category);
210            $gi->iteminstance = $coursecategory->id;
211
212            $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
213            if (!empty($existinggradeitem)) {
214                $data->id = $newitemid = $existinggradeitem->id;
215                $DB->update_record('grade_items', $data);
216            }
217        } else if ($data->itemtype == 'manual') {
218            // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
219            $gi = array(
220                'itemtype' => $data->itemtype,
221                'courseid' => $data->courseid,
222                'itemname' => $data->itemname,
223                'categoryid' => $data->categoryid,
224            );
225            $newitemid = $DB->get_field('grade_items', 'id', $gi);
226        }
227
228        if (empty($newitemid)) {
229            //in case we found the course category but still need to insert the course grade item
230            if ($data->itemtype=='course' && !empty($coursecategory)) {
231                $data->iteminstance = $coursecategory->id;
232            }
233
234            $newitemid = $DB->insert_record('grade_items', $data);
235        }
236        $this->set_mapping('grade_item', $oldid, $newitemid);
237    }
238
239    protected function process_grade_grade($data) {
240        global $DB;
241
242        $data = (object)$data;
243        $oldid = $data->id;
244        $olduserid = $data->userid;
245
246        $data->itemid = $this->get_new_parentid('grade_item');
247
248        $data->userid = $this->get_mappingid('user', $data->userid, null);
249        if (!empty($data->userid)) {
250            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
251            $data->locktime     = $this->apply_date_offset($data->locktime);
252            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
253            $data->overridden = $this->apply_date_offset($data->overridden);
254            $data->timecreated  = $this->apply_date_offset($data->timecreated);
255            $data->timemodified = $this->apply_date_offset($data->timemodified);
256
257            $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
258            if ($gradeexists) {
259                $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
260                $this->log($message, backup::LOG_DEBUG);
261            } else {
262                $newitemid = $DB->insert_record('grade_grades', $data);
263                $this->set_mapping('grade_grades', $oldid, $newitemid);
264            }
265        } else {
266            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
267            $this->log($message, backup::LOG_DEBUG);
268        }
269    }
270
271    protected function process_grade_category($data) {
272        global $DB;
273
274        $data = (object)$data;
275        $oldid = $data->id;
276
277        $data->course = $this->get_courseid();
278        $data->courseid = $data->course;
279
280        $data->timecreated  = $this->apply_date_offset($data->timecreated);
281        $data->timemodified = $this->apply_date_offset($data->timemodified);
282
283        $newitemid = null;
284        //no parent means a course level grade category. That may have been created when the course was created
285        if(empty($data->parent)) {
286            //parent was being saved as 0 when it should be null
287            $data->parent = null;
288
289            //get the already created course level grade category
290            $category = new stdclass();
291            $category->courseid = $this->get_courseid();
292            $category->parent = null;
293
294            $coursecategory = $DB->get_record('grade_categories', (array)$category);
295            if (!empty($coursecategory)) {
296                $data->id = $newitemid = $coursecategory->id;
297                $DB->update_record('grade_categories', $data);
298            }
299        }
300
301        // Add a warning about a removed setting.
302        if (!empty($data->aggregatesubcats)) {
303            set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
304        }
305
306        //need to insert a course category
307        if (empty($newitemid)) {
308            $newitemid = $DB->insert_record('grade_categories', $data);
309        }
310        $this->set_mapping('grade_category', $oldid, $newitemid);
311    }
312    protected function process_grade_letter($data) {
313        global $DB;
314
315        $data = (object)$data;
316        $oldid = $data->id;
317
318        $data->contextid = context_course::instance($this->get_courseid())->id;
319
320        $gradeletter = (array)$data;
321        unset($gradeletter['id']);
322        if (!$DB->record_exists('grade_letters', $gradeletter)) {
323            $newitemid = $DB->insert_record('grade_letters', $data);
324        } else {
325            $newitemid = $data->id;
326        }
327
328        $this->set_mapping('grade_letter', $oldid, $newitemid);
329    }
330    protected function process_grade_setting($data) {
331        global $DB;
332
333        $data = (object)$data;
334        $oldid = $data->id;
335
336        $data->courseid = $this->get_courseid();
337
338        $target = $this->get_task()->get_target();
339        if ($data->name == 'minmaxtouse' &&
340                ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
341            // We never restore minmaxtouse during merge.
342            return;
343        }
344
345        if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
346            $newitemid = $DB->insert_record('grade_settings', $data);
347        } else {
348            $newitemid = $data->id;
349        }
350
351        if (!empty($oldid)) {
352            // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
353            $this->set_mapping('grade_setting', $oldid, $newitemid);
354        }
355    }
356
357    /**
358     * put all activity grade items in the correct grade category and mark all for recalculation
359     */
360    protected function after_execute() {
361        global $DB;
362
363        $conditions = array(
364            'backupid' => $this->get_restoreid(),
365            'itemname' => 'grade_item'//,
366            //'itemid'   => $itemid
367        );
368        $rs = $DB->get_recordset('backup_ids_temp', $conditions);
369
370        // We need this for calculation magic later on.
371        $mappings = array();
372
373        if (!empty($rs)) {
374            foreach($rs as $grade_item_backup) {
375
376                // Store the oldid with the new id.
377                $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
378
379                $updateobj = new stdclass();
380                $updateobj->id = $grade_item_backup->newitemid;
381
382                //if this is an activity grade item that needs to be put back in its correct category
383                if (!empty($grade_item_backup->parentitemid)) {
384                    $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
385                    if (!is_null($oldcategoryid)) {
386                        $updateobj->categoryid = $oldcategoryid;
387                        $DB->update_record('grade_items', $updateobj);
388                    }
389                } else {
390                    //mark course and category items as needing to be recalculated
391                    $updateobj->needsupdate=1;
392                    $DB->update_record('grade_items', $updateobj);
393                }
394            }
395        }
396        $rs->close();
397
398        // We need to update the calculations for calculated grade items that may reference old
399        // grade item ids using ##gi\d+##.
400        // $mappings can be empty, use 0 if so (won't match ever)
401        list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
402        $sql = "SELECT gi.id, gi.calculation
403                  FROM {grade_items} gi
404                 WHERE gi.id {$sql} AND
405                       calculation IS NOT NULL";
406        $rs = $DB->get_recordset_sql($sql, $params);
407        foreach ($rs as $gradeitem) {
408            // Collect all of the used grade item id references
409            if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
410                // This calculation doesn't reference any other grade items... EASY!
411                continue;
412            }
413            // For this next bit we are going to do the replacement of id's in two steps:
414            // 1. We will replace all old id references with a special mapping reference.
415            // 2. We will replace all mapping references with id's
416            // Why do we do this?
417            // Because there potentially there will be an overlap of ids within the query and we
418            // we substitute the wrong id.. safest way around this is the two step system
419            $calculationmap = array();
420            $mapcount = 0;
421            foreach ($matches[1] as $match) {
422                // Check that the old id is known to us, if not it was broken to begin with and will
423                // continue to be broken.
424                if (!array_key_exists($match, $mappings)) {
425                    continue;
426                }
427                // Our special mapping key
428                $mapping = '##MAPPING'.$mapcount.'##';
429                // The old id that exists within the calculation now
430                $oldid = '##gi'.$match.'##';
431                // The new id that we want to replace the old one with.
432                $newid = '##gi'.$mappings[$match].'##';
433                // Replace in the special mapping key
434                $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
435                // And record the mapping
436                $calculationmap[$mapping] = $newid;
437                $mapcount++;
438            }
439            // Iterate all special mappings for this calculation and replace in the new id's
440            foreach ($calculationmap as $mapping => $newid) {
441                $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
442            }
443            // Update the calculation now that its being remapped
444            $DB->update_record('grade_items', $gradeitem);
445        }
446        $rs->close();
447
448        // Need to correct the grade category path and parent
449        $conditions = array(
450            'courseid' => $this->get_courseid()
451        );
452
453        $rs = $DB->get_recordset('grade_categories', $conditions);
454        // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
455        foreach ($rs as $gc) {
456            if (!empty($gc->parent)) {
457                $grade_category = new stdClass();
458                $grade_category->id = $gc->id;
459                $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
460                $DB->update_record('grade_categories', $grade_category);
461            }
462        }
463        $rs->close();
464
465        // Now we can rebuild all the paths
466        $rs = $DB->get_recordset('grade_categories', $conditions);
467        foreach ($rs as $gc) {
468            $grade_category = new stdClass();
469            $grade_category->id = $gc->id;
470            $grade_category->path = grade_category::build_path($gc);
471            $grade_category->depth = substr_count($grade_category->path, '/') - 1;
472            $DB->update_record('grade_categories', $grade_category);
473        }
474        $rs->close();
475
476        // Check what to do with the minmaxtouse setting.
477        $this->check_minmaxtouse();
478
479        // Freeze gradebook calculations if needed.
480        $this->gradebook_calculation_freeze();
481
482        // Restore marks items as needing update. Update everything now.
483        grade_regrade_final_grades($this->get_courseid());
484    }
485
486    /**
487     * Freeze gradebook calculation if needed.
488     *
489     * This is similar to various upgrade scripts that check if the freeze is needed.
490     */
491    protected function gradebook_calculation_freeze() {
492        global $CFG;
493        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
494        preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
495        $backupbuild = (int)$matches[1];
496
497        // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
498        if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
499            require_once($CFG->libdir . '/db/upgradelib.php');
500            upgrade_extra_credit_weightoverride($this->get_courseid());
501        }
502        // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
503        if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
504            require_once($CFG->libdir . '/db/upgradelib.php');
505            upgrade_calculated_grade_items($this->get_courseid());
506        }
507    }
508
509    /**
510     * Checks what should happen with the course grade setting minmaxtouse.
511     *
512     * This is related to the upgrade step at the time the setting was added.
513     *
514     * @see MDL-48618
515     * @return void
516     */
517    protected function check_minmaxtouse() {
518        global $CFG, $DB;
519        require_once($CFG->libdir . '/gradelib.php');
520
521        $userinfo = $this->task->get_setting_value('users');
522        $settingname = 'minmaxtouse';
523        $courseid = $this->get_courseid();
524        $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
525        $version28start = 2014111000.00;
526        $version28last = 2014111006.05;
527        $version29start = 2015051100.00;
528        $version29last = 2015060400.02;
529
530        $target = $this->get_task()->get_target();
531        if ($minmaxtouse === false &&
532                ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
533            // The setting was not found because this setting did not exist at the time the backup was made.
534            // And we are not restoring as merge, in which case we leave the course as it was.
535            $version = $this->get_task()->get_info()->moodle_version;
536
537            if ($version < $version28start) {
538                // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
539                if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
540                    grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
541                }
542
543            } else if (($version >= $version28start && $version < $version28last) ||
544                    ($version >= $version29start && $version < $version29last)) {
545                // They should be using grade_grade when the course has inconsistencies.
546
547                $sql = "SELECT gi.id
548                          FROM {grade_items} gi
549                          JOIN {grade_grades} gg
550                            ON gg.itemid = gi.id
551                         WHERE gi.courseid = ?
552                           AND (gi.itemtype != ? AND gi.itemtype != ?)
553                           AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
554
555                // The course can only have inconsistencies when we restore the user info,
556                // we do not need to act on existing grades that were not restored as part of this backup.
557                if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
558
559                    // Display the notice as we do during upgrade.
560                    set_config('show_min_max_grades_changed_' . $courseid, 1);
561
562                    if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
563                        // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
564                        // If they are using the site-wide grade_grade setting, we only want to notice them.
565                        grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
566                    }
567                }
568
569            } else {
570                // This should never happen because from now on minmaxtouse is always saved in backups.
571            }
572        }
573    }
574}
575
576/**
577 * Step in charge of restoring the grade history of a course.
578 *
579 * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
580 * we do not want to restore the history if the gradebook and its content has not been
581 * restored. At least for now.
582 */
583class restore_grade_history_structure_step extends restore_structure_step {
584
585     protected function execute_condition() {
586        global $CFG, $DB;
587
588        if ($this->get_courseid() == SITEID) {
589            return false;
590        }
591
592        // No gradebook info found, don't execute.
593        $fullpath = $this->task->get_taskbasepath();
594        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
595        if (!file_exists($fullpath)) {
596            return false;
597        }
598
599        // Some module present in backup file isn't available to restore in this site, don't execute.
600        if ($this->task->is_missing_modules()) {
601            return false;
602        }
603
604        // Some activity has been excluded to be restored, don't execute.
605        if ($this->task->is_excluding_activities()) {
606            return false;
607        }
608
609        // There should only be one grade category (the 1 associated with the course itself).
610        $category = new stdclass();
611        $category->courseid  = $this->get_courseid();
612        $catcount = $DB->count_records('grade_categories', (array)$category);
613        if ($catcount > 1) {
614            return false;
615        }
616
617        // Arrived here, execute the step.
618        return true;
619     }
620
621    protected function define_structure() {
622        $paths = array();
623
624        // Settings to use.
625        $userinfo = $this->get_setting_value('users');
626        $history = $this->get_setting_value('grade_histories');
627
628        if ($userinfo && $history) {
629            $paths[] = new restore_path_element('grade_grade',
630               '/grade_history/grade_grades/grade_grade');
631        }
632
633        return $paths;
634    }
635
636    protected function process_grade_grade($data) {
637        global $DB;
638
639        $data = (object)($data);
640        $olduserid = $data->userid;
641        unset($data->id);
642
643        $data->userid = $this->get_mappingid('user', $data->userid, null);
644        if (!empty($data->userid)) {
645            // Do not apply the date offsets as this is history.
646            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
647            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
648            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
649            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
650            $DB->insert_record('grade_grades_history', $data);
651        } else {
652            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
653            $this->log($message, backup::LOG_DEBUG);
654        }
655    }
656
657}
658
659/**
660 * decode all the interlinks present in restored content
661 * relying 100% in the restore_decode_processor that handles
662 * both the contents to modify and the rules to be applied
663 */
664class restore_decode_interlinks extends restore_execution_step {
665
666    protected function define_execution() {
667        // Get the decoder (from the plan)
668        $decoder = $this->task->get_decoder();
669        restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
670        // And launch it, everything will be processed
671        $decoder->execute();
672    }
673}
674
675/**
676 * first, ensure that we have no gaps in section numbers
677 * and then, rebuid the course cache
678 */
679class restore_rebuild_course_cache extends restore_execution_step {
680
681    protected function define_execution() {
682        global $DB;
683
684        // Although there is some sort of auto-recovery of missing sections
685        // present in course/formats... here we check that all the sections
686        // from 0 to MAX(section->section) exist, creating them if necessary
687        $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
688        // Iterate over all sections
689        for ($i = 0; $i <= $maxsection; $i++) {
690            // If the section $i doesn't exist, create it
691            if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
692                $sectionrec = array(
693                    'course' => $this->get_courseid(),
694                    'section' => $i);
695                $DB->insert_record('course_sections', $sectionrec); // missing section created
696            }
697        }
698
699        // Rebuild cache now that all sections are in place
700        rebuild_course_cache($this->get_courseid());
701        cache_helper::purge_by_event('changesincourse');
702        cache_helper::purge_by_event('changesincoursecat');
703    }
704}
705
706/**
707 * Review all the tasks having one after_restore method
708 * executing it to perform some final adjustments of information
709 * not available when the task was executed.
710 */
711class restore_execute_after_restore extends restore_execution_step {
712
713    protected function define_execution() {
714
715        // Simply call to the execute_after_restore() method of the task
716        // that always is the restore_final_task
717        $this->task->launch_execute_after_restore();
718    }
719}
720
721
722/**
723 * Review all the (pending) block positions in backup_ids, matching by
724 * contextid, creating positions as needed. This is executed by the
725 * final task, once all the contexts have been created
726 */
727class restore_review_pending_block_positions extends restore_execution_step {
728
729    protected function define_execution() {
730        global $DB;
731
732        // Get all the block_position objects pending to match
733        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
734        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
735        // Process block positions, creating them or accumulating for final step
736        foreach($rs as $posrec) {
737            // Get the complete position object out of the info field.
738            $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
739            // If position is for one already mapped (known) contextid
740            // process it now, creating the position, else nothing to
741            // do, position finally discarded
742            if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
743                $position->contextid = $newctx->newitemid;
744                // Create the block position
745                $DB->insert_record('block_positions', $position);
746            }
747        }
748        $rs->close();
749    }
750}
751
752
753/**
754 * Updates the availability data for course modules and sections.
755 *
756 * Runs after the restore of all course modules, sections, and grade items has
757 * completed. This is necessary in order to update IDs that have changed during
758 * restore.
759 *
760 * @package core_backup
761 * @copyright 2014 The Open University
762 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
763 */
764class restore_update_availability extends restore_execution_step {
765
766    protected function define_execution() {
767        global $CFG, $DB;
768
769        // Note: This code runs even if availability is disabled when restoring.
770        // That will ensure that if you later turn availability on for the site,
771        // there will be no incorrect IDs. (It doesn't take long if the restored
772        // data does not contain any availability information.)
773
774        // Get modinfo with all data after resetting cache.
775        rebuild_course_cache($this->get_courseid(), true);
776        $modinfo = get_fast_modinfo($this->get_courseid());
777
778        // Get the date offset for this restore.
779        $dateoffset = $this->apply_date_offset(1) - 1;
780
781        // Update all sections that were restored.
782        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
783        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
784        $sectionsbyid = null;
785        foreach ($rs as $rec) {
786            if (is_null($sectionsbyid)) {
787                $sectionsbyid = array();
788                foreach ($modinfo->get_section_info_all() as $section) {
789                    $sectionsbyid[$section->id] = $section;
790                }
791            }
792            if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
793                // If the section was not fully restored for some reason
794                // (e.g. due to an earlier error), skip it.
795                $this->get_logger()->process('Section not fully restored: id ' .
796                        $rec->newitemid, backup::LOG_WARNING);
797                continue;
798            }
799            $section = $sectionsbyid[$rec->newitemid];
800            if (!is_null($section->availability)) {
801                $info = new \core_availability\info_section($section);
802                $info->update_after_restore($this->get_restoreid(),
803                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
804            }
805        }
806        $rs->close();
807
808        // Update all modules that were restored.
809        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
810        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
811        foreach ($rs as $rec) {
812            if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
813                // If the module was not fully restored for some reason
814                // (e.g. due to an earlier error), skip it.
815                $this->get_logger()->process('Module not fully restored: id ' .
816                        $rec->newitemid, backup::LOG_WARNING);
817                continue;
818            }
819            $cm = $modinfo->get_cm($rec->newitemid);
820            if (!is_null($cm->availability)) {
821                $info = new \core_availability\info_module($cm);
822                $info->update_after_restore($this->get_restoreid(),
823                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
824            }
825        }
826        $rs->close();
827    }
828}
829
830
831/**
832 * Process legacy module availability records in backup_ids.
833 *
834 * Matches course modules and grade item id once all them have been already restored.
835 * Only if all matchings are satisfied the availability condition will be created.
836 * At the same time, it is required for the site to have that functionality enabled.
837 *
838 * This step is included only to handle legacy backups (2.6 and before). It does not
839 * do anything for newer backups.
840 *
841 * @copyright 2014 The Open University
842 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
843 */
844class restore_process_course_modules_availability extends restore_execution_step {
845
846    protected function define_execution() {
847        global $CFG, $DB;
848
849        // Site hasn't availability enabled
850        if (empty($CFG->enableavailability)) {
851            return;
852        }
853
854        // Do both modules and sections.
855        foreach (array('module', 'section') as $table) {
856            // Get all the availability objects to process.
857            $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
858            $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
859            // Process availabilities, creating them if everything matches ok.
860            foreach ($rs as $availrec) {
861                $allmatchesok = true;
862                // Get the complete legacy availability object.
863                $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
864
865                // Note: This code used to update IDs, but that is now handled by the
866                // current code (after restore) instead of this legacy code.
867
868                // Get showavailability option.
869                $thingid = ($table === 'module') ? $availability->coursemoduleid :
870                        $availability->coursesectionid;
871                $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
872                        $table . '_showavailability', $thingid);
873                if (!$showrec) {
874                    // Should not happen.
875                    throw new coding_exception('No matching showavailability record');
876                }
877                $show = $showrec->info->showavailability;
878
879                // The $availability object is now in the format used in the old
880                // system. Interpret this and convert to new system.
881                $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
882                        array('id' => $thingid), MUST_EXIST);
883                $newvalue = \core_availability\info::add_legacy_availability_condition(
884                        $currentvalue, $availability, $show);
885                $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
886                        array('id' => $thingid));
887            }
888        }
889        $rs->close();
890    }
891}
892
893
894/*
895 * Execution step that, *conditionally* (if there isn't preloaded information)
896 * will load the inforef files for all the included course/section/activity tasks
897 * to backup_temp_ids. They will be stored with "xxxxref" as itemname
898 */
899class restore_load_included_inforef_records extends restore_execution_step {
900
901    protected function define_execution() {
902
903        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
904            return;
905        }
906
907        // Get all the included tasks
908        $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
909        $progress = $this->task->get_progress();
910        $progress->start_progress($this->get_name(), count($tasks));
911        foreach ($tasks as $task) {
912            // Load the inforef.xml file if exists
913            $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
914            if (file_exists($inforefpath)) {
915                // Load each inforef file to temp_ids.
916                restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
917            }
918        }
919        $progress->end_progress();
920    }
921}
922
923/*
924 * Execution step that will load all the needed files into backup_files_temp
925 *   - info: contains the whole original object (times, names...)
926 * (all them being original ids as loaded from xml)
927 */
928class restore_load_included_files extends restore_structure_step {
929
930    protected function define_structure() {
931
932        $file = new restore_path_element('file', '/files/file');
933
934        return array($file);
935    }
936
937    /**
938     * Process one <file> element from files.xml
939     *
940     * @param array $data the element data
941     */
942    public function process_file($data) {
943
944        $data = (object)$data; // handy
945
946        // load it if needed:
947        //   - it it is one of the annotated inforef files (course/section/activity/block)
948        //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
949        // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
950        //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
951        $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
952        $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
953                        $data->component == 'grouping' || $data->component == 'grade' ||
954                        $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
955        if ($isfileref || $iscomponent) {
956            restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
957        }
958    }
959}
960
961/**
962 * Execution step that, *conditionally* (if there isn't preloaded information),
963 * will load all the needed roles to backup_temp_ids. They will be stored with
964 * "role" itemname. Also it will perform one automatic mapping to roles existing
965 * in the target site, based in permissions of the user performing the restore,
966 * archetypes and other bits. At the end, each original role will have its associated
967 * target role or 0 if it's going to be skipped. Note we wrap everything over one
968 * restore_dbops method, as far as the same stuff is going to be also executed
969 * by restore prechecks
970 */
971class restore_load_and_map_roles extends restore_execution_step {
972
973    protected function define_execution() {
974        if ($this->task->get_preloaded_information()) { // if info is already preloaded
975            return;
976        }
977
978        $file = $this->get_basepath() . '/roles.xml';
979        // Load needed toles to temp_ids
980        restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
981
982        // Process roles, mapping/skipping. Any error throws exception
983        // Note we pass controller's info because it can contain role mapping information
984        // about manual mappings performed by UI
985        restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_info()->role_mappings);
986    }
987}
988
989/**
990 * Execution step that, *conditionally* (if there isn't preloaded information
991 * and users have been selected in settings, will load all the needed users
992 * to backup_temp_ids. They will be stored with "user" itemname and with
993 * their original contextid as paremitemid
994 */
995class restore_load_included_users extends restore_execution_step {
996
997    protected function define_execution() {
998
999        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1000            return;
1001        }
1002        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1003            return;
1004        }
1005        $file = $this->get_basepath() . '/users.xml';
1006        // Load needed users to temp_ids.
1007        restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
1008    }
1009}
1010
1011/**
1012 * Execution step that, *conditionally* (if there isn't preloaded information
1013 * and users have been selected in settings, will process all the needed users
1014 * in order to decide and perform any action with them (create / map / error)
1015 * Note: Any error will cause exception, as far as this is the same processing
1016 * than the one into restore prechecks (that should have stopped process earlier)
1017 */
1018class restore_process_included_users extends restore_execution_step {
1019
1020    protected function define_execution() {
1021
1022        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1023            return;
1024        }
1025        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1026            return;
1027        }
1028        restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
1029                $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
1030    }
1031}
1032
1033/**
1034 * Execution step that will create all the needed users as calculated
1035 * by @restore_process_included_users (those having newiteind = 0)
1036 */
1037class restore_create_included_users extends restore_execution_step {
1038
1039    protected function define_execution() {
1040
1041        restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
1042                $this->task->get_userid(), $this->task->get_progress());
1043    }
1044}
1045
1046/**
1047 * Structure step that will create all the needed groups and groupings
1048 * by loading them from the groups.xml file performing the required matches.
1049 * Note group members only will be added if restoring user info
1050 */
1051class restore_groups_structure_step extends restore_structure_step {
1052
1053    protected function define_structure() {
1054
1055        $paths = array(); // Add paths here
1056
1057        // Do not include group/groupings information if not requested.
1058        $groupinfo = $this->get_setting_value('groups');
1059        if ($groupinfo) {
1060            $paths[] = new restore_path_element('group', '/groups/group');
1061            $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
1062            $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
1063        }
1064        return $paths;
1065    }
1066
1067    // Processing functions go here
1068    public function process_group($data) {
1069        global $DB;
1070
1071        $data = (object)$data; // handy
1072        $data->courseid = $this->get_courseid();
1073
1074        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1075        // another a group in the same course
1076        $context = context_course::instance($data->courseid);
1077        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1078            if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
1079                unset($data->idnumber);
1080            }
1081        } else {
1082            unset($data->idnumber);
1083        }
1084
1085        $oldid = $data->id;    // need this saved for later
1086
1087        $restorefiles = false; // Only if we end creating the group
1088
1089        // Search if the group already exists (by name & description) in the target course
1090        $description_clause = '';
1091        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1092        if (!empty($data->description)) {
1093            $description_clause = ' AND ' .
1094                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1095           $params['description'] = $data->description;
1096        }
1097        if (!$groupdb = $DB->get_record_sql("SELECT *
1098                                               FROM {groups}
1099                                              WHERE courseid = :courseid
1100                                                AND name = :grname $description_clause", $params)) {
1101            // group doesn't exist, create
1102            $newitemid = $DB->insert_record('groups', $data);
1103            $restorefiles = true; // We'll restore the files
1104        } else {
1105            // group exists, use it
1106            $newitemid = $groupdb->id;
1107        }
1108        // Save the id mapping
1109        $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
1110        // Invalidate the course group data cache just in case.
1111        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1112    }
1113
1114    public function process_grouping($data) {
1115        global $DB;
1116
1117        $data = (object)$data; // handy
1118        $data->courseid = $this->get_courseid();
1119
1120        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1121        // another a grouping in the same course
1122        $context = context_course::instance($data->courseid);
1123        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1124            if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
1125                unset($data->idnumber);
1126            }
1127        } else {
1128            unset($data->idnumber);
1129        }
1130
1131        $oldid = $data->id;    // need this saved for later
1132        $restorefiles = false; // Only if we end creating the grouping
1133
1134        // Search if the grouping already exists (by name & description) in the target course
1135        $description_clause = '';
1136        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1137        if (!empty($data->description)) {
1138            $description_clause = ' AND ' .
1139                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1140           $params['description'] = $data->description;
1141        }
1142        if (!$groupingdb = $DB->get_record_sql("SELECT *
1143                                                  FROM {groupings}
1144                                                 WHERE courseid = :courseid
1145                                                   AND name = :grname $description_clause", $params)) {
1146            // grouping doesn't exist, create
1147            $newitemid = $DB->insert_record('groupings', $data);
1148            $restorefiles = true; // We'll restore the files
1149        } else {
1150            // grouping exists, use it
1151            $newitemid = $groupingdb->id;
1152        }
1153        // Save the id mapping
1154        $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1155        // Invalidate the course group data cache just in case.
1156        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1157    }
1158
1159    public function process_grouping_group($data) {
1160        global $CFG;
1161
1162        require_once($CFG->dirroot.'/group/lib.php');
1163
1164        $data = (object)$data;
1165        groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1166    }
1167
1168    protected function after_execute() {
1169        // Add group related files, matching with "group" mappings
1170        $this->add_related_files('group', 'icon', 'group');
1171        $this->add_related_files('group', 'description', 'group');
1172        // Add grouping related files, matching with "grouping" mappings
1173        $this->add_related_files('grouping', 'description', 'grouping');
1174        // Invalidate the course group data.
1175        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1176    }
1177
1178}
1179
1180/**
1181 * Structure step that will create all the needed group memberships
1182 * by loading them from the groups.xml file performing the required matches.
1183 */
1184class restore_groups_members_structure_step extends restore_structure_step {
1185
1186    protected $plugins = null;
1187
1188    protected function define_structure() {
1189
1190        $paths = array(); // Add paths here
1191
1192        if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
1193            $paths[] = new restore_path_element('group', '/groups/group');
1194            $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1195        }
1196
1197        return $paths;
1198    }
1199
1200    public function process_group($data) {
1201        $data = (object)$data; // handy
1202
1203        // HACK ALERT!
1204        // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1205        // Let's fake internal state to make $this->get_new_parentid('group') work.
1206
1207        $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1208    }
1209
1210    public function process_member($data) {
1211        global $DB, $CFG;
1212        require_once("$CFG->dirroot/group/lib.php");
1213
1214        // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1215
1216        $data = (object)$data; // handy
1217
1218        // get parent group->id
1219        $data->groupid = $this->get_new_parentid('group');
1220
1221        // map user newitemid and insert if not member already
1222        if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1223            if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1224                // Check the component, if any, exists.
1225                if (empty($data->component)) {
1226                    groups_add_member($data->groupid, $data->userid);
1227
1228                } else if ((strpos($data->component, 'enrol_') === 0)) {
1229                    // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1230                    // it is possible that enrolment was restored using different plugin type.
1231                    if (!isset($this->plugins)) {
1232                        $this->plugins = enrol_get_plugins(true);
1233                    }
1234                    if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1235                        if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1236                            if (isset($this->plugins[$instance->enrol])) {
1237                                $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1238                            }
1239                        }
1240                    }
1241
1242                } else {
1243                    $dir = core_component::get_component_directory($data->component);
1244                    if ($dir and is_dir($dir)) {
1245                        if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1246                            return;
1247                        }
1248                    }
1249                    // Bad luck, plugin could not restore the data, let's add normal membership.
1250                    groups_add_member($data->groupid, $data->userid);
1251                    $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1252                    $this->log($message, backup::LOG_WARNING);
1253                }
1254            }
1255        }
1256    }
1257}
1258
1259/**
1260 * Structure step that will create all the needed scales
1261 * by loading them from the scales.xml
1262 */
1263class restore_scales_structure_step extends restore_structure_step {
1264
1265    protected function define_structure() {
1266
1267        $paths = array(); // Add paths here
1268        $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1269        return $paths;
1270    }
1271
1272    protected function process_scale($data) {
1273        global $DB;
1274
1275        $data = (object)$data;
1276
1277        $restorefiles = false; // Only if we end creating the group
1278
1279        $oldid = $data->id;    // need this saved for later
1280
1281        // Look for scale (by 'scale' both in standard (course=0) and current course
1282        // with priority to standard scales (ORDER clause)
1283        // scale is not course unique, use get_record_sql to suppress warning
1284        // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1285        $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1286        $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1287        if (!$scadb = $DB->get_record_sql("SELECT *
1288                                            FROM {scale}
1289                                           WHERE courseid IN (0, :courseid)
1290                                             AND $compare_scale_clause
1291                                        ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1292            // Remap the user if possible, defaut to user performing the restore if not
1293            $userid = $this->get_mappingid('user', $data->userid);
1294            $data->userid = $userid ? $userid : $this->task->get_userid();
1295            // Remap the course if course scale
1296            $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1297            // If global scale (course=0), check the user has perms to create it
1298            // falling to course scale if not
1299            $systemctx = context_system::instance();
1300            if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1301                $data->courseid = $this->get_courseid();
1302            }
1303            // scale doesn't exist, create
1304            $newitemid = $DB->insert_record('scale', $data);
1305            $restorefiles = true; // We'll restore the files
1306        } else {
1307            // scale exists, use it
1308            $newitemid = $scadb->id;
1309        }
1310        // Save the id mapping (with files support at system context)
1311        $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1312    }
1313
1314    protected function after_execute() {
1315        // Add scales related files, matching with "scale" mappings
1316        $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1317    }
1318}
1319
1320
1321/**
1322 * Structure step that will create all the needed outocomes
1323 * by loading them from the outcomes.xml
1324 */
1325class restore_outcomes_structure_step extends restore_structure_step {
1326
1327    protected function define_structure() {
1328
1329        $paths = array(); // Add paths here
1330        $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1331        return $paths;
1332    }
1333
1334    protected function process_outcome($data) {
1335        global $DB;
1336
1337        $data = (object)$data;
1338
1339        $restorefiles = false; // Only if we end creating the group
1340
1341        $oldid = $data->id;    // need this saved for later
1342
1343        // Look for outcome (by shortname both in standard (courseid=null) and current course
1344        // with priority to standard outcomes (ORDER clause)
1345        // outcome is not course unique, use get_record_sql to suppress warning
1346        $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1347        if (!$outdb = $DB->get_record_sql('SELECT *
1348                                             FROM {grade_outcomes}
1349                                            WHERE shortname = :shortname
1350                                              AND (courseid = :courseid OR courseid IS NULL)
1351                                         ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1352            // Remap the user
1353            $userid = $this->get_mappingid('user', $data->usermodified);
1354            $data->usermodified = $userid ? $userid : $this->task->get_userid();
1355            // Remap the scale
1356            $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1357            // Remap the course if course outcome
1358            $data->courseid = $data->courseid ? $this->get_courseid() : null;
1359            // If global outcome (course=null), check the user has perms to create it
1360            // falling to course outcome if not
1361            $systemctx = context_system::instance();
1362            if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1363                $data->courseid = $this->get_courseid();
1364            }
1365            // outcome doesn't exist, create
1366            $newitemid = $DB->insert_record('grade_outcomes', $data);
1367            $restorefiles = true; // We'll restore the files
1368        } else {
1369            // scale exists, use it
1370            $newitemid = $outdb->id;
1371        }
1372        // Set the corresponding grade_outcomes_courses record
1373        $outcourserec = new stdclass();
1374        $outcourserec->courseid  = $this->get_courseid();
1375        $outcourserec->outcomeid = $newitemid;
1376        if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1377            $DB->insert_record('grade_outcomes_courses', $outcourserec);
1378        }
1379        // Save the id mapping (with files support at system context)
1380        $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1381    }
1382
1383    protected function after_execute() {
1384        // Add outcomes related files, matching with "outcome" mappings
1385        $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1386    }
1387}
1388
1389/**
1390 * Execution step that, *conditionally* (if there isn't preloaded information
1391 * will load all the question categories and questions (header info only)
1392 * to backup_temp_ids. They will be stored with "question_category" and
1393 * "question" itemnames and with their original contextid and question category
1394 * id as paremitemids
1395 */
1396class restore_load_categories_and_questions extends restore_execution_step {
1397
1398    protected function define_execution() {
1399
1400        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1401            return;
1402        }
1403        $file = $this->get_basepath() . '/questions.xml';
1404        restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1405    }
1406}
1407
1408/**
1409 * Execution step that, *conditionally* (if there isn't preloaded information)
1410 * will process all the needed categories and questions
1411 * in order to decide and perform any action with them (create / map / error)
1412 * Note: Any error will cause exception, as far as this is the same processing
1413 * than the one into restore prechecks (that should have stopped process earlier)
1414 */
1415class restore_process_categories_and_questions extends restore_execution_step {
1416
1417    protected function define_execution() {
1418
1419        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1420            return;
1421        }
1422        restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1423    }
1424}
1425
1426/**
1427 * Structure step that will read the section.xml creating/updating sections
1428 * as needed, rebuilding course cache and other friends
1429 */
1430class restore_section_structure_step extends restore_structure_step {
1431    /** @var array Cache: Array of id => course format */
1432    private static $courseformats = array();
1433
1434    /**
1435     * Resets a static cache of course formats. Required for unit testing.
1436     */
1437    public static function reset_caches() {
1438        self::$courseformats = array();
1439    }
1440
1441    protected function define_structure() {
1442        global $CFG;
1443
1444        $paths = array();
1445
1446        $section = new restore_path_element('section', '/section');
1447        $paths[] = $section;
1448        if ($CFG->enableavailability) {
1449            $paths[] = new restore_path_element('availability', '/section/availability');
1450            $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1451        }
1452        $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1453
1454        // Apply for 'format' plugins optional paths at section level
1455        $this->add_plugin_structure('format', $section);
1456
1457        // Apply for 'local' plugins optional paths at section level
1458        $this->add_plugin_structure('local', $section);
1459
1460        return $paths;
1461    }
1462
1463    public function process_section($data) {
1464        global $CFG, $DB;
1465        $data = (object)$data;
1466        $oldid = $data->id; // We'll need this later
1467
1468        $restorefiles = false;
1469
1470        // Look for the section
1471        $section = new stdclass();
1472        $section->course  = $this->get_courseid();
1473        $section->section = $data->number;
1474        // Section doesn't exist, create it with all the info from backup
1475        if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
1476            $section->name = $data->name;
1477            $section->summary = $data->summary;
1478            $section->summaryformat = $data->summaryformat;
1479            $section->sequence = '';
1480            $section->visible = $data->visible;
1481            if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1482                $section->availability = null;
1483            } else {
1484                $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1485                // Include legacy [<2.7] availability data if provided.
1486                if (is_null($section->availability)) {
1487                    $section->availability = \core_availability\info::convert_legacy_fields(
1488                            $data, true);
1489                }
1490            }
1491            $newitemid = $DB->insert_record('course_sections', $section);
1492            $restorefiles = true;
1493
1494        // Section exists, update non-empty information
1495        } else {
1496            $section->id = $secrec->id;
1497            if ((string)$secrec->name === '') {
1498                $section->name = $data->name;
1499            }
1500            if (empty($secrec->summary)) {
1501                $section->summary = $data->summary;
1502                $section->summaryformat = $data->summaryformat;
1503                $restorefiles = true;
1504            }
1505
1506            // Don't update availability (I didn't see a useful way to define
1507            // whether existing or new one should take precedence).
1508
1509            $DB->update_record('course_sections', $section);
1510            $newitemid = $secrec->id;
1511        }
1512
1513        // Annotate the section mapping, with restorefiles option if needed
1514        $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1515
1516        // set the new course_section id in the task
1517        $this->task->set_sectionid($newitemid);
1518
1519        // If there is the legacy showavailability data, store this for later use.
1520        // (This data is not present when restoring 'new' backups.)
1521        if (isset($data->showavailability)) {
1522            // Cache the showavailability flag using the backup_ids data field.
1523            restore_dbops::set_backup_ids_record($this->get_restoreid(),
1524                    'section_showavailability', $newitemid, 0, null,
1525                    (object)array('showavailability' => $data->showavailability));
1526        }
1527
1528        // Commented out. We never modify course->numsections as far as that is used
1529        // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1530        // Note: We keep the code here, to know about and because of the possibility of making this
1531        // optional based on some setting/attribute in the future
1532        // If needed, adjust course->numsections
1533        //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1534        //    if ($numsections < $section->section) {
1535        //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1536        //    }
1537        //}
1538    }
1539
1540    /**
1541     * Process the legacy availability table record. This table does not exist
1542     * in Moodle 2.7+ but we still support restore.
1543     *
1544     * @param stdClass $data Record data
1545     */
1546    public function process_availability($data) {
1547        $data = (object)$data;
1548        // Simply going to store the whole availability record now, we'll process
1549        // all them later in the final task (once all activities have been restored)
1550        // Let's call the low level one to be able to store the whole object.
1551        $data->coursesectionid = $this->task->get_sectionid();
1552        restore_dbops::set_backup_ids_record($this->get_restoreid(),
1553                'section_availability', $data->id, 0, null, $data);
1554    }
1555
1556    /**
1557     * Process the legacy availability fields table record. This table does not
1558     * exist in Moodle 2.7+ but we still support restore.
1559     *
1560     * @param stdClass $data Record data
1561     */
1562    public function process_availability_field($data) {
1563        global $DB;
1564        $data = (object)$data;
1565        // Mark it is as passed by default
1566        $passed = true;
1567        $customfieldid = null;
1568
1569        // If a customfield has been used in order to pass we must be able to match an existing
1570        // customfield by name (data->customfield) and type (data->customfieldtype)
1571        if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1572            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1573            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1574            $passed = false;
1575        } else if (!is_null($data->customfield)) {
1576            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1577            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1578            $passed = ($customfieldid !== false);
1579        }
1580
1581        if ($passed) {
1582            // Create the object to insert into the database
1583            $availfield = new stdClass();
1584            $availfield->coursesectionid = $this->task->get_sectionid();
1585            $availfield->userfield = $data->userfield;
1586            $availfield->customfieldid = $customfieldid;
1587            $availfield->operator = $data->operator;
1588            $availfield->value = $data->value;
1589
1590            // Get showavailability option.
1591            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1592                    'section_showavailability', $availfield->coursesectionid);
1593            if (!$showrec) {
1594                // Should not happen.
1595                throw new coding_exception('No matching showavailability record');
1596            }
1597            $show = $showrec->info->showavailability;
1598
1599            // The $availfield object is now in the format used in the old
1600            // system. Interpret this and convert to new system.
1601            $currentvalue = $DB->get_field('course_sections', 'availability',
1602                    array('id' => $availfield->coursesectionid), MUST_EXIST);
1603            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1604                    $currentvalue, $availfield, $show);
1605            $DB->set_field('course_sections', 'availability', $newvalue,
1606                    array('id' => $availfield->coursesectionid));
1607        }
1608    }
1609
1610    public function process_course_format_options($data) {
1611        global $DB;
1612        $courseid = $this->get_courseid();
1613        if (!array_key_exists($courseid, self::$courseformats)) {
1614            // It is safe to have a static cache of course formats because format can not be changed after this point.
1615            self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
1616        }
1617        $data = (array)$data;
1618        if (self::$courseformats[$courseid] === $data['format']) {
1619            // Import section format options only if both courses (the one that was backed up
1620            // and the one we are restoring into) have same formats.
1621            $params = array(
1622                'courseid' => $this->get_courseid(),
1623                'sectionid' => $this->task->get_sectionid(),
1624                'format' => $data['format'],
1625                'name' => $data['name']
1626            );
1627            if ($record = $DB->get_record('course_format_options', $params, 'id, value')) {
1628                // Do not overwrite existing information.
1629                $newid = $record->id;
1630            } else {
1631                $params['value'] = $data['value'];
1632                $newid = $DB->insert_record('course_format_options', $params);
1633            }
1634            $this->set_mapping('course_format_options', $data['id'], $newid);
1635        }
1636    }
1637
1638    protected function after_execute() {
1639        // Add section related files, with 'course_section' itemid to match
1640        $this->add_related_files('course', 'section', 'course_section');
1641    }
1642}
1643
1644/**
1645 * Structure step that will read the course.xml file, loading it and performing
1646 * various actions depending of the site/restore settings. Note that target
1647 * course always exist before arriving here so this step will be updating
1648 * the course record (never inserting)
1649 */
1650class restore_course_structure_step extends restore_structure_step {
1651    /**
1652     * @var bool this gets set to true by {@link process_course()} if we are
1653     * restoring an old coures that used the legacy 'module security' feature.
1654     * If so, we have to do more work in {@link after_execute()}.
1655     */
1656    protected $legacyrestrictmodules = false;
1657
1658    /**
1659     * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1660     * array with array keys the module names ('forum', 'quiz', etc.). These are
1661     * the modules that are allowed according to the data in the backup file.
1662     * In {@link after_execute()} we then have to prevent adding of all the other
1663     * types of activity.
1664     */
1665    protected $legacyallowedmodules = array();
1666
1667    protected function define_structure() {
1668
1669        $course = new restore_path_element('course', '/course');
1670        $category = new restore_path_element('category', '/course/category');
1671        $tag = new restore_path_element('tag', '/course/tags/tag');
1672        $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1673
1674        // Apply for 'format' plugins optional paths at course level
1675        $this->add_plugin_structure('format', $course);
1676
1677        // Apply for 'theme' plugins optional paths at course level
1678        $this->add_plugin_structure('theme', $course);
1679
1680        // Apply for 'report' plugins optional paths at course level
1681        $this->add_plugin_structure('report', $course);
1682
1683        // Apply for 'course report' plugins optional paths at course level
1684        $this->add_plugin_structure('coursereport', $course);
1685
1686        // Apply for plagiarism plugins optional paths at course level
1687        $this->add_plugin_structure('plagiarism', $course);
1688
1689        // Apply for local plugins optional paths at course level
1690        $this->add_plugin_structure('local', $course);
1691
1692        return array($course, $category, $tag, $allowed_module);
1693    }
1694
1695    /**
1696     * Processing functions go here
1697     *
1698     * @global moodledatabase $DB
1699     * @param stdClass $data
1700     */
1701    public function process_course($data) {
1702        global $CFG, $DB;
1703
1704        $data = (object)$data;
1705
1706        $fullname  = $this->get_setting_value('course_fullname');
1707        $shortname = $this->get_setting_value('course_shortname');
1708        $startdate = $this->get_setting_value('course_startdate');
1709
1710        // Calculate final course names, to avoid dupes
1711        list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
1712
1713        // Need to change some fields before updating the course record
1714        $data->id = $this->get_courseid();
1715        $data->fullname = $fullname;
1716        $data->shortname= $shortname;
1717
1718        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1719        // another course on this site.
1720        $context = context::instance_by_id($this->task->get_contextid());
1721        if (!empty($data->idnumber) && has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid()) &&
1722                $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1723            // Do not reset idnumber.
1724        } else {
1725            $data->idnumber = '';
1726        }
1727
1728        // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1729        // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1730        if (empty($data->hiddensections)) {
1731            $data->hiddensections = 0;
1732        }
1733
1734        // Set legacyrestrictmodules to true if the course was resticting modules. If so
1735        // then we will need to process restricted modules after execution.
1736        $this->legacyrestrictmodules = !empty($data->restrictmodules);
1737
1738        $data->startdate= $this->apply_date_offset($data->startdate);
1739        if ($data->defaultgroupingid) {
1740            $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1741        }
1742        if (empty($CFG->enablecompletion)) {
1743            $data->enablecompletion = 0;
1744            $data->completionstartonenrol = 0;
1745            $data->completionnotify = 0;
1746        }
1747        $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1748        if (!array_key_exists($data->lang, $languages)) {
1749            $data->lang = '';
1750        }
1751
1752        $themes = get_list_of_themes(); // Get themes for quick search later
1753        if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1754            $data->theme = '';
1755        }
1756
1757        // Check if this is an old SCORM course format.
1758        if ($data->format == 'scorm') {
1759            $data->format = 'singleactivity';
1760            $data->activitytype = 'scorm';
1761        }
1762
1763        // Course record ready, update it
1764        $DB->update_record('course', $data);
1765
1766        course_get_format($data)->update_course_format_options($data);
1767
1768        // Role name aliases
1769        restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1770    }
1771
1772    public function process_category($data) {
1773        // Nothing to do with the category. UI sets it before restore starts
1774    }
1775
1776    public function process_tag($data) {
1777        global $CFG, $DB;
1778
1779        $data = (object)$data;
1780
1781        if (!empty($CFG->usetags)) { // if enabled in server
1782            // TODO: This is highly inneficient. Each time we add one tag
1783            // we fetch all the existing because tag_set() deletes them
1784            // so everything must be reinserted on each call
1785            $tags = array();
1786            $existingtags = tag_get_tags('course', $this->get_courseid());
1787            // Re-add all the existitng tags
1788            foreach ($existingtags as $existingtag) {
1789                $tags[] = $existingtag->rawname;
1790            }
1791            // Add the one being restored
1792            $tags[] = $data->rawname;
1793            // Send all the tags back to the course
1794            tag_set('course', $this->get_courseid(), $tags, 'core',
1795                context_course::instance($this->get_courseid())->id);
1796        }
1797    }
1798
1799    public function process_allowed_module($data) {
1800        $data = (object)$data;
1801
1802        // Backwards compatiblity support for the data that used to be in the
1803        // course_allowed_modules table.
1804        if ($this->legacyrestrictmodules) {
1805            $this->legacyallowedmodules[$data->modulename] = 1;
1806        }
1807    }
1808
1809    protected function after_execute() {
1810        global $DB;
1811
1812        // Add course related files, without itemid to match
1813        $this->add_related_files('course', 'summary', null);
1814        $this->add_related_files('course', 'overviewfiles', null);
1815
1816        // Deal with legacy allowed modules.
1817        if ($this->legacyrestrictmodules) {
1818            $context = context_course::instance($this->get_courseid());
1819
1820            list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1821            list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1822            foreach ($managerroleids as $roleid) {
1823                unset($roleids[$roleid]);
1824            }
1825
1826            foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1827                if (isset($this->legacyallowedmodules[$modname])) {
1828                    // Module is allowed, no worries.
1829                    continue;
1830                }
1831
1832                $capability = 'mod/' . $modname . ':addinstance';
1833                foreach ($roleids as $roleid) {
1834                    assign_capability($capability, CAP_PREVENT, $roleid, $context);
1835                }
1836            }
1837        }
1838    }
1839}
1840
1841/**
1842 * Execution step that will migrate legacy files if present.
1843 */
1844class restore_course_legacy_files_step extends restore_execution_step {
1845    public function define_execution() {
1846        global $DB;
1847
1848        // Do a check for legacy files and skip if there are none.
1849        $sql = 'SELECT count(*)
1850                  FROM {backup_files_temp}
1851                 WHERE backupid = ?
1852                   AND contextid = ?
1853                   AND component = ?
1854                   AND filearea  = ?';
1855        $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
1856
1857        if ($DB->count_records_sql($sql, $params)) {
1858            $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
1859            restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
1860                'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
1861        }
1862    }
1863}
1864
1865/*
1866 * Structure step that will read the roles.xml file (at course/activity/block levels)
1867 * containing all the role_assignments and overrides for that context. If corresponding to
1868 * one mapped role, they will be applied to target context. Will observe the role_assignments
1869 * setting to decide if ras are restored.
1870 *
1871 * Note: this needs to be executed after all users are enrolled.
1872 */
1873class restore_ras_and_caps_structure_step extends restore_structure_step {
1874    protected $plugins = null;
1875
1876    protected function define_structure() {
1877
1878        $paths = array();
1879
1880        // Observe the role_assignments setting
1881        if ($this->get_setting_value('role_assignments')) {
1882            $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
1883        }
1884        $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
1885
1886        return $paths;
1887    }
1888
1889    /**
1890     * Assign roles
1891     *
1892     * This has to be called after enrolments processing.
1893     *
1894     * @param mixed $data
1895     * @return void
1896     */
1897    public function process_assignment($data) {
1898        global $DB;
1899
1900        $data = (object)$data;
1901
1902        // Check roleid, userid are one of the mapped ones
1903        if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
1904            return;
1905        }
1906        if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
1907            return;
1908        }
1909        if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
1910            // Only assign roles to not deleted users
1911            return;
1912        }
1913        if (!$contextid = $this->task->get_contextid()) {
1914            return;
1915        }
1916
1917        if (empty($data->component)) {
1918            // assign standard manual roles
1919            // TODO: role_assign() needs one userid param to be able to specify our restore userid
1920            role_assign($newroleid, $newuserid, $contextid);
1921
1922        } else if ((strpos($data->component, 'enrol_') === 0)) {
1923            // Deal with enrolment roles - ignore the component and just find out the instance via new id,
1924            // it is possible that enrolment was restored using different plugin type.
1925            if (!isset($this->plugins)) {
1926                $this->plugins = enrol_get_plugins(true);
1927            }
1928            if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1929                if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1930                    if (isset($this->plugins[$instance->enrol])) {
1931                        $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
1932                    }
1933                }
1934            }
1935
1936        } else {
1937            $data->roleid    = $newroleid;
1938            $data->userid    = $newuserid;
1939            $data->contextid = $contextid;
1940            $dir = core_component::get_component_directory($data->component);
1941            if ($dir and is_dir($dir)) {
1942                if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
1943                    return;
1944                }
1945            }
1946            // Bad luck, plugin could not restore the data, let's add normal membership.
1947            role_assign($data->roleid, $data->userid, $data->contextid);
1948            $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
1949            $this->log($message, backup::LOG_WARNING);
1950        }
1951    }
1952
1953    public function process_override($data) {
1954        $data = (object)$data;
1955
1956        // Check roleid is one of the mapped ones
1957        $newroleid = $this->get_mappingid('role', $data->roleid);
1958        // If newroleid and context are valid assign it via API (it handles dupes and so on)
1959        if ($newroleid && $this->task->get_contextid()) {
1960            // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1961            // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1962            assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1963        }
1964    }
1965}
1966
1967/**
1968 * If no instances yet add default enrol methods the same way as when creating new course in UI.
1969 */
1970class restore_default_enrolments_step extends restore_execution_step {
1971
1972    public function define_execution() {
1973        global $DB;
1974
1975        // No enrolments in front page.
1976        if ($this->get_courseid() == SITEID) {
1977            return;
1978        }
1979
1980        $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
1981
1982        if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
1983            // Something already added instances, do not add default instances.
1984            $plugins = enrol_get_plugins(true);
1985            foreach ($plugins as $plugin) {
1986                $plugin->restore_sync_course($course);
1987            }
1988
1989        } else {
1990            // Looks like a newly created course.
1991            enrol_course_updated(true, $course, null);
1992        }
1993    }
1994}
1995
1996/**
1997 * This structure steps restores the enrol plugins and their underlying
1998 * enrolments, performing all the mappings and/or movements required
1999 */
2000class restore_enrolments_structure_step extends restore_structure_step {
2001    protected $enrolsynced = false;
2002    protected $plugins = null;
2003    protected $originalstatus = array();
2004
2005    /**
2006     * Conditionally decide if this step should be executed.
2007     *
2008     * This function checks the following parameter:
2009     *
2010     *   1. the course/enrolments.xml file exists
2011     *
2012     * @return bool true is safe to execute, false otherwise
2013     */
2014    protected function execute_condition() {
2015
2016        if ($this->get_courseid() == SITEID) {
2017            return false;
2018        }
2019
2020        // Check it is included in the backup
2021        $fullpath = $this->task->get_taskbasepath();
2022        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2023        if (!file_exists($fullpath)) {
2024            // Not found, can't restore enrolments info
2025            return false;
2026        }
2027
2028        return true;
2029    }
2030
2031    protected function define_structure() {
2032
2033        $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
2034        $enrolment = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
2035        // Attach local plugin stucture to enrol element.
2036        $this->add_plugin_structure('enrol', $enrol);
2037
2038        return array($enrol, $enrolment);
2039    }
2040
2041    /**
2042     * Create enrolment instances.
2043     *
2044     * This has to be called after creation of roles
2045     * and before adding of role assignments.
2046     *
2047     * @param mixed $data
2048     * @return void
2049     */
2050    public function process_enrol($data) {
2051        global $DB;
2052
2053        $data = (object)$data;
2054        $oldid = $data->id; // We'll need this later.
2055        unset($data->id);
2056
2057        $this->originalstatus[$oldid] = $data->status;
2058
2059        if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
2060            $this->set_mapping('enrol', $oldid, 0);
2061            return;
2062        }
2063
2064        if (!isset($this->plugins)) {
2065            $this->plugins = enrol_get_plugins(true);
2066        }
2067
2068        if (!$this->enrolsynced) {
2069            // Make sure that all plugin may create instances and enrolments automatically
2070            // before the first instance restore - this is suitable especially for plugins
2071            // that synchronise data automatically using course->idnumber or by course categories.
2072            foreach ($this->plugins as $plugin) {
2073                $plugin->restore_sync_course($courserec);
2074            }
2075            $this->enrolsynced = true;
2076        }
2077
2078        // Map standard fields - plugin has to process custom fields manually.
2079        $data->roleid   = $this->get_mappingid('role', $data->roleid);
2080        $data->courseid = $courserec->id;
2081
2082        if ($this->get_setting_value('enrol_migratetomanual')) {
2083            unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
2084            if (!enrol_is_enabled('manual')) {
2085                $this->set_mapping('enrol', $oldid, 0);
2086                return;
2087            }
2088            if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
2089                $instance = reset($instances);
2090                $this->set_mapping('enrol', $oldid, $instance->id);
2091            } else {
2092                if ($data->enrol === 'manual') {
2093                    $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
2094                } else {
2095                    $instanceid = $this->plugins['manual']->add_default_instance($courserec);
2096                }
2097                $this->set_mapping('enrol', $oldid, $instanceid);
2098            }
2099
2100        } else {
2101            if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
2102                $this->set_mapping('enrol', $oldid, 0);
2103                $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
2104                $this->log($message, backup::LOG_WARNING);
2105                return;
2106            }
2107            if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
2108                // Let's keep the sortorder in old backups.
2109            } else {
2110                // Prevent problems with colliding sortorders in old backups,
2111                // new 2.4 backups do not need sortorder because xml elements are ordered properly.
2112                unset($data->sortorder);
2113            }
2114            // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
2115            $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
2116        }
2117    }
2118
2119    /**
2120     * Create user enrolments.
2121     *
2122     * This has to be called after creation of enrolment instances
2123     * and before adding of role assignments.
2124     *
2125     * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
2126     *
2127     * @param mixed $data
2128     * @return void
2129     */
2130    public function process_enrolment($data) {
2131        global $DB;
2132
2133        if (!isset($this->plugins)) {
2134            $this->plugins = enrol_get_plugins(true);
2135        }
2136
2137        $data = (object)$data;
2138
2139        // Process only if parent instance have been mapped.
2140        if ($enrolid = $this->get_new_parentid('enrol')) {
2141            $oldinstancestatus = ENROL_INSTANCE_ENABLED;
2142            $oldenrolid = $this->get_old_parentid('enrol');
2143            if (isset($this->originalstatus[$oldenrolid])) {
2144                $oldinstancestatus = $this->originalstatus[$oldenrolid];
2145            }
2146            if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2147                // And only if user is a mapped one.
2148                if ($userid = $this->get_mappingid('user', $data->userid)) {
2149                    if (isset($this->plugins[$instance->enrol])) {
2150                        $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
2151                    }
2152                }
2153            }
2154        }
2155    }
2156}
2157
2158
2159/**
2160 * Make sure the user restoring the course can actually access it.
2161 */
2162class restore_fix_restorer_access_step extends restore_execution_step {
2163    protected function define_execution() {
2164        global $CFG, $DB;
2165
2166        if (!$userid = $this->task->get_userid()) {
2167            return;
2168        }
2169
2170        if (empty($CFG->restorernewroleid)) {
2171            // Bad luck, no fallback role for restorers specified
2172            return;
2173        }
2174
2175        $courseid = $this->get_courseid();
2176        $context = context_course::instance($courseid);
2177
2178        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2179            // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2180            return;
2181        }
2182
2183        // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2184        role_assign($CFG->restorernewroleid, $userid, $context);
2185
2186        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2187            // Extra role is enough, yay!
2188            return;
2189        }
2190
2191        // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2192        // hopefully admin selected suitable $CFG->restorernewroleid ...
2193        if (!enrol_is_enabled('manual')) {
2194            return;
2195        }
2196        if (!$enrol = enrol_get_plugin('manual')) {
2197            return;
2198        }
2199        if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2200            $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2201            $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2202            $enrol->add_instance($course, $fields);
2203        }
2204
2205        enrol_try_internal_enrol($courseid, $userid);
2206    }
2207}
2208
2209
2210/**
2211 * This structure steps restores the filters and their configs
2212 */
2213class restore_filters_structure_step extends restore_structure_step {
2214
2215    protected function define_structure() {
2216
2217        $paths = array();
2218
2219        $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2220        $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2221
2222        return $paths;
2223    }
2224
2225    public function process_active($data) {
2226
2227        $data = (object)$data;
2228
2229        if (strpos($data->filter, 'filter/') === 0) {
2230            $data->filter = substr($data->filter, 7);
2231
2232        } else if (strpos($data->filter, '/') !== false) {
2233            // Unsupported old filter.
2234            return;
2235        }
2236
2237        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2238            return;
2239        }
2240        filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2241    }
2242
2243    public function process_config($data) {
2244
2245        $data = (object)$data;
2246
2247        if (strpos($data->filter, 'filter/') === 0) {
2248            $data->filter = substr($data->filter, 7);
2249
2250        } else if (strpos($data->filter, '/') !== false) {
2251            // Unsupported old filter.
2252            return;
2253        }
2254
2255        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2256            return;
2257        }
2258        filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2259    }
2260}
2261
2262
2263/**
2264 * This structure steps restores the comments
2265 * Note: Cannot use the comments API because defaults to USER->id.
2266 * That should change allowing to pass $userid
2267 */
2268class restore_comments_structure_step extends restore_structure_step {
2269
2270    protected function define_structure() {
2271
2272        $paths = array();
2273
2274        $paths[] = new restore_path_element('comment', '/comments/comment');
2275
2276        return $paths;
2277    }
2278
2279    public function process_comment($data) {
2280        global $DB;
2281
2282        $data = (object)$data;
2283
2284        // First of all, if the comment has some itemid, ask to the task what to map
2285        $mapping = false;
2286        if ($data->itemid) {
2287            $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2288            $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2289        }
2290        // Only restore the comment if has no mapping OR we have found the matching mapping
2291        if (!$mapping || $data->itemid) {
2292            // Only if user mapping and context
2293            $data->userid = $this->get_mappingid('user', $data->userid);
2294            if ($data->userid && $this->task->get_contextid()) {
2295                $data->contextid = $this->task->get_contextid();
2296                // Only if there is another comment with same context/user/timecreated
2297                $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2298                if (!$DB->record_exists('comments', $params)) {
2299                    $DB->insert_record('comments', $data);
2300                }
2301            }
2302        }
2303    }
2304}
2305
2306/**
2307 * This structure steps restores the badges and their configs
2308 */
2309class restore_badges_structure_step extends restore_structure_step {
2310
2311    /**
2312     * Conditionally decide if this step should be executed.
2313     *
2314     * This function checks the following parameters:
2315     *
2316     *   1. Badges and course badges are enabled on the site.
2317     *   2. The course/badges.xml file exists.
2318     *   3. All modules are restorable.
2319     *   4. All modules are marked for restore.
2320     *
2321     * @return bool True is safe to execute, false otherwise
2322     */
2323    protected function execute_condition() {
2324        global $CFG;
2325
2326        // First check is badges and course level badges are enabled on this site.
2327        if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2328            // Disabled, don't restore course badges.
2329            return false;
2330        }
2331
2332        // Check if badges.xml is included in the backup.
2333        $fullpath = $this->task->get_taskbasepath();
2334        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2335        if (!file_exists($fullpath)) {
2336            // Not found, can't restore course badges.
2337            return false;
2338        }
2339
2340        // Check we are able to restore all backed up modules.
2341        if ($this->task->is_missing_modules()) {
2342            return false;
2343        }
2344
2345        // Finally check all modules within the backup are being restored.
2346        if ($this->task->is_excluding_activities()) {
2347            return false;
2348        }
2349
2350        return true;
2351    }
2352
2353    protected function define_structure() {
2354        $paths = array();
2355        $paths[] = new restore_path_element('badge', '/badges/badge');
2356        $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2357        $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2358        $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2359
2360        return $paths;
2361    }
2362
2363    public function process_badge($data) {
2364        global $DB, $CFG;
2365
2366        require_once($CFG->libdir . '/badgeslib.php');
2367
2368        $data = (object)$data;
2369        $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2370        if (empty($data->usercreated)) {
2371            $data->usercreated = $this->task->get_userid();
2372        }
2373        $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2374        if (empty($data->usermodified)) {
2375            $data->usermodified = $this->task->get_userid();
2376        }
2377
2378        // We'll restore the badge image.
2379        $restorefiles = true;
2380
2381        $courseid = $this->get_courseid();
2382
2383        $params = array(
2384                'name'           => $data->name,
2385                'description'    => $data->description,
2386                'timecreated'    => $this->apply_date_offset($data->timecreated),
2387                'timemodified'   => $this->apply_date_offset($data->timemodified),
2388                'usercreated'    => $data->usercreated,
2389                'usermodified'   => $data->usermodified,
2390                'issuername'     => $data->issuername,
2391                'issuerurl'      => $data->issuerurl,
2392                'issuercontact'  => $data->issuercontact,
2393                'expiredate'     => $this->apply_date_offset($data->expiredate),
2394                'expireperiod'   => $data->expireperiod,
2395                'type'           => BADGE_TYPE_COURSE,
2396                'courseid'       => $courseid,
2397                'message'        => $data->message,
2398                'messagesubject' => $data->messagesubject,
2399                'attachment'     => $data->attachment,
2400                'notification'   => $data->notification,
2401                'status'         => BADGE_STATUS_INACTIVE,
2402                'nextcron'       => $this->apply_date_offset($data->nextcron)
2403        );
2404
2405        $newid = $DB->insert_record('badge', $params);
2406        $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2407    }
2408
2409    public function process_criterion($data) {
2410        global $DB;
2411
2412        $data = (object)$data;
2413
2414        $params = array(
2415                'badgeid'           => $this->get_new_parentid('badge'),
2416                'criteriatype'      => $data->criteriatype,
2417                'method'            => $data->method,
2418                'description'       => isset($data->description) ? $data->description : '',
2419                'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
2420        );
2421        $newid = $DB->insert_record('badge_criteria', $params);
2422        $this->set_mapping('criterion', $data->id, $newid);
2423    }
2424
2425    public function process_parameter($data) {
2426        global $DB, $CFG;
2427
2428        require_once($CFG->libdir . '/badgeslib.php');
2429
2430        $data = (object)$data;
2431        $criteriaid = $this->get_new_parentid('criterion');
2432
2433        // Parameter array that will go to database.
2434        $params = array();
2435        $params['critid'] = $criteriaid;
2436
2437        $oldparam = explode('_', $data->name);
2438
2439        if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2440            $module = $this->get_mappingid('course_module', $oldparam[1]);
2441            $params['name'] = $oldparam[0] . '_' . $module;
2442            $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2443        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2444            $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2445            $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2446        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2447            $role = $this->get_mappingid('role', $data->value);
2448            if (!empty($role)) {
2449                $params['name'] = 'role_' . $role;
2450                $params['value'] = $role;
2451            } else {
2452                return;
2453            }
2454        }
2455
2456        if (!$DB->record_exists('badge_criteria_param', $params)) {
2457            $DB->insert_record('badge_criteria_param', $params);
2458        }
2459    }
2460
2461    public function process_manual_award($data) {
2462        global $DB;
2463
2464        $data = (object)$data;
2465        $role = $this->get_mappingid('role', $data->issuerrole);
2466
2467        if (!empty($role)) {
2468            $award = array(
2469                'badgeid'     => $this->get_new_parentid('badge'),
2470                'recipientid' => $this->get_mappingid('user', $data->recipientid),
2471                'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2472                'issuerrole'  => $role,
2473                'datemet'     => $this->apply_date_offset($data->datemet)
2474            );
2475
2476            // Skip the manual award if recipient or issuer can not be mapped to.
2477            if (empty($award['recipientid']) || empty($award['issuerid'])) {
2478                return;
2479            }
2480
2481            $DB->insert_record('badge_manual_award', $award);
2482        }
2483    }
2484
2485    protected function after_execute() {
2486        // Add related files.
2487        $this->add_related_files('badges', 'badgeimage', 'badge');
2488    }
2489}
2490
2491/**
2492 * This structure steps restores the calendar events
2493 */
2494class restore_calendarevents_structure_step extends restore_structure_step {
2495
2496    protected function define_structure() {
2497
2498        $paths = array();
2499
2500        $paths[] = new restore_path_element('calendarevents', '/events/event');
2501
2502        return $paths;
2503    }
2504
2505    public function process_calendarevents($data) {
2506        global $DB, $SITE, $USER;
2507
2508        $data = (object)$data;
2509        $oldid = $data->id;
2510        $restorefiles = true; // We'll restore the files
2511        // Find the userid and the groupid associated with the event.
2512        $data->userid = $this->get_mappingid('user', $data->userid);
2513        if ($data->userid === false) {
2514            // Blank user ID means that we are dealing with module generated events such as quiz starting times.
2515            // Use the current user ID for these events.
2516            $data->userid = $USER->id;
2517        }
2518        if (!empty($data->groupid)) {
2519            $data->groupid = $this->get_mappingid('group', $data->groupid);
2520            if ($data->groupid === false) {
2521                return;
2522            }
2523        }
2524        // Handle events with empty eventtype //MDL-32827
2525        if(empty($data->eventtype)) {
2526            if ($data->courseid == $SITE->id) {                                // Site event
2527                $data->eventtype = "site";
2528            } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2529                // Course assingment event
2530                $data->eventtype = "due";
2531            } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2532                $data->eventtype = "course";
2533            } else if ($data->groupid) {                                      // Group event
2534                $data->eventtype = "group";
2535            } else if ($data->userid) {                                       // User event
2536                $data->eventtype = "user";
2537            } else {
2538                return;
2539            }
2540        }
2541
2542        $params = array(
2543                'name'           => $data->name,
2544                'description'    => $data->description,
2545                'format'         => $data->format,
2546                'courseid'       => $this->get_courseid(),
2547                'groupid'        => $data->groupid,
2548                'userid'         => $data->userid,
2549                'repeatid'       => $data->repeatid,
2550                'modulename'     => $data->modulename,
2551                'eventtype'      => $data->eventtype,
2552                'timestart'      => $this->apply_date_offset($data->timestart),
2553                'timeduration'   => $data->timeduration,
2554                'visible'        => $data->visible,
2555                'uuid'           => $data->uuid,
2556                'sequence'       => $data->sequence,
2557                'timemodified'    => $this->apply_date_offset($data->timemodified));
2558        if ($this->name == 'activity_calendar') {
2559            $params['instance'] = $this->task->get_activityid();
2560        } else {
2561            $params['instance'] = 0;
2562        }
2563        $sql = "SELECT id
2564                  FROM {event}
2565                 WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2566                   AND courseid = ?
2567                   AND repeatid = ?
2568                   AND modulename = ?
2569                   AND timestart = ?
2570                   AND timeduration = ?
2571                   AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2572        $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
2573        $result = $DB->record_exists_sql($sql, $arg);
2574        if (empty($result)) {
2575            $newitemid = $DB->insert_record('event', $params);
2576            $this->set_mapping('event', $oldid, $newitemid);
2577            $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2578        }
2579
2580    }
2581    protected function after_execute() {
2582        // Add related files
2583        $this->add_related_files('calendar', 'event_description', 'event_description');
2584    }
2585}
2586
2587class restore_course_completion_structure_step extends restore_structure_step {
2588
2589    /**
2590     * Conditionally decide if this step should be executed.
2591     *
2592     * This function checks parameters that are not immediate settings to ensure
2593     * that the enviroment is suitable for the restore of course completion info.
2594     *
2595     * This function checks the following four parameters:
2596     *
2597     *   1. Course completion is enabled on the site
2598     *   2. The backup includes course completion information
2599     *   3. All modules are restorable
2600     *   4. All modules are marked for restore.
2601     *   5. No completion criteria already exist for the course.
2602     *
2603     * @return bool True is safe to execute, false otherwise
2604     */
2605    protected function execute_condition() {
2606        global $CFG, $DB;
2607
2608        // First check course completion is enabled on this site
2609        if (empty($CFG->enablecompletion)) {
2610            // Disabled, don't restore course completion
2611            return false;
2612        }
2613
2614        // No course completion on the front page.
2615        if ($this->get_courseid() == SITEID) {
2616            return false;
2617        }
2618
2619        // Check it is included in the backup
2620        $fullpath = $this->task->get_taskbasepath();
2621        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2622        if (!file_exists($fullpath)) {
2623            // Not found, can't restore course completion
2624            return false;
2625        }
2626
2627        // Check we are able to restore all backed up modules
2628        if ($this->task->is_missing_modules()) {
2629            return false;
2630        }
2631
2632        // Check all modules within the backup are being restored.
2633        if ($this->task->is_excluding_activities()) {
2634            return false;
2635        }
2636
2637        // Check that no completion criteria is already set for the course.
2638        if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
2639            return false;
2640        }
2641
2642        return true;
2643    }
2644
2645    /**
2646     * Define the course completion structure
2647     *
2648     * @return array Array of restore_path_element
2649     */
2650    protected function define_structure() {
2651
2652        // To know if we are including user completion info
2653        $userinfo = $this->get_setting_value('userscompletion');
2654
2655        $paths = array();
2656        $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
2657        $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
2658
2659        if ($userinfo) {
2660            $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
2661            $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
2662        }
2663
2664        return $paths;
2665
2666    }
2667
2668    /**
2669     * Process course completion criteria
2670     *
2671     * @global moodle_database $DB
2672     * @param stdClass $data
2673     */
2674    public function process_course_completion_criteria($data) {
2675        global $DB;
2676
2677        $data = (object)$data;
2678        $data->course = $this->get_courseid();
2679
2680        // Apply the date offset to the time end field
2681        $data->timeend = $this->apply_date_offset($data->timeend);
2682
2683        // Map the role from the criteria
2684        if (isset($data->role) && $data->role != '') {
2685            // Newer backups should include roleshortname, which makes this much easier.
2686            if (!empty($data->roleshortname)) {
2687                $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname));
2688                if (!$roleinstanceid) {
2689                    $this->log(
2690                        'Could not match the role shortname in course_completion_criteria, so skipping',
2691                        backup::LOG_DEBUG
2692                    );
2693                    return;
2694                }
2695                $data->role = $roleinstanceid;
2696            } else {
2697                $data->role = $this->get_mappingid('role', $data->role);
2698            }
2699
2700            // Check we have an id, otherwise it causes all sorts of bugs.
2701            if (!$data->role) {
2702                $this->log(
2703                    'Could not match role in course_completion_criteria, so skipping',
2704                    backup::LOG_DEBUG
2705                );
2706                return;
2707            }
2708        }
2709
2710        // If the completion criteria is for a module we need to map the module instance
2711        // to the new module id.
2712        if (!empty($data->moduleinstance) && !empty($data->module)) {
2713            $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
2714            if (empty($data->moduleinstance)) {
2715                $this->log(
2716                    'Could not match the module instance in course_completion_criteria, so skipping',
2717                    backup::LOG_DEBUG
2718                );
2719                return;
2720            }
2721        } else {
2722            $data->module = null;
2723            $data->moduleinstance = null;
2724        }
2725
2726        // We backup the course shortname rather than the ID so that we can match back to the course
2727        if (!empty($data->courseinstanceshortname)) {
2728            $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
2729            if (!$courseinstanceid) {
2730                $this->log(
2731                    'Could not match the course instance in course_completion_criteria, so skipping',
2732                    backup::LOG_DEBUG
2733                );
2734                return;
2735            }
2736        } else {
2737            $courseinstanceid = null;
2738        }
2739        $data->courseinstance = $courseinstanceid;
2740
2741        $params = array(
2742            'course'         => $data->course,
2743            'criteriatype'   => $data->criteriatype,
2744            'enrolperiod'    => $data->enrolperiod,
2745            'courseinstance' => $data->courseinstance,
2746            'module'         => $data->module,
2747            'moduleinstance' => $data->moduleinstance,
2748            'timeend'        => $data->timeend,
2749            'gradepass'      => $data->gradepass,
2750            'role'           => $data->role
2751        );
2752        $newid = $DB->insert_record('course_completion_criteria', $params);
2753        $this->set_mapping('course_completion_criteria', $data->id, $newid);
2754    }
2755
2756    /**
2757     * Processes course compltion criteria complete records
2758     *
2759     * @global moodle_database $DB
2760     * @param stdClass $data
2761     */
2762    public function process_course_completion_crit_compl($data) {
2763        global $DB;
2764
2765        $data = (object)$data;
2766
2767        // This may be empty if criteria could not be restored
2768        $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
2769
2770        $data->course = $this->get_courseid();
2771        $data->userid = $this->get_mappingid('user', $data->userid);
2772
2773        if (!empty($data->criteriaid) && !empty($data->userid)) {
2774            $params = array(
2775                'userid' => $data->userid,
2776                'course' => $data->course,
2777                'criteriaid' => $data->criteriaid,
2778                'timecompleted' => $this->apply_date_offset($data->timecompleted)
2779            );
2780            if (isset($data->gradefinal)) {
2781                $params['gradefinal'] = $data->gradefinal;
2782            }
2783            if (isset($data->unenroled)) {
2784                $params['unenroled'] = $data->unenroled;
2785            }
2786            $DB->insert_record('course_completion_crit_compl', $params);
2787        }
2788    }
2789
2790    /**
2791     * Process course completions
2792     *
2793     * @global moodle_database $DB
2794     * @param stdClass $data
2795     */
2796    public function process_course_completions($data) {
2797        global $DB;
2798
2799        $data = (object)$data;
2800
2801        $data->course = $this->get_courseid();
2802        $data->userid = $this->get_mappingid('user', $data->userid);
2803
2804        if (!empty($data->userid)) {
2805            $params = array(
2806                'userid' => $data->userid,
2807                'course' => $data->course,
2808                'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
2809                'timestarted' => $this->apply_date_offset($data->timestarted),
2810                'timecompleted' => $this->apply_date_offset($data->timecompleted),
2811                'reaggregate' => $data->reaggregate
2812            );
2813
2814            $existing = $DB->get_record('course_completions', array(
2815                'userid' => $data->userid,
2816                'course' => $data->course
2817            ));
2818
2819            // MDL-46651 - If cron writes out a new record before we get to it
2820            // then we should replace it with the Truth data from the backup.
2821            // This may be obsolete after MDL-48518 is resolved
2822            if ($existing) {
2823                $params['id'] = $existing->id;
2824                $DB->update_record('course_completions', $params);
2825            } else {
2826                $DB->insert_record('course_completions', $params);
2827            }
2828        }
2829    }
2830
2831    /**
2832     * Process course completion aggregate methods
2833     *
2834     * @global moodle_database $DB
2835     * @param stdClass $data
2836     */
2837    public function process_course_completion_aggr_methd($data) {
2838        global $DB;
2839
2840        $data = (object)$data;
2841
2842        $data->course = $this->get_courseid();
2843
2844        // Only create the course_completion_aggr_methd records if
2845        // the target course has not them defined. MDL-28180
2846        if (!$DB->record_exists('course_completion_aggr_methd', array(
2847                    'course' => $data->course,
2848                    'criteriatype' => $data->criteriatype))) {
2849            $params = array(
2850                'course' => $data->course,
2851                'criteriatype' => $data->criteriatype,
2852                'method' => $data->method,
2853                'value' => $data->value,
2854            );
2855            $DB->insert_record('course_completion_aggr_methd', $params);
2856        }
2857    }
2858}
2859
2860
2861/**
2862 * This structure step restores course logs (cmid = 0), delegating
2863 * the hard work to the corresponding {@link restore_logs_processor} passing the
2864 * collection of {@link restore_log_rule} rules to be observed as they are defined
2865 * by the task. Note this is only executed based in the 'logs' setting.
2866 *
2867 * NOTE: This is executed by final task, to have all the activities already restored
2868 *
2869 * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
2870 * records are. There are others like 'calendar' and 'upload' that will be handled
2871 * later.
2872 *
2873 * NOTE: All the missing actions (not able to be restored) are sent to logs for
2874 * debugging purposes
2875 */
2876class restore_course_logs_structure_step extends restore_structure_step {
2877
2878    /**
2879     * Conditionally decide if this step should be executed.
2880     *
2881     * This function checks the following parameter:
2882     *
2883     *   1. the course/logs.xml file exists
2884     *
2885     * @return bool true is safe to execute, false otherwise
2886     */
2887    protected function execute_condition() {
2888
2889        // Check it is included in the backup
2890        $fullpath = $this->task->get_taskbasepath();
2891        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2892        if (!file_exists($fullpath)) {
2893            // Not found, can't restore course logs
2894            return false;
2895        }
2896
2897        return true;
2898    }
2899
2900    protected function define_structure() {
2901
2902        $paths = array();
2903
2904        // Simple, one plain level of information contains them
2905        $paths[] = new restore_path_element('log', '/logs/log');
2906
2907        return $paths;
2908    }
2909
2910    protected function process_log($data) {
2911        global $DB;
2912
2913        $data = (object)($data);
2914
2915        $data->time = $this->apply_date_offset($data->time);
2916        $data->userid = $this->get_mappingid('user', $data->userid);
2917        $data->course = $this->get_courseid();
2918        $data->cmid = 0;
2919
2920        // For any reason user wasn't remapped ok, stop processing this
2921        if (empty($data->userid)) {
2922            return;
2923        }
2924
2925        // Everything ready, let's delegate to the restore_logs_processor
2926
2927        // Set some fixed values that will save tons of DB requests
2928        $values = array(
2929            'course' => $this->get_courseid());
2930        // Get instance and process log record
2931        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2932
2933        // If we have data, insert it, else something went wrong in the restore_logs_processor
2934        if ($data) {
2935            if (empty($data->url)) {
2936                $data->url = '';
2937            }
2938            if (empty($data->info)) {
2939                $data->info = '';
2940            }
2941            // Store the data in the legacy log table if we are still using it.
2942            $manager = get_log_manager();
2943            if (method_exists($manager, 'legacy_add_to_log')) {
2944                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2945                    $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
2946            }
2947        }
2948    }
2949}
2950
2951/**
2952 * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
2953 * sharing its same structure but modifying the way records are handled
2954 */
2955class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
2956
2957    protected function process_log($data) {
2958        global $DB;
2959
2960        $data = (object)($data);
2961
2962        $data->time = $this->apply_date_offset($data->time);
2963        $data->userid = $this->get_mappingid('user', $data->userid);
2964        $data->course = $this->get_courseid();
2965        $data->cmid = $this->task->get_moduleid();
2966
2967        // For any reason user wasn't remapped ok, stop processing this
2968        if (empty($data->userid)) {
2969            return;
2970        }
2971
2972        // Everything ready, let's delegate to the restore_logs_processor
2973
2974        // Set some fixed values that will save tons of DB requests
2975        $values = array(
2976            'course' => $this->get_courseid(),
2977            'course_module' => $this->task->get_moduleid(),
2978            $this->task->get_modulename() => $this->task->get_activityid());
2979        // Get instance and process log record
2980        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2981
2982        // If we have data, insert it, else something went wrong in the restore_logs_processor
2983        if ($data) {
2984            if (empty($data->url)) {
2985                $data->url = '';
2986            }
2987            if (empty($data->info)) {
2988                $data->info = '';
2989            }
2990            // Store the data in the legacy log table if we are still using it.
2991            $manager = get_log_manager();
2992            if (method_exists($manager, 'legacy_add_to_log')) {
2993                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2994                    $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
2995            }
2996        }
2997    }
2998}
2999
3000/**
3001 * Structure step in charge of restoring the logstores.xml file for the course logs.
3002 *
3003 * This restore step will rebuild the logs for all the enabled logstore subplugins supporting
3004 * it, for logs belonging to the course level.
3005 */
3006class restore_course_logstores_structure_step extends restore_structure_step {
3007
3008    /**
3009     * Conditionally decide if this step should be executed.
3010     *
3011     * This function checks the following parameter:
3012     *
3013     *   1. the logstores.xml file exists
3014     *
3015     * @return bool true is safe to execute, false otherwise
3016     */
3017    protected function execute_condition() {
3018
3019        // Check it is included in the backup.
3020        $fullpath = $this->task->get_taskbasepath();
3021        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3022        if (!file_exists($fullpath)) {
3023            // Not found, can't restore logstores.xml information.
3024            return false;
3025        }
3026
3027        return true;
3028    }
3029
3030    /**
3031     * Return the elements to be processed on restore of logstores.
3032     *
3033     * @return restore_path_element[] array of elements to be processed on restore.
3034     */
3035    protected function define_structure() {
3036
3037        $paths = array();
3038
3039        $logstore = new restore_path_element('logstore', '/logstores/logstore');
3040        $paths[] = $logstore;
3041
3042        // Add logstore subplugin support to the 'logstore' element.
3043        $this->add_subplugin_structure('logstore', $logstore, 'tool', 'log');
3044
3045        return array($logstore);
3046    }
3047
3048    /**
3049     * Process the 'logstore' element,
3050     *
3051     * Note: This is empty by definition in backup, because stores do not share any
3052     * data between them, so there is nothing to process here.
3053     *
3054     * @param array $data element data
3055     */
3056    protected function process_logstore($data) {
3057        return;
3058    }
3059}
3060
3061/**
3062 * Structure step in charge of restoring the logstores.xml file for the activity logs.
3063 *
3064 * Note: Activity structure is completely equivalent to the course one, so just extend it.
3065 */
3066class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step {
3067}
3068
3069
3070/**
3071 * Defines the restore step for advanced grading methods attached to the activity module
3072 */
3073class restore_activity_grading_structure_step extends restore_structure_step {
3074
3075    /**
3076     * This step is executed only if the grading file is present
3077     */
3078     protected function execute_condition() {
3079
3080        if ($this->get_courseid() == SITEID) {
3081            return false;
3082        }
3083
3084        $fullpath = $this->task->get_taskbasepath();
3085        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3086        if (!file_exists($fullpath)) {
3087            return false;
3088        }
3089
3090        return true;
3091    }
3092
3093
3094    /**
3095     * Declares paths in the grading.xml file we are interested in
3096     */
3097    protected function define_structure() {
3098
3099        $paths = array();
3100        $userinfo = $this->get_setting_value('userinfo');
3101
3102        $area = new restore_path_element('grading_area', '/areas/area');
3103        $paths[] = $area;
3104        // attach local plugin stucture to $area element
3105        $this->add_plugin_structure('local', $area);
3106
3107        $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
3108        $paths[] = $definition;
3109        $this->add_plugin_structure('gradingform', $definition);
3110        // attach local plugin stucture to $definition element
3111        $this->add_plugin_structure('local', $definition);
3112
3113
3114        if ($userinfo) {
3115            $instance = new restore_path_element('grading_instance',
3116                '/areas/area/definitions/definition/instances/instance');
3117            $paths[] = $instance;
3118            $this->add_plugin_structure('gradingform', $instance);
3119            // attach local plugin stucture to $intance element
3120            $this->add_plugin_structure('local', $instance);
3121        }
3122
3123        return $paths;
3124    }
3125
3126    /**
3127     * Processes one grading area element
3128     *
3129     * @param array $data element data
3130     */
3131    protected function process_grading_area($data) {
3132        global $DB;
3133
3134        $task = $this->get_task();
3135        $data = (object)$data;
3136        $oldid = $data->id;
3137        $data->component = 'mod_'.$task->get_modulename();
3138        $data->contextid = $task->get_contextid();
3139
3140        $newid = $DB->insert_record('grading_areas', $data);
3141        $this->set_mapping('grading_area', $oldid, $newid);
3142    }
3143
3144    /**
3145     * Processes one grading definition element
3146     *
3147     * @param array $data element data
3148     */
3149    protected function process_grading_definition($data) {
3150        global $DB;
3151
3152        $task = $this->get_task();
3153        $data = (object)$data;
3154        $oldid = $data->id;
3155        $data->areaid = $this->get_new_parentid('grading_area');
3156        $data->copiedfromid = null;
3157        $data->timecreated = time();
3158        $data->usercreated = $task->get_userid();
3159        $data->timemodified = $data->timecreated;
3160        $data->usermodified = $data->usercreated;
3161
3162        $newid = $DB->insert_record('grading_definitions', $data);
3163        $this->set_mapping('grading_definition', $oldid, $newid, true);
3164    }
3165
3166    /**
3167     * Processes one grading form instance element
3168     *
3169     * @param array $data element data
3170     */
3171    protected function process_grading_instance($data) {
3172        global $DB;
3173
3174        $data = (object)$data;
3175
3176        // new form definition id
3177        $newformid = $this->get_new_parentid('grading_definition');
3178
3179        // get the name of the area we are restoring to
3180        $sql = "SELECT ga.areaname
3181                  FROM {grading_definitions} gd
3182                  JOIN {grading_areas} ga ON gd.areaid = ga.id
3183                 WHERE gd.id = ?";
3184        $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
3185
3186        // get the mapped itemid - the activity module is expected to define the mappings
3187        // for each gradable area
3188        $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
3189
3190        $oldid = $data->id;
3191        $data->definitionid = $newformid;
3192        $data->raterid = $this->get_mappingid('user', $data->raterid);
3193        $data->itemid = $newitemid;
3194
3195        $newid = $DB->insert_record('grading_instances', $data);
3196        $this->set_mapping('grading_instance', $oldid, $newid);
3197    }
3198
3199    /**
3200     * Final operations when the database records are inserted
3201     */
3202    protected function after_execute() {
3203        // Add files embedded into the definition description
3204        $this->add_related_files('grading', 'description', 'grading_definition');
3205    }
3206}
3207
3208
3209/**
3210 * This structure step restores the grade items associated with one activity
3211 * All the grade items are made child of the "course" grade item but the original
3212 * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
3213 * the complete gradebook (categories and calculations), that information is
3214 * available there
3215 */
3216class restore_activity_grades_structure_step extends restore_structure_step {
3217
3218    /**
3219     * No grades in front page.
3220     * @return bool
3221     */
3222    protected function execute_condition() {
3223        return ($this->get_courseid() != SITEID);
3224    }
3225
3226    protected function define_structure() {
3227
3228        $paths = array();
3229        $userinfo = $this->get_setting_value('userinfo');
3230
3231        $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
3232        $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
3233        if ($userinfo) {
3234            $paths[] = new restore_path_element('grade_grade',
3235                           '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
3236        }
3237        return $paths;
3238    }
3239
3240    protected function process_grade_item($data) {
3241        global $DB;
3242
3243        $data = (object)($data);
3244        $oldid       = $data->id;        // We'll need these later
3245        $oldparentid = $data->categoryid;
3246        $courseid = $this->get_courseid();
3247
3248        $idnumber = null;
3249        if (!empty($data->idnumber)) {
3250            // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
3251            // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
3252            // so the best is to keep the ones already in the gradebook
3253            // Potential problem: duplicates if same items are restored more than once. :-(
3254            // This needs to be fixed in some way (outcomes & activities with multiple items)
3255            // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
3256            // In any case, verify always for uniqueness
3257            $sql = "SELECT cm.id
3258                      FROM {course_modules} cm
3259                     WHERE cm.course = :courseid AND
3260                           cm.idnumber = :idnumber AND
3261                           cm.id <> :cmid";
3262            $params = array(
3263                'courseid' => $courseid,
3264                'idnumber' => $data->idnumber,
3265                'cmid' => $this->task->get_moduleid()
3266            );
3267            if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
3268                $idnumber = $data->idnumber;
3269            }
3270        }
3271
3272        if (!empty($data->categoryid)) {
3273            // If the grade category id of the grade item being restored belongs to this course
3274            // then it is a fair assumption that this is the correct grade category for the activity
3275            // and we should leave it in place, if not then unset it.
3276            // TODO MDL-34790 Gradebook does not import if target course has gradebook categories.
3277            $conditions = array('id' => $data->categoryid, 'courseid' => $courseid);
3278            if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) {
3279                unset($data->categoryid);
3280            }
3281        }
3282
3283        unset($data->id);
3284        $data->courseid     = $this->get_courseid();
3285        $data->iteminstance = $this->task->get_activityid();
3286        $data->idnumber     = $idnumber;
3287        $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
3288        $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
3289        $data->timecreated  = $this->apply_date_offset($data->timecreated);
3290        $data->timemodified = $this->apply_date_offset($data->timemodified);
3291
3292        $gradeitem = new grade_item($data, false);
3293        $gradeitem->insert('restore');
3294
3295        //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
3296        $gradeitem->sortorder = $data->sortorder;
3297        $gradeitem->update('restore');
3298
3299        // Set mapping, saving the original category id into parentitemid
3300        // gradebook restore (final task) will need it to reorganise items
3301        $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
3302    }
3303
3304    protected function process_grade_grade($data) {
3305        $data = (object)($data);
3306        $olduserid = $data->userid;
3307        $oldid = $data->id;
3308        unset($data->id);
3309
3310        $data->itemid = $this->get_new_parentid('grade_item');
3311
3312        $data->userid = $this->get_mappingid('user', $data->userid, null);
3313        if (!empty($data->userid)) {
3314            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3315            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3316            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
3317            $data->overridden = $this->apply_date_offset($data->overridden);
3318
3319            $grade = new grade_grade($data, false);
3320            $grade->insert('restore');
3321            $this->set_mapping('grade_grades', $oldid, $grade->id);
3322        } else {
3323            debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
3324        }
3325    }
3326
3327    /**
3328     * process activity grade_letters. Note that, while these are possible,
3329     * because grade_letters are contextid based, in practice, only course
3330     * context letters can be defined. So we keep here this method knowing
3331     * it won't be executed ever. gradebook restore will restore course letters.
3332     */
3333    protected function process_grade_letter($data) {
3334        global $DB;
3335
3336        $data['contextid'] = $this->task->get_contextid();
3337        $gradeletter = (object)$data;
3338
3339        // Check if it exists before adding it
3340        unset($data['id']);
3341        if (!$DB->record_exists('grade_letters', $data)) {
3342            $newitemid = $DB->insert_record('grade_letters', $gradeletter);
3343        }
3344        // no need to save any grade_letter mapping
3345    }
3346
3347    public function after_restore() {
3348        // Fix grade item's sortorder after restore, as it might have duplicates.
3349        $courseid = $this->get_task()->get_courseid();
3350        grade_item::fix_duplicate_sortorder($courseid);
3351    }
3352}
3353
3354/**
3355 * Step in charge of restoring the grade history of an activity.
3356 *
3357 * This step is added to the task regardless of the setting 'grade_histories'.
3358 * The reason is to allow for a more flexible step in case the logic needs to be
3359 * split accross different settings to control the history of items and/or grades.
3360 */
3361class restore_activity_grade_history_structure_step extends restore_structure_step {
3362
3363    /**
3364     * This step is executed only if the grade history file is present.
3365     */
3366     protected function execute_condition() {
3367
3368        if ($this->get_courseid() == SITEID) {
3369            return false;
3370        }
3371
3372        $fullpath = $this->task->get_taskbasepath();
3373        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3374        if (!file_exists($fullpath)) {
3375            return false;
3376        }
3377        return true;
3378    }
3379
3380    protected function define_structure() {
3381        $paths = array();
3382
3383        // Settings to use.
3384        $userinfo = $this->get_setting_value('userinfo');
3385        $history = $this->get_setting_value('grade_histories');
3386
3387        if ($userinfo && $history) {
3388            $paths[] = new restore_path_element('grade_grade',
3389               '/grade_history/grade_grades/grade_grade');
3390        }
3391
3392        return $paths;
3393    }
3394
3395    protected function process_grade_grade($data) {
3396        global $DB;
3397
3398        $data = (object) $data;
3399        $olduserid = $data->userid;
3400        unset($data->id);
3401
3402        $data->userid = $this->get_mappingid('user', $data->userid, null);
3403        if (!empty($data->userid)) {
3404            // Do not apply the date offsets as this is history.
3405            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
3406            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
3407            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3408            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3409            $DB->insert_record('grade_grades_history', $data);
3410        } else {
3411            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
3412            $this->log($message, backup::LOG_DEBUG);
3413        }
3414    }
3415
3416}
3417
3418/**
3419 * This structure steps restores one instance + positions of one block
3420 * Note: Positions corresponding to one existing context are restored
3421 * here, but all the ones having unknown contexts are sent to backup_ids
3422 * for a later chance to be restored at the end (final task)
3423 */
3424class restore_block_instance_structure_step extends restore_structure_step {
3425
3426    protected function define_structure() {
3427
3428        $paths = array();
3429
3430        $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
3431        $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
3432
3433        return $paths;
3434    }
3435
3436    public function process_block($data) {
3437        global $DB, $CFG;
3438
3439        $data = (object)$data; // Handy
3440        $oldcontextid = $data->contextid;
3441        $oldid        = $data->id;
3442        $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
3443
3444        // Look for the parent contextid
3445        if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
3446            throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
3447        }
3448
3449        // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
3450        // If there is already one block of that type in the parent context
3451        // and the block is not multiple, stop processing
3452        // Use blockslib loader / method executor
3453        if (!$bi = block_instance($data->blockname)) {
3454            return false;
3455        }
3456
3457        if (!$bi->instance_allow_multiple()) {
3458            // The block cannot be added twice, so we will check if the same block is already being
3459            // displayed on the same page. For this, rather than mocking a page and using the block_manager
3460            // we use a similar query to the one in block_manager::load_blocks(), this will give us
3461            // a very good idea of the blocks already displayed in the context.
3462            $params =  array(
3463                'blockname' => $data->blockname
3464            );
3465
3466            // Context matching test.
3467            $context = context::instance_by_id($data->parentcontextid);
3468            $contextsql = 'bi.parentcontextid = :contextid';
3469            $params['contextid'] = $context->id;
3470
3471            $parentcontextids = $context->get_parent_context_ids();
3472            if ($parentcontextids) {
3473                list($parentcontextsql, $parentcontextparams) =
3474                        $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
3475                $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
3476                $params = array_merge($params, $parentcontextparams);
3477            }
3478
3479            // Page type pattern test.
3480            $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
3481            list($pagetypepatternsql, $pagetypepatternparams) =
3482                $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
3483            $params = array_merge($params, $pagetypepatternparams);
3484
3485            // Sub page pattern test.
3486            $subpagepatternsql = 'bi.subpagepattern IS NULL';
3487            if ($data->subpagepattern !== null) {
3488                $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
3489                $params['subpagepattern'] = $data->subpagepattern;
3490            }
3491
3492            $exists = $DB->record_exists_sql("SELECT bi.id
3493                                                FROM {block_instances} bi
3494                                                JOIN {block} b ON b.name = bi.blockname
3495                                               WHERE bi.blockname = :blockname
3496                                                 AND $contextsql
3497                                                 AND bi.pagetypepattern $pagetypepatternsql
3498                                                 AND $subpagepatternsql", $params);
3499            if ($exists) {
3500                // There is at least one very similar block visible on the page where we
3501                // are trying to restore the block. In these circumstances the block API
3502                // would not allow the user to add another instance of the block, so we
3503                // apply the same rule here.
3504                return false;
3505            }
3506        }
3507
3508        // If there is already one block of that type in the parent context
3509        // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
3510        // stop processing
3511        $params = array(
3512            'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
3513            'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
3514            'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
3515        if ($birecs = $DB->get_records('block_instances', $params)) {
3516            foreach($birecs as $birec) {
3517                if ($birec->configdata == $data->configdata) {
3518                    return false;
3519                }
3520            }
3521        }
3522
3523        // Set task old contextid, blockid and blockname once we know them
3524        $this->task->set_old_contextid($oldcontextid);
3525        $this->task->set_old_blockid($oldid);
3526        $this->task->set_blockname($data->blockname);
3527
3528        // Let's look for anything within configdata neededing processing
3529        // (nulls and uses of legacy file.php)
3530        if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
3531            $configdata = (array)unserialize(base64_decode($data->configdata));
3532            foreach ($configdata as $attribute => $value) {
3533                if (in_array($attribute, $attrstotransform)) {
3534                    $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
3535                }
3536            }
3537            $data->configdata = base64_encode(serialize((object)$configdata));
3538        }
3539
3540        // Create the block instance
3541        $newitemid = $DB->insert_record('block_instances', $data);
3542        // Save the mapping (with restorefiles support)
3543        $this->set_mapping('block_instance', $oldid, $newitemid, true);
3544        // Create the block context
3545        $newcontextid = context_block::instance($newitemid)->id;
3546        // Save the block contexts mapping and sent it to task
3547        $this->set_mapping('context', $oldcontextid, $newcontextid);
3548        $this->task->set_contextid($newcontextid);
3549        $this->task->set_blockid($newitemid);
3550
3551        // Restore block fileareas if declared
3552        $component = 'block_' . $this->task->get_blockname();
3553        foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
3554            $this->add_related_files($component, $filearea, null);
3555        }
3556
3557        // Process block positions, creating them or accumulating for final step
3558        foreach($positions as $position) {
3559            $position = (object)$position;
3560            $position->blockinstanceid = $newitemid; // The instance is always the restored one
3561            // If position is for one already mapped (known) contextid
3562            // process it now, creating the position
3563            if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
3564                $position->contextid = $newpositionctxid;
3565                // Create the block position
3566                $DB->insert_record('block_positions', $position);
3567
3568            // The position belongs to an unknown context, send it to backup_ids
3569            // to process them as part of the final steps of restore. We send the
3570            // whole $position object there, hence use the low level method.
3571            } else {
3572                restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
3573            }
3574        }
3575    }
3576}
3577
3578/**
3579 * Structure step to restore common course_module information
3580 *
3581 * This step will process the module.xml file for one activity, in order to restore
3582 * the corresponding information to the course_modules table, skipping various bits
3583 * of information based on CFG settings (groupings, completion...) in order to fullfill
3584 * all the reqs to be able to create the context to be used by all the rest of steps
3585 * in the activity restore task
3586 */
3587class restore_module_structure_step extends restore_structure_step {
3588
3589    protected function define_structure() {
3590        global $CFG;
3591
3592        $paths = array();
3593
3594        $module = new restore_path_element('module', '/module');
3595        $paths[] = $module;
3596        if ($CFG->enableavailability) {
3597            $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
3598            $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
3599        }
3600
3601        // Apply for 'format' plugins optional paths at module level
3602        $this->add_plugin_structure('format', $module);
3603
3604        // Apply for 'plagiarism' plugins optional paths at module level
3605        $this->add_plugin_structure('plagiarism', $module);
3606
3607        // Apply for 'local' plugins optional paths at module level
3608        $this->add_plugin_structure('local', $module);
3609
3610        return $paths;
3611    }
3612
3613    protected function process_module($data) {
3614        global $CFG, $DB;
3615
3616        $data = (object)$data;
3617        $oldid = $data->id;
3618        $this->task->set_old_moduleversion($data->version);
3619
3620        $data->course = $this->task->get_courseid();
3621        $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
3622        // Map section (first try by course_section mapping match. Useful in course and section restores)
3623        $data->section = $this->get_mappingid('course_section', $data->sectionid);
3624        if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
3625            $params = array(
3626                'course' => $this->get_courseid(),
3627                'section' => $data->sectionnumber);
3628            $data->section = $DB->get_field('course_sections', 'id', $params);
3629        }
3630        if (!$data->section) { // sectionnumber failed, try to get first section in course
3631            $params = array(
3632                'course' => $this->get_courseid());
3633            $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
3634        }
3635        if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
3636            $sectionrec = array(
3637                'course' => $this->get_courseid(),
3638                'section' => 0);
3639            $DB->insert_record('course_sections', $sectionrec); // section 0
3640            $sectionrec = array(
3641                'course' => $this->get_courseid(),
3642                'section' => 1);
3643            $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
3644        }
3645        $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
3646        if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
3647            $data->idnumber = '';
3648        }
3649        if (empty($CFG->enablecompletion)) { // completion
3650            $data->completion = 0;
3651            $data->completiongradeitemnumber = null;
3652            $data->completionview = 0;
3653            $data->completionexpected = 0;
3654        } else {
3655            $data->completionexpected = $this->apply_date_offset($data->completionexpected);
3656        }
3657        if (empty($CFG->enableavailability)) {
3658            $data->availability = null;
3659        }
3660        // Backups that did not include showdescription, set it to default 0
3661        // (this is not totally necessary as it has a db default, but just to
3662        // be explicit).
3663        if (!isset($data->showdescription)) {
3664            $data->showdescription = 0;
3665        }
3666        $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
3667
3668        if (empty($data->availability)) {
3669            // If there are legacy availablility data fields (and no new format data),
3670            // convert the old fields.
3671            $data->availability = \core_availability\info::convert_legacy_fields(
3672                    $data, false);
3673        } else if (!empty($data->groupmembersonly)) {
3674            // There is current availability data, but it still has groupmembersonly
3675            // as well (2.7 backups), convert just that part.
3676            require_once($CFG->dirroot . '/lib/db/upgradelib.php');
3677            $data->availability = upgrade_group_members_only($data->groupingid, $data->availability);
3678        }
3679
3680        // course_module record ready, insert it
3681        $newitemid = $DB->insert_record('course_modules', $data);
3682        // save mapping
3683        $this->set_mapping('course_module', $oldid, $newitemid);
3684        // set the new course_module id in the task
3685        $this->task->set_moduleid($newitemid);
3686        // we can now create the context safely
3687        $ctxid = context_module::instance($newitemid)->id;
3688        // set the new context id in the task
3689        $this->task->set_contextid($ctxid);
3690        // update sequence field in course_section
3691        if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
3692            $sequence .= ',' . $newitemid;
3693        } else {
3694            $sequence = $newitemid;
3695        }
3696        $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
3697
3698        // If there is the legacy showavailability data, store this for later use.
3699        // (This data is not present when restoring 'new' backups.)
3700        if (isset($data->showavailability)) {
3701            // Cache the showavailability flag using the backup_ids data field.
3702            restore_dbops::set_backup_ids_record($this->get_restoreid(),
3703                    'module_showavailability', $newitemid, 0, null,
3704                    (object)array('showavailability' => $data->showavailability));
3705        }
3706    }
3707
3708    /**
3709     * Process the legacy availability table record. This table does not exist
3710     * in Moodle 2.7+ but we still support restore.
3711     *
3712     * @param stdClass $data Record data
3713     */
3714    protected function process_availability($data) {
3715        $data = (object)$data;
3716        // Simply going to store the whole availability record now, we'll process
3717        // all them later in the final task (once all activities have been restored)
3718        // Let's call the low level one to be able to store the whole object
3719        $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
3720        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
3721    }
3722
3723    /**
3724     * Process the legacy availability fields table record. This table does not
3725     * exist in Moodle 2.7+ but we still support restore.
3726     *
3727     * @param stdClass $data Record data
3728     */
3729    protected function process_availability_field($data) {
3730        global $DB;
3731        $data = (object)$data;
3732        // Mark it is as passed by default
3733        $passed = true;
3734        $customfieldid = null;
3735
3736        // If a customfield has been used in order to pass we must be able to match an existing
3737        // customfield by name (data->customfield) and type (data->customfieldtype)
3738        if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
3739            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
3740            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
3741            $passed = false;
3742        } else if (!empty($data->customfield)) {
3743            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
3744            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
3745            $passed = ($customfieldid !== false);
3746        }
3747
3748        if ($passed) {
3749            // Create the object to insert into the database
3750            $availfield = new stdClass();
3751            $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
3752            $availfield->userfield = $data->userfield;
3753            $availfield->customfieldid = $customfieldid;
3754            $availfield->operator = $data->operator;
3755            $availfield->value = $data->value;
3756
3757            // Get showavailability option.
3758            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
3759                    'module_showavailability', $availfield->coursemoduleid);
3760            if (!$showrec) {
3761                // Should not happen.
3762                throw new coding_exception('No matching showavailability record');
3763            }
3764            $show = $showrec->info->showavailability;
3765
3766            // The $availfieldobject is now in the format used in the old
3767            // system. Interpret this and convert to new system.
3768            $currentvalue = $DB->get_field('course_modules', 'availability',
3769                    array('id' => $availfield->coursemoduleid), MUST_EXIST);
3770            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
3771                    $currentvalue, $availfield, $show);
3772            $DB->set_field('course_modules', 'availability', $newvalue,
3773                    array('id' => $availfield->coursemoduleid));
3774        }
3775    }
3776}
3777
3778/**
3779 * Structure step that will process the user activity completion
3780 * information if all these conditions are met:
3781 *  - Target site has completion enabled ($CFG->enablecompletion)
3782 *  - Activity includes completion info (file_exists)
3783 */
3784class restore_userscompletion_structure_step extends restore_structure_step {
3785    /**
3786     * To conditionally decide if this step must be executed
3787     * Note the "settings" conditions are evaluated in the
3788     * corresponding task. Here we check for other conditions
3789     * not being restore settings (files, site settings...)
3790     */
3791     protected function execute_condition() {
3792         global $CFG;
3793
3794         // Completion disabled in this site, don't execute
3795         if (empty($CFG->enablecompletion)) {
3796             return false;
3797         }
3798
3799        // No completion on the front page.
3800        if ($this->get_courseid() == SITEID) {
3801            return false;
3802        }
3803
3804         // No user completion info found, don't execute
3805        $fullpath = $this->task->get_taskbasepath();
3806        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3807         if (!file_exists($fullpath)) {
3808             return false;
3809         }
3810
3811         // Arrived here, execute the step
3812         return true;
3813     }
3814
3815     protected function define_structure() {
3816
3817        $paths = array();
3818
3819        $paths[] = new restore_path_element('completion', '/completions/completion');
3820
3821        return $paths;
3822    }
3823
3824    protected function process_completion($data) {
3825        global $DB;
3826
3827        $data = (object)$data;
3828
3829        $data->coursemoduleid = $this->task->get_moduleid();
3830        $data->userid = $this->get_mappingid('user', $data->userid);
3831        $data->timemodified = $this->apply_date_offset($data->timemodified);
3832
3833        // Find the existing record
3834        $existing = $DB->get_record('course_modules_completion', array(
3835                'coursemoduleid' => $data->coursemoduleid,
3836                'userid' => $data->userid), 'id, timemodified');
3837        // Check we didn't already insert one for this cmid and userid
3838        // (there aren't supposed to be duplicates in that field, but
3839        // it was possible until MDL-28021 was fixed).
3840        if ($existing) {
3841            // Update it to these new values, but only if the time is newer
3842            if ($existing->timemodified < $data->timemodified) {
3843                $data->id = $existing->id;
3844                $DB->update_record('course_modules_completion', $data);
3845            }
3846        } else {
3847            // Normal entry where it doesn't exist already
3848            $DB->insert_record('course_modules_completion', $data);
3849        }
3850    }
3851}
3852
3853/**
3854 * Abstract structure step, parent of all the activity structure steps. Used to support
3855 * the main <activity ...> tag and process it.
3856 */
3857abstract class restore_activity_structure_step extends restore_structure_step {
3858
3859    /**
3860     * Adds support for the 'activity' path that is common to all the activities
3861     * and will be processed globally here
3862     */
3863    protected function prepare_activity_structure($paths) {
3864
3865        $paths[] = new restore_path_element('activity', '/activity');
3866
3867        return $paths;
3868    }
3869
3870    /**
3871     * Process the activity path, informing the task about various ids, needed later
3872     */
3873    protected function process_activity($data) {
3874        $data = (object)$data;
3875        $this->task->set_old_contextid($data->contextid); // Save old contextid in task
3876        $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
3877        $this->task->set_old_activityid($data->id); // Save old activityid in task
3878    }
3879
3880    /**
3881     * This must be invoked immediately after creating the "module" activity record (forum, choice...)
3882     * and will adjust the new activity id (the instance) in various places
3883     */
3884    protected function apply_activity_instance($newitemid) {
3885        global $DB;
3886
3887        $this->task->set_activityid($newitemid); // Save activity id in task
3888        // Apply the id to course_sections->instanceid
3889        $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
3890        // Do the mapping for modulename, preparing it for files by oldcontext
3891        $modulename = $this->task->get_modulename();
3892        $oldid = $this->task->get_old_activityid();
3893        $this->set_mapping($modulename, $oldid, $newitemid, true);
3894    }
3895}
3896
3897/**
3898 * Structure step in charge of creating/mapping all the qcats and qs
3899 * by parsing the questions.xml file and checking it against the
3900 * results calculated by {@link restore_process_categories_and_questions}
3901 * and stored in backup_ids_temp
3902 */
3903class restore_create_categories_and_questions extends restore_structure_step {
3904
3905    /** @var array $cachecategory store a question category */
3906    protected $cachedcategory = null;
3907
3908    protected function define_structure() {
3909
3910        $category = new restore_path_element('question_category', '/question_categories/question_category');
3911        $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
3912        $hint = new restore_path_element('question_hint',
3913                '/question_categories/question_category/questions/question/question_hints/question_hint');
3914
3915        $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
3916
3917        // Apply for 'qtype' plugins optional paths at question level
3918        $this->add_plugin_structure('qtype', $question);
3919
3920        // Apply for 'local' plugins optional paths at question level
3921        $this->add_plugin_structure('local', $question);
3922
3923        return array($category, $question, $hint, $tag);
3924    }
3925
3926    protected function process_question_category($data) {
3927        global $DB;
3928
3929        $data = (object)$data;
3930        $oldid = $data->id;
3931
3932        // Check we have one mapping for this category
3933        if (!$mapping = $this->get_mapping('question_category', $oldid)) {
3934            return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
3935        }
3936
3937        // Check we have to create the category (newitemid = 0)
3938        if ($mapping->newitemid) {
3939            return; // newitemid != 0, this category is going to be mapped. Nothing to do
3940        }
3941
3942        // Arrived here, newitemid = 0, we need to create the category
3943        // we'll do it at parentitemid context, but for CONTEXT_MODULE
3944        // categories, that will be created at CONTEXT_COURSE and moved
3945        // to module context later when the activity is created
3946        if ($mapping->info->contextlevel == CONTEXT_MODULE) {
3947            $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
3948        }
3949        $data->contextid = $mapping->parentitemid;
3950
3951        // Let's create the question_category and save mapping
3952        $newitemid = $DB->insert_record('question_categories', $data);
3953        $this->set_mapping('question_category', $oldid, $newitemid);
3954        // Also annotate them as question_category_created, we need
3955        // that later when remapping parents
3956        $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
3957    }
3958
3959    protected function process_question($data) {
3960        global $DB;
3961
3962        $data = (object)$data;
3963        $oldid = $data->id;
3964
3965        // Check we have one mapping for this question
3966        if (!$questionmapping = $this->get_mapping('question', $oldid)) {
3967            return; // No mapping = this question doesn't need to be created/mapped
3968        }
3969
3970        // Get the mapped category (cannot use get_new_parentid() because not
3971        // all the categories have been created, so it is not always available
3972        // Instead we get the mapping for the question->parentitemid because
3973        // we have loaded qcatids there for all parsed questions
3974        $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
3975
3976        // In the past, there were some very sloppy values of penalty. Fix them.
3977        if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
3978            $data->penalty = 0.3333333;
3979        }
3980        if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
3981            $data->penalty = 0.6666667;
3982        }
3983        if ($data->penalty >= 1) {
3984            $data->penalty = 1;
3985        }
3986
3987        $userid = $this->get_mappingid('user', $data->createdby);
3988        $data->createdby = $userid ? $userid : $this->task->get_userid();
3989
3990        $userid = $this->get_mappingid('user', $data->modifiedby);
3991        $data->modifiedby = $userid ? $userid : $this->task->get_userid();
3992
3993        // With newitemid = 0, let's create the question
3994        if (!$questionmapping->newitemid) {
3995            $newitemid = $DB->insert_record('question', $data);
3996            $this->set_mapping('question', $oldid, $newitemid);
3997            // Also annotate them as question_created, we need
3998            // that later when remapping parents (keeping the old categoryid as parentid)
3999            $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
4000        } else {
4001            // By performing this set_mapping() we make get_old/new_parentid() to work for all the
4002            // children elements of the 'question' one (so qtype plugins will know the question they belong to)
4003            $this->set_mapping('question', $oldid, $questionmapping->newitemid);
4004        }
4005
4006        // Note, we don't restore any question files yet
4007        // as far as the CONTEXT_MODULE categories still
4008        // haven't their contexts to be restored to
4009        // The {@link restore_create_question_files}, executed in the final step
4010        // step will be in charge of restoring all the question files
4011    }
4012
4013    protected function process_question_hint($data) {
4014        global $DB;
4015
4016        $data = (object)$data;
4017        $oldid = $data->id;
4018
4019        // Detect if the question is created or mapped
4020        $oldquestionid   = $this->get_old_parentid('question');
4021        $newquestionid   = $this->get_new_parentid('question');
4022        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
4023
4024        // If the question has been created by restore, we need to create its question_answers too
4025        if ($questioncreated) {
4026            // Adjust some columns
4027            $data->questionid = $newquestionid;
4028            // Insert record
4029            $newitemid = $DB->insert_record('question_hints', $data);
4030
4031        // The question existed, we need to map the existing question_hints
4032        } else {
4033            // Look in question_hints by hint text matching
4034            $sql = 'SELECT id
4035                      FROM {question_hints}
4036                     WHERE questionid = ?
4037                       AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
4038            $params = array($newquestionid, $data->hint);
4039            $newitemid = $DB->get_field_sql($sql, $params);
4040
4041            // Not able to find the hint, let's try cleaning the hint text
4042            // of all the question's hints in DB as slower fallback. MDL-33863.
4043            if (!$newitemid) {
4044                $potentialhints = $DB->get_records('question_hints',
4045                        array('questionid' => $newquestionid), '', 'id, hint');
4046                foreach ($potentialhints as $potentialhint) {
4047                    // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
4048                    $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
4049                    $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
4050                    if ($cleanhint === $data->hint) {
4051                        $newitemid = $data->id;
4052                    }
4053                }
4054            }
4055
4056            // If we haven't found the newitemid, something has gone really wrong, question in DB
4057            // is missing hints, exception
4058            if (!$newitemid) {
4059                $info = new stdClass();
4060                $info->filequestionid = $oldquestionid;
4061                $info->dbquestionid   = $newquestionid;
4062                $info->hint           = $data->hint;
4063                throw new restore_step_exception('error_question_hint_missing_in_db', $info);
4064            }
4065        }
4066        // Create mapping (I'm not sure if this is really needed?)
4067        $this->set_mapping('question_hint', $oldid, $newitemid);
4068    }
4069
4070    protected function process_tag($data) {
4071        global $CFG, $DB;
4072
4073        $data = (object)$data;
4074        $newquestion = $this->get_new_parentid('question');
4075        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));
4076        if (!$questioncreated) {
4077            // This question already exists in the question bank. Nothing for us to do.
4078            return;
4079        }
4080
4081        if (!empty($CFG->usetags)) { // if enabled in server
4082            // TODO: This is highly inefficient. Each time we add one tag
4083            // we fetch all the existing because tag_set() deletes them
4084            // so everything must be reinserted on each call
4085            $tags = array();
4086            $existingtags = tag_get_tags('question', $newquestion);
4087            // Re-add all the existitng tags
4088            foreach ($existingtags as $existingtag) {
4089                $tags[] = $existingtag->rawname;
4090            }
4091            // Add the one being restored
4092            $tags[] = $data->rawname;
4093            // Get the category, so we can then later get the context.
4094            $categoryid = $this->get_new_parentid('question_category');
4095            if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
4096                $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
4097            }
4098            // Send all the tags back to the question
4099            tag_set('question', $newquestion, $tags, 'core_question', $this->cachedcategory->contextid);
4100        }
4101    }
4102
4103    protected function after_execute() {
4104        global $DB;
4105
4106        // First of all, recode all the created question_categories->parent fields
4107        $qcats = $DB->get_records('backup_ids_temp', array(
4108                     'backupid' => $this->get_restoreid(),
4109                     'itemname' => 'question_category_created'));
4110        foreach ($qcats as $qcat) {
4111            $newparent = 0;
4112            $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
4113            // Get new parent (mapped or created, so we look in quesiton_category mappings)
4114            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
4115                                 'backupid' => $this->get_restoreid(),
4116                                 'itemname' => 'question_category',
4117                                 'itemid'   => $dbcat->parent))) {
4118                // contextids must match always, as far as we always include complete qbanks, just check it
4119                $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
4120                if ($dbcat->contextid == $newparentctxid) {
4121                    $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
4122                } else {
4123                    $newparent = 0; // No ctx match for both cats, no parent relationship
4124                }
4125            }
4126            // Here with $newparent empty, problem with contexts or remapping, set it to top cat
4127            if (!$newparent) {
4128                $DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id));
4129            }
4130        }
4131
4132        // Now, recode all the created question->parent fields
4133        $qs = $DB->get_records('backup_ids_temp', array(
4134                  'backupid' => $this->get_restoreid(),
4135                  'itemname' => 'question_created'));
4136        foreach ($qs as $q) {
4137            $newparent = 0;
4138            $dbq = $DB->get_record('question', array('id' => $q->newitemid));
4139            // Get new parent (mapped or created, so we look in question mappings)
4140            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
4141                                 'backupid' => $this->get_restoreid(),
4142                                 'itemname' => 'question',
4143                                 'itemid'   => $dbq->parent))) {
4144                $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
4145            }
4146        }
4147
4148        // Note, we don't restore any question files yet
4149        // as far as the CONTEXT_MODULE categories still
4150        // haven't their contexts to be restored to
4151        // The {@link restore_create_question_files}, executed in the final step
4152        // step will be in charge of restoring all the question files
4153    }
4154}
4155
4156/**
4157 * Execution step that will move all the CONTEXT_MODULE question categories
4158 * created at early stages of restore in course context (because modules weren't
4159 * created yet) to their target module (matching by old-new-contextid mapping)
4160 */
4161class restore_move_module_questions_categories extends restore_execution_step {
4162
4163    protected function define_execution() {
4164        global $DB;
4165
4166        $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
4167        foreach ($contexts as $contextid => $contextlevel) {
4168            // Only if context mapping exists (i.e. the module has been restored)
4169            if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
4170                // Update all the qcats having their parentitemid set to the original contextid
4171                $modulecats = $DB->get_records_sql("SELECT itemid, newitemid
4172                                                      FROM {backup_ids_temp}
4173                                                     WHERE backupid = ?
4174                                                       AND itemname = 'question_category'
4175                                                       AND parentitemid = ?", array($this->get_restoreid(), $contextid));
4176                foreach ($modulecats as $modulecat) {
4177                    $DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid));
4178                    // And set new contextid also in question_category mapping (will be
4179                    // used by {@link restore_create_question_files} later
4180                    restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid);
4181                }
4182            }
4183        }
4184    }
4185}
4186
4187/**
4188 * Execution step that will create all the question/answers/qtype-specific files for the restored
4189 * questions. It must be executed after {@link restore_move_module_questions_categories}
4190 * because only then each question is in its final category and only then the
4191 * contexts can be determined.
4192 */
4193class restore_create_question_files extends restore_execution_step {
4194
4195    /** @var array Question-type specific component items cache. */
4196    private $qtypecomponentscache = array();
4197
4198    /**
4199     * Preform the restore_create_question_files step.
4200     */
4201    protected function define_execution() {
4202        global $DB;
4203
4204        // Track progress, as this task can take a long time.
4205        $progress = $this->task->get_progress();
4206        $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
4207
4208        // Parentitemids of question_createds in backup_ids_temp are the category it is in.
4209        // MUST use a recordset, as there is no unique key in the first (or any) column.
4210        $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype
4211                                               FROM {backup_ids_temp} bi
4212                                               JOIN {question} q ON q.id = bi.newitemid
4213                                              WHERE bi.backupid = ?
4214                                                AND bi.itemname = 'question_created'
4215                                           ORDER BY categoryid ASC", array($this->get_restoreid()));
4216
4217        $currentcatid = -1;
4218        foreach ($catqtypes as $categoryid => $row) {
4219            $qtype = $row->qtype;
4220
4221            // Check if we are in a new category.
4222            if ($currentcatid !== $categoryid) {
4223                // Report progress for each category.
4224                $progress->progress();
4225
4226                if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(),
4227                        'question_category', $categoryid)) {
4228                    // Something went really wrong, cannot find the question_category for the question_created records.
4229                    debugging('Error fetching target context for question', DEBUG_DEVELOPER);
4230                    continue;
4231                }
4232
4233                // Calculate source and target contexts.
4234                $oldctxid = $qcatmapping->info->contextid;
4235                $newctxid = $qcatmapping->parentitemid;
4236
4237                $this->send_common_files($oldctxid, $newctxid, $progress);
4238                $currentcatid = $categoryid;
4239            }
4240
4241            $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress);
4242        }
4243        $catqtypes->close();
4244        $progress->end_progress();
4245    }
4246
4247    /**
4248     * Send the common question files to a new context.
4249     *
4250     * @param int             $oldctxid Old context id.
4251     * @param int             $newctxid New context id.
4252     * @param \core\progress  $progress Progress object to use.
4253     */
4254    private function send_common_files($oldctxid, $newctxid, $progress) {
4255        // Add common question files (question and question_answer ones).
4256        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
4257                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
4258        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
4259                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
4260        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
4261                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
4262        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
4263                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
4264        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
4265                $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
4266        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
4267                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
4268        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
4269                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
4270        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
4271                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
4272    }
4273
4274    /**
4275     * Send the question type specific files to a new context.
4276     *
4277     * @param text            $qtype The qtype name to send.
4278     * @param int             $oldctxid Old context id.
4279     * @param int             $newctxid New context id.
4280     * @param \core\progress  $progress Progress object to use.
4281     */
4282    private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) {
4283        if (!isset($this->qtypecomponentscache[$qtype])) {
4284            $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype);
4285        }
4286        $components = $this->qtypecomponentscache[$qtype];
4287        foreach ($components as $component => $fileareas) {
4288            foreach ($fileareas as $filearea => $mapping) {
4289                restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,
4290                        $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress);
4291            }
4292        }
4293    }
4294}
4295
4296/**
4297 * Try to restore aliases and references to external files.
4298 *
4299 * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}.
4300 * We expect that all regular (non-alias) files have already been restored. Make sure
4301 * there is no restore step executed after this one that would call send_files_to_pool() again.
4302 *
4303 * You may notice we have hardcoded support for Server files, Legacy course files
4304 * and user Private files here at the moment. This could be eventually replaced with a set of
4305 * callbacks in the future if needed.
4306 *
4307 * @copyright 2012 David Mudrak <david@moodle.com>
4308 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4309 */
4310class restore_process_file_aliases_queue extends restore_execution_step {
4311
4312    /** @var array internal cache for {@link choose_repository()} */
4313    private $cachereposbyid = array();
4314
4315    /** @var array internal cache for {@link choose_repository()} */
4316    private $cachereposbytype = array();
4317
4318    /**
4319     * What to do when this step is executed.
4320     */
4321    protected function define_execution() {
4322        global $DB;
4323
4324        $this->log('processing file aliases queue', backup::LOG_DEBUG);
4325
4326        $fs = get_file_storage();
4327
4328        // Load the queue.
4329        $rs = $DB->get_recordset('backup_ids_temp',
4330            array('backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'),
4331            '', 'info');
4332
4333        // Iterate over aliases in the queue.
4334        foreach ($rs as $record) {
4335            $info = backup_controller_dbops::decode_backup_temp_info($record->info);
4336
4337            // Try to pick a repository instance that should serve the alias.
4338            $repository = $this->choose_repository($info);
4339
4340            if (is_null($repository)) {
4341                $this->notify_failure($info, 'unable to find a matching repository instance');
4342                continue;
4343            }
4344
4345            if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
4346                // Aliases to Server files and Legacy course files may refer to a file
4347                // contained in the backup file or to some existing file (if we are on the
4348                // same site).
4349                try {
4350                    $reference = file_storage::unpack_reference($info->oldfile->reference);
4351                } catch (Exception $e) {
4352                    $this->notify_failure($info, 'invalid reference field format');
4353                    continue;
4354                }
4355
4356                // Let's see if the referred source file was also included in the backup.
4357                $candidates = $DB->get_recordset('backup_files_temp', array(
4358                        'backupid' => $this->get_restoreid(),
4359                        'contextid' => $reference['contextid'],
4360                        'component' => $reference['component'],
4361                        'filearea' => $reference['filearea'],
4362                        'itemid' => $reference['itemid'],
4363                    ), '', 'info, newcontextid, newitemid');
4364
4365                $source = null;
4366
4367                foreach ($candidates as $candidate) {
4368                    $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
4369                    if ($candidateinfo->filename === $reference['filename']
4370                            and $candidateinfo->filepath === $reference['filepath']
4371                            and !is_null($candidate->newcontextid)
4372                            and !is_null($candidate->newitemid) ) {
4373                        $source = $candidateinfo;
4374                        $source->contextid = $candidate->newcontextid;
4375                        $source->itemid = $candidate->newitemid;
4376                        break;
4377                    }
4378                }
4379                $candidates->close();
4380
4381                if ($source) {
4382                    // We have an alias that refers to another file also included in
4383                    // the backup. Let us change the reference field so that it refers
4384                    // to the restored copy of the original file.
4385                    $reference = file_storage::pack_reference($source);
4386
4387                    // Send the new alias to the filepool.
4388                    $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4389                    $this->notify_success($info);
4390                    continue;
4391
4392                } else {
4393                    // This is a reference to some moodle file that was not contained in the backup
4394                    // file. If we are restoring to the same site, keep the reference untouched
4395                    // and restore the alias as is if the referenced file exists.
4396                    if ($this->task->is_samesite()) {
4397                        if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
4398                                $reference['itemid'], $reference['filepath'], $reference['filename'])) {
4399                            $reference = file_storage::pack_reference($reference);
4400                            $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4401                            $this->notify_success($info);
4402                            continue;
4403                        } else {
4404                            $this->notify_failure($info, 'referenced file not found');
4405                            continue;
4406                        }
4407
4408                    // If we are at other site, we can't restore this alias.
4409                    } else {
4410                        $this->notify_failure($info, 'referenced file not included');
4411                        continue;
4412                    }
4413                }
4414
4415            } else if ($info->oldfile->repositorytype === 'user') {
4416                if ($this->task->is_samesite()) {
4417                    // For aliases to user Private files at the same site, we have a chance to check
4418                    // if the referenced file still exists.
4419                    try {
4420                        $reference = file_storage::unpack_reference($info->oldfile->reference);
4421                    } catch (Exception $e) {
4422                        $this->notify_failure($info, 'invalid reference field format');
4423                        continue;
4424                    }
4425                    if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
4426                            $reference['itemid'], $reference['filepath'], $reference['filename'])) {
4427                        $reference = file_storage::pack_reference($reference);
4428                        $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4429                        $this->notify_success($info);
4430                        continue;
4431                    } else {
4432                        $this->notify_failure($info, 'referenced file not found');
4433                        continue;
4434                    }
4435
4436                // If we are at other site, we can't restore this alias.
4437                } else {
4438                    $this->notify_failure($info, 'restoring at another site');
4439                    continue;
4440                }
4441
4442            } else {
4443                // This is a reference to some external file such as in boxnet or dropbox.
4444                // If we are restoring to the same site, keep the reference untouched and
4445                // restore the alias as is.
4446                if ($this->task->is_samesite()) {
4447                    $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference);
4448                    $this->notify_success($info);
4449                    continue;
4450
4451                // If we are at other site, we can't restore this alias.
4452                } else {
4453                    $this->notify_failure($info, 'restoring at another site');
4454                    continue;
4455                }
4456            }
4457        }
4458        $rs->close();
4459    }
4460
4461    /**
4462     * Choose the repository instance that should handle the alias.
4463     *
4464     * At the same site, we can rely on repository instance id and we just
4465     * check it still exists. On other site, try to find matching Server files or
4466     * Legacy course files repository instance. Return null if no matching
4467     * repository instance can be found.
4468     *
4469     * @param stdClass $info
4470     * @return repository|null
4471     */
4472    private function choose_repository(stdClass $info) {
4473        global $DB, $CFG;
4474        require_once($CFG->dirroot.'/repository/lib.php');
4475
4476        if ($this->task->is_samesite()) {
4477            // We can rely on repository instance id.
4478
4479            if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) {
4480                return $this->cachereposbyid[$info->oldfile->repositoryid];
4481            }
4482
4483            $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1);
4484
4485            try {
4486                $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID);
4487                return $this->cachereposbyid[$info->oldfile->repositoryid];
4488            } catch (Exception $e) {
4489                $this->cachereposbyid[$info->oldfile->repositoryid] = null;
4490                return null;
4491            }
4492
4493        } else {
4494            // We can rely on repository type only.
4495
4496            if (empty($info->oldfile->repositorytype)) {
4497                return null;
4498            }
4499
4500            if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) {
4501                return $this->cachereposbytype[$info->oldfile->repositorytype];
4502            }
4503
4504            $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1);
4505
4506            // Both Server files and Legacy course files repositories have a single
4507            // instance at the system context to use. Let us try to find it.
4508            if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
4509                $sql = "SELECT ri.id
4510                          FROM {repository} r
4511                          JOIN {repository_instances} ri ON ri.typeid = r.id
4512                         WHERE r.type = ? AND ri.contextid = ?";
4513                $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID));
4514                if (empty($ris)) {
4515                    return null;
4516                }
4517                $repoids = array_keys($ris);
4518                $repoid = reset($repoids);
4519                try {
4520                    $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID);
4521                    return $this->cachereposbytype[$info->oldfile->repositorytype];
4522                } catch (Exception $e) {
4523                    $this->cachereposbytype[$info->oldfile->repositorytype] = null;
4524                    return null;
4525                }
4526            }
4527
4528            $this->cachereposbytype[$info->oldfile->repositorytype] = null;
4529            return null;
4530        }
4531    }
4532
4533    /**
4534     * Let the user know that the given alias was successfully restored
4535     *
4536     * @param stdClass $info
4537     */
4538    private function notify_success(stdClass $info) {
4539        $filedesc = $this->describe_alias($info);
4540        $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1);
4541    }
4542
4543    /**
4544     * Let the user know that the given alias can't be restored
4545     *
4546     * @param stdClass $info
4547     * @param string $reason detailed reason to be logged
4548     */
4549    private function notify_failure(stdClass $info, $reason = '') {
4550        $filedesc = $this->describe_alias($info);
4551        if ($reason) {
4552            $reason = ' ('.$reason.')';
4553        }
4554        $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1);
4555        $this->add_result_item('file_aliases_restore_failures', $filedesc);
4556    }
4557
4558    /**
4559     * Return a human readable description of the alias file
4560     *
4561     * @param stdClass $info
4562     * @return string
4563     */
4564    private function describe_alias(stdClass $info) {
4565
4566        $filedesc = $this->expected_alias_location($info->newfile);
4567
4568        if (!is_null($info->oldfile->source)) {
4569            $filedesc .= ' ('.$info->oldfile->source.')';
4570        }
4571
4572        return $filedesc;
4573    }
4574
4575    /**
4576     * Return the expected location of a file
4577     *
4578     * Please note this may and may not work as a part of URL to pluginfile.php
4579     * (depends on how the given component/filearea deals with the itemid).
4580     *
4581     * @param stdClass $filerecord
4582     * @return string
4583     */
4584    private function expected_alias_location($filerecord) {
4585
4586        $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea;
4587        if (!is_null($filerecord->itemid)) {
4588            $filedesc .= '/'.$filerecord->itemid;
4589        }
4590        $filedesc .= $filerecord->filepath.$filerecord->filename;
4591
4592        return $filedesc;
4593    }
4594
4595    /**
4596     * Append a value to the given resultset
4597     *
4598     * @param string $name name of the result containing a list of values
4599     * @param mixed $value value to add as another item in that result
4600     */
4601    private function add_result_item($name, $value) {
4602
4603        $results = $this->task->get_results();
4604
4605        if (isset($results[$name])) {
4606            if (!is_array($results[$name])) {
4607                throw new coding_exception('Unable to append a result item into a non-array structure.');
4608            }
4609            $current = $results[$name];
4610            $current[] = $value;
4611            $this->task->add_result(array($name => $current));
4612
4613        } else {
4614            $this->task->add_result(array($name => array($value)));
4615        }
4616    }
4617}
4618
4619
4620/**
4621 * Abstract structure step, to be used by all the activities using core questions stuff
4622 * (like the quiz module), to support qtype plugins, states and sessions
4623 */
4624abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
4625    /** @var array question_attempt->id to qtype. */
4626    protected $qtypes = array();
4627    /** @var array question_attempt->id to questionid. */
4628    protected $newquestionids = array();
4629
4630    /**
4631     * Attach below $element (usually attempts) the needed restore_path_elements
4632     * to restore question_usages and all they contain.
4633     *
4634     * If you use the $nameprefix parameter, then you will need to implement some
4635     * extra methods in your class, like
4636     *
4637     * protected function process_{nameprefix}question_attempt($data) {
4638     *     $this->restore_question_usage_worker($data, '{nameprefix}');
4639     * }
4640     * protected function process_{nameprefix}question_attempt($data) {
4641     *     $this->restore_question_attempt_worker($data, '{nameprefix}');
4642     * }
4643     * protected function process_{nameprefix}question_attempt_step($data) {
4644     *     $this->restore_question_attempt_step_worker($data, '{nameprefix}');
4645     * }
4646     *
4647     * @param restore_path_element $element the parent element that the usages are stored inside.
4648     * @param array $paths the paths array that is being built.
4649     * @param string $nameprefix should match the prefix passed to the corresponding
4650     *      backup_questions_activity_structure_step::add_question_usages call.
4651     */
4652    protected function add_question_usages($element, &$paths, $nameprefix = '') {
4653        // Check $element is restore_path_element
4654        if (! $element instanceof restore_path_element) {
4655            throw new restore_step_exception('element_must_be_restore_path_element', $element);
4656        }
4657
4658        // Check $paths is one array
4659        if (!is_array($paths)) {
4660            throw new restore_step_exception('paths_must_be_array', $paths);
4661        }
4662        $paths[] = new restore_path_element($nameprefix . 'question_usage',
4663                $element->get_path() . "/{$nameprefix}question_usage");
4664        $paths[] = new restore_path_element($nameprefix . 'question_attempt',
4665                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
4666        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
4667                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
4668                true);
4669        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
4670                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
4671    }
4672
4673    /**
4674     * Process question_usages
4675     */
4676    protected function process_question_usage($data) {
4677        $this->restore_question_usage_worker($data, '');
4678    }
4679
4680    /**
4681     * Process question_attempts
4682     */
4683    protected function process_question_attempt($data) {
4684        $this->restore_question_attempt_worker($data, '');
4685    }
4686
4687    /**
4688     * Process question_attempt_steps
4689     */
4690    protected function process_question_attempt_step($data) {
4691        $this->restore_question_attempt_step_worker($data, '');
4692    }
4693
4694    /**
4695     * This method does the acutal work for process_question_usage or
4696     * process_{nameprefix}_question_usage.
4697     * @param array $data the data from the XML file.
4698     * @param string $nameprefix the element name prefix.
4699     */
4700    protected function restore_question_usage_worker($data, $nameprefix) {
4701        global $DB;
4702
4703        // Clear our caches.
4704        $this->qtypes = array();
4705        $this->newquestionids = array();
4706
4707        $data = (object)$data;
4708        $oldid = $data->id;
4709
4710        $oldcontextid = $this->get_task()->get_old_contextid();
4711        $data->contextid  = $this->get_mappingid('context', $this->task->get_old_contextid());
4712
4713        // Everything ready, insert (no mapping needed)
4714        $newitemid = $DB->insert_record('question_usages', $data);
4715
4716        $this->inform_new_usage_id($newitemid);
4717
4718        $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
4719    }
4720
4721    /**
4722     * When process_question_usage creates the new usage, it calls this method
4723     * to let the activity link to the new usage. For example, the quiz uses
4724     * this method to set quiz_attempts.uniqueid to the new usage id.
4725     * @param integer $newusageid
4726     */
4727    abstract protected function inform_new_usage_id($newusageid);
4728
4729    /**
4730     * This method does the acutal work for process_question_attempt or
4731     * process_{nameprefix}_question_attempt.
4732     * @param array $data the data from the XML file.
4733     * @param string $nameprefix the element name prefix.
4734     */
4735    protected function restore_question_attempt_worker($data, $nameprefix) {
4736        global $DB;
4737
4738        $data = (object)$data;
4739        $oldid = $data->id;
4740        $question = $this->get_mapping('question', $data->questionid);
4741
4742        $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
4743        $data->questionid      = $question->newitemid;
4744        if (!property_exists($data, 'variant')) {
4745            $data->variant = 1;
4746        }
4747        $data->timemodified    = $this->apply_date_offset($data->timemodified);
4748
4749        if (!property_exists($data, 'maxfraction')) {
4750            $data->maxfraction = 1;
4751        }
4752
4753        $newitemid = $DB->insert_record('question_attempts', $data);
4754
4755        $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
4756        $this->qtypes[$newitemid] = $question->info->qtype;
4757        $this->newquestionids[$newitemid] = $data->questionid;
4758    }
4759
4760    /**
4761     * This method does the acutal work for process_question_attempt_step or
4762     * process_{nameprefix}_question_attempt_step.
4763     * @param array $data the data from the XML file.
4764     * @param string $nameprefix the element name prefix.
4765     */
4766    protected function restore_question_attempt_step_worker($data, $nameprefix) {
4767        global $DB;
4768
4769        $data = (object)$data;
4770        $oldid = $data->id;
4771
4772        // Pull out the response data.
4773        $response = array();
4774        if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
4775            foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
4776                $response[$variable['name']] = $variable['value'];
4777            }
4778        }
4779        unset($data->response);
4780
4781        $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
4782        $data->timecreated = $this->apply_date_offset($data->timecreated);
4783        $data->userid      = $this->get_mappingid('user', $data->userid);
4784
4785        // Everything ready, insert and create mapping (needed by question_sessions)
4786        $newitemid = $DB->insert_record('question_attempt_steps', $data);
4787        $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
4788
4789        // Now process the response data.
4790        $response = $this->questions_recode_response_data(
4791                $this->qtypes[$data->questionattemptid],
4792                $this->newquestionids[$data->questionattemptid],
4793                $data->sequencenumber, $response);
4794
4795        foreach ($response as $name => $value) {
4796            $row = new stdClass();
4797            $row->attemptstepid = $newitemid;
4798            $row->name = $name;
4799            $row->value = $value;
4800            $DB->insert_record('question_attempt_step_data', $row, false);
4801        }
4802    }
4803
4804    /**
4805     * Recode the respones data for a particular step of an attempt at at particular question.
4806     * @param string $qtype the question type.
4807     * @param int $newquestionid the question id.
4808     * @param int $sequencenumber the sequence number.
4809     * @param array $response the response data to recode.
4810     */
4811    public function questions_recode_response_data(
4812            $qtype, $newquestionid, $sequencenumber, array $response) {
4813        $qtyperestorer = $this->get_qtype_restorer($qtype);
4814        if ($qtyperestorer) {
4815            $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response);
4816        }
4817        return $response;
4818    }
4819
4820    /**
4821     * Given a list of question->ids, separated by commas, returns the
4822     * recoded list, with all the restore question mappings applied.
4823     * Note: Used by quiz->questions and quiz_attempts->layout
4824     * Note: 0 = page break (unconverted)
4825     */
4826    protected function questions_recode_layout($layout) {
4827        // Extracts question id from sequence
4828        if ($questionids = explode(',', $layout)) {
4829            foreach ($questionids as $id => $questionid) {
4830                if ($questionid) { // If it is zero then this is a pagebreak, don't translate
4831                    $newquestionid = $this->get_mappingid('question', $questionid);
4832                    $questionids[$id] = $newquestionid;
4833                }
4834            }
4835        }
4836        return implode(',', $questionids);
4837    }
4838
4839    /**
4840     * Get the restore_qtype_plugin subclass for a specific question type.
4841     * @param string $qtype e.g. multichoice.
4842     * @return restore_qtype_plugin instance.
4843     */
4844    protected function get_qtype_restorer($qtype) {
4845        // Build one static cache to store {@link restore_qtype_plugin}
4846        // while we are needing them, just to save zillions of instantiations
4847        // or using static stuff that will break our nice API
4848        static $qtypeplugins = array();
4849
4850        if (!isset($qtypeplugins[$qtype])) {
4851            $classname = 'restore_qtype_' . $qtype . '_plugin';
4852            if (class_exists($classname)) {
4853                $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
4854            } else {
4855                $qtypeplugins[$qtype] = null;
4856            }
4857        }
4858        return $qtypeplugins[$qtype];
4859    }
4860
4861    protected function after_execute() {
4862        parent::after_execute();
4863
4864        // Restore any files belonging to responses.
4865        foreach (question_engine::get_all_response_file_areas() as $filearea) {
4866            $this->add_related_files('question', $filearea, 'question_attempt_step');
4867        }
4868    }
4869
4870    /**
4871     * Attach below $element (usually attempts) the needed restore_path_elements
4872     * to restore question attempt data from Moodle 2.0.
4873     *
4874     * When using this method, the parent element ($element) must be defined with
4875     * $grouped = true. Then, in that elements process method, you must call
4876     * {@link process_legacy_attempt_data()} with the groupded data. See, for
4877     * example, the usage of this method in