Merge branch 'MDL-62244_master_label_view' of https://github.com/sheesania/moodle
[moodle.git] / backup / moodle2 / restore_stepslib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * 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  */
28 defined('MOODLE_INTERNAL') || die();
30 /**
31  * delete old directories and conditionally create backup_temp_ids table
32  */
33 class restore_create_and_clean_temp_stuff extends restore_execution_step {
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);
56     }
57 }
59 /**
60  * Drop temp ids table and delete the temp dir used by backup/restore (conditionally).
61  */
62 class restore_drop_and_clean_temp_stuff extends restore_execution_step {
64     protected function define_execution() {
65         global $CFG;
66         restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
67         if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
68             $progress = $this->task->get_progress();
69             $progress->start_progress('Deleting backup dir');
70             backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
71             $progress->end_progress();
72         }
73     }
74 }
76 /**
77  * Restore calculated grade items, grade categories etc
78  */
79 class restore_gradebook_structure_step extends restore_structure_step {
81     /**
82      * To conditionally decide if this step must be executed
83      * Note the "settings" conditions are evaluated in the
84      * corresponding task. Here we check for other conditions
85      * not being restore settings (files, site settings...)
86      */
87      protected function execute_condition() {
88         global $CFG, $DB;
90         if ($this->get_courseid() == SITEID) {
91             return false;
92         }
94         // No gradebook info found, don't execute
95         $fullpath = $this->task->get_taskbasepath();
96         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
97         if (!file_exists($fullpath)) {
98             return false;
99         }
101         // Some module present in backup file isn't available to restore
102         // in this site, don't execute
103         if ($this->task->is_missing_modules()) {
104             return false;
105         }
107         // Some activity has been excluded to be restored, don't execute
108         if ($this->task->is_excluding_activities()) {
109             return false;
110         }
112         // There should only be one grade category (the 1 associated with the course itself)
113         // If other categories already exist we're restoring into an existing course.
114         // Restoring categories into a course with an existing category structure is unlikely to go well
115         $category = new stdclass();
116         $category->courseid  = $this->get_courseid();
117         $catcount = $DB->count_records('grade_categories', (array)$category);
118         if ($catcount>1) {
119             return false;
120         }
122         // Identify the backup we're dealing with.
123         $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
124         $backupbuild = 0;
125         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
126         if (!empty($matches[1])) {
127             $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
128         }
130         // On older versions the freeze value has to be converted.
131         // We do this from here as it is happening right before the file is read.
132         // This only targets the backup files that can contain the legacy freeze.
133         if ($backupbuild > 20150618 && (version_compare($backuprelease, '3.0', '<') || $backupbuild < 20160527)) {
134             $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
135         }
137         // Arrived here, execute the step
138         return true;
139      }
141     protected function define_structure() {
142         $paths = array();
143         $userinfo = $this->task->get_setting_value('users');
145         $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
146         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
148         $gradeitem = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
149         $paths[] = $gradeitem;
150         $this->add_plugin_structure('local', $gradeitem);
152         if ($userinfo) {
153             $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
154         }
155         $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
156         $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
158         return $paths;
159     }
161     protected function process_attributes($data) {
162         // For non-merge restore types:
163         // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
164         $target = $this->get_task()->get_target();
165         if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
166             set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
167         }
168         if (!empty($data['calculations_freeze'])) {
169             if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
170                     $target == backup::TARGET_EXISTING_DELETING) {
171                 set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
172             }
173         }
174     }
176     protected function process_grade_item($data) {
177         global $DB;
179         $data = (object)$data;
181         $oldid = $data->id;
182         $data->course = $this->get_courseid();
184         $data->courseid = $this->get_courseid();
186         if ($data->itemtype=='manual') {
187             // manual grade items store category id in categoryid
188             $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
189             // if mapping failed put in course's grade category
190             if (NULL == $data->categoryid) {
191                 $coursecat = grade_category::fetch_course_category($this->get_courseid());
192                 $data->categoryid = $coursecat->id;
193             }
194         } else if ($data->itemtype=='course') {
195             // course grade item stores their category id in iteminstance
196             $coursecat = grade_category::fetch_course_category($this->get_courseid());
197             $data->iteminstance = $coursecat->id;
198         } else if ($data->itemtype=='category') {
199             // category grade items store their category id in iteminstance
200             $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
201         } else {
202             throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
203         }
205         $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
206         $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
208         $data->locktime = $this->apply_date_offset($data->locktime);
210         $coursecategory = $newitemid = null;
211         //course grade item should already exist so updating instead of inserting
212         if($data->itemtype=='course') {
213             //get the ID of the already created grade item
214             $gi = new stdclass();
215             $gi->courseid  = $this->get_courseid();
216             $gi->itemtype  = $data->itemtype;
218             //need to get the id of the grade_category that was automatically created for the course
219             $category = new stdclass();
220             $category->courseid  = $this->get_courseid();
221             $category->parent  = null;
222             //course category fullname starts out as ? but may be edited
223             //$category->fullname  = '?';
224             $coursecategory = $DB->get_record('grade_categories', (array)$category);
225             $gi->iteminstance = $coursecategory->id;
227             $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
228             if (!empty($existinggradeitem)) {
229                 $data->id = $newitemid = $existinggradeitem->id;
230                 $DB->update_record('grade_items', $data);
231             }
232         } else if ($data->itemtype == 'manual') {
233             // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
234             $gi = array(
235                 'itemtype' => $data->itemtype,
236                 'courseid' => $data->courseid,
237                 'itemname' => $data->itemname,
238                 'categoryid' => $data->categoryid,
239             );
240             $newitemid = $DB->get_field('grade_items', 'id', $gi);
241         }
243         if (empty($newitemid)) {
244             //in case we found the course category but still need to insert the course grade item
245             if ($data->itemtype=='course' && !empty($coursecategory)) {
246                 $data->iteminstance = $coursecategory->id;
247             }
249             $newitemid = $DB->insert_record('grade_items', $data);
250             $data->id = $newitemid;
251             $gradeitem = new grade_item($data);
252             core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger();
253         }
254         $this->set_mapping('grade_item', $oldid, $newitemid);
255     }
257     protected function process_grade_grade($data) {
258         global $DB;
260         $data = (object)$data;
261         $oldid = $data->id;
262         $olduserid = $data->userid;
264         $data->itemid = $this->get_new_parentid('grade_item');
266         $data->userid = $this->get_mappingid('user', $data->userid, null);
267         if (!empty($data->userid)) {
268             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
269             $data->locktime     = $this->apply_date_offset($data->locktime);
271             $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
272             if ($gradeexists) {
273                 $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
274                 $this->log($message, backup::LOG_DEBUG);
275             } else {
276                 $newitemid = $DB->insert_record('grade_grades', $data);
277                 $this->set_mapping('grade_grades', $oldid, $newitemid);
278             }
279         } else {
280             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
281             $this->log($message, backup::LOG_DEBUG);
282         }
283     }
285     protected function process_grade_category($data) {
286         global $DB;
288         $data = (object)$data;
289         $oldid = $data->id;
291         $data->course = $this->get_courseid();
292         $data->courseid = $data->course;
294         $newitemid = null;
295         //no parent means a course level grade category. That may have been created when the course was created
296         if(empty($data->parent)) {
297             //parent was being saved as 0 when it should be null
298             $data->parent = null;
300             //get the already created course level grade category
301             $category = new stdclass();
302             $category->courseid = $this->get_courseid();
303             $category->parent = null;
305             $coursecategory = $DB->get_record('grade_categories', (array)$category);
306             if (!empty($coursecategory)) {
307                 $data->id = $newitemid = $coursecategory->id;
308                 $DB->update_record('grade_categories', $data);
309             }
310         }
312         // Add a warning about a removed setting.
313         if (!empty($data->aggregatesubcats)) {
314             set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
315         }
317         //need to insert a course category
318         if (empty($newitemid)) {
319             $newitemid = $DB->insert_record('grade_categories', $data);
320         }
321         $this->set_mapping('grade_category', $oldid, $newitemid);
322     }
323     protected function process_grade_letter($data) {
324         global $DB;
326         $data = (object)$data;
327         $oldid = $data->id;
329         $data->contextid = context_course::instance($this->get_courseid())->id;
331         $gradeletter = (array)$data;
332         unset($gradeletter['id']);
333         if (!$DB->record_exists('grade_letters', $gradeletter)) {
334             $newitemid = $DB->insert_record('grade_letters', $data);
335         } else {
336             $newitemid = $data->id;
337         }
339         $this->set_mapping('grade_letter', $oldid, $newitemid);
340     }
341     protected function process_grade_setting($data) {
342         global $DB;
344         $data = (object)$data;
345         $oldid = $data->id;
347         $data->courseid = $this->get_courseid();
349         $target = $this->get_task()->get_target();
350         if ($data->name == 'minmaxtouse' &&
351                 ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
352             // We never restore minmaxtouse during merge.
353             return;
354         }
356         if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
357             $newitemid = $DB->insert_record('grade_settings', $data);
358         } else {
359             $newitemid = $data->id;
360         }
362         if (!empty($oldid)) {
363             // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
364             $this->set_mapping('grade_setting', $oldid, $newitemid);
365         }
366     }
368     /**
369      * put all activity grade items in the correct grade category and mark all for recalculation
370      */
371     protected function after_execute() {
372         global $DB;
374         $conditions = array(
375             'backupid' => $this->get_restoreid(),
376             'itemname' => 'grade_item'//,
377             //'itemid'   => $itemid
378         );
379         $rs = $DB->get_recordset('backup_ids_temp', $conditions);
381         // We need this for calculation magic later on.
382         $mappings = array();
384         if (!empty($rs)) {
385             foreach($rs as $grade_item_backup) {
387                 // Store the oldid with the new id.
388                 $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
390                 $updateobj = new stdclass();
391                 $updateobj->id = $grade_item_backup->newitemid;
393                 //if this is an activity grade item that needs to be put back in its correct category
394                 if (!empty($grade_item_backup->parentitemid)) {
395                     $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
396                     if (!is_null($oldcategoryid)) {
397                         $updateobj->categoryid = $oldcategoryid;
398                         $DB->update_record('grade_items', $updateobj);
399                     }
400                 } else {
401                     //mark course and category items as needing to be recalculated
402                     $updateobj->needsupdate=1;
403                     $DB->update_record('grade_items', $updateobj);
404                 }
405             }
406         }
407         $rs->close();
409         // We need to update the calculations for calculated grade items that may reference old
410         // grade item ids using ##gi\d+##.
411         // $mappings can be empty, use 0 if so (won't match ever)
412         list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
413         $sql = "SELECT gi.id, gi.calculation
414                   FROM {grade_items} gi
415                  WHERE gi.id {$sql} AND
416                        calculation IS NOT NULL";
417         $rs = $DB->get_recordset_sql($sql, $params);
418         foreach ($rs as $gradeitem) {
419             // Collect all of the used grade item id references
420             if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
421                 // This calculation doesn't reference any other grade items... EASY!
422                 continue;
423             }
424             // For this next bit we are going to do the replacement of id's in two steps:
425             // 1. We will replace all old id references with a special mapping reference.
426             // 2. We will replace all mapping references with id's
427             // Why do we do this?
428             // Because there potentially there will be an overlap of ids within the query and we
429             // we substitute the wrong id.. safest way around this is the two step system
430             $calculationmap = array();
431             $mapcount = 0;
432             foreach ($matches[1] as $match) {
433                 // Check that the old id is known to us, if not it was broken to begin with and will
434                 // continue to be broken.
435                 if (!array_key_exists($match, $mappings)) {
436                     continue;
437                 }
438                 // Our special mapping key
439                 $mapping = '##MAPPING'.$mapcount.'##';
440                 // The old id that exists within the calculation now
441                 $oldid = '##gi'.$match.'##';
442                 // The new id that we want to replace the old one with.
443                 $newid = '##gi'.$mappings[$match].'##';
444                 // Replace in the special mapping key
445                 $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
446                 // And record the mapping
447                 $calculationmap[$mapping] = $newid;
448                 $mapcount++;
449             }
450             // Iterate all special mappings for this calculation and replace in the new id's
451             foreach ($calculationmap as $mapping => $newid) {
452                 $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
453             }
454             // Update the calculation now that its being remapped
455             $DB->update_record('grade_items', $gradeitem);
456         }
457         $rs->close();
459         // Need to correct the grade category path and parent
460         $conditions = array(
461             'courseid' => $this->get_courseid()
462         );
464         $rs = $DB->get_recordset('grade_categories', $conditions);
465         // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
466         foreach ($rs as $gc) {
467             if (!empty($gc->parent)) {
468                 $grade_category = new stdClass();
469                 $grade_category->id = $gc->id;
470                 $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
471                 $DB->update_record('grade_categories', $grade_category);
472             }
473         }
474         $rs->close();
476         // Now we can rebuild all the paths
477         $rs = $DB->get_recordset('grade_categories', $conditions);
478         foreach ($rs as $gc) {
479             $grade_category = new stdClass();
480             $grade_category->id = $gc->id;
481             $grade_category->path = grade_category::build_path($gc);
482             $grade_category->depth = substr_count($grade_category->path, '/') - 1;
483             $DB->update_record('grade_categories', $grade_category);
484         }
485         $rs->close();
487         // Check what to do with the minmaxtouse setting.
488         $this->check_minmaxtouse();
490         // Freeze gradebook calculations if needed.
491         $this->gradebook_calculation_freeze();
493         // Ensure the module cache is current when recalculating grades.
494         rebuild_course_cache($this->get_courseid(), true);
496         // Restore marks items as needing update. Update everything now.
497         grade_regrade_final_grades($this->get_courseid());
498     }
500     /**
501      * Freeze gradebook calculation if needed.
502      *
503      * This is similar to various upgrade scripts that check if the freeze is needed.
504      */
505     protected function gradebook_calculation_freeze() {
506         global $CFG;
507         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
508         preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
509         $backupbuild = (int)$matches[1];
510         $backuprelease = $this->get_task()->get_info()->backup_release; // The major version: 2.9, 3.0, 3.10...
512         // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
513         if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
514             require_once($CFG->libdir . '/db/upgradelib.php');
515             upgrade_extra_credit_weightoverride($this->get_courseid());
516         }
517         // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
518         if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
519             require_once($CFG->libdir . '/db/upgradelib.php');
520             upgrade_calculated_grade_items($this->get_courseid());
521         }
522         // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
523         // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
524         // be checked for this problem.
525         if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || version_compare($backuprelease, '2.9', '<='))) {
526             require_once($CFG->libdir . '/db/upgradelib.php');
527             upgrade_course_letter_boundary($this->get_courseid());
528         }
530     }
532     /**
533      * Checks what should happen with the course grade setting minmaxtouse.
534      *
535      * This is related to the upgrade step at the time the setting was added.
536      *
537      * @see MDL-48618
538      * @return void
539      */
540     protected function check_minmaxtouse() {
541         global $CFG, $DB;
542         require_once($CFG->libdir . '/gradelib.php');
544         $userinfo = $this->task->get_setting_value('users');
545         $settingname = 'minmaxtouse';
546         $courseid = $this->get_courseid();
547         $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
548         $version28start = 2014111000.00;
549         $version28last = 2014111006.05;
550         $version29start = 2015051100.00;
551         $version29last = 2015060400.02;
553         $target = $this->get_task()->get_target();
554         if ($minmaxtouse === false &&
555                 ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
556             // The setting was not found because this setting did not exist at the time the backup was made.
557             // And we are not restoring as merge, in which case we leave the course as it was.
558             $version = $this->get_task()->get_info()->moodle_version;
560             if ($version < $version28start) {
561                 // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
562                 if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
563                     grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
564                 }
566             } else if (($version >= $version28start && $version < $version28last) ||
567                     ($version >= $version29start && $version < $version29last)) {
568                 // They should be using grade_grade when the course has inconsistencies.
570                 $sql = "SELECT gi.id
571                           FROM {grade_items} gi
572                           JOIN {grade_grades} gg
573                             ON gg.itemid = gi.id
574                          WHERE gi.courseid = ?
575                            AND (gi.itemtype != ? AND gi.itemtype != ?)
576                            AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
578                 // The course can only have inconsistencies when we restore the user info,
579                 // we do not need to act on existing grades that were not restored as part of this backup.
580                 if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
582                     // Display the notice as we do during upgrade.
583                     set_config('show_min_max_grades_changed_' . $courseid, 1);
585                     if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
586                         // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
587                         // If they are using the site-wide grade_grade setting, we only want to notice them.
588                         grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
589                     }
590                 }
592             } else {
593                 // This should never happen because from now on minmaxtouse is always saved in backups.
594             }
595         }
596     }
598     /**
599      * Rewrite step definition to handle the legacy freeze attribute.
600      *
601      * In previous backups the calculations_freeze property was stored as an attribute of the
602      * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
603      * It only processes definitive children, and their parent attributes.
604      *
605      * We had:
606      *
607      * <gradebook calculations_freeze="20160511">
608      *   <grade_categories>
609      *     <grade_category id="10">
610      *       <depth>1</depth>
611      *       ...
612      *     </grade_category>
613      *   </grade_categories>
614      *   ...
615      * </gradebook>
616      *
617      * And this method will convert it to:
618      *
619      * <gradebook >
620      *   <attributes>
621      *     <calculations_freeze>20160511</calculations_freeze>
622      *   </attributes>
623      *   <grade_categories>
624      *     <grade_category id="10">
625      *       <depth>1</depth>
626      *       ...
627      *     </grade_category>
628      *   </grade_categories>
629      *   ...
630      * </gradebook>
631      *
632      * Note that we cannot just load the XML file in memory as it could potentially be huge.
633      * We can also completely ignore if the node <attributes> is already in the backup
634      * file as it never existed before.
635      *
636      * @param string $filepath The absolute path to the XML file.
637      * @return void
638      */
639     protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
640         $foundnode = false;
641         $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
642         $fr = fopen($filepath, 'r');
643         $fw = fopen($newfile, 'w');
644         if ($fr && $fw) {
645             while (($line = fgets($fr, 4096)) !== false) {
646                 if (!$foundnode && strpos($line, '<gradebook ') === 0) {
647                     $foundnode = true;
648                     $matches = array();
649                     $pattern = '@calculations_freeze=.([0-9]+).@';
650                     if (preg_match($pattern, $line, $matches)) {
651                         $freeze = $matches[1];
652                         $line = preg_replace($pattern, '', $line);
653                         $line .= "  <attributes>\n    <calculations_freeze>$freeze</calculations_freeze>\n  </attributes>\n";
654                     }
655                 }
656                 fputs($fw, $line);
657             }
658             if (!feof($fr)) {
659                 throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
660             }
661             fclose($fr);
662             fclose($fw);
663             if (!rename($newfile, $filepath)) {
664                 throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
665             }
666         } else {
667             if ($fr) {
668                 fclose($fr);
669             }
670             if ($fw) {
671                 fclose($fw);
672             }
673         }
674     }
678 /**
679  * Step in charge of restoring the grade history of a course.
680  *
681  * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
682  * we do not want to restore the history if the gradebook and its content has not been
683  * restored. At least for now.
684  */
685 class restore_grade_history_structure_step extends restore_structure_step {
687      protected function execute_condition() {
688         global $CFG, $DB;
690         if ($this->get_courseid() == SITEID) {
691             return false;
692         }
694         // No gradebook info found, don't execute.
695         $fullpath = $this->task->get_taskbasepath();
696         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
697         if (!file_exists($fullpath)) {
698             return false;
699         }
701         // Some module present in backup file isn't available to restore in this site, don't execute.
702         if ($this->task->is_missing_modules()) {
703             return false;
704         }
706         // Some activity has been excluded to be restored, don't execute.
707         if ($this->task->is_excluding_activities()) {
708             return false;
709         }
711         // There should only be one grade category (the 1 associated with the course itself).
712         $category = new stdclass();
713         $category->courseid  = $this->get_courseid();
714         $catcount = $DB->count_records('grade_categories', (array)$category);
715         if ($catcount > 1) {
716             return false;
717         }
719         // Arrived here, execute the step.
720         return true;
721      }
723     protected function define_structure() {
724         $paths = array();
726         // Settings to use.
727         $userinfo = $this->get_setting_value('users');
728         $history = $this->get_setting_value('grade_histories');
730         if ($userinfo && $history) {
731             $paths[] = new restore_path_element('grade_grade',
732                '/grade_history/grade_grades/grade_grade');
733         }
735         return $paths;
736     }
738     protected function process_grade_grade($data) {
739         global $DB;
741         $data = (object)($data);
742         $olduserid = $data->userid;
743         unset($data->id);
745         $data->userid = $this->get_mappingid('user', $data->userid, null);
746         if (!empty($data->userid)) {
747             // Do not apply the date offsets as this is history.
748             $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
749             $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
750             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
751             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
752             $DB->insert_record('grade_grades_history', $data);
753         } else {
754             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
755             $this->log($message, backup::LOG_DEBUG);
756         }
757     }
761 /**
762  * decode all the interlinks present in restored content
763  * relying 100% in the restore_decode_processor that handles
764  * both the contents to modify and the rules to be applied
765  */
766 class restore_decode_interlinks extends restore_execution_step {
768     protected function define_execution() {
769         // Get the decoder (from the plan)
770         $decoder = $this->task->get_decoder();
771         restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
772         // And launch it, everything will be processed
773         $decoder->execute();
774     }
777 /**
778  * first, ensure that we have no gaps in section numbers
779  * and then, rebuid the course cache
780  */
781 class restore_rebuild_course_cache extends restore_execution_step {
783     protected function define_execution() {
784         global $DB;
786         // Although there is some sort of auto-recovery of missing sections
787         // present in course/formats... here we check that all the sections
788         // from 0 to MAX(section->section) exist, creating them if necessary
789         $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
790         // Iterate over all sections
791         for ($i = 0; $i <= $maxsection; $i++) {
792             // If the section $i doesn't exist, create it
793             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
794                 $sectionrec = array(
795                     'course' => $this->get_courseid(),
796                     'section' => $i,
797                     'timemodified' => time());
798                 $DB->insert_record('course_sections', $sectionrec); // missing section created
799             }
800         }
802         // Rebuild cache now that all sections are in place
803         rebuild_course_cache($this->get_courseid());
804         cache_helper::purge_by_event('changesincourse');
805         cache_helper::purge_by_event('changesincoursecat');
806     }
809 /**
810  * Review all the tasks having one after_restore method
811  * executing it to perform some final adjustments of information
812  * not available when the task was executed.
813  */
814 class restore_execute_after_restore extends restore_execution_step {
816     protected function define_execution() {
818         // Simply call to the execute_after_restore() method of the task
819         // that always is the restore_final_task
820         $this->task->launch_execute_after_restore();
821     }
825 /**
826  * Review all the (pending) block positions in backup_ids, matching by
827  * contextid, creating positions as needed. This is executed by the
828  * final task, once all the contexts have been created
829  */
830 class restore_review_pending_block_positions extends restore_execution_step {
832     protected function define_execution() {
833         global $DB;
835         // Get all the block_position objects pending to match
836         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
837         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
838         // Process block positions, creating them or accumulating for final step
839         foreach($rs as $posrec) {
840             // Get the complete position object out of the info field.
841             $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
842             // If position is for one already mapped (known) contextid
843             // process it now, creating the position, else nothing to
844             // do, position finally discarded
845             if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
846                 $position->contextid = $newctx->newitemid;
847                 // Create the block position
848                 $DB->insert_record('block_positions', $position);
849             }
850         }
851         $rs->close();
852     }
856 /**
857  * Updates the availability data for course modules and sections.
858  *
859  * Runs after the restore of all course modules, sections, and grade items has
860  * completed. This is necessary in order to update IDs that have changed during
861  * restore.
862  *
863  * @package core_backup
864  * @copyright 2014 The Open University
865  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
866  */
867 class restore_update_availability extends restore_execution_step {
869     protected function define_execution() {
870         global $CFG, $DB;
872         // Note: This code runs even if availability is disabled when restoring.
873         // That will ensure that if you later turn availability on for the site,
874         // there will be no incorrect IDs. (It doesn't take long if the restored
875         // data does not contain any availability information.)
877         // Get modinfo with all data after resetting cache.
878         rebuild_course_cache($this->get_courseid(), true);
879         $modinfo = get_fast_modinfo($this->get_courseid());
881         // Get the date offset for this restore.
882         $dateoffset = $this->apply_date_offset(1) - 1;
884         // Update all sections that were restored.
885         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
886         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
887         $sectionsbyid = null;
888         foreach ($rs as $rec) {
889             if (is_null($sectionsbyid)) {
890                 $sectionsbyid = array();
891                 foreach ($modinfo->get_section_info_all() as $section) {
892                     $sectionsbyid[$section->id] = $section;
893                 }
894             }
895             if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
896                 // If the section was not fully restored for some reason
897                 // (e.g. due to an earlier error), skip it.
898                 $this->get_logger()->process('Section not fully restored: id ' .
899                         $rec->newitemid, backup::LOG_WARNING);
900                 continue;
901             }
902             $section = $sectionsbyid[$rec->newitemid];
903             if (!is_null($section->availability)) {
904                 $info = new \core_availability\info_section($section);
905                 $info->update_after_restore($this->get_restoreid(),
906                         $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
907             }
908         }
909         $rs->close();
911         // Update all modules that were restored.
912         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
913         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
914         foreach ($rs as $rec) {
915             if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
916                 // If the module was not fully restored for some reason
917                 // (e.g. due to an earlier error), skip it.
918                 $this->get_logger()->process('Module not fully restored: id ' .
919                         $rec->newitemid, backup::LOG_WARNING);
920                 continue;
921             }
922             $cm = $modinfo->get_cm($rec->newitemid);
923             if (!is_null($cm->availability)) {
924                 $info = new \core_availability\info_module($cm);
925                 $info->update_after_restore($this->get_restoreid(),
926                         $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
927             }
928         }
929         $rs->close();
930     }
934 /**
935  * Process legacy module availability records in backup_ids.
936  *
937  * Matches course modules and grade item id once all them have been already restored.
938  * Only if all matchings are satisfied the availability condition will be created.
939  * At the same time, it is required for the site to have that functionality enabled.
940  *
941  * This step is included only to handle legacy backups (2.6 and before). It does not
942  * do anything for newer backups.
943  *
944  * @copyright 2014 The Open University
945  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
946  */
947 class restore_process_course_modules_availability extends restore_execution_step {
949     protected function define_execution() {
950         global $CFG, $DB;
952         // Site hasn't availability enabled
953         if (empty($CFG->enableavailability)) {
954             return;
955         }
957         // Do both modules and sections.
958         foreach (array('module', 'section') as $table) {
959             // Get all the availability objects to process.
960             $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
961             $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
962             // Process availabilities, creating them if everything matches ok.
963             foreach ($rs as $availrec) {
964                 $allmatchesok = true;
965                 // Get the complete legacy availability object.
966                 $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
968                 // Note: This code used to update IDs, but that is now handled by the
969                 // current code (after restore) instead of this legacy code.
971                 // Get showavailability option.
972                 $thingid = ($table === 'module') ? $availability->coursemoduleid :
973                         $availability->coursesectionid;
974                 $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
975                         $table . '_showavailability', $thingid);
976                 if (!$showrec) {
977                     // Should not happen.
978                     throw new coding_exception('No matching showavailability record');
979                 }
980                 $show = $showrec->info->showavailability;
982                 // The $availability object is now in the format used in the old
983                 // system. Interpret this and convert to new system.
984                 $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
985                         array('id' => $thingid), MUST_EXIST);
986                 $newvalue = \core_availability\info::add_legacy_availability_condition(
987                         $currentvalue, $availability, $show);
988                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
989                         array('id' => $thingid));
990             }
991             $rs->close();
992         }
993     }
997 /*
998  * Execution step that, *conditionally* (if there isn't preloaded information)
999  * will load the inforef files for all the included course/section/activity tasks
1000  * to backup_temp_ids. They will be stored with "xxxxref" as itemname
1001  */
1002 class restore_load_included_inforef_records extends restore_execution_step {
1004     protected function define_execution() {
1006         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1007             return;
1008         }
1010         // Get all the included tasks
1011         $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
1012         $progress = $this->task->get_progress();
1013         $progress->start_progress($this->get_name(), count($tasks));
1014         foreach ($tasks as $task) {
1015             // Load the inforef.xml file if exists
1016             $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
1017             if (file_exists($inforefpath)) {
1018                 // Load each inforef file to temp_ids.
1019                 restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
1020             }
1021         }
1022         $progress->end_progress();
1023     }
1026 /*
1027  * Execution step that will load all the needed files into backup_files_temp
1028  *   - info: contains the whole original object (times, names...)
1029  * (all them being original ids as loaded from xml)
1030  */
1031 class restore_load_included_files extends restore_structure_step {
1033     protected function define_structure() {
1035         $file = new restore_path_element('file', '/files/file');
1037         return array($file);
1038     }
1040     /**
1041      * Process one <file> element from files.xml
1042      *
1043      * @param array $data the element data
1044      */
1045     public function process_file($data) {
1047         $data = (object)$data; // handy
1049         // load it if needed:
1050         //   - it it is one of the annotated inforef files (course/section/activity/block)
1051         //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
1052         // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
1053         //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
1054         $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
1055         $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
1056                         $data->component == 'grouping' || $data->component == 'grade' ||
1057                         $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
1058         if ($isfileref || $iscomponent) {
1059             restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
1060         }
1061     }
1064 /**
1065  * Execution step that, *conditionally* (if there isn't preloaded information),
1066  * will load all the needed roles to backup_temp_ids. They will be stored with
1067  * "role" itemname. Also it will perform one automatic mapping to roles existing
1068  * in the target site, based in permissions of the user performing the restore,
1069  * archetypes and other bits. At the end, each original role will have its associated
1070  * target role or 0 if it's going to be skipped. Note we wrap everything over one
1071  * restore_dbops method, as far as the same stuff is going to be also executed
1072  * by restore prechecks
1073  */
1074 class restore_load_and_map_roles extends restore_execution_step {
1076     protected function define_execution() {
1077         if ($this->task->get_preloaded_information()) { // if info is already preloaded
1078             return;
1079         }
1081         $file = $this->get_basepath() . '/roles.xml';
1082         // Load needed toles to temp_ids
1083         restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
1085         // Process roles, mapping/skipping. Any error throws exception
1086         // Note we pass controller's info because it can contain role mapping information
1087         // about manual mappings performed by UI
1088         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);
1089     }
1092 /**
1093  * Execution step that, *conditionally* (if there isn't preloaded information
1094  * and users have been selected in settings, will load all the needed users
1095  * to backup_temp_ids. They will be stored with "user" itemname and with
1096  * their original contextid as paremitemid
1097  */
1098 class restore_load_included_users extends restore_execution_step {
1100     protected function define_execution() {
1102         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1103             return;
1104         }
1105         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1106             return;
1107         }
1108         $file = $this->get_basepath() . '/users.xml';
1109         // Load needed users to temp_ids.
1110         restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
1111     }
1114 /**
1115  * Execution step that, *conditionally* (if there isn't preloaded information
1116  * and users have been selected in settings, will process all the needed users
1117  * in order to decide and perform any action with them (create / map / error)
1118  * Note: Any error will cause exception, as far as this is the same processing
1119  * than the one into restore prechecks (that should have stopped process earlier)
1120  */
1121 class restore_process_included_users extends restore_execution_step {
1123     protected function define_execution() {
1125         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1126             return;
1127         }
1128         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1129             return;
1130         }
1131         restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
1132                 $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
1133     }
1136 /**
1137  * Execution step that will create all the needed users as calculated
1138  * by @restore_process_included_users (those having newiteind = 0)
1139  */
1140 class restore_create_included_users extends restore_execution_step {
1142     protected function define_execution() {
1144         restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
1145                 $this->task->get_userid(), $this->task->get_progress());
1146     }
1149 /**
1150  * Structure step that will create all the needed groups and groupings
1151  * by loading them from the groups.xml file performing the required matches.
1152  * Note group members only will be added if restoring user info
1153  */
1154 class restore_groups_structure_step extends restore_structure_step {
1156     protected function define_structure() {
1158         $paths = array(); // Add paths here
1160         // Do not include group/groupings information if not requested.
1161         $groupinfo = $this->get_setting_value('groups');
1162         if ($groupinfo) {
1163             $paths[] = new restore_path_element('group', '/groups/group');
1164             $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
1165             $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
1166         }
1167         return $paths;
1168     }
1170     // Processing functions go here
1171     public function process_group($data) {
1172         global $DB;
1174         $data = (object)$data; // handy
1175         $data->courseid = $this->get_courseid();
1177         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1178         // another a group in the same course
1179         $context = context_course::instance($data->courseid);
1180         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1181             if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
1182                 unset($data->idnumber);
1183             }
1184         } else {
1185             unset($data->idnumber);
1186         }
1188         $oldid = $data->id;    // need this saved for later
1190         $restorefiles = false; // Only if we end creating the group
1192         // This is for backwards compatibility with old backups. If the backup data for a group contains a non-empty value of
1193         // hidepicture, then we'll exclude this group's picture from being restored.
1194         if (!empty($data->hidepicture)) {
1195             // Exclude the group picture from being restored if hidepicture is set to 1 in the backup data.
1196             unset($data->picture);
1197         }
1199         // Search if the group already exists (by name & description) in the target course
1200         $description_clause = '';
1201         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1202         if (!empty($data->description)) {
1203             $description_clause = ' AND ' .
1204                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1205            $params['description'] = $data->description;
1206         }
1207         if (!$groupdb = $DB->get_record_sql("SELECT *
1208                                                FROM {groups}
1209                                               WHERE courseid = :courseid
1210                                                 AND name = :grname $description_clause", $params)) {
1211             // group doesn't exist, create
1212             $newitemid = $DB->insert_record('groups', $data);
1213             $restorefiles = true; // We'll restore the files
1214         } else {
1215             // group exists, use it
1216             $newitemid = $groupdb->id;
1217         }
1218         // Save the id mapping
1219         $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
1221         // Add the related group picture file if it's available at this point.
1222         if (!empty($data->picture)) {
1223             $this->add_related_files('group', 'icon', 'group', null, $oldid);
1224         }
1226         // Invalidate the course group data cache just in case.
1227         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1228     }
1230     public function process_grouping($data) {
1231         global $DB;
1233         $data = (object)$data; // handy
1234         $data->courseid = $this->get_courseid();
1236         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1237         // another a grouping in the same course
1238         $context = context_course::instance($data->courseid);
1239         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1240             if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
1241                 unset($data->idnumber);
1242             }
1243         } else {
1244             unset($data->idnumber);
1245         }
1247         $oldid = $data->id;    // need this saved for later
1248         $restorefiles = false; // Only if we end creating the grouping
1250         // Search if the grouping already exists (by name & description) in the target course
1251         $description_clause = '';
1252         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1253         if (!empty($data->description)) {
1254             $description_clause = ' AND ' .
1255                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1256            $params['description'] = $data->description;
1257         }
1258         if (!$groupingdb = $DB->get_record_sql("SELECT *
1259                                                   FROM {groupings}
1260                                                  WHERE courseid = :courseid
1261                                                    AND name = :grname $description_clause", $params)) {
1262             // grouping doesn't exist, create
1263             $newitemid = $DB->insert_record('groupings', $data);
1264             $restorefiles = true; // We'll restore the files
1265         } else {
1266             // grouping exists, use it
1267             $newitemid = $groupingdb->id;
1268         }
1269         // Save the id mapping
1270         $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1271         // Invalidate the course group data cache just in case.
1272         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1273     }
1275     public function process_grouping_group($data) {
1276         global $CFG;
1278         require_once($CFG->dirroot.'/group/lib.php');
1280         $data = (object)$data;
1281         groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1282     }
1284     protected function after_execute() {
1285         // Add group related files, matching with "group" mappings.
1286         $this->add_related_files('group', 'description', 'group');
1287         // Add grouping related files, matching with "grouping" mappings
1288         $this->add_related_files('grouping', 'description', 'grouping');
1289         // Invalidate the course group data.
1290         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1291     }
1295 /**
1296  * Structure step that will create all the needed group memberships
1297  * by loading them from the groups.xml file performing the required matches.
1298  */
1299 class restore_groups_members_structure_step extends restore_structure_step {
1301     protected $plugins = null;
1303     protected function define_structure() {
1305         $paths = array(); // Add paths here
1307         if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
1308             $paths[] = new restore_path_element('group', '/groups/group');
1309             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1310         }
1312         return $paths;
1313     }
1315     public function process_group($data) {
1316         $data = (object)$data; // handy
1318         // HACK ALERT!
1319         // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1320         // Let's fake internal state to make $this->get_new_parentid('group') work.
1322         $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1323     }
1325     public function process_member($data) {
1326         global $DB, $CFG;
1327         require_once("$CFG->dirroot/group/lib.php");
1329         // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1331         $data = (object)$data; // handy
1333         // get parent group->id
1334         $data->groupid = $this->get_new_parentid('group');
1336         // map user newitemid and insert if not member already
1337         if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1338             if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1339                 // Check the component, if any, exists.
1340                 if (empty($data->component)) {
1341                     groups_add_member($data->groupid, $data->userid);
1343                 } else if ((strpos($data->component, 'enrol_') === 0)) {
1344                     // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1345                     // it is possible that enrolment was restored using different plugin type.
1346                     if (!isset($this->plugins)) {
1347                         $this->plugins = enrol_get_plugins(true);
1348                     }
1349                     if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1350                         if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1351                             if (isset($this->plugins[$instance->enrol])) {
1352                                 $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1353                             }
1354                         }
1355                     }
1357                 } else {
1358                     $dir = core_component::get_component_directory($data->component);
1359                     if ($dir and is_dir($dir)) {
1360                         if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1361                             return;
1362                         }
1363                     }
1364                     // Bad luck, plugin could not restore the data, let's add normal membership.
1365                     groups_add_member($data->groupid, $data->userid);
1366                     $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1367                     $this->log($message, backup::LOG_WARNING);
1368                 }
1369             }
1370         }
1371     }
1374 /**
1375  * Structure step that will create all the needed scales
1376  * by loading them from the scales.xml
1377  */
1378 class restore_scales_structure_step extends restore_structure_step {
1380     protected function define_structure() {
1382         $paths = array(); // Add paths here
1383         $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1384         return $paths;
1385     }
1387     protected function process_scale($data) {
1388         global $DB;
1390         $data = (object)$data;
1392         $restorefiles = false; // Only if we end creating the group
1394         $oldid = $data->id;    // need this saved for later
1396         // Look for scale (by 'scale' both in standard (course=0) and current course
1397         // with priority to standard scales (ORDER clause)
1398         // scale is not course unique, use get_record_sql to suppress warning
1399         // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1400         $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1401         $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1402         if (!$scadb = $DB->get_record_sql("SELECT *
1403                                             FROM {scale}
1404                                            WHERE courseid IN (0, :courseid)
1405                                              AND $compare_scale_clause
1406                                         ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1407             // Remap the user if possible, defaut to user performing the restore if not
1408             $userid = $this->get_mappingid('user', $data->userid);
1409             $data->userid = $userid ? $userid : $this->task->get_userid();
1410             // Remap the course if course scale
1411             $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1412             // If global scale (course=0), check the user has perms to create it
1413             // falling to course scale if not
1414             $systemctx = context_system::instance();
1415             if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1416                 $data->courseid = $this->get_courseid();
1417             }
1418             // scale doesn't exist, create
1419             $newitemid = $DB->insert_record('scale', $data);
1420             $restorefiles = true; // We'll restore the files
1421         } else {
1422             // scale exists, use it
1423             $newitemid = $scadb->id;
1424         }
1425         // Save the id mapping (with files support at system context)
1426         $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1427     }
1429     protected function after_execute() {
1430         // Add scales related files, matching with "scale" mappings
1431         $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1432     }
1436 /**
1437  * Structure step that will create all the needed outocomes
1438  * by loading them from the outcomes.xml
1439  */
1440 class restore_outcomes_structure_step extends restore_structure_step {
1442     protected function define_structure() {
1444         $paths = array(); // Add paths here
1445         $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1446         return $paths;
1447     }
1449     protected function process_outcome($data) {
1450         global $DB;
1452         $data = (object)$data;
1454         $restorefiles = false; // Only if we end creating the group
1456         $oldid = $data->id;    // need this saved for later
1458         // Look for outcome (by shortname both in standard (courseid=null) and current course
1459         // with priority to standard outcomes (ORDER clause)
1460         // outcome is not course unique, use get_record_sql to suppress warning
1461         $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1462         if (!$outdb = $DB->get_record_sql('SELECT *
1463                                              FROM {grade_outcomes}
1464                                             WHERE shortname = :shortname
1465                                               AND (courseid = :courseid OR courseid IS NULL)
1466                                          ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1467             // Remap the user
1468             $userid = $this->get_mappingid('user', $data->usermodified);
1469             $data->usermodified = $userid ? $userid : $this->task->get_userid();
1470             // Remap the scale
1471             $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1472             // Remap the course if course outcome
1473             $data->courseid = $data->courseid ? $this->get_courseid() : null;
1474             // If global outcome (course=null), check the user has perms to create it
1475             // falling to course outcome if not
1476             $systemctx = context_system::instance();
1477             if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1478                 $data->courseid = $this->get_courseid();
1479             }
1480             // outcome doesn't exist, create
1481             $newitemid = $DB->insert_record('grade_outcomes', $data);
1482             $restorefiles = true; // We'll restore the files
1483         } else {
1484             // scale exists, use it
1485             $newitemid = $outdb->id;
1486         }
1487         // Set the corresponding grade_outcomes_courses record
1488         $outcourserec = new stdclass();
1489         $outcourserec->courseid  = $this->get_courseid();
1490         $outcourserec->outcomeid = $newitemid;
1491         if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1492             $DB->insert_record('grade_outcomes_courses', $outcourserec);
1493         }
1494         // Save the id mapping (with files support at system context)
1495         $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1496     }
1498     protected function after_execute() {
1499         // Add outcomes related files, matching with "outcome" mappings
1500         $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1501     }
1504 /**
1505  * Execution step that, *conditionally* (if there isn't preloaded information
1506  * will load all the question categories and questions (header info only)
1507  * to backup_temp_ids. They will be stored with "question_category" and
1508  * "question" itemnames and with their original contextid and question category
1509  * id as paremitemids
1510  */
1511 class restore_load_categories_and_questions extends restore_execution_step {
1513     protected function define_execution() {
1515         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1516             return;
1517         }
1518         $file = $this->get_basepath() . '/questions.xml';
1519         restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1520     }
1523 /**
1524  * Execution step that, *conditionally* (if there isn't preloaded information)
1525  * will process all the needed categories and questions
1526  * in order to decide and perform any action with them (create / map / error)
1527  * Note: Any error will cause exception, as far as this is the same processing
1528  * than the one into restore prechecks (that should have stopped process earlier)
1529  */
1530 class restore_process_categories_and_questions extends restore_execution_step {
1532     protected function define_execution() {
1534         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1535             return;
1536         }
1537         restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1538     }
1541 /**
1542  * Structure step that will read the section.xml creating/updating sections
1543  * as needed, rebuilding course cache and other friends
1544  */
1545 class restore_section_structure_step extends restore_structure_step {
1546     /** @var array Cache: Array of id => course format */
1547     private static $courseformats = array();
1549     /**
1550      * Resets a static cache of course formats. Required for unit testing.
1551      */
1552     public static function reset_caches() {
1553         self::$courseformats = array();
1554     }
1556     protected function define_structure() {
1557         global $CFG;
1559         $paths = array();
1561         $section = new restore_path_element('section', '/section');
1562         $paths[] = $section;
1563         if ($CFG->enableavailability) {
1564             $paths[] = new restore_path_element('availability', '/section/availability');
1565             $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1566         }
1567         $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1569         // Apply for 'format' plugins optional paths at section level
1570         $this->add_plugin_structure('format', $section);
1572         // Apply for 'local' plugins optional paths at section level
1573         $this->add_plugin_structure('local', $section);
1575         return $paths;
1576     }
1578     public function process_section($data) {
1579         global $CFG, $DB;
1580         $data = (object)$data;
1581         $oldid = $data->id; // We'll need this later
1583         $restorefiles = false;
1585         // Look for the section
1586         $section = new stdclass();
1587         $section->course  = $this->get_courseid();
1588         $section->section = $data->number;
1589         $section->timemodified = $data->timemodified ?? 0;
1590         // Section doesn't exist, create it with all the info from backup
1591         if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
1592             $section->name = $data->name;
1593             $section->summary = $data->summary;
1594             $section->summaryformat = $data->summaryformat;
1595             $section->sequence = '';
1596             $section->visible = $data->visible;
1597             if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1598                 $section->availability = null;
1599             } else {
1600                 $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1601                 // Include legacy [<2.7] availability data if provided.
1602                 if (is_null($section->availability)) {
1603                     $section->availability = \core_availability\info::convert_legacy_fields(
1604                             $data, true);
1605                 }
1606             }
1607             $newitemid = $DB->insert_record('course_sections', $section);
1608             $section->id = $newitemid;
1610             core\event\course_section_created::create_from_section($section)->trigger();
1612             $restorefiles = true;
1614         // Section exists, update non-empty information
1615         } else {
1616             $section->id = $secrec->id;
1617             if ((string)$secrec->name === '') {
1618                 $section->name = $data->name;
1619             }
1620             if (empty($secrec->summary)) {
1621                 $section->summary = $data->summary;
1622                 $section->summaryformat = $data->summaryformat;
1623                 $restorefiles = true;
1624             }
1626             // Don't update availability (I didn't see a useful way to define
1627             // whether existing or new one should take precedence).
1629             $DB->update_record('course_sections', $section);
1630             $newitemid = $secrec->id;
1632             // Trigger an event for course section update.
1633             $event = \core\event\course_section_updated::create(
1634                 array(
1635                     'objectid' => $section->id,
1636                     'courseid' => $section->course,
1637                     'context' => context_course::instance($section->course),
1638                     'other' => array('sectionnum' => $section->section)
1639                 )
1640             );
1641             $event->trigger();
1642         }
1644         // Annotate the section mapping, with restorefiles option if needed
1645         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1647         // set the new course_section id in the task
1648         $this->task->set_sectionid($newitemid);
1650         // If there is the legacy showavailability data, store this for later use.
1651         // (This data is not present when restoring 'new' backups.)
1652         if (isset($data->showavailability)) {
1653             // Cache the showavailability flag using the backup_ids data field.
1654             restore_dbops::set_backup_ids_record($this->get_restoreid(),
1655                     'section_showavailability', $newitemid, 0, null,
1656                     (object)array('showavailability' => $data->showavailability));
1657         }
1659         // Commented out. We never modify course->numsections as far as that is used
1660         // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1661         // Note: We keep the code here, to know about and because of the possibility of making this
1662         // optional based on some setting/attribute in the future
1663         // If needed, adjust course->numsections
1664         //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1665         //    if ($numsections < $section->section) {
1666         //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1667         //    }
1668         //}
1669     }
1671     /**
1672      * Process the legacy availability table record. This table does not exist
1673      * in Moodle 2.7+ but we still support restore.
1674      *
1675      * @param stdClass $data Record data
1676      */
1677     public function process_availability($data) {
1678         $data = (object)$data;
1679         // Simply going to store the whole availability record now, we'll process
1680         // all them later in the final task (once all activities have been restored)
1681         // Let's call the low level one to be able to store the whole object.
1682         $data->coursesectionid = $this->task->get_sectionid();
1683         restore_dbops::set_backup_ids_record($this->get_restoreid(),
1684                 'section_availability', $data->id, 0, null, $data);
1685     }
1687     /**
1688      * Process the legacy availability fields table record. This table does not
1689      * exist in Moodle 2.7+ but we still support restore.
1690      *
1691      * @param stdClass $data Record data
1692      */
1693     public function process_availability_field($data) {
1694         global $DB;
1695         $data = (object)$data;
1696         // Mark it is as passed by default
1697         $passed = true;
1698         $customfieldid = null;
1700         // If a customfield has been used in order to pass we must be able to match an existing
1701         // customfield by name (data->customfield) and type (data->customfieldtype)
1702         if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1703             // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1704             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1705             $passed = false;
1706         } else if (!is_null($data->customfield)) {
1707             $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1708             $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1709             $passed = ($customfieldid !== false);
1710         }
1712         if ($passed) {
1713             // Create the object to insert into the database
1714             $availfield = new stdClass();
1715             $availfield->coursesectionid = $this->task->get_sectionid();
1716             $availfield->userfield = $data->userfield;
1717             $availfield->customfieldid = $customfieldid;
1718             $availfield->operator = $data->operator;
1719             $availfield->value = $data->value;
1721             // Get showavailability option.
1722             $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1723                     'section_showavailability', $availfield->coursesectionid);
1724             if (!$showrec) {
1725                 // Should not happen.
1726                 throw new coding_exception('No matching showavailability record');
1727             }
1728             $show = $showrec->info->showavailability;
1730             // The $availfield object is now in the format used in the old
1731             // system. Interpret this and convert to new system.
1732             $currentvalue = $DB->get_field('course_sections', 'availability',
1733                     array('id' => $availfield->coursesectionid), MUST_EXIST);
1734             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1735                     $currentvalue, $availfield, $show);
1737             $section = new stdClass();
1738             $section->id = $availfield->coursesectionid;
1739             $section->availability = $newvalue;
1740             $section->timemodified = time();
1741             $DB->update_record('course_sections', $section);
1742         }
1743     }
1745     public function process_course_format_options($data) {
1746         global $DB;
1747         $courseid = $this->get_courseid();
1748         if (!array_key_exists($courseid, self::$courseformats)) {
1749             // It is safe to have a static cache of course formats because format can not be changed after this point.
1750             self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
1751         }
1752         $data = (array)$data;
1753         if (self::$courseformats[$courseid] === $data['format']) {
1754             // Import section format options only if both courses (the one that was backed up
1755             // and the one we are restoring into) have same formats.
1756             $params = array(
1757                 'courseid' => $this->get_courseid(),
1758                 'sectionid' => $this->task->get_sectionid(),
1759                 'format' => $data['format'],
1760                 'name' => $data['name']
1761             );
1762             if ($record = $DB->get_record('course_format_options', $params, 'id, value')) {
1763                 // Do not overwrite existing information.
1764                 $newid = $record->id;
1765             } else {
1766                 $params['value'] = $data['value'];
1767                 $newid = $DB->insert_record('course_format_options', $params);
1768             }
1769             $this->set_mapping('course_format_options', $data['id'], $newid);
1770         }
1771     }
1773     protected function after_execute() {
1774         // Add section related files, with 'course_section' itemid to match
1775         $this->add_related_files('course', 'section', 'course_section');
1776     }
1779 /**
1780  * Structure step that will read the course.xml file, loading it and performing
1781  * various actions depending of the site/restore settings. Note that target
1782  * course always exist before arriving here so this step will be updating
1783  * the course record (never inserting)
1784  */
1785 class restore_course_structure_step extends restore_structure_step {
1786     /**
1787      * @var bool this gets set to true by {@link process_course()} if we are
1788      * restoring an old coures that used the legacy 'module security' feature.
1789      * If so, we have to do more work in {@link after_execute()}.
1790      */
1791     protected $legacyrestrictmodules = false;
1793     /**
1794      * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1795      * array with array keys the module names ('forum', 'quiz', etc.). These are
1796      * the modules that are allowed according to the data in the backup file.
1797      * In {@link after_execute()} we then have to prevent adding of all the other
1798      * types of activity.
1799      */
1800     protected $legacyallowedmodules = array();
1802     protected function define_structure() {
1804         $course = new restore_path_element('course', '/course');
1805         $category = new restore_path_element('category', '/course/category');
1806         $tag = new restore_path_element('tag', '/course/tags/tag');
1807         $customfield = new restore_path_element('customfield', '/course/customfields/customfield');
1808         $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1810         // Apply for 'format' plugins optional paths at course level
1811         $this->add_plugin_structure('format', $course);
1813         // Apply for 'theme' plugins optional paths at course level
1814         $this->add_plugin_structure('theme', $course);
1816         // Apply for 'report' plugins optional paths at course level
1817         $this->add_plugin_structure('report', $course);
1819         // Apply for 'course report' plugins optional paths at course level
1820         $this->add_plugin_structure('coursereport', $course);
1822         // Apply for plagiarism plugins optional paths at course level
1823         $this->add_plugin_structure('plagiarism', $course);
1825         // Apply for local plugins optional paths at course level
1826         $this->add_plugin_structure('local', $course);
1828         // Apply for admin tool plugins optional paths at course level.
1829         $this->add_plugin_structure('tool', $course);
1831         return array($course, $category, $tag, $customfield, $allowed_module);
1832     }
1834     /**
1835      * Processing functions go here
1836      *
1837      * @global moodledatabase $DB
1838      * @param stdClass $data
1839      */
1840     public function process_course($data) {
1841         global $CFG, $DB;
1842         $context = context::instance_by_id($this->task->get_contextid());
1843         $userid = $this->task->get_userid();
1844         $target = $this->get_task()->get_target();
1845         $isnewcourse = $target == backup::TARGET_NEW_COURSE;
1847         // When restoring to a new course we can set all the things except for the ID number.
1848         $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid);
1849         $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid);
1850         $canforcelanguage = has_capability('moodle/course:setforcedlanguage', $context, $userid);
1852         $data = (object)$data;
1853         $data->id = $this->get_courseid();
1855         // Calculate final course names, to avoid dupes.
1856         $fullname  = $this->get_setting_value('course_fullname');
1857         $shortname = $this->get_setting_value('course_shortname');
1858         list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names($this->get_courseid(),
1859             $fullname === false ? $data->fullname : $fullname,
1860             $shortname === false ? $data->shortname : $shortname);
1861         // Do not modify the course names at all when merging and user selected to keep the names (or prohibited by cap).
1862         if (!$isnewcourse && $fullname === false) {
1863             unset($data->fullname);
1864         }
1865         if (!$isnewcourse && $shortname === false) {
1866             unset($data->shortname);
1867         }
1869         // Unset summary if user can't change it.
1870         if (!$canchangesummary) {
1871             unset($data->summary);
1872             unset($data->summaryformat);
1873         }
1875         // Unset lang if user can't change it.
1876         if (!$canforcelanguage) {
1877             unset($data->lang);
1878         }
1880         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1881         // another course on this site.
1882         if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite()
1883                 && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1884             // Do not reset idnumber.
1886         } else if (!$isnewcourse) {
1887             // Prevent override when restoring as merge.
1888             unset($data->idnumber);
1890         } else {
1891             $data->idnumber = '';
1892         }
1894         // If we restore a course from this site, let's capture the original course id.
1895         if ($isnewcourse && $this->get_task()->is_samesite()) {
1896             $data->originalcourseid = $this->get_task()->get_old_courseid();
1897         }
1899         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1900         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1901         if (empty($data->hiddensections)) {
1902             $data->hiddensections = 0;
1903         }
1905         // Set legacyrestrictmodules to true if the course was resticting modules. If so
1906         // then we will need to process restricted modules after execution.
1907         $this->legacyrestrictmodules = !empty($data->restrictmodules);
1909         $data->startdate= $this->apply_date_offset($data->startdate);
1910         if (isset($data->enddate)) {
1911             $data->enddate = $this->apply_date_offset($data->enddate);
1912         }
1914         if ($data->defaultgroupingid) {
1915             $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1916         }
1917         if (empty($CFG->enablecompletion)) {
1918             $data->enablecompletion = 0;
1919             $data->completionstartonenrol = 0;
1920             $data->completionnotify = 0;
1921         }
1922         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1923         if (isset($data->lang) && !array_key_exists($data->lang, $languages)) {
1924             $data->lang = '';
1925         }
1927         $themes = get_list_of_themes(); // Get themes for quick search later
1928         if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1929             $data->theme = '';
1930         }
1932         // Check if this is an old SCORM course format.
1933         if ($data->format == 'scorm') {
1934             $data->format = 'singleactivity';
1935             $data->activitytype = 'scorm';
1936         }
1938         // Course record ready, update it
1939         $DB->update_record('course', $data);
1941         course_get_format($data)->update_course_format_options($data);
1943         // Role name aliases
1944         restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1945     }
1947     public function process_category($data) {
1948         // Nothing to do with the category. UI sets it before restore starts
1949     }
1951     public function process_tag($data) {
1952         global $CFG, $DB;
1954         $data = (object)$data;
1956         core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(),
1957                 context_course::instance($this->get_courseid()), $data->rawname);
1958     }
1960     /**
1961      * Process custom fields
1962      *
1963      * @param array $data
1964      */
1965     public function process_customfield($data) {
1966         $handler = core_course\customfield\course_handler::create();
1967         $handler->restore_instance_data_from_backup($this->task, $data);
1968     }
1970     public function process_allowed_module($data) {
1971         $data = (object)$data;
1973         // Backwards compatiblity support for the data that used to be in the
1974         // course_allowed_modules table.
1975         if ($this->legacyrestrictmodules) {
1976             $this->legacyallowedmodules[$data->modulename] = 1;
1977         }
1978     }
1980     protected function after_execute() {
1981         global $DB;
1983         // Add course related files, without itemid to match
1984         $this->add_related_files('course', 'summary', null);
1985         $this->add_related_files('course', 'overviewfiles', null);
1987         // Deal with legacy allowed modules.
1988         if ($this->legacyrestrictmodules) {
1989             $context = context_course::instance($this->get_courseid());
1991             list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1992             list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1993             foreach ($managerroleids as $roleid) {
1994                 unset($roleids[$roleid]);
1995             }
1997             foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1998                 if (isset($this->legacyallowedmodules[$modname])) {
1999                     // Module is allowed, no worries.
2000                     continue;
2001                 }
2003                 $capability = 'mod/' . $modname . ':addinstance';
2005                 if (!get_capability_info($capability)) {
2006                     $this->log("Capability '{$capability}' was not found!", backup::LOG_WARNING);
2007                     continue;
2008                 }
2010                 foreach ($roleids as $roleid) {
2011                     assign_capability($capability, CAP_PREVENT, $roleid, $context);
2012                 }
2013             }
2014         }
2015     }
2018 /**
2019  * Execution step that will migrate legacy files if present.
2020  */
2021 class restore_course_legacy_files_step extends restore_execution_step {
2022     public function define_execution() {
2023         global $DB;
2025         // Do a check for legacy files and skip if there are none.
2026         $sql = 'SELECT count(*)
2027                   FROM {backup_files_temp}
2028                  WHERE backupid = ?
2029                    AND contextid = ?
2030                    AND component = ?
2031                    AND filearea  = ?';
2032         $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
2034         if ($DB->count_records_sql($sql, $params)) {
2035             $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
2036             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
2037                 'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
2038         }
2039     }
2042 /*
2043  * Structure step that will read the roles.xml file (at course/activity/block levels)
2044  * containing all the role_assignments and overrides for that context. If corresponding to
2045  * one mapped role, they will be applied to target context. Will observe the role_assignments
2046  * setting to decide if ras are restored.
2047  *
2048  * Note: this needs to be executed after all users are enrolled.
2049  */
2050 class restore_ras_and_caps_structure_step extends restore_structure_step {
2051     protected $plugins = null;
2053     protected function define_structure() {
2055         $paths = array();
2057         // Observe the role_assignments setting
2058         if ($this->get_setting_value('role_assignments')) {
2059             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
2060         }
2061         $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
2063         return $paths;
2064     }
2066     /**
2067      * Assign roles
2068      *
2069      * This has to be called after enrolments processing.
2070      *
2071      * @param mixed $data
2072      * @return void
2073      */
2074     public function process_assignment($data) {
2075         global $DB;
2077         $data = (object)$data;
2079         // Check roleid, userid are one of the mapped ones
2080         if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
2081             return;
2082         }
2083         if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
2084             return;
2085         }
2086         if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
2087             // Only assign roles to not deleted users
2088             return;
2089         }
2090         if (!$contextid = $this->task->get_contextid()) {
2091             return;
2092         }
2094         if (empty($data->component)) {
2095             // assign standard manual roles
2096             // TODO: role_assign() needs one userid param to be able to specify our restore userid
2097             role_assign($newroleid, $newuserid, $contextid);
2099         } else if ((strpos($data->component, 'enrol_') === 0)) {
2100             // Deal with enrolment roles - ignore the component and just find out the instance via new id,
2101             // it is possible that enrolment was restored using different plugin type.
2102             if (!isset($this->plugins)) {
2103                 $this->plugins = enrol_get_plugins(true);
2104             }
2105             if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
2106                 if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2107                     if (isset($this->plugins[$instance->enrol])) {
2108                         $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
2109                     }
2110                 }
2111             }
2113         } else {
2114             $data->roleid    = $newroleid;
2115             $data->userid    = $newuserid;
2116             $data->contextid = $contextid;
2117             $dir = core_component::get_component_directory($data->component);
2118             if ($dir and is_dir($dir)) {
2119                 if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
2120                     return;
2121                 }
2122             }
2123             // Bad luck, plugin could not restore the data, let's add normal membership.
2124             role_assign($data->roleid, $data->userid, $data->contextid);
2125             $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
2126             $this->log($message, backup::LOG_WARNING);
2127         }
2128     }
2130     public function process_override($data) {
2131         $data = (object)$data;
2133         // Check roleid is one of the mapped ones
2134         $newrole = $this->get_mapping('role', $data->roleid);
2135         $newroleid = $newrole->newitemid ?? false;
2136         $userid = $this->task->get_userid();
2138         // If newroleid and context are valid assign it via API (it handles dupes and so on)
2139         if ($newroleid && $this->task->get_contextid()) {
2140             if (!$capability = get_capability_info($data->capability)) {
2141                 $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING);
2142             } else {
2143                 $context = context::instance_by_id($this->task->get_contextid());
2144                 $overrideableroles = get_overridable_roles($context, ROLENAME_SHORT);
2145                 $safecapability = is_safe_capability($capability);
2147                 // Check if the new role is an overrideable role AND if the user performing the restore has the
2148                 // capability to assign the capability.
2149                 if (in_array($newrole->info['shortname'], $overrideableroles) &&
2150                     ($safecapability && has_capability('moodle/role:safeoverride', $context, $userid) ||
2151                         !$safecapability && has_capability('moodle/role:override', $context, $userid))
2152                 ) {
2153                     assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
2154                 } else {
2155                     $this->log("Insufficient capability to assign capability '{$data->capability}' to role!", backup::LOG_WARNING);
2156                 }
2157             }
2158         }
2159     }
2162 /**
2163  * If no instances yet add default enrol methods the same way as when creating new course in UI.
2164  */
2165 class restore_default_enrolments_step extends restore_execution_step {
2167     public function define_execution() {
2168         global $DB;
2170         // No enrolments in front page.
2171         if ($this->get_courseid() == SITEID) {
2172             return;
2173         }
2175         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
2176         // Return any existing course enrolment instances.
2177         $enrolinstances = enrol_get_instances($course->id, false);
2179         if ($enrolinstances) {
2180             // Something already added instances.
2181             // Get the existing enrolment methods in the course.
2182             $enrolmethods = array_map(function($enrolinstance) {
2183                 return $enrolinstance->enrol;
2184             }, $enrolinstances);
2186             $plugins = enrol_get_plugins(true);
2187             foreach ($plugins as $pluginname => $plugin) {
2188                 // Make sure all default enrolment methods exist in the course.
2189                 if (!in_array($pluginname, $enrolmethods)) {
2190                     $plugin->course_updated(true, $course, null);
2191                 }
2192                 $plugin->restore_sync_course($course);
2193             }
2195         } else {
2196             // Looks like a newly created course.
2197             enrol_course_updated(true, $course, null);
2198         }
2199     }
2202 /**
2203  * This structure steps restores the enrol plugins and their underlying
2204  * enrolments, performing all the mappings and/or movements required
2205  */
2206 class restore_enrolments_structure_step extends restore_structure_step {
2207     protected $enrolsynced = false;
2208     protected $plugins = null;
2209     protected $originalstatus = array();
2211     /**
2212      * Conditionally decide if this step should be executed.
2213      *
2214      * This function checks the following parameter:
2215      *
2216      *   1. the course/enrolments.xml file exists
2217      *
2218      * @return bool true is safe to execute, false otherwise
2219      */
2220     protected function execute_condition() {
2222         if ($this->get_courseid() == SITEID) {
2223             return false;
2224         }
2226         // Check it is included in the backup
2227         $fullpath = $this->task->get_taskbasepath();
2228         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2229         if (!file_exists($fullpath)) {
2230             // Not found, can't restore enrolments info
2231             return false;
2232         }
2234         return true;
2235     }
2237     protected function define_structure() {
2239         $userinfo = $this->get_setting_value('users');
2241         $paths = [];
2242         $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
2243         if ($userinfo) {
2244             $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
2245         }
2246         // Attach local plugin stucture to enrol element.
2247         $this->add_plugin_structure('enrol', $enrol);
2249         return $paths;
2250     }
2252     /**
2253      * Create enrolment instances.
2254      *
2255      * This has to be called after creation of roles
2256      * and before adding of role assignments.
2257      *
2258      * @param mixed $data
2259      * @return void
2260      */
2261     public function process_enrol($data) {
2262         global $DB;
2264         $data = (object)$data;
2265         $oldid = $data->id; // We'll need this later.
2266         unset($data->id);
2268         $this->originalstatus[$oldid] = $data->status;
2270         if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
2271             $this->set_mapping('enrol', $oldid, 0);
2272             return;
2273         }
2275         if (!isset($this->plugins)) {
2276             $this->plugins = enrol_get_plugins(true);
2277         }
2279         if (!$this->enrolsynced) {
2280             // Make sure that all plugin may create instances and enrolments automatically
2281             // before the first instance restore - this is suitable especially for plugins
2282             // that synchronise data automatically using course->idnumber or by course categories.
2283             foreach ($this->plugins as $plugin) {
2284                 $plugin->restore_sync_course($courserec);
2285             }
2286             $this->enrolsynced = true;
2287         }
2289         // Map standard fields - plugin has to process custom fields manually.
2290         $data->roleid   = $this->get_mappingid('role', $data->roleid);
2291         $data->courseid = $courserec->id;
2293         if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) {
2294             $converttomanual = true;
2295         } else {
2296             $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER);
2297         }
2299         if ($converttomanual) {
2300             // Restore enrolments as manual enrolments.
2301             unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
2302             if (!enrol_is_enabled('manual')) {
2303                 $this->set_mapping('enrol', $oldid, 0);
2304                 return;
2305             }
2306             if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
2307                 $instance = reset($instances);
2308                 $this->set_mapping('enrol', $oldid, $instance->id);
2309             } else {
2310                 if ($data->enrol === 'manual') {
2311                     $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
2312                 } else {
2313                     $instanceid = $this->plugins['manual']->add_default_instance($courserec);
2314                 }
2315                 $this->set_mapping('enrol', $oldid, $instanceid);
2316             }
2318         } else {
2319             if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
2320                 $this->set_mapping('enrol', $oldid, 0);
2321                 $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods";
2322                 $this->log($message, backup::LOG_WARNING);
2323                 return;
2324             }
2325             if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
2326                 // Let's keep the sortorder in old backups.
2327             } else {
2328                 // Prevent problems with colliding sortorders in old backups,
2329                 // new 2.4 backups do not need sortorder because xml elements are ordered properly.
2330                 unset($data->sortorder);
2331             }
2332             // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
2333             $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
2334         }
2335     }
2337     /**
2338      * Create user enrolments.
2339      *
2340      * This has to be called after creation of enrolment instances
2341      * and before adding of role assignments.
2342      *
2343      * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
2344      *
2345      * @param mixed $data
2346      * @return void
2347      */
2348     public function process_enrolment($data) {
2349         global $DB;
2351         if (!isset($this->plugins)) {
2352             $this->plugins = enrol_get_plugins(true);
2353         }
2355         $data = (object)$data;
2357         // Process only if parent instance have been mapped.
2358         if ($enrolid = $this->get_new_parentid('enrol')) {
2359             $oldinstancestatus = ENROL_INSTANCE_ENABLED;
2360             $oldenrolid = $this->get_old_parentid('enrol');
2361             if (isset($this->originalstatus[$oldenrolid])) {
2362                 $oldinstancestatus = $this->originalstatus[$oldenrolid];
2363             }
2364             if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2365                 // And only if user is a mapped one.
2366                 if ($userid = $this->get_mappingid('user', $data->userid)) {
2367                     if (isset($this->plugins[$instance->enrol])) {
2368                         $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
2369                     }
2370                 }
2371             }
2372         }
2373     }
2377 /**
2378  * Make sure the user restoring the course can actually access it.
2379  */
2380 class restore_fix_restorer_access_step extends restore_execution_step {
2381     protected function define_execution() {
2382         global $CFG, $DB;
2384         if (!$userid = $this->task->get_userid()) {
2385             return;
2386         }
2388         if (empty($CFG->restorernewroleid)) {
2389             // Bad luck, no fallback role for restorers specified
2390             return;
2391         }
2393         $courseid = $this->get_courseid();
2394         $context = context_course::instance($courseid);
2396         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2397             // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2398             return;
2399         }
2401         // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2402         role_assign($CFG->restorernewroleid, $userid, $context);
2404         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2405             // Extra role is enough, yay!
2406             return;
2407         }
2409         // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2410         // hopefully admin selected suitable $CFG->restorernewroleid ...
2411         if (!enrol_is_enabled('manual')) {
2412             return;
2413         }
2414         if (!$enrol = enrol_get_plugin('manual')) {
2415             return;
2416         }
2417         if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2418             $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2419             $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2420             $enrol->add_instance($course, $fields);
2421         }
2423         enrol_try_internal_enrol($courseid, $userid);
2424     }
2428 /**
2429  * This structure steps restores the filters and their configs
2430  */
2431 class restore_filters_structure_step extends restore_structure_step {
2433     protected function define_structure() {
2435         $paths = array();
2437         $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2438         $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2440         return $paths;
2441     }
2443     public function process_active($data) {
2445         $data = (object)$data;
2447         if (strpos($data->filter, 'filter/') === 0) {
2448             $data->filter = substr($data->filter, 7);
2450         } else if (strpos($data->filter, '/') !== false) {
2451             // Unsupported old filter.
2452             return;
2453         }
2455         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2456             return;
2457         }
2458         filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2459     }
2461     public function process_config($data) {
2463         $data = (object)$data;
2465         if (strpos($data->filter, 'filter/') === 0) {
2466             $data->filter = substr($data->filter, 7);
2468         } else if (strpos($data->filter, '/') !== false) {
2469             // Unsupported old filter.
2470             return;
2471         }
2473         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2474             return;
2475         }
2476         filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2477     }
2481 /**
2482  * This structure steps restores the comments
2483  * Note: Cannot use the comments API because defaults to USER->id.
2484  * That should change allowing to pass $userid
2485  */
2486 class restore_comments_structure_step extends restore_structure_step {
2488     protected function define_structure() {
2490         $paths = array();
2492         $paths[] = new restore_path_element('comment', '/comments/comment');
2494         return $paths;
2495     }
2497     public function process_comment($data) {
2498         global $DB;
2500         $data = (object)$data;
2502         // First of all, if the comment has some itemid, ask to the task what to map
2503         $mapping = false;
2504         if ($data->itemid) {
2505             $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2506             $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2507         }
2508         // Only restore the comment if has no mapping OR we have found the matching mapping
2509         if (!$mapping || $data->itemid) {
2510             // Only if user mapping and context
2511             $data->userid = $this->get_mappingid('user', $data->userid);
2512             if ($data->userid && $this->task->get_contextid()) {
2513                 $data->contextid = $this->task->get_contextid();
2514                 // Only if there is another comment with same context/user/timecreated
2515                 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2516                 if (!$DB->record_exists('comments', $params)) {
2517                     $DB->insert_record('comments', $data);
2518                 }
2519             }
2520         }
2521     }
2524 /**
2525  * This structure steps restores the badges and their configs
2526  */
2527 class restore_badges_structure_step extends restore_structure_step {
2529     /**
2530      * Conditionally decide if this step should be executed.
2531      *
2532      * This function checks the following parameters:
2533      *
2534      *   1. Badges and course badges are enabled on the site.
2535      *   2. The course/badges.xml file exists.
2536      *   3. All modules are restorable.
2537      *   4. All modules are marked for restore.
2538      *
2539      * @return bool True is safe to execute, false otherwise
2540      */
2541     protected function execute_condition() {
2542         global $CFG;
2544         // First check is badges and course level badges are enabled on this site.
2545         if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2546             // Disabled, don't restore course badges.
2547             return false;
2548         }
2550         // Check if badges.xml is included in the backup.
2551         $fullpath = $this->task->get_taskbasepath();
2552         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2553         if (!file_exists($fullpath)) {
2554             // Not found, can't restore course badges.
2555             return false;
2556         }
2558         // Check we are able to restore all backed up modules.
2559         if ($this->task->is_missing_modules()) {
2560             return false;
2561         }
2563         // Finally check all modules within the backup are being restored.
2564         if ($this->task->is_excluding_activities()) {
2565             return false;
2566         }
2568         return true;
2569     }
2571     protected function define_structure() {
2572         $paths = array();
2573         $paths[] = new restore_path_element('badge', '/badges/badge');
2574         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2575         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2576         $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
2577         $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
2578         $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
2579         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2581         return $paths;
2582     }
2584     public function process_badge($data) {
2585         global $DB, $CFG;
2587         require_once($CFG->libdir . '/badgeslib.php');
2589         $data = (object)$data;
2590         $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2591         if (empty($data->usercreated)) {
2592             $data->usercreated = $this->task->get_userid();
2593         }
2594         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2595         if (empty($data->usermodified)) {
2596             $data->usermodified = $this->task->get_userid();
2597         }
2599         // We'll restore the badge image.
2600         $restorefiles = true;
2602         $courseid = $this->get_courseid();
2604         $params = array(
2605                 'name'           => $data->name,
2606                 'description'    => $data->description,
2607                 'timecreated'    => $data->timecreated,
2608                 'timemodified'   => $data->timemodified,
2609                 'usercreated'    => $data->usercreated,
2610                 'usermodified'   => $data->usermodified,
2611                 'issuername'     => $data->issuername,
2612                 'issuerurl'      => $data->issuerurl,
2613                 'issuercontact'  => $data->issuercontact,
2614                 'expiredate'     => $this->apply_date_offset($data->expiredate),
2615                 'expireperiod'   => $data->expireperiod,
2616                 'type'           => BADGE_TYPE_COURSE,
2617                 'courseid'       => $courseid,
2618                 'message'        => $data->message,
2619                 'messagesubject' => $data->messagesubject,
2620                 'attachment'     => $data->attachment,
2621                 'notification'   => $data->notification,
2622                 'status'         => BADGE_STATUS_INACTIVE,
2623                 'nextcron'       => $data->nextcron,
2624                 'version'        => $data->version,
2625                 'language'       => $data->language,
2626                 'imageauthorname' => $data->imageauthorname,
2627                 'imageauthoremail' => $data->imageauthoremail,
2628                 'imageauthorurl' => $data->imageauthorurl,
2629                 'imagecaption'   => $data->imagecaption
2630         );
2632         $newid = $DB->insert_record('badge', $params);
2633         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2634     }
2636     /**
2637      * Create an endorsement for a badge.
2638      *
2639      * @param mixed $data
2640      * @return void
2641      */
2642     public function process_endorsement($data) {
2643         global $DB;
2645         $data = (object)$data;
2647         $params = [
2648             'badgeid' => $this->get_new_parentid('badge'),
2649             'issuername' => $data->issuername,
2650             'issuerurl' => $data->issuerurl,
2651             'issueremail' => $data->issueremail,
2652             'claimid' => $data->claimid,
2653             'claimcomment' => $data->claimcomment,
2654             'dateissued' => $this->apply_date_offset($data->dateissued)
2655         ];
2656         $newid = $DB->insert_record('badge_endorsement', $params);
2657         $this->set_mapping('endorsement', $data->id, $newid);
2658     }
2660     /**
2661      * Link to related badges for a badge. This relies on post processing in after_execute().
2662      *
2663      * @param mixed $data
2664      * @return void
2665      */
2666     public function process_relatedbadge($data) {
2667         global $DB;
2669         $data = (object)$data;
2670         $relatedbadgeid = $data->relatedbadgeid;
2672         if ($relatedbadgeid) {
2673             // Only backup and restore related badges if they are contained in the backup file.
2674             $params = array(
2675                     'badgeid'           => $this->get_new_parentid('badge'),
2676                     'relatedbadgeid'    => $relatedbadgeid
2677             );
2678             $newid = $DB->insert_record('badge_related', $params);
2679         }
2680     }
2682     /**
2683      * Link to an alignment for a badge.
2684      *
2685      * @param mixed $data
2686      * @return void
2687      */
2688     public function process_alignment($data) {
2689         global $DB;
2691         $data = (object)$data;
2692         $params = array(
2693                 'badgeid'           => $this->get_new_parentid('badge'),
2694                 'targetname'        => $data->targetname,
2695                 'targeturl'         => $data->targeturl,
2696                 'targetdescription' => $data->targetdescription,
2697                 'targetframework'   => $data->targetframework,
2698                 'targetcode'        => $data->targetcode
2699         );
2700         $newid = $DB->insert_record('badge_alignment', $params);
2701         $this->set_mapping('alignment', $data->id, $newid);
2702     }
2704     public function process_criterion($data) {
2705         global $DB;
2707         $data = (object)$data;
2709         $params = array(
2710                 'badgeid'           => $this->get_new_parentid('badge'),
2711                 'criteriatype'      => $data->criteriatype,
2712                 'method'            => $data->method,
2713                 'description'       => isset($data->description) ? $data->description : '',
2714                 'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
2715         );
2717         $newid = $DB->insert_record('badge_criteria', $params);
2718         $this->set_mapping('criterion', $data->id, $newid);
2719     }
2721     public function process_parameter($data) {
2722         global $DB, $CFG;
2724         require_once($CFG->libdir . '/badgeslib.php');
2726         $data = (object)$data;
2727         $criteriaid = $this->get_new_parentid('criterion');
2729         // Parameter array that will go to database.
2730         $params = array();
2731         $params['critid'] = $criteriaid;
2733         $oldparam = explode('_', $data->name);
2735         if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2736             $module = $this->get_mappingid('course_module', $oldparam[1]);
2737             $params['name'] = $oldparam[0] . '_' . $module;
2738             $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2739         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2740             $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2741             $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2742         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2743             $role = $this->get_mappingid('role', $data->value);
2744             if (!empty($role)) {
2745                 $params['name'] = 'role_' . $role;
2746                 $params['value'] = $role;
2747             } else {
2748                 return;
2749             }
2750         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
2751             $competencyid = $this->get_mappingid('competency', $data->value);
2752             if (!empty($competencyid)) {
2753                 $params['name'] = 'competency_' . $competencyid;
2754                 $params['value'] = $competencyid;
2755             } else {
2756                 return;
2757             }
2758         }
2760         if (!$DB->record_exists('badge_criteria_param', $params)) {
2761             $DB->insert_record('badge_criteria_param', $params);
2762         }
2763     }
2765     public function process_manual_award($data) {
2766         global $DB;
2768         $data = (object)$data;
2769         $role = $this->get_mappingid('role', $data->issuerrole);
2771         if (!empty($role)) {
2772             $award = array(
2773                 'badgeid'     => $this->get_new_parentid('badge'),
2774                 'recipientid' => $this->get_mappingid('user', $data->recipientid),
2775                 'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2776                 'issuerrole'  => $role,
2777                 'datemet'     => $this->apply_date_offset($data->datemet)
2778             );
2780             // Skip the manual award if recipient or issuer can not be mapped to.
2781             if (empty($award['recipientid']) || empty($award['issuerid'])) {
2782                 return;
2783             }
2785             $DB->insert_record('badge_manual_award', $award);
2786         }
2787     }
2789     protected function after_execute() {
2790         global $DB;
2791         // Add related files.
2792         $this->add_related_files('badges', 'badgeimage', 'badge');
2794         $badgeid = $this->get_new_parentid('badge');
2795         // Remap any related badges.
2796         // We do this in the DB directly because this is backup/restore it is not valid to call into
2797         // the component API.
2798         $params = array('badgeid' => $badgeid);
2799         $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
2800                     FROM {badge_related} br
2801                    WHERE (br.badgeid = :badgeid)";
2802         $relatedbadges = $DB->get_records_sql($query, $params);
2803         $newrelatedids = [];
2804         foreach ($relatedbadges as $relatedbadge) {
2805             $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
2806             $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
2807             $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
2808             if ($relatedid) {
2809                 $newrelatedids[] = $relatedid;
2810             }
2811         }
2812         if (!empty($newrelatedids)) {
2813             $relatedbadges = [];
2814             foreach ($newrelatedids as $relatedid) {
2815                 $relatedbadge = new stdClass();
2816                 $relatedbadge->badgeid = $badgeid;
2817                 $relatedbadge->relatedbadgeid = $relatedid;
2818                 $relatedbadges[] = $relatedbadge;
2819             }
2820             $DB->insert_records('badge_related', $relatedbadges);
2821         }
2822     }
2825 /**
2826  * This structure steps restores the calendar events
2827  */
2828 class restore_calendarevents_structure_step extends restore_structure_step {
2830     protected function define_structure() {
2832         $paths = array();
2834         $paths[] = new restore_path_element('calendarevents', '/events/event');
2836         return $paths;
2837     }
2839     public function process_calendarevents($data) {
2840         global $DB, $SITE, $USER;
2842         $data = (object)$data;
2843         $oldid = $data->id;
2844         $restorefiles = true; // We'll restore the files
2846         // If this is a new action event, it will automatically be populated by the adhoc task.
2847         // Nothing to do here.
2848         if (isset($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION) {
2849             return;
2850         }
2852         // User overrides for activities are identified by having a courseid of zero with
2853         // both a modulename and instance value set.
2854         $isuseroverride = !$data->courseid && $data->modulename && $data->instance;
2856         // If we don't want to include user data and this record is a user override event
2857         // for an activity then we should not create it. (Only activity events can be user override events - which must have this
2858         // setting).
2859         if ($isuseroverride && $this->task->setting_exists('userinfo') && !$this->task->get_setting_value('userinfo')) {
2860             return;
2861         }
2863         // Find the userid and the groupid associated with the event.
2864         $data->userid = $this->get_mappingid('user', $data->userid);
2865         if ($data->userid === false) {
2866             // Blank user ID means that we are dealing with module generated events such as quiz starting times.
2867             // Use the current user ID for these events.
2868             $data->userid = $USER->id;
2869         }
2870         if (!empty($data->groupid)) {
2871             $data->groupid = $this->get_mappingid('group', $data->groupid);
2872             if ($data->groupid === false) {
2873                 return;
2874             }
2875         }
2876         // Handle events with empty eventtype //MDL-32827
2877         if(empty($data->eventtype)) {
2878             if ($data->courseid == $SITE->id) {                                // Site event
2879                 $data->eventtype = "site";
2880             } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2881                 // Course assingment event
2882                 $data->eventtype = "due";
2883             } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2884                 $data->eventtype = "course";
2885             } else if ($data->groupid) {                                      // Group event
2886                 $data->eventtype = "group";
2887             } else if ($data->userid) {                                       // User event
2888                 $data->eventtype = "user";
2889             } else {
2890                 return;
2891             }
2892         }
2894         $params = array(
2895                 'name'           => $data->name,
2896                 'description'    => $data->description,
2897                 'format'         => $data->format,
2898                 // User overrides in activities use a course id of zero. All other event types
2899                 // must use the mapped course id.
2900                 'courseid'       => $data->courseid ? $this->get_courseid() : 0,
2901                 'groupid'        => $data->groupid,
2902                 'userid'         => $data->userid,
2903                 'repeatid'       => $this->get_mappingid('event', $data->repeatid),
2904                 'modulename'     => $data->modulename,
2905                 'type'           => isset($data->type) ? $data->type : 0,
2906                 'eventtype'      => $data->eventtype,
2907                 'timestart'      => $this->apply_date_offset($data->timestart),
2908                 'timeduration'   => $data->timeduration,
2909                 'timesort'       => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null,
2910                 'visible'        => $data->visible,
2911                 'uuid'           => $data->uuid,
2912                 'sequence'       => $data->sequence,
2913                 'timemodified'   => $data->timemodified,
2914                 'priority'       => isset($data->priority) ? $data->priority : null,
2915                 'location'       => isset($data->location) ? $data->location : null);
2916         if ($this->name == 'activity_calendar') {
2917             $params['instance'] = $this->task->get_activityid();
2918         } else {
2919             $params['instance'] = 0;
2920         }
2921         $sql = "SELECT id
2922                   FROM {event}
2923                  WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2924                    AND courseid = ?
2925                    AND modulename = ?
2926                    AND instance = ?
2927                    AND timestart = ?
2928                    AND timeduration = ?
2929                    AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2930         $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['instance'], $params['timestart'], $params['timeduration'], $params['description']);
2931         $result = $DB->record_exists_sql($sql, $arg);
2932         if (empty($result)) {
2933             $newitemid = $DB->insert_record('event', $params);
2934             $this->set_mapping('event', $oldid, $newitemid);
2935             $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2936         }
2937         // With repeating events, each event has the repeatid pointed at the first occurrence.
2938         // Since the repeatid will be empty when the first occurrence is restored,
2939         // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence.
2940         // Then keep a list of repeatids so we only perform this update once.
2941         static $repeatids = array();
2942         if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) {
2943             // This entry is repeated so the repeatid field must be set.
2944             $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid']));
2945             $repeatids[] = $params['repeatid'];
2946         }
2948     }
2949     protected function after_execute() {
2950         // Add related files
2951         $this->add_related_files('calendar', 'event_description', 'event_description');
2952     }
2955 class restore_course_completion_structure_step extends restore_structure_step {
2957     /**
2958      * Conditionally decide if this step should be executed.
2959      *
2960      * This function checks parameters that are not immediate settings to ensure
2961      * that the enviroment is suitable for the restore of course completion info.
2962      *
2963      * This function checks the following four parameters:
2964      *
2965      *   1. Course completion is enabled on the site
2966      *   2. The backup includes course completion information
2967      *   3. All modules are restorable
2968      *   4. All modules are marked for restore.
2969      *   5. No completion criteria already exist for the course.
2970      *
2971      * @return bool True is safe to execute, false otherwise
2972      */
2973     protected function execute_condition() {
2974         global $CFG, $DB;
2976         // First check course completion is enabled on this site
2977         if (empty($CFG->enablecompletion)) {
2978             // Disabled, don't restore course completion
2979             return false;
2980         }
2982         // No course completion on the front page.
2983         if ($this->get_courseid() == SITEID) {
2984             return false;
2985         }
2987         // Check it is included in the backup
2988         $fullpath = $this->task->get_taskbasepath();
2989         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2990         if (!file_exists($fullpath)) {
2991             // Not found, can't restore course completion
2992             return false;
2993         }
2995         // Check we are able to restore all backed up modules
2996         if ($this->task->is_missing_modules()) {
2997             return false;
2998         }
3000         // Check all modules within the backup are being restored.
3001         if ($this->task->is_excluding_activities()) {
3002             return false;
3003         }
3005         // Check that no completion criteria is already set for the course.
3006         if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
3007             return false;
3008         }
3010         return true;
3011     }
3013     /**
3014      * Define the course completion structure
3015      *
3016      * @return array Array of restore_path_element
3017      */
3018     protected function define_structure() {
3020         // To know if we are including user completion info
3021         $userinfo = $this->get_setting_value('userscompletion');
3023         $paths = array();
3024         $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
3025         $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
3027         if ($userinfo) {
3028             $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
3029             $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
3030         }
3032         return $paths;
3034     }
3036     /**
3037      * Process course completion criteria
3038      *
3039      * @global moodle_database $DB
3040      * @param stdClass $data
3041      */
3042     public function process_course_completion_criteria($data) {
3043         global $DB;
3045         $data = (object)$data;
3046         $data->course = $this->get_courseid();
3048         // Apply the date offset to the time end field
3049         $data->timeend = $this->apply_date_offset($data->timeend);
3051         // Map the role from the criteria
3052         if (isset($data->role) && $data->role != '') {
3053             // Newer backups should include roleshortname, which makes this much easier.
3054             if (!empty($data->roleshortname)) {
3055                 $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname));
3056                 if (!$roleinstanceid) {
3057                     $this->log(
3058                         'Could not match the role shortname in course_completion_criteria, so skipping',
3059                         backup::LOG_DEBUG
3060                     );
3061                     return;
3062                 }
3063                 $data->role = $roleinstanceid;
3064             } else {
3065                 $data->role = $this->get_mappingid('role', $data->role);
3066             }
3068             // Check we have an id, otherwise it causes all sorts of bugs.
3069             if (!$data->role) {
3070                 $this->log(
3071                     'Could not match role in course_completion_criteria, so skipping',
3072                     backup::LOG_DEBUG
3073                 );
3074                 return;
3075             }
3076         }
3078         // If the completion criteria is for a module we need to map the module instance
3079         // to the new module id.
3080         if (!empty($data->moduleinstance) && !empty($data->module)) {
3081             $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
3082             if (empty($data->moduleinstance)) {
3083                 $this->log(
3084                     'Could not match the module instance in course_completion_criteria, so skipping',
3085                     backup::LOG_DEBUG
3086                 );
3087                 return;
3088             }
3089         } else {
3090             $data->module = null;
3091             $data->moduleinstance = null;
3092         }
3094         // We backup the course shortname rather than the ID so that we can match back to the course
3095         if (!empty($data->courseinstanceshortname)) {
3096             $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
3097             if (!$courseinstanceid) {
3098                 $this->log(
3099                     'Could not match the course instance in course_completion_criteria, so skipping',
3100                     backup::LOG_DEBUG
3101                 );
3102                 return;
3103             }
3104         } else {
3105             $courseinstanceid = null;
3106         }
3107         $data->courseinstance = $courseinstanceid;
3109         $params = array(
3110             'course'         => $data->course,
3111             'criteriatype'   => $data->criteriatype,
3112             'enrolperiod'    => $data->enrolperiod,
3113             'courseinstance' => $data->courseinstance,
3114             'module'         => $data->module,
3115             'moduleinstance' => $data->moduleinstance,
3116             'timeend'        => $data->timeend,
3117             'gradepass'      => $data->gradepass,
3118             'role'           => $data->role
3119         );
3120         $newid = $DB->insert_record('course_completion_criteria', $params);
3121         $this->set_mapping('course_completion_criteria', $data->id, $newid);
3122     }
3124     /**
3125      * Processes course compltion criteria complete records
3126      *
3127      * @global moodle_database $DB
3128      * @param stdClass $data
3129      */
3130     public function process_course_completion_crit_compl($data) {
3131         global $DB;
3133         $data = (object)$data;
3135         // This may be empty if criteria could not be restored
3136         $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
3138         $data->course = $this->get_courseid();
3139         $data->userid = $this->get_mappingid('user', $data->userid);
3141         if (!empty($data->criteriaid) && !empty($data->userid)) {
3142             $params = array(
3143                 'userid' => $data->userid,
3144                 'course' => $data->course,
3145                 'criteriaid' => $data->criteriaid,
3146                 'timecompleted' => $data->timecompleted
3147             );
3148             if (isset($data->gradefinal)) {
3149                 $params['gradefinal'] = $data->gradefinal;
3150             }
3151             if (isset($data->unenroled)) {
3152                 $params['unenroled'] = $data->unenroled;
3153             }
3154             $DB->insert_record('course_completion_crit_compl', $params);
3155         }
3156     }
3158     /**
3159      * Process course completions
3160      *
3161      * @global moodle_database $DB
3162      * @param stdClass $data
3163      */
3164     public function process_course_completions($data) {
3165         global $DB;
3167         $data = (object)$data;
3169         $data->course = $this->get_courseid();
3170         $data->userid = $this->get_mappingid('user', $data->userid);
3172         if (!empty($data->userid)) {
3173             $params = array(
3174                 'userid' => $data->userid,
3175                 'course' => $data->course,
3176                 'timeenrolled' => $data->timeenrolled,
3177                 'timestarted' => $data->timestarted,
3178                 'timecompleted' => $data->timecompleted,
3179                 'reaggregate' => $data->reaggregate
3180             );
3182             $existing = $DB->get_record('course_completions', array(
3183                 'userid' => $data->userid,
3184                 'course' => $data->course
3185             ));
3187             // MDL-46651 - If cron writes out a new record before we get to it
3188             // then we should replace it with the Truth data from the backup.
3189             // This may be obsolete after MDL-48518 is resolved
3190             if ($existing) {
3191                 $params['id'] = $existing->id;
3192                 $DB->update_record('course_completions', $params);
3193             } else {
3194                 $DB->insert_record('course_completions', $params);
3195             }
3196         }
3197     }
3199     /**
3200      * Process course completion aggregate methods
3201      *
3202      * @global moodle_database $DB
3203      * @param stdClass $data
3204      */
3205     public function process_course_completion_aggr_methd($data) {
3206         global $DB;
3208         $data = (object)$data;
3210         $data->course = $this->get_courseid();
3212         // Only create the course_completion_aggr_methd records if
3213         // the target course has not them defined. MDL-28180
3214         if (!$DB->record_exists('course_completion_aggr_methd', array(
3215                     'course' => $data->course,
3216   &nbs