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