Merge branch 'MDL-10971_cloze_shuffle_fix' of git://github.com/timhunt/moodle
[moodle.git] / backup / moodle2 / restore_stepslib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Defines various restore steps that will be used by common tasks in restore
20  *
21  * @package     core_backup
22  * @subpackage  moodle2
23  * @category    backup
24  * @copyright   2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
25  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 defined('MOODLE_INTERNAL') || die();
30 /**
31  * delete old directories and conditionally create backup_temp_ids table
32  */
33 class restore_create_and_clean_temp_stuff extends restore_execution_step {
35     protected function define_execution() {
36         $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
37         // If the table already exists, it's because restore_prechecks have been executed in the same
38         // request (without problems) and it already contains a bunch of preloaded information (users...)
39         // that we aren't going to execute again
40         if ($exists) { // Inform plan about preloaded information
41             $this->task->set_preloaded_information();
42         }
43         // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
44         $itemid = $this->task->get_old_contextid();
45         $newitemid = context_course::instance($this->get_courseid())->id;
46         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
47         // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
48         $itemid = $this->task->get_old_system_contextid();
49         $newitemid = context_system::instance()->id;
50         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
51         // Create the old-course-id to new-course-id mapping, we need that available since the beginning
52         $itemid = $this->task->get_old_courseid();
53         $newitemid = $this->get_courseid();
54         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
56     }
57 }
59 /**
60  * 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         // No gradebook info found, don't execute
93         $fullpath = $this->task->get_taskbasepath();
94         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
95         if (!file_exists($fullpath)) {
96             return false;
97         }
99         // Some module present in backup file isn't available to restore
100         // in this site, don't execute
101         if ($this->task->is_missing_modules()) {
102             return false;
103         }
105         // Some activity has been excluded to be restored, don't execute
106         if ($this->task->is_excluding_activities()) {
107             return false;
108         }
110         // There should only be one grade category (the 1 associated with the course itself)
111         // If other categories already exist we're restoring into an existing course.
112         // Restoring categories into a course with an existing category structure is unlikely to go well
113         $category = new stdclass();
114         $category->courseid  = $this->get_courseid();
115         $catcount = $DB->count_records('grade_categories', (array)$category);
116         if ($catcount>1) {
117             return false;
118         }
120         // Arrived here, execute the step
121         return true;
122      }
124     protected function define_structure() {
125         $paths = array();
126         $userinfo = $this->task->get_setting_value('users');
128         $paths[] = new restore_path_element('gradebook', '/gradebook');
129         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
130         $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
131         if ($userinfo) {
132             $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
133         }
134         $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
135         $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
137         return $paths;
138     }
140     protected function process_gradebook($data) {
141     }
143     protected function process_grade_item($data) {
144         global $DB;
146         $data = (object)$data;
148         $oldid = $data->id;
149         $data->course = $this->get_courseid();
151         $data->courseid = $this->get_courseid();
153         if ($data->itemtype=='manual') {
154             // manual grade items store category id in categoryid
155             $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
156             // if mapping failed put in course's grade category
157             if (NULL == $data->categoryid) {
158                 $coursecat = grade_category::fetch_course_category($this->get_courseid());
159                 $data->categoryid = $coursecat->id;
160             }
161         } else if ($data->itemtype=='course') {
162             // course grade item stores their category id in iteminstance
163             $coursecat = grade_category::fetch_course_category($this->get_courseid());
164             $data->iteminstance = $coursecat->id;
165         } else if ($data->itemtype=='category') {
166             // category grade items store their category id in iteminstance
167             $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
168         } else {
169             throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
170         }
172         $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
173         $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
175         $data->locktime     = $this->apply_date_offset($data->locktime);
176         $data->timecreated  = $this->apply_date_offset($data->timecreated);
177         $data->timemodified = $this->apply_date_offset($data->timemodified);
179         $coursecategory = $newitemid = null;
180         //course grade item should already exist so updating instead of inserting
181         if($data->itemtype=='course') {
182             //get the ID of the already created grade item
183             $gi = new stdclass();
184             $gi->courseid  = $this->get_courseid();
185             $gi->itemtype  = $data->itemtype;
187             //need to get the id of the grade_category that was automatically created for the course
188             $category = new stdclass();
189             $category->courseid  = $this->get_courseid();
190             $category->parent  = null;
191             //course category fullname starts out as ? but may be edited
192             //$category->fullname  = '?';
193             $coursecategory = $DB->get_record('grade_categories', (array)$category);
194             $gi->iteminstance = $coursecategory->id;
196             $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
197             if (!empty($existinggradeitem)) {
198                 $data->id = $newitemid = $existinggradeitem->id;
199                 $DB->update_record('grade_items', $data);
200             }
201         } else if ($data->itemtype == 'manual') {
202             // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
203             $gi = array(
204                 'itemtype' => $data->itemtype,
205                 'courseid' => $data->courseid,
206                 'itemname' => $data->itemname,
207                 'categoryid' => $data->categoryid,
208             );
209             $newitemid = $DB->get_field('grade_items', 'id', $gi);
210         }
212         if (empty($newitemid)) {
213             //in case we found the course category but still need to insert the course grade item
214             if ($data->itemtype=='course' && !empty($coursecategory)) {
215                 $data->iteminstance = $coursecategory->id;
216             }
218             $newitemid = $DB->insert_record('grade_items', $data);
219         }
220         $this->set_mapping('grade_item', $oldid, $newitemid);
221     }
223     protected function process_grade_grade($data) {
224         global $DB;
226         $data = (object)$data;
227         $oldid = $data->id;
228         $olduserid = $data->userid;
230         $data->itemid = $this->get_new_parentid('grade_item');
232         $data->userid = $this->get_mappingid('user', $data->userid, null);
233         if (!empty($data->userid)) {
234             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
235             $data->locktime     = $this->apply_date_offset($data->locktime);
236             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
237             $data->overridden = $this->apply_date_offset($data->overridden);
238             $data->timecreated  = $this->apply_date_offset($data->timecreated);
239             $data->timemodified = $this->apply_date_offset($data->timemodified);
241             $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
242             if ($gradeexists) {
243                 $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
244                 $this->log($message, backup::LOG_DEBUG);
245             } else {
246                 $newitemid = $DB->insert_record('grade_grades', $data);
247                 $this->set_mapping('grade_grades', $oldid, $newitemid);
248             }
249         } else {
250             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
251             $this->log($message, backup::LOG_DEBUG);
252         }
253     }
255     protected function process_grade_category($data) {
256         global $DB;
258         $data = (object)$data;
259         $oldid = $data->id;
261         $data->course = $this->get_courseid();
262         $data->courseid = $data->course;
264         $data->timecreated  = $this->apply_date_offset($data->timecreated);
265         $data->timemodified = $this->apply_date_offset($data->timemodified);
267         $newitemid = null;
268         //no parent means a course level grade category. That may have been created when the course was created
269         if(empty($data->parent)) {
270             //parent was being saved as 0 when it should be null
271             $data->parent = null;
273             //get the already created course level grade category
274             $category = new stdclass();
275             $category->courseid = $this->get_courseid();
276             $category->parent = null;
278             $coursecategory = $DB->get_record('grade_categories', (array)$category);
279             if (!empty($coursecategory)) {
280                 $data->id = $newitemid = $coursecategory->id;
281                 $DB->update_record('grade_categories', $data);
282             }
283         }
285         //need to insert a course category
286         if (empty($newitemid)) {
287             $newitemid = $DB->insert_record('grade_categories', $data);
288         }
289         $this->set_mapping('grade_category', $oldid, $newitemid);
290     }
291     protected function process_grade_letter($data) {
292         global $DB;
294         $data = (object)$data;
295         $oldid = $data->id;
297         $data->contextid = context_course::instance($this->get_courseid())->id;
299         $gradeletter = (array)$data;
300         unset($gradeletter['id']);
301         if (!$DB->record_exists('grade_letters', $gradeletter)) {
302             $newitemid = $DB->insert_record('grade_letters', $data);
303         } else {
304             $newitemid = $data->id;
305         }
307         $this->set_mapping('grade_letter', $oldid, $newitemid);
308     }
309     protected function process_grade_setting($data) {
310         global $DB;
312         $data = (object)$data;
313         $oldid = $data->id;
315         $data->courseid = $this->get_courseid();
317         if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
318             $newitemid = $DB->insert_record('grade_settings', $data);
319         } else {
320             $newitemid = $data->id;
321         }
323         $this->set_mapping('grade_setting', $oldid, $newitemid);
324     }
326     /**
327      * put all activity grade items in the correct grade category and mark all for recalculation
328      */
329     protected function after_execute() {
330         global $DB;
332         $conditions = array(
333             'backupid' => $this->get_restoreid(),
334             'itemname' => 'grade_item'//,
335             //'itemid'   => $itemid
336         );
337         $rs = $DB->get_recordset('backup_ids_temp', $conditions);
339         // We need this for calculation magic later on.
340         $mappings = array();
342         if (!empty($rs)) {
343             foreach($rs as $grade_item_backup) {
345                 // Store the oldid with the new id.
346                 $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
348                 $updateobj = new stdclass();
349                 $updateobj->id = $grade_item_backup->newitemid;
351                 //if this is an activity grade item that needs to be put back in its correct category
352                 if (!empty($grade_item_backup->parentitemid)) {
353                     $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
354                     if (!is_null($oldcategoryid)) {
355                         $updateobj->categoryid = $oldcategoryid;
356                         $DB->update_record('grade_items', $updateobj);
357                     }
358                 } else {
359                     //mark course and category items as needing to be recalculated
360                     $updateobj->needsupdate=1;
361                     $DB->update_record('grade_items', $updateobj);
362                 }
363             }
364         }
365         $rs->close();
367         // We need to update the calculations for calculated grade items that may reference old
368         // grade item ids using ##gi\d+##.
369         // $mappings can be empty, use 0 if so (won't match ever)
370         list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
371         $sql = "SELECT gi.id, gi.calculation
372                   FROM {grade_items} gi
373                  WHERE gi.id {$sql} AND
374                        calculation IS NOT NULL";
375         $rs = $DB->get_recordset_sql($sql, $params);
376         foreach ($rs as $gradeitem) {
377             // Collect all of the used grade item id references
378             if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
379                 // This calculation doesn't reference any other grade items... EASY!
380                 continue;
381             }
382             // For this next bit we are going to do the replacement of id's in two steps:
383             // 1. We will replace all old id references with a special mapping reference.
384             // 2. We will replace all mapping references with id's
385             // Why do we do this?
386             // Because there potentially there will be an overlap of ids within the query and we
387             // we substitute the wrong id.. safest way around this is the two step system
388             $calculationmap = array();
389             $mapcount = 0;
390             foreach ($matches[1] as $match) {
391                 // Check that the old id is known to us, if not it was broken to begin with and will
392                 // continue to be broken.
393                 if (!array_key_exists($match, $mappings)) {
394                     continue;
395                 }
396                 // Our special mapping key
397                 $mapping = '##MAPPING'.$mapcount.'##';
398                 // The old id that exists within the calculation now
399                 $oldid = '##gi'.$match.'##';
400                 // The new id that we want to replace the old one with.
401                 $newid = '##gi'.$mappings[$match].'##';
402                 // Replace in the special mapping key
403                 $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
404                 // And record the mapping
405                 $calculationmap[$mapping] = $newid;
406                 $mapcount++;
407             }
408             // Iterate all special mappings for this calculation and replace in the new id's
409             foreach ($calculationmap as $mapping => $newid) {
410                 $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
411             }
412             // Update the calculation now that its being remapped
413             $DB->update_record('grade_items', $gradeitem);
414         }
415         $rs->close();
417         // Need to correct the grade category path and parent
418         $conditions = array(
419             'courseid' => $this->get_courseid()
420         );
422         $rs = $DB->get_recordset('grade_categories', $conditions);
423         // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
424         foreach ($rs as $gc) {
425             if (!empty($gc->parent)) {
426                 $grade_category = new stdClass();
427                 $grade_category->id = $gc->id;
428                 $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
429                 $DB->update_record('grade_categories', $grade_category);
430             }
431         }
432         $rs->close();
434         // Now we can rebuild all the paths
435         $rs = $DB->get_recordset('grade_categories', $conditions);
436         foreach ($rs as $gc) {
437             $grade_category = new stdClass();
438             $grade_category->id = $gc->id;
439             $grade_category->path = grade_category::build_path($gc);
440             $grade_category->depth = substr_count($grade_category->path, '/') - 1;
441             $DB->update_record('grade_categories', $grade_category);
442         }
443         $rs->close();
445         // Restore marks items as needing update. Update everything now.
446         grade_regrade_final_grades($this->get_courseid());
447     }
450 /**
451  * Step in charge of restoring the grade history of a course.
452  *
453  * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
454  * we do not want to restore the history if the gradebook and its content has not been
455  * restored. At least for now.
456  */
457 class restore_grade_history_structure_step extends restore_structure_step {
459      protected function execute_condition() {
460         global $CFG, $DB;
462         // No gradebook info found, don't execute.
463         $fullpath = $this->task->get_taskbasepath();
464         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
465         if (!file_exists($fullpath)) {
466             return false;
467         }
469         // Some module present in backup file isn't available to restore in this site, don't execute.
470         if ($this->task->is_missing_modules()) {
471             return false;
472         }
474         // Some activity has been excluded to be restored, don't execute.
475         if ($this->task->is_excluding_activities()) {
476             return false;
477         }
479         // There should only be one grade category (the 1 associated with the course itself).
480         $category = new stdclass();
481         $category->courseid  = $this->get_courseid();
482         $catcount = $DB->count_records('grade_categories', (array)$category);
483         if ($catcount > 1) {
484             return false;
485         }
487         // Arrived here, execute the step.
488         return true;
489      }
491     protected function define_structure() {
492         $paths = array();
494         // Settings to use.
495         $userinfo = $this->get_setting_value('users');
496         $history = $this->get_setting_value('grade_histories');
498         if ($userinfo && $history) {
499             $paths[] = new restore_path_element('grade_grade',
500                '/grade_history/grade_grades/grade_grade');
501         }
503         return $paths;
504     }
506     protected function process_grade_grade($data) {
507         global $DB;
509         $data = (object)($data);
510         $olduserid = $data->userid;
511         unset($data->id);
513         $data->userid = $this->get_mappingid('user', $data->userid, null);
514         if (!empty($data->userid)) {
515             // Do not apply the date offsets as this is history.
516             $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
517             $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
518             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
519             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
520             $DB->insert_record('grade_grades_history', $data);
521         } else {
522             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
523             $this->log($message, backup::LOG_DEBUG);
524         }
525     }
529 /**
530  * decode all the interlinks present in restored content
531  * relying 100% in the restore_decode_processor that handles
532  * both the contents to modify and the rules to be applied
533  */
534 class restore_decode_interlinks extends restore_execution_step {
536     protected function define_execution() {
537         // Get the decoder (from the plan)
538         $decoder = $this->task->get_decoder();
539         restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
540         // And launch it, everything will be processed
541         $decoder->execute();
542     }
545 /**
546  * first, ensure that we have no gaps in section numbers
547  * and then, rebuid the course cache
548  */
549 class restore_rebuild_course_cache extends restore_execution_step {
551     protected function define_execution() {
552         global $DB;
554         // Although there is some sort of auto-recovery of missing sections
555         // present in course/formats... here we check that all the sections
556         // from 0 to MAX(section->section) exist, creating them if necessary
557         $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
558         // Iterate over all sections
559         for ($i = 0; $i <= $maxsection; $i++) {
560             // If the section $i doesn't exist, create it
561             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
562                 $sectionrec = array(
563                     'course' => $this->get_courseid(),
564                     'section' => $i);
565                 $DB->insert_record('course_sections', $sectionrec); // missing section created
566             }
567         }
569         // Rebuild cache now that all sections are in place
570         rebuild_course_cache($this->get_courseid());
571         cache_helper::purge_by_event('changesincourse');
572         cache_helper::purge_by_event('changesincoursecat');
573     }
576 /**
577  * Review all the tasks having one after_restore method
578  * executing it to perform some final adjustments of information
579  * not available when the task was executed.
580  */
581 class restore_execute_after_restore extends restore_execution_step {
583     protected function define_execution() {
585         // Simply call to the execute_after_restore() method of the task
586         // that always is the restore_final_task
587         $this->task->launch_execute_after_restore();
588     }
592 /**
593  * Review all the (pending) block positions in backup_ids, matching by
594  * contextid, creating positions as needed. This is executed by the
595  * final task, once all the contexts have been created
596  */
597 class restore_review_pending_block_positions extends restore_execution_step {
599     protected function define_execution() {
600         global $DB;
602         // Get all the block_position objects pending to match
603         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
604         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
605         // Process block positions, creating them or accumulating for final step
606         foreach($rs as $posrec) {
607             // Get the complete position object out of the info field.
608             $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
609             // If position is for one already mapped (known) contextid
610             // process it now, creating the position, else nothing to
611             // do, position finally discarded
612             if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
613                 $position->contextid = $newctx->newitemid;
614                 // Create the block position
615                 $DB->insert_record('block_positions', $position);
616             }
617         }
618         $rs->close();
619     }
623 /**
624  * Updates the availability data for course modules and sections.
625  *
626  * Runs after the restore of all course modules, sections, and grade items has
627  * completed. This is necessary in order to update IDs that have changed during
628  * restore.
629  *
630  * @package core_backup
631  * @copyright 2014 The Open University
632  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
633  */
634 class restore_update_availability extends restore_execution_step {
636     protected function define_execution() {
637         global $CFG, $DB;
639         // Note: This code runs even if availability is disabled when restoring.
640         // That will ensure that if you later turn availability on for the site,
641         // there will be no incorrect IDs. (It doesn't take long if the restored
642         // data does not contain any availability information.)
644         // Get modinfo with all data after resetting cache.
645         rebuild_course_cache($this->get_courseid(), true);
646         $modinfo = get_fast_modinfo($this->get_courseid());
648         // Get the date offset for this restore.
649         $dateoffset = $this->apply_date_offset(1) - 1;
651         // Update all sections that were restored.
652         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
653         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
654         $sectionsbyid = null;
655         foreach ($rs as $rec) {
656             if (is_null($sectionsbyid)) {
657                 $sectionsbyid = array();
658                 foreach ($modinfo->get_section_info_all() as $section) {
659                     $sectionsbyid[$section->id] = $section;
660                 }
661             }
662             if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
663                 // If the section was not fully restored for some reason
664                 // (e.g. due to an earlier error), skip it.
665                 $this->get_logger()->process('Section not fully restored: id ' .
666                         $rec->newitemid, backup::LOG_WARNING);
667                 continue;
668             }
669             $section = $sectionsbyid[$rec->newitemid];
670             if (!is_null($section->availability)) {
671                 $info = new \core_availability\info_section($section);
672                 $info->update_after_restore($this->get_restoreid(),
673                         $this->get_courseid(), $this->get_logger(), $dateoffset);
674             }
675         }
676         $rs->close();
678         // Update all modules that were restored.
679         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
680         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
681         foreach ($rs as $rec) {
682             if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
683                 // If the module was not fully restored for some reason
684                 // (e.g. due to an earlier error), skip it.
685                 $this->get_logger()->process('Module not fully restored: id ' .
686                         $rec->newitemid, backup::LOG_WARNING);
687                 continue;
688             }
689             $cm = $modinfo->get_cm($rec->newitemid);
690             if (!is_null($cm->availability)) {
691                 $info = new \core_availability\info_module($cm);
692                 $info->update_after_restore($this->get_restoreid(),
693                         $this->get_courseid(), $this->get_logger(), $dateoffset);
694             }
695         }
696         $rs->close();
697     }
701 /**
702  * Process legacy module availability records in backup_ids.
703  *
704  * Matches course modules and grade item id once all them have been already restored.
705  * Only if all matchings are satisfied the availability condition will be created.
706  * At the same time, it is required for the site to have that functionality enabled.
707  *
708  * This step is included only to handle legacy backups (2.6 and before). It does not
709  * do anything for newer backups.
710  *
711  * @copyright 2014 The Open University
712  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
713  */
714 class restore_process_course_modules_availability extends restore_execution_step {
716     protected function define_execution() {
717         global $CFG, $DB;
719         // Site hasn't availability enabled
720         if (empty($CFG->enableavailability)) {
721             return;
722         }
724         // Do both modules and sections.
725         foreach (array('module', 'section') as $table) {
726             // Get all the availability objects to process.
727             $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
728             $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
729             // Process availabilities, creating them if everything matches ok.
730             foreach ($rs as $availrec) {
731                 $allmatchesok = true;
732                 // Get the complete legacy availability object.
733                 $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
735                 // Note: This code used to update IDs, but that is now handled by the
736                 // current code (after restore) instead of this legacy code.
738                 // Get showavailability option.
739                 $thingid = ($table === 'module') ? $availability->coursemoduleid :
740                         $availability->coursesectionid;
741                 $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
742                         $table . '_showavailability', $thingid);
743                 if (!$showrec) {
744                     // Should not happen.
745                     throw new coding_exception('No matching showavailability record');
746                 }
747                 $show = $showrec->info->showavailability;
749                 // The $availability object is now in the format used in the old
750                 // system. Interpret this and convert to new system.
751                 $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
752                         array('id' => $thingid), MUST_EXIST);
753                 $newvalue = \core_availability\info::add_legacy_availability_condition(
754                         $currentvalue, $availability, $show);
755                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
756                         array('id' => $thingid));
757             }
758         }
759         $rs->close();
760     }
764 /*
765  * Execution step that, *conditionally* (if there isn't preloaded information)
766  * will load the inforef files for all the included course/section/activity tasks
767  * to backup_temp_ids. They will be stored with "xxxxref" as itemname
768  */
769 class restore_load_included_inforef_records extends restore_execution_step {
771     protected function define_execution() {
773         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
774             return;
775         }
777         // Get all the included tasks
778         $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
779         $progress = $this->task->get_progress();
780         $progress->start_progress($this->get_name(), count($tasks));
781         foreach ($tasks as $task) {
782             // Load the inforef.xml file if exists
783             $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
784             if (file_exists($inforefpath)) {
785                 // Load each inforef file to temp_ids.
786                 restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
787             }
788         }
789         $progress->end_progress();
790     }
793 /*
794  * Execution step that will load all the needed files into backup_files_temp
795  *   - info: contains the whole original object (times, names...)
796  * (all them being original ids as loaded from xml)
797  */
798 class restore_load_included_files extends restore_structure_step {
800     protected function define_structure() {
802         $file = new restore_path_element('file', '/files/file');
804         return array($file);
805     }
807     /**
808      * Process one <file> element from files.xml
809      *
810      * @param array $data the element data
811      */
812     public function process_file($data) {
814         $data = (object)$data; // handy
816         // load it if needed:
817         //   - it it is one of the annotated inforef files (course/section/activity/block)
818         //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
819         // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
820         //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
821         $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
822         $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
823                         $data->component == 'grouping' || $data->component == 'grade' ||
824                         $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
825         if ($isfileref || $iscomponent) {
826             restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
827         }
828     }
831 /**
832  * Execution step that, *conditionally* (if there isn't preloaded information),
833  * will load all the needed roles to backup_temp_ids. They will be stored with
834  * "role" itemname. Also it will perform one automatic mapping to roles existing
835  * in the target site, based in permissions of the user performing the restore,
836  * archetypes and other bits. At the end, each original role will have its associated
837  * target role or 0 if it's going to be skipped. Note we wrap everything over one
838  * restore_dbops method, as far as the same stuff is going to be also executed
839  * by restore prechecks
840  */
841 class restore_load_and_map_roles extends restore_execution_step {
843     protected function define_execution() {
844         if ($this->task->get_preloaded_information()) { // if info is already preloaded
845             return;
846         }
848         $file = $this->get_basepath() . '/roles.xml';
849         // Load needed toles to temp_ids
850         restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
852         // Process roles, mapping/skipping. Any error throws exception
853         // Note we pass controller's info because it can contain role mapping information
854         // about manual mappings performed by UI
855         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);
856     }
859 /**
860  * Execution step that, *conditionally* (if there isn't preloaded information
861  * and users have been selected in settings, will load all the needed users
862  * to backup_temp_ids. They will be stored with "user" itemname and with
863  * their original contextid as paremitemid
864  */
865 class restore_load_included_users extends restore_execution_step {
867     protected function define_execution() {
869         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
870             return;
871         }
872         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
873             return;
874         }
875         $file = $this->get_basepath() . '/users.xml';
876         // Load needed users to temp_ids.
877         restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
878     }
881 /**
882  * Execution step that, *conditionally* (if there isn't preloaded information
883  * and users have been selected in settings, will process all the needed users
884  * in order to decide and perform any action with them (create / map / error)
885  * Note: Any error will cause exception, as far as this is the same processing
886  * than the one into restore prechecks (that should have stopped process earlier)
887  */
888 class restore_process_included_users extends restore_execution_step {
890     protected function define_execution() {
892         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
893             return;
894         }
895         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
896             return;
897         }
898         restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
899                 $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
900     }
903 /**
904  * Execution step that will create all the needed users as calculated
905  * by @restore_process_included_users (those having newiteind = 0)
906  */
907 class restore_create_included_users extends restore_execution_step {
909     protected function define_execution() {
911         restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
912                 $this->task->get_userid(), $this->task->get_progress());
913     }
916 /**
917  * Structure step that will create all the needed groups and groupings
918  * by loading them from the groups.xml file performing the required matches.
919  * Note group members only will be added if restoring user info
920  */
921 class restore_groups_structure_step extends restore_structure_step {
923     protected function define_structure() {
925         $paths = array(); // Add paths here
927         $paths[] = new restore_path_element('group', '/groups/group');
928         $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
929         $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
931         return $paths;
932     }
934     // Processing functions go here
935     public function process_group($data) {
936         global $DB;
938         $data = (object)$data; // handy
939         $data->courseid = $this->get_courseid();
941         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
942         // another a group in the same course
943         $context = context_course::instance($data->courseid);
944         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
945             if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
946                 unset($data->idnumber);
947             }
948         } else {
949             unset($data->idnumber);
950         }
952         $oldid = $data->id;    // need this saved for later
954         $restorefiles = false; // Only if we end creating the group
956         // Search if the group already exists (by name & description) in the target course
957         $description_clause = '';
958         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
959         if (!empty($data->description)) {
960             $description_clause = ' AND ' .
961                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
962            $params['description'] = $data->description;
963         }
964         if (!$groupdb = $DB->get_record_sql("SELECT *
965                                                FROM {groups}
966                                               WHERE courseid = :courseid
967                                                 AND name = :grname $description_clause", $params)) {
968             // group doesn't exist, create
969             $newitemid = $DB->insert_record('groups', $data);
970             $restorefiles = true; // We'll restore the files
971         } else {
972             // group exists, use it
973             $newitemid = $groupdb->id;
974         }
975         // Save the id mapping
976         $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
977         // Invalidate the course group data cache just in case.
978         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
979     }
981     public function process_grouping($data) {
982         global $DB;
984         $data = (object)$data; // handy
985         $data->courseid = $this->get_courseid();
987         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
988         // another a grouping in the same course
989         $context = context_course::instance($data->courseid);
990         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
991             if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
992                 unset($data->idnumber);
993             }
994         } else {
995             unset($data->idnumber);
996         }
998         $oldid = $data->id;    // need this saved for later
999         $restorefiles = false; // Only if we end creating the grouping
1001         // Search if the grouping already exists (by name & description) in the target course
1002         $description_clause = '';
1003         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1004         if (!empty($data->description)) {
1005             $description_clause = ' AND ' .
1006                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1007            $params['description'] = $data->description;
1008         }
1009         if (!$groupingdb = $DB->get_record_sql("SELECT *
1010                                                   FROM {groupings}
1011                                                  WHERE courseid = :courseid
1012                                                    AND name = :grname $description_clause", $params)) {
1013             // grouping doesn't exist, create
1014             $newitemid = $DB->insert_record('groupings', $data);
1015             $restorefiles = true; // We'll restore the files
1016         } else {
1017             // grouping exists, use it
1018             $newitemid = $groupingdb->id;
1019         }
1020         // Save the id mapping
1021         $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1022         // Invalidate the course group data cache just in case.
1023         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1024     }
1026     public function process_grouping_group($data) {
1027         global $CFG;
1029         require_once($CFG->dirroot.'/group/lib.php');
1031         $data = (object)$data;
1032         groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1033     }
1035     protected function after_execute() {
1036         // Add group related files, matching with "group" mappings
1037         $this->add_related_files('group', 'icon', 'group');
1038         $this->add_related_files('group', 'description', 'group');
1039         // Add grouping related files, matching with "grouping" mappings
1040         $this->add_related_files('grouping', 'description', 'grouping');
1041         // Invalidate the course group data.
1042         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1043     }
1047 /**
1048  * Structure step that will create all the needed group memberships
1049  * by loading them from the groups.xml file performing the required matches.
1050  */
1051 class restore_groups_members_structure_step extends restore_structure_step {
1053     protected $plugins = null;
1055     protected function define_structure() {
1057         $paths = array(); // Add paths here
1059         if ($this->get_setting_value('users')) {
1060             $paths[] = new restore_path_element('group', '/groups/group');
1061             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1062         }
1064         return $paths;
1065     }
1067     public function process_group($data) {
1068         $data = (object)$data; // handy
1070         // HACK ALERT!
1071         // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1072         // Let's fake internal state to make $this->get_new_parentid('group') work.
1074         $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1075     }
1077     public function process_member($data) {
1078         global $DB, $CFG;
1079         require_once("$CFG->dirroot/group/lib.php");
1081         // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1083         $data = (object)$data; // handy
1085         // get parent group->id
1086         $data->groupid = $this->get_new_parentid('group');
1088         // map user newitemid and insert if not member already
1089         if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1090             if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1091                 // Check the component, if any, exists.
1092                 if (empty($data->component)) {
1093                     groups_add_member($data->groupid, $data->userid);
1095                 } else if ((strpos($data->component, 'enrol_') === 0)) {
1096                     // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1097                     // it is possible that enrolment was restored using different plugin type.
1098                     if (!isset($this->plugins)) {
1099                         $this->plugins = enrol_get_plugins(true);
1100                     }
1101                     if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1102                         if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1103                             if (isset($this->plugins[$instance->enrol])) {
1104                                 $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1105                             }
1106                         }
1107                     }
1109                 } else {
1110                     $dir = core_component::get_component_directory($data->component);
1111                     if ($dir and is_dir($dir)) {
1112                         if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1113                             return;
1114                         }
1115                     }
1116                     // Bad luck, plugin could not restore the data, let's add normal membership.
1117                     groups_add_member($data->groupid, $data->userid);
1118                     $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1119                     $this->log($message, backup::LOG_WARNING);
1120                 }
1121             }
1122         }
1123     }
1126 /**
1127  * Structure step that will create all the needed scales
1128  * by loading them from the scales.xml
1129  */
1130 class restore_scales_structure_step extends restore_structure_step {
1132     protected function define_structure() {
1134         $paths = array(); // Add paths here
1135         $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1136         return $paths;
1137     }
1139     protected function process_scale($data) {
1140         global $DB;
1142         $data = (object)$data;
1144         $restorefiles = false; // Only if we end creating the group
1146         $oldid = $data->id;    // need this saved for later
1148         // Look for scale (by 'scale' both in standard (course=0) and current course
1149         // with priority to standard scales (ORDER clause)
1150         // scale is not course unique, use get_record_sql to suppress warning
1151         // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1152         $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1153         $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1154         if (!$scadb = $DB->get_record_sql("SELECT *
1155                                             FROM {scale}
1156                                            WHERE courseid IN (0, :courseid)
1157                                              AND $compare_scale_clause
1158                                         ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1159             // Remap the user if possible, defaut to user performing the restore if not
1160             $userid = $this->get_mappingid('user', $data->userid);
1161             $data->userid = $userid ? $userid : $this->task->get_userid();
1162             // Remap the course if course scale
1163             $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1164             // If global scale (course=0), check the user has perms to create it
1165             // falling to course scale if not
1166             $systemctx = context_system::instance();
1167             if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1168                 $data->courseid = $this->get_courseid();
1169             }
1170             // scale doesn't exist, create
1171             $newitemid = $DB->insert_record('scale', $data);
1172             $restorefiles = true; // We'll restore the files
1173         } else {
1174             // scale exists, use it
1175             $newitemid = $scadb->id;
1176         }
1177         // Save the id mapping (with files support at system context)
1178         $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1179     }
1181     protected function after_execute() {
1182         // Add scales related files, matching with "scale" mappings
1183         $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1184     }
1188 /**
1189  * Structure step that will create all the needed outocomes
1190  * by loading them from the outcomes.xml
1191  */
1192 class restore_outcomes_structure_step extends restore_structure_step {
1194     protected function define_structure() {
1196         $paths = array(); // Add paths here
1197         $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1198         return $paths;
1199     }
1201     protected function process_outcome($data) {
1202         global $DB;
1204         $data = (object)$data;
1206         $restorefiles = false; // Only if we end creating the group
1208         $oldid = $data->id;    // need this saved for later
1210         // Look for outcome (by shortname both in standard (courseid=null) and current course
1211         // with priority to standard outcomes (ORDER clause)
1212         // outcome is not course unique, use get_record_sql to suppress warning
1213         $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1214         if (!$outdb = $DB->get_record_sql('SELECT *
1215                                              FROM {grade_outcomes}
1216                                             WHERE shortname = :shortname
1217                                               AND (courseid = :courseid OR courseid IS NULL)
1218                                          ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1219             // Remap the user
1220             $userid = $this->get_mappingid('user', $data->usermodified);
1221             $data->usermodified = $userid ? $userid : $this->task->get_userid();
1222             // Remap the scale
1223             $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1224             // Remap the course if course outcome
1225             $data->courseid = $data->courseid ? $this->get_courseid() : null;
1226             // If global outcome (course=null), check the user has perms to create it
1227             // falling to course outcome if not
1228             $systemctx = context_system::instance();
1229             if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1230                 $data->courseid = $this->get_courseid();
1231             }
1232             // outcome doesn't exist, create
1233             $newitemid = $DB->insert_record('grade_outcomes', $data);
1234             $restorefiles = true; // We'll restore the files
1235         } else {
1236             // scale exists, use it
1237             $newitemid = $outdb->id;
1238         }
1239         // Set the corresponding grade_outcomes_courses record
1240         $outcourserec = new stdclass();
1241         $outcourserec->courseid  = $this->get_courseid();
1242         $outcourserec->outcomeid = $newitemid;
1243         if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1244             $DB->insert_record('grade_outcomes_courses', $outcourserec);
1245         }
1246         // Save the id mapping (with files support at system context)
1247         $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1248     }
1250     protected function after_execute() {
1251         // Add outcomes related files, matching with "outcome" mappings
1252         $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1253     }
1256 /**
1257  * Execution step that, *conditionally* (if there isn't preloaded information
1258  * will load all the question categories and questions (header info only)
1259  * to backup_temp_ids. They will be stored with "question_category" and
1260  * "question" itemnames and with their original contextid and question category
1261  * id as paremitemids
1262  */
1263 class restore_load_categories_and_questions extends restore_execution_step {
1265     protected function define_execution() {
1267         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1268             return;
1269         }
1270         $file = $this->get_basepath() . '/questions.xml';
1271         restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1272     }
1275 /**
1276  * Execution step that, *conditionally* (if there isn't preloaded information)
1277  * will process all the needed categories and questions
1278  * in order to decide and perform any action with them (create / map / error)
1279  * Note: Any error will cause exception, as far as this is the same processing
1280  * than the one into restore prechecks (that should have stopped process earlier)
1281  */
1282 class restore_process_categories_and_questions extends restore_execution_step {
1284     protected function define_execution() {
1286         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1287             return;
1288         }
1289         restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1290     }
1293 /**
1294  * Structure step that will read the section.xml creating/updating sections
1295  * as needed, rebuilding course cache and other friends
1296  */
1297 class restore_section_structure_step extends restore_structure_step {
1299     protected function define_structure() {
1300         global $CFG;
1302         $paths = array();
1304         $section = new restore_path_element('section', '/section');
1305         $paths[] = $section;
1306         if ($CFG->enableavailability) {
1307             $paths[] = new restore_path_element('availability', '/section/availability');
1308             $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1309         }
1310         $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1312         // Apply for 'format' plugins optional paths at section level
1313         $this->add_plugin_structure('format', $section);
1315         // Apply for 'local' plugins optional paths at section level
1316         $this->add_plugin_structure('local', $section);
1318         return $paths;
1319     }
1321     public function process_section($data) {
1322         global $CFG, $DB;
1323         $data = (object)$data;
1324         $oldid = $data->id; // We'll need this later
1326         $restorefiles = false;
1328         // Look for the section
1329         $section = new stdclass();
1330         $section->course  = $this->get_courseid();
1331         $section->section = $data->number;
1332         // Section doesn't exist, create it with all the info from backup
1333         if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
1334             $section->name = $data->name;
1335             $section->summary = $data->summary;
1336             $section->summaryformat = $data->summaryformat;
1337             $section->sequence = '';
1338             $section->visible = $data->visible;
1339             if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1340                 $section->availability = null;
1341             } else {
1342                 $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1343                 // Include legacy [<2.7] availability data if provided.
1344                 if (is_null($section->availability)) {
1345                     $section->availability = \core_availability\info::convert_legacy_fields(
1346                             $data, true);
1347                 }
1348             }
1349             $newitemid = $DB->insert_record('course_sections', $section);
1350             $restorefiles = true;
1352         // Section exists, update non-empty information
1353         } else {
1354             $section->id = $secrec->id;
1355             if ((string)$secrec->name === '') {
1356                 $section->name = $data->name;
1357             }
1358             if (empty($secrec->summary)) {
1359                 $section->summary = $data->summary;
1360                 $section->summaryformat = $data->summaryformat;
1361                 $restorefiles = true;
1362             }
1364             // Don't update availability (I didn't see a useful way to define
1365             // whether existing or new one should take precedence).
1367             $DB->update_record('course_sections', $section);
1368             $newitemid = $secrec->id;
1369         }
1371         // Annotate the section mapping, with restorefiles option if needed
1372         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1374         // set the new course_section id in the task
1375         $this->task->set_sectionid($newitemid);
1377         // If there is the legacy showavailability data, store this for later use.
1378         // (This data is not present when restoring 'new' backups.)
1379         if (isset($data->showavailability)) {
1380             // Cache the showavailability flag using the backup_ids data field.
1381             restore_dbops::set_backup_ids_record($this->get_restoreid(),
1382                     'section_showavailability', $newitemid, 0, null,
1383                     (object)array('showavailability' => $data->showavailability));
1384         }
1386         // Commented out. We never modify course->numsections as far as that is used
1387         // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1388         // Note: We keep the code here, to know about and because of the possibility of making this
1389         // optional based on some setting/attribute in the future
1390         // If needed, adjust course->numsections
1391         //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1392         //    if ($numsections < $section->section) {
1393         //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1394         //    }
1395         //}
1396     }
1398     /**
1399      * Process the legacy availability table record. This table does not exist
1400      * in Moodle 2.7+ but we still support restore.
1401      *
1402      * @param stdClass $data Record data
1403      */
1404     public function process_availability($data) {
1405         $data = (object)$data;
1406         // Simply going to store the whole availability record now, we'll process
1407         // all them later in the final task (once all activities have been restored)
1408         // Let's call the low level one to be able to store the whole object.
1409         $data->coursesectionid = $this->task->get_sectionid();
1410         restore_dbops::set_backup_ids_record($this->get_restoreid(),
1411                 'section_availability', $data->id, 0, null, $data);
1412     }
1414     /**
1415      * Process the legacy availability fields table record. This table does not
1416      * exist in Moodle 2.7+ but we still support restore.
1417      *
1418      * @param stdClass $data Record data
1419      */
1420     public function process_availability_field($data) {
1421         global $DB;
1422         $data = (object)$data;
1423         // Mark it is as passed by default
1424         $passed = true;
1425         $customfieldid = null;
1427         // If a customfield has been used in order to pass we must be able to match an existing
1428         // customfield by name (data->customfield) and type (data->customfieldtype)
1429         if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1430             // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1431             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1432             $passed = false;
1433         } else if (!is_null($data->customfield)) {
1434             $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1435             $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1436             $passed = ($customfieldid !== false);
1437         }
1439         if ($passed) {
1440             // Create the object to insert into the database
1441             $availfield = new stdClass();
1442             $availfield->coursesectionid = $this->task->get_sectionid();
1443             $availfield->userfield = $data->userfield;
1444             $availfield->customfieldid = $customfieldid;
1445             $availfield->operator = $data->operator;
1446             $availfield->value = $data->value;
1448             // Get showavailability option.
1449             $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1450                     'section_showavailability', $availfield->coursesectionid);
1451             if (!$showrec) {
1452                 // Should not happen.
1453                 throw new coding_exception('No matching showavailability record');
1454             }
1455             $show = $showrec->info->showavailability;
1457             // The $availfield object is now in the format used in the old
1458             // system. Interpret this and convert to new system.
1459             $currentvalue = $DB->get_field('course_sections', 'availability',
1460                     array('id' => $availfield->coursesectionid), MUST_EXIST);
1461             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1462                     $currentvalue, $availfield, $show);
1463             $DB->set_field('course_sections', 'availability', $newvalue,
1464                     array('id' => $availfield->coursesectionid));
1465         }
1466     }
1468     public function process_course_format_options($data) {
1469         global $DB;
1470         $data = (object)$data;
1471         $oldid = $data->id;
1472         unset($data->id);
1473         $data->sectionid = $this->task->get_sectionid();
1474         $data->courseid = $this->get_courseid();
1475         $newid = $DB->insert_record('course_format_options', $data);
1476         $this->set_mapping('course_format_options', $oldid, $newid);
1477     }
1479     protected function after_execute() {
1480         // Add section related files, with 'course_section' itemid to match
1481         $this->add_related_files('course', 'section', 'course_section');
1482     }
1485 /**
1486  * Structure step that will read the course.xml file, loading it and performing
1487  * various actions depending of the site/restore settings. Note that target
1488  * course always exist before arriving here so this step will be updating
1489  * the course record (never inserting)
1490  */
1491 class restore_course_structure_step extends restore_structure_step {
1492     /**
1493      * @var bool this gets set to true by {@link process_course()} if we are
1494      * restoring an old coures that used the legacy 'module security' feature.
1495      * If so, we have to do more work in {@link after_execute()}.
1496      */
1497     protected $legacyrestrictmodules = false;
1499     /**
1500      * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1501      * array with array keys the module names ('forum', 'quiz', etc.). These are
1502      * the modules that are allowed according to the data in the backup file.
1503      * In {@link after_execute()} we then have to prevent adding of all the other
1504      * types of activity.
1505      */
1506     protected $legacyallowedmodules = array();
1508     protected function define_structure() {
1510         $course = new restore_path_element('course', '/course');
1511         $category = new restore_path_element('category', '/course/category');
1512         $tag = new restore_path_element('tag', '/course/tags/tag');
1513         $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1515         // Apply for 'format' plugins optional paths at course level
1516         $this->add_plugin_structure('format', $course);
1518         // Apply for 'theme' plugins optional paths at course level
1519         $this->add_plugin_structure('theme', $course);
1521         // Apply for 'report' plugins optional paths at course level
1522         $this->add_plugin_structure('report', $course);
1524         // Apply for 'course report' plugins optional paths at course level
1525         $this->add_plugin_structure('coursereport', $course);
1527         // Apply for plagiarism plugins optional paths at course level
1528         $this->add_plugin_structure('plagiarism', $course);
1530         // Apply for local plugins optional paths at course level
1531         $this->add_plugin_structure('local', $course);
1533         return array($course, $category, $tag, $allowed_module);
1534     }
1536     /**
1537      * Processing functions go here
1538      *
1539      * @global moodledatabase $DB
1540      * @param stdClass $data
1541      */
1542     public function process_course($data) {
1543         global $CFG, $DB;
1545         $data = (object)$data;
1547         $fullname  = $this->get_setting_value('course_fullname');
1548         $shortname = $this->get_setting_value('course_shortname');
1549         $startdate = $this->get_setting_value('course_startdate');
1551         // Calculate final course names, to avoid dupes
1552         list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
1554         // Need to change some fields before updating the course record
1555         $data->id = $this->get_courseid();
1556         $data->fullname = $fullname;
1557         $data->shortname= $shortname;
1559         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1560         // another course on this site.
1561         $context = context::instance_by_id($this->task->get_contextid());
1562         if (!empty($data->idnumber) && has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid()) &&
1563                 $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1564             // Do not reset idnumber.
1565         } else {
1566             $data->idnumber = '';
1567         }
1569         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1570         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1571         if (empty($data->hiddensections)) {
1572             $data->hiddensections = 0;
1573         }
1575         // Set legacyrestrictmodules to true if the course was resticting modules. If so
1576         // then we will need to process restricted modules after execution.
1577         $this->legacyrestrictmodules = !empty($data->restrictmodules);
1579         $data->startdate= $this->apply_date_offset($data->startdate);
1580         if ($data->defaultgroupingid) {
1581             $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1582         }
1583         if (empty($CFG->enablecompletion)) {
1584             $data->enablecompletion = 0;
1585             $data->completionstartonenrol = 0;
1586             $data->completionnotify = 0;
1587         }
1588         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1589         if (!array_key_exists($data->lang, $languages)) {
1590             $data->lang = '';
1591         }
1593         $themes = get_list_of_themes(); // Get themes for quick search later
1594         if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1595             $data->theme = '';
1596         }
1598         // Check if this is an old SCORM course format.
1599         if ($data->format == 'scorm') {
1600             $data->format = 'singleactivity';
1601             $data->activitytype = 'scorm';
1602         }
1604         // Course record ready, update it
1605         $DB->update_record('course', $data);
1607         course_get_format($data)->update_course_format_options($data);
1609         // Role name aliases
1610         restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1611     }
1613     public function process_category($data) {
1614         // Nothing to do with the category. UI sets it before restore starts
1615     }
1617     public function process_tag($data) {
1618         global $CFG, $DB;
1620         $data = (object)$data;
1622         if (!empty($CFG->usetags)) { // if enabled in server
1623             // TODO: This is highly inneficient. Each time we add one tag
1624             // we fetch all the existing because tag_set() deletes them
1625             // so everything must be reinserted on each call
1626             $tags = array();
1627             $existingtags = tag_get_tags('course', $this->get_courseid());
1628             // Re-add all the existitng tags
1629             foreach ($existingtags as $existingtag) {
1630                 $tags[] = $existingtag->rawname;
1631             }
1632             // Add the one being restored
1633             $tags[] = $data->rawname;
1634             // Send all the tags back to the course
1635             tag_set('course', $this->get_courseid(), $tags, 'core',
1636                 context_course::instance($this->get_courseid())->id);
1637         }
1638     }
1640     public function process_allowed_module($data) {
1641         $data = (object)$data;
1643         // Backwards compatiblity support for the data that used to be in the
1644         // course_allowed_modules table.
1645         if ($this->legacyrestrictmodules) {
1646             $this->legacyallowedmodules[$data->modulename] = 1;
1647         }
1648     }
1650     protected function after_execute() {
1651         global $DB;
1653         // Add course related files, without itemid to match
1654         $this->add_related_files('course', 'summary', null);
1655         $this->add_related_files('course', 'overviewfiles', null);
1657         // Deal with legacy allowed modules.
1658         if ($this->legacyrestrictmodules) {
1659             $context = context_course::instance($this->get_courseid());
1661             list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1662             list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1663             foreach ($managerroleids as $roleid) {
1664                 unset($roleids[$roleid]);
1665             }
1667             foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1668                 if (isset($this->legacyallowedmodules[$modname])) {
1669                     // Module is allowed, no worries.
1670                     continue;
1671                 }
1673                 $capability = 'mod/' . $modname . ':addinstance';
1674                 foreach ($roleids as $roleid) {
1675                     assign_capability($capability, CAP_PREVENT, $roleid, $context);
1676                 }
1677             }
1678         }
1679     }
1682 /**
1683  * Execution step that will migrate legacy files if present.
1684  */
1685 class restore_course_legacy_files_step extends restore_execution_step {
1686     public function define_execution() {
1687         global $DB;
1689         // Do a check for legacy files and skip if there are none.
1690         $sql = 'SELECT count(*)
1691                   FROM {backup_files_temp}
1692                  WHERE backupid = ?
1693                    AND contextid = ?
1694                    AND component = ?
1695                    AND filearea  = ?';
1696         $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
1698         if ($DB->count_records_sql($sql, $params)) {
1699             $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
1700             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
1701                 'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
1702         }
1703     }
1706 /*
1707  * Structure step that will read the roles.xml file (at course/activity/block levels)
1708  * containing all the role_assignments and overrides for that context. If corresponding to
1709  * one mapped role, they will be applied to target context. Will observe the role_assignments
1710  * setting to decide if ras are restored.
1711  *
1712  * Note: this needs to be executed after all users are enrolled.
1713  */
1714 class restore_ras_and_caps_structure_step extends restore_structure_step {
1715     protected $plugins = null;
1717     protected function define_structure() {
1719         $paths = array();
1721         // Observe the role_assignments setting
1722         if ($this->get_setting_value('role_assignments')) {
1723             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
1724         }
1725         $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
1727         return $paths;
1728     }
1730     /**
1731      * Assign roles
1732      *
1733      * This has to be called after enrolments processing.
1734      *
1735      * @param mixed $data
1736      * @return void
1737      */
1738     public function process_assignment($data) {
1739         global $DB;
1741         $data = (object)$data;
1743         // Check roleid, userid are one of the mapped ones
1744         if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
1745             return;
1746         }
1747         if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
1748             return;
1749         }
1750         if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
1751             // Only assign roles to not deleted users
1752             return;
1753         }
1754         if (!$contextid = $this->task->get_contextid()) {
1755             return;
1756         }
1758         if (empty($data->component)) {
1759             // assign standard manual roles
1760             // TODO: role_assign() needs one userid param to be able to specify our restore userid
1761             role_assign($newroleid, $newuserid, $contextid);
1763         } else if ((strpos($data->component, 'enrol_') === 0)) {
1764             // Deal with enrolment roles - ignore the component and just find out the instance via new id,
1765             // it is possible that enrolment was restored using different plugin type.
1766             if (!isset($this->plugins)) {
1767                 $this->plugins = enrol_get_plugins(true);
1768             }
1769             if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1770                 if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1771                     if (isset($this->plugins[$instance->enrol])) {
1772                         $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
1773                     }
1774                 }
1775             }
1777         } else {
1778             $data->roleid    = $newroleid;
1779             $data->userid    = $newuserid;
1780             $data->contextid = $contextid;
1781             $dir = core_component::get_component_directory($data->component);
1782             if ($dir and is_dir($dir)) {
1783                 if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
1784                     return;
1785                 }
1786             }
1787             // Bad luck, plugin could not restore the data, let's add normal membership.
1788             role_assign($data->roleid, $data->userid, $data->contextid);
1789             $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
1790             $this->log($message, backup::LOG_WARNING);
1791         }
1792     }
1794     public function process_override($data) {
1795         $data = (object)$data;
1797         // Check roleid is one of the mapped ones
1798         $newroleid = $this->get_mappingid('role', $data->roleid);
1799         // If newroleid and context are valid assign it via API (it handles dupes and so on)
1800         if ($newroleid && $this->task->get_contextid()) {
1801             // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1802             // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1803             assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1804         }
1805     }
1808 /**
1809  * If no instances yet add default enrol methods the same way as when creating new course in UI.
1810  */
1811 class restore_default_enrolments_step extends restore_execution_step {
1812     public function define_execution() {
1813         global $DB;
1815         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
1817         if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
1818             // Something already added instances, do not add default instances.
1819             $plugins = enrol_get_plugins(true);
1820             foreach ($plugins as $plugin) {
1821                 $plugin->restore_sync_course($course);
1822             }
1824         } else {
1825             // Looks like a newly created course.
1826             enrol_course_updated(true, $course, null);
1827         }
1828     }
1831 /**
1832  * This structure steps restores the enrol plugins and their underlying
1833  * enrolments, performing all the mappings and/or movements required
1834  */
1835 class restore_enrolments_structure_step extends restore_structure_step {
1836     protected $enrolsynced = false;
1837     protected $plugins = null;
1838     protected $originalstatus = array();
1840     /**
1841      * Conditionally decide if this step should be executed.
1842      *
1843      * This function checks the following parameter:
1844      *
1845      *   1. the course/enrolments.xml file exists
1846      *
1847      * @return bool true is safe to execute, false otherwise
1848      */
1849     protected function execute_condition() {
1851         // Check it is included in the backup
1852         $fullpath = $this->task->get_taskbasepath();
1853         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
1854         if (!file_exists($fullpath)) {
1855             // Not found, can't restore enrolments info
1856             return false;
1857         }
1859         return true;
1860     }
1862     protected function define_structure() {
1864         $paths = array();
1866         $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
1867         $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
1869         return $paths;
1870     }
1872     /**
1873      * Create enrolment instances.
1874      *
1875      * This has to be called after creation of roles
1876      * and before adding of role assignments.
1877      *
1878      * @param mixed $data
1879      * @return void
1880      */
1881     public function process_enrol($data) {
1882         global $DB;
1884         $data = (object)$data;
1885         $oldid = $data->id; // We'll need this later.
1886         unset($data->id);
1888         $this->originalstatus[$oldid] = $data->status;
1890         if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
1891             $this->set_mapping('enrol', $oldid, 0);
1892             return;
1893         }
1895         if (!isset($this->plugins)) {
1896             $this->plugins = enrol_get_plugins(true);
1897         }
1899         if (!$this->enrolsynced) {
1900             // Make sure that all plugin may create instances and enrolments automatically
1901             // before the first instance restore - this is suitable especially for plugins
1902             // that synchronise data automatically using course->idnumber or by course categories.
1903             foreach ($this->plugins as $plugin) {
1904                 $plugin->restore_sync_course($courserec);
1905             }
1906             $this->enrolsynced = true;
1907         }
1909         // Map standard fields - plugin has to process custom fields manually.
1910         $data->roleid   = $this->get_mappingid('role', $data->roleid);
1911         $data->courseid = $courserec->id;
1913         if ($this->get_setting_value('enrol_migratetomanual')) {
1914             unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
1915             if (!enrol_is_enabled('manual')) {
1916                 $this->set_mapping('enrol', $oldid, 0);
1917                 return;
1918             }
1919             if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
1920                 $instance = reset($instances);
1921                 $this->set_mapping('enrol', $oldid, $instance->id);
1922             } else {
1923                 if ($data->enrol === 'manual') {
1924                     $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
1925                 } else {
1926                     $instanceid = $this->plugins['manual']->add_default_instance($courserec);
1927                 }
1928                 $this->set_mapping('enrol', $oldid, $instanceid);
1929             }
1931         } else {
1932             if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
1933                 $this->set_mapping('enrol', $oldid, 0);
1934                 $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
1935                 $this->log($message, backup::LOG_WARNING);
1936                 return;
1937             }
1938             if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
1939                 // Let's keep the sortorder in old backups.
1940             } else {
1941                 // Prevent problems with colliding sortorders in old backups,
1942                 // new 2.4 backups do not need sortorder because xml elements are ordered properly.
1943                 unset($data->sortorder);
1944             }
1945             // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
1946             $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
1947         }
1948     }
1950     /**
1951      * Create user enrolments.
1952      *
1953      * This has to be called after creation of enrolment instances
1954      * and before adding of role assignments.
1955      *
1956      * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
1957      *
1958      * @param mixed $data
1959      * @return void
1960      */
1961     public function process_enrolment($data) {
1962         global $DB;
1964         if (!isset($this->plugins)) {
1965             $this->plugins = enrol_get_plugins(true);
1966         }
1968         $data = (object)$data;
1970         // Process only if parent instance have been mapped.
1971         if ($enrolid = $this->get_new_parentid('enrol')) {
1972             $oldinstancestatus = ENROL_INSTANCE_ENABLED;
1973             $oldenrolid = $this->get_old_parentid('enrol');
1974             if (isset($this->originalstatus[$oldenrolid])) {
1975                 $oldinstancestatus = $this->originalstatus[$oldenrolid];
1976             }
1977             if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1978                 // And only if user is a mapped one.
1979                 if ($userid = $this->get_mappingid('user', $data->userid)) {
1980                     if (isset($this->plugins[$instance->enrol])) {
1981                         $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
1982                     }
1983                 }
1984             }
1985         }
1986     }
1990 /**
1991  * Make sure the user restoring the course can actually access it.
1992  */
1993 class restore_fix_restorer_access_step extends restore_execution_step {
1994     protected function define_execution() {
1995         global $CFG, $DB;
1997         if (!$userid = $this->task->get_userid()) {
1998             return;
1999         }
2001         if (empty($CFG->restorernewroleid)) {
2002             // Bad luck, no fallback role for restorers specified
2003             return;
2004         }
2006         $courseid = $this->get_courseid();
2007         $context = context_course::instance($courseid);
2009         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2010             // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2011             return;
2012         }
2014         // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2015         role_assign($CFG->restorernewroleid, $userid, $context);
2017         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2018             // Extra role is enough, yay!
2019             return;
2020         }
2022         // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2023         // hopefully admin selected suitable $CFG->restorernewroleid ...
2024         if (!enrol_is_enabled('manual')) {
2025             return;
2026         }
2027         if (!$enrol = enrol_get_plugin('manual')) {
2028             return;
2029         }
2030         if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2031             $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2032             $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2033             $enrol->add_instance($course, $fields);
2034         }
2036         enrol_try_internal_enrol($courseid, $userid);
2037     }
2041 /**
2042  * This structure steps restores the filters and their configs
2043  */
2044 class restore_filters_structure_step extends restore_structure_step {
2046     protected function define_structure() {
2048         $paths = array();
2050         $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2051         $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2053         return $paths;
2054     }
2056     public function process_active($data) {
2058         $data = (object)$data;
2060         if (strpos($data->filter, 'filter/') === 0) {
2061             $data->filter = substr($data->filter, 7);
2063         } else if (strpos($data->filter, '/') !== false) {
2064             // Unsupported old filter.
2065             return;
2066         }
2068         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2069             return;
2070         }
2071         filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2072     }
2074     public function process_config($data) {
2076         $data = (object)$data;
2078         if (strpos($data->filter, 'filter/') === 0) {
2079             $data->filter = substr($data->filter, 7);
2081         } else if (strpos($data->filter, '/') !== false) {
2082             // Unsupported old filter.
2083             return;
2084         }
2086         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2087             return;
2088         }
2089         filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2090     }
2094 /**
2095  * This structure steps restores the comments
2096  * Note: Cannot use the comments API because defaults to USER->id.
2097  * That should change allowing to pass $userid
2098  */
2099 class restore_comments_structure_step extends restore_structure_step {
2101     protected function define_structure() {
2103         $paths = array();
2105         $paths[] = new restore_path_element('comment', '/comments/comment');
2107         return $paths;
2108     }
2110     public function process_comment($data) {
2111         global $DB;
2113         $data = (object)$data;
2115         // First of all, if the comment has some itemid, ask to the task what to map
2116         $mapping = false;
2117         if ($data->itemid) {
2118             $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2119             $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2120         }
2121         // Only restore the comment if has no mapping OR we have found the matching mapping
2122         if (!$mapping || $data->itemid) {
2123             // Only if user mapping and context
2124             $data->userid = $this->get_mappingid('user', $data->userid);
2125             if ($data->userid && $this->task->get_contextid()) {
2126                 $data->contextid = $this->task->get_contextid();
2127                 // Only if there is another comment with same context/user/timecreated
2128                 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2129                 if (!$DB->record_exists('comments', $params)) {
2130                     $DB->insert_record('comments', $data);
2131                 }
2132             }
2133         }
2134     }
2137 /**
2138  * This structure steps restores the badges and their configs
2139  */
2140 class restore_badges_structure_step extends restore_structure_step {
2142     /**
2143      * Conditionally decide if this step should be executed.
2144      *
2145      * This function checks the following parameters:
2146      *
2147      *   1. Badges and course badges are enabled on the site.
2148      *   2. The course/badges.xml file exists.
2149      *   3. All modules are restorable.
2150      *   4. All modules are marked for restore.
2151      *
2152      * @return bool True is safe to execute, false otherwise
2153      */
2154     protected function execute_condition() {
2155         global $CFG;
2157         // First check is badges and course level badges are enabled on this site.
2158         if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2159             // Disabled, don't restore course badges.
2160             return false;
2161         }
2163         // Check if badges.xml is included in the backup.
2164         $fullpath = $this->task->get_taskbasepath();
2165         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2166         if (!file_exists($fullpath)) {
2167             // Not found, can't restore course badges.
2168             return false;
2169         }
2171         // Check we are able to restore all backed up modules.
2172         if ($this->task->is_missing_modules()) {
2173             return false;
2174         }
2176         // Finally check all modules within the backup are being restored.
2177         if ($this->task->is_excluding_activities()) {
2178             return false;
2179         }
2181         return true;
2182     }
2184     protected function define_structure() {
2185         $paths = array();
2186         $paths[] = new restore_path_element('badge', '/badges/badge');
2187         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2188         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2189         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2191         return $paths;
2192     }
2194     public function process_badge($data) {
2195         global $DB, $CFG;
2197         require_once($CFG->libdir . '/badgeslib.php');
2199         $data = (object)$data;
2200         $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2201         if (empty($data->usercreated)) {
2202             $data->usercreated = $this->task->get_userid();
2203         }
2204         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2205         if (empty($data->usermodified)) {
2206             $data->usermodified = $this->task->get_userid();
2207         }
2209         // We'll restore the badge image.
2210         $restorefiles = true;
2212         $courseid = $this->get_courseid();
2214         $params = array(
2215                 'name'           => $data->name,
2216                 'description'    => $data->description,
2217                 'timecreated'    => $this->apply_date_offset($data->timecreated),
2218                 'timemodified'   => $this->apply_date_offset($data->timemodified),
2219                 'usercreated'    => $data->usercreated,
2220                 'usermodified'   => $data->usermodified,
2221                 'issuername'     => $data->issuername,
2222                 'issuerurl'      => $data->issuerurl,
2223                 'issuercontact'  => $data->issuercontact,
2224                 'expiredate'     => $this->apply_date_offset($data->expiredate),
2225                 'expireperiod'   => $data->expireperiod,
2226                 'type'           => BADGE_TYPE_COURSE,
2227                 'courseid'       => $courseid,
2228                 'message'        => $data->message,
2229                 'messagesubject' => $data->messagesubject,
2230                 'attachment'     => $data->attachment,
2231                 'notification'   => $data->notification,
2232                 'status'         => BADGE_STATUS_INACTIVE,
2233                 'nextcron'       => $this->apply_date_offset($data->nextcron)
2234         );
2236         $newid = $DB->insert_record('badge', $params);
2237         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2238     }
2240     public function process_criterion($data) {
2241         global $DB;
2243         $data = (object)$data;
2245         $params = array(
2246                 'badgeid'      => $this->get_new_parentid('badge'),
2247                 'criteriatype' => $data->criteriatype,
2248                 'method'       => $data->method
2249         );
2250         $newid = $DB->insert_record('badge_criteria', $params);
2251         $this->set_mapping('criterion', $data->id, $newid);
2252     }
2254     public function process_parameter($data) {
2255         global $DB, $CFG;
2257         require_once($CFG->libdir . '/badgeslib.php');
2259         $data = (object)$data;
2260         $criteriaid = $this->get_new_parentid('criterion');
2262         // Parameter array that will go to database.
2263         $params = array();
2264         $params['critid'] = $criteriaid;
2266         $oldparam = explode('_', $data->name);
2268         if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2269             $module = $this->get_mappingid('course_module', $oldparam[1]);
2270             $params['name'] = $oldparam[0] . '_' . $module;
2271             $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2272         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2273             $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2274             $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2275         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2276             $role = $this->get_mappingid('role', $data->value);
2277             if (!empty($role)) {
2278                 $params['name'] = 'role_' . $role;
2279                 $params['value'] = $role;
2280             } else {
2281                 return;
2282             }
2283         }
2285         if (!$DB->record_exists('badge_criteria_param', $params)) {
2286             $DB->insert_record('badge_criteria_param', $params);
2287         }
2288     }
2290     public function process_manual_award($data) {
2291         global $DB;
2293         $data = (object)$data;
2294         $role = $this->get_mappingid('role', $data->issuerrole);
2296         if (!empty($role)) {
2297             $award = array(
2298                 'badgeid'     => $this->get_new_parentid('badge'),
2299                 'recipientid' => $this->get_mappingid('user', $data->recipientid),
2300                 'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2301                 'issuerrole'  => $role,
2302                 'datemet'     => $this->apply_date_offset($data->datemet)
2303             );
2305             // Skip the manual award if recipient or issuer can not be mapped to.
2306             if (empty($award['recipientid']) || empty($award['issuerid'])) {
2307                 return;
2308             }
2310             $DB->insert_record('badge_manual_award', $award);
2311         }
2312     }
2314     protected function after_execute() {
2315         // Add related files.
2316         $this->add_related_files('badges', 'badgeimage', 'badge');
2317     }
2320 /**
2321  * This structure steps restores the calendar events
2322  */
2323 class restore_calendarevents_structure_step extends restore_structure_step {
2325     protected function define_structure() {
2327         $paths = array();
2329         $paths[] = new restore_path_element('calendarevents', '/events/event');
2331         return $paths;
2332     }
2334     public function process_calendarevents($data) {
2335         global $DB, $SITE;
2337         $data = (object)$data;
2338         $oldid = $data->id;
2339         $restorefiles = true; // We'll restore the files
2340         // Find the userid and the groupid associated with the event. Return if not found.
2341         $data->userid = $this->get_mappingid('user', $data->userid);
2342         if ($data->userid === false) {
2343             return;
2344         }
2345         if (!empty($data->groupid)) {
2346             $data->groupid = $this->get_mappingid('group', $data->groupid);
2347             if ($data->groupid === false) {
2348                 return;
2349             }
2350         }
2351         // Handle events with empty eventtype //MDL-32827
2352         if(empty($data->eventtype)) {
2353             if ($data->courseid == $SITE->id) {                                // Site event
2354                 $data->eventtype = "site";
2355             } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2356                 // Course assingment event
2357                 $data->eventtype = "due";
2358             } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2359                 $data->eventtype = "course";
2360             } else if ($data->groupid) {                                      // Group event
2361                 $data->eventtype = "group";
2362             } else if ($data->userid) {                                       // User event
2363                 $data->eventtype = "user";
2364             } else {
2365                 return;
2366             }
2367         }
2369         $params = array(
2370                 'name'           => $data->name,
2371                 'description'    => $data->description,
2372                 'format'         => $data->format,
2373                 'courseid'       => $this->get_courseid(),
2374                 'groupid'        => $data->groupid,
2375                 'userid'         => $data->userid,
2376                 'repeatid'       => $data->repeatid,
2377                 'modulename'     => $data->modulename,
2378                 'eventtype'      => $data->eventtype,
2379                 'timestart'      => $this->apply_date_offset($data->timestart),
2380                 'timeduration'   => $data->timeduration,
2381                 'visible'        => $data->visible,
2382                 'uuid'           => $data->uuid,
2383                 'sequence'       => $data->sequence,
2384                 'timemodified'    => $this->apply_date_offset($data->timemodified));
2385         if ($this->name == 'activity_calendar') {
2386             $params['instance'] = $this->task->get_activityid();
2387         } else {
2388             $params['instance'] = 0;
2389         }
2390         $sql = "SELECT id
2391                   FROM {event}
2392                  WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2393                    AND courseid = ?
2394                    AND repeatid = ?
2395                    AND modulename = ?
2396                    AND timestart = ?
2397                    AND timeduration = ?
2398                    AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2399         $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
2400         $result = $DB->record_exists_sql($sql, $arg);
2401         if (empty($result)) {
2402             $newitemid = $DB->insert_record('event', $params);
2403             $this->set_mapping('event', $oldid, $newitemid);
2404             $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2405         }
2407     }
2408     protected function after_execute() {
2409         // Add related files
2410         $this->add_related_files('calendar', 'event_description', 'event_description');
2411     }
2414 class restore_course_completion_structure_step extends restore_structure_step {
2416     /**
2417      * Conditionally decide if this step should be executed.
2418      *
2419      * This function checks parameters that are not immediate settings to ensure
2420      * that the enviroment is suitable for the restore of course completion info.
2421      *
2422      * This function checks the following four parameters:
2423      *
2424      *   1. Course completion is enabled on the site
2425      *   2. The backup includes course completion information
2426      *   3. All modules are restorable
2427      *   4. All modules are marked for restore.
2428      *
2429      * @return bool True is safe to execute, false otherwise
2430      */
2431     protected function execute_condition() {
2432         global $CFG;
2434         // First check course completion is enabled on this site
2435         if (empty($CFG->enablecompletion)) {
2436             // Disabled, don't restore course completion
2437             return false;
2438         }
2440         // Check it is included in the backup
2441         $fullpath = $this->task->get_taskbasepath();
2442         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2443         if (!file_exists($fullpath)) {
2444             // Not found, can't restore course completion
2445             return false;
2446         }
2448         // Check we are able to restore all backed up modules
2449         if ($this->task->is_missing_modules()) {
2450             return false;
2451         }
2453         // Finally check all modules within the backup are being restored.
2454         if ($this->task->is_excluding_activities()) {
2455             return false;
2456         }
2458         return true;
2459     }
2461     /**
2462      * Define the course completion structure
2463      *
2464      * @return array Array of restore_path_element
2465      */
2466     protected function define_structure() {
2468         // To know if we are including user completion info
2469         $userinfo = $this->get_setting_value('userscompletion');
2471         $paths = array();
2472         $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
2473         $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
2475         if ($userinfo) {
2476             $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
2477             $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
2478         }
2480         return $paths;
2482     }
2484     /**
2485      * Process course completion criteria
2486      *
2487      * @global moodle_database $DB
2488      * @param stdClass $data
2489      */
2490     public function process_course_completion_criteria($data) {
2491         global $DB;
2493         $data = (object)$data;
2494         $data->course = $this->get_courseid();
2496         // Apply the date offset to the time end field
2497         $data->timeend = $this->apply_date_offset($data->timeend);
2499         // Map the role from the criteria
2500         if (!empty($data->role)) {
2501             $data->role = $this->get_mappingid('role', $data->role);
2502         }
2504         $skipcriteria = false;
2506         // If the completion criteria is for a module we need to map the module instance
2507         // to the new module id.
2508         if (!empty($data->moduleinstance) && !empty($data->module)) {
2509             $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
2510             if (empty($data->moduleinstance)) {
2511                 $skipcriteria = true;
2512             }
2513         } else {
2514             $data->module = null;
2515             $data->moduleinstance = null;
2516         }
2518         // We backup the course shortname rather than the ID so that we can match back to the course
2519         if (!empty($data->courseinstanceshortname)) {
2520             $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
2521             if (!$courseinstanceid) {
2522                 $skipcriteria = true;
2523             }
2524         } else {
2525             $courseinstanceid = null;
2526         }
2527         $data->courseinstance = $courseinstanceid;
2529         if (!$skipcriteria) {
2530             $params = array(
2531                 'course'         => $data->course,
2532                 'criteriatype'   => $data->criteriatype,
2533                 'enrolperiod'    => $data->enrolperiod,
2534                 'courseinstance' => $data->courseinstance,
2535                 'module'         => $data->module,
2536                 'moduleinstance' => $data->moduleinstance,
2537                 'timeend'        => $data->timeend,
2538                 'gradepass'      => $data->gradepass,
2539                 'role'           => $data->role
2540             );
2541             $newid = $DB->insert_record('course_completion_criteria', $params);
2542             $this->set_mapping('course_completion_criteria', $data->id, $newid);
2543         }
2544     }
2546     /**
2547      * Processes course compltion criteria complete records
2548      *
2549      * @global moodle_database $DB
2550      * @param stdClass $data
2551      */
2552     public function process_course_completion_crit_compl($data) {
2553         global $DB;
2555         $data = (object)$data;
2557         // This may be empty if criteria could not be restored
2558         $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
2560         $data->course = $this->get_courseid();
2561         $data->userid = $this->get_mappingid('user', $data->userid);
2563         if (!empty($data->criteriaid) && !empty($data->userid)) {
2564             $params = array(
2565                 'userid' => $data->userid,
2566                 'course' => $data->course,
2567                 'criteriaid' => $data->criteriaid,
2568                 'timecompleted' => $this->apply_date_offset($data->timecompleted)
2569             );
2570             if (isset($data->gradefinal)) {
2571                 $params['gradefinal'] = $data->gradefinal;
2572             }
2573             if (isset($data->unenroled)) {
2574                 $params['unenroled'] = $data->unenroled;
2575             }
2576             $DB->insert_record('course_completion_crit_compl', $params);
2577         }
2578     }
2580     /**
2581      * Process course completions
2582      *
2583      * @global moodle_database $DB
2584      * @param stdClass $data
2585      */
2586     public function process_course_completions($data) {
2587         global $DB;
2589         $data = (object)$data;
2591         $data->course = $this->get_courseid();
2592         $data->userid = $this->get_mappingid('user', $data->userid);
2594         if (!empty($data->userid)) {
2595             $params = array(
2596                 'userid' => $data->userid,
2597                 'course' => $data->course,
2598                 'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
2599                 'timestarted' => $this->apply_date_offset($data->timestarted),
2600                 'timecompleted' => $this->apply_date_offset($data->timecompleted),
2601                 'reaggregate' => $data->reaggregate
2602             );
2603             $DB->insert_record('course_completions', $params);
2604         }
2605     }
2607     /**
2608      * Process course completion aggregate methods
2609      *
2610      * @global moodle_database $DB
2611      * @param stdClass $data
2612      */
2613     public function process_course_completion_aggr_methd($data) {
2614         global $DB;
2616         $data = (object)$data;
2618         $data->course = $this->get_courseid();
2620         // Only create the course_completion_aggr_methd records if
2621         // the target course has not them defined. MDL-28180
2622         if (!$DB->record_exists('course_completion_aggr_methd', array(
2623                     'course' => $data->course,
2624                     'criteriatype' => $data->criteriatype))) {
2625             $params = array(
2626                 'course' => $data->course,
2627                 'criteriatype' => $data->criteriatype,
2628                 'method' => $data->method,
2629                 'value' => $data->value,
2630             );
2631             $DB->insert_record('course_completion_aggr_methd', $params);
2632         }
2633     }
2637 /**
2638  * This structure step restores course logs (cmid = 0), delegating
2639  * the hard work to the corresponding {@link restore_logs_processor} passing the
2640  * collection of {@link restore_log_rule} rules to be observed as they are defined
2641  * by the task. Note this is only executed based in the 'logs' setting.
2642  *
2643  * NOTE: This is executed by final task, to have all the activities already restored
2644  *
2645  * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
2646  * records are. There are others like 'calendar' and 'upload' that will be handled
2647  * later.
2648  *
2649  * NOTE: All the missing actions (not able to be restored) are sent to logs for
2650  * debugging purposes
2651  */
2652 class restore_course_logs_structure_step extends restore_structure_step {
2654     /**
2655      * Conditionally decide if this step should be executed.
2656      *
2657      * This function checks the following parameter:
2658      *
2659      *   1. the course/logs.xml file exists
2660      *
2661      * @return bool true is safe to execute, false otherwise
2662      */
2663     protected function execute_condition() {
2665         // Check it is included in the backup
2666         $fullpath = $this->task->get_taskbasepath();
2667         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2668         if (!file_exists($fullpath)) {
2669             // Not found, can't restore course logs
2670             return false;
2671         }
2673         return true;
2674     }
2676     protected function define_structure() {
2678         $paths = array();
2680         // Simple, one plain level of information contains them
2681         $paths[] = new restore_path_element('log', '/logs/log');
2683         return $paths;
2684     }
2686     protected function process_log($data) {
2687         global $DB;
2689         $data = (object)($data);
2691         $data->time = $this->apply_date_offset($data->time);
2692         $data->userid = $this->get_mappingid('user', $data->userid);
2693         $data->course = $this->get_courseid();
2694         $data->cmid = 0;
2696         // For any reason user wasn't remapped ok, stop processing this
2697         if (empty($data->userid)) {
2698             return;
2699         }
2701         // Everything ready, let's delegate to the restore_logs_processor
2703         // Set some fixed values that will save tons of DB requests
2704         $values = array(
2705             'course' => $this->get_courseid());
2706         // Get instance and process log record
2707         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2709         // If we have data, insert it, else something went wrong in the restore_logs_processor
2710         if ($data) {
2711             if (empty($data->url)) {
2712                 $data->url = '';
2713             }
2714             if (empty($data->info)) {
2715                 $data->info = '';
2716             }
2717             // Store the data in the legacy log table if we are still using it.
2718             $manager = get_log_manager();
2719             if (method_exists($manager, 'legacy_add_to_log')) {
2720                 $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2721                     $data->info, $data->cmid, $data->userid);
2722             }
2723         }
2724     }
2727 /**
2728  * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
2729  * sharing its same structure but modifying the way records are handled
2730  */
2731 class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
2733     protected function process_log($data) {
2734         global $DB;
2736         $data = (object)($data);
2738         $data->time = $this->apply_date_offset($data->time);
2739         $data->userid = $this->get_mappingid('user', $data->userid);
2740         $data->course = $this->get_courseid();
2741         $data->cmid = $this->task->get_moduleid();
2743         // For any reason user wasn't remapped ok, stop processing this
2744         if (empty($data->userid)) {
2745             return;
2746         }
2748         // Everything ready, let's delegate to the restore_logs_processor
2750         // Set some fixed values that will save tons of DB requests
2751         $values = array(
2752             'course' => $this->get_courseid(),
2753             'course_module' => $this->task->get_moduleid(),
2754             $this->task->get_modulename() => $this->task->get_activityid());
2755         // Get instance and process log record
2756         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2758         // If we have data, insert it, else something went wrong in the restore_logs_processor
2759         if ($data) {
2760             if (empty($data->url)) {
2761                 $data->url = '';
2762             }
2763             if (empty($data->info)) {
2764                 $data->info = '';
2765             }
2766             // Store the data in the legacy log table if we are still using it.
2767             $manager = get_log_manager();
2768             if (method_exists($manager, 'legacy_add_to_log')) {
2769                 $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2770                     $data->info, $data->cmid, $data->userid);
2771             }
2772         }
2773     }
2777 /**
2778  * Defines the restore step for advanced grading methods attached to the activity module
2779  */
2780 class restore_activity_grading_structure_step extends restore_structure_step {
2782     /**
2783      * This step is executed only if the grading file is present
2784      */
2785      protected function execute_condition() {
2787         $fullpath = $this->task->get_taskbasepath();
2788         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2789         if (!file_exists($fullpath)) {
2790             return false;
2791         }
2793         return true;
2794     }
2797     /**
2798      * Declares paths in the grading.xml file we are interested in
2799      */
2800     protected function define_structure() {
2802         $paths = array();
2803         $userinfo = $this->get_setting_value('userinfo');
2805         $area = new restore_path_element('grading_area', '/areas/area');
2806         $paths[] = $area;
2807         // attach local plugin stucture to $area element
2808         $this->add_plugin_structure('local', $area);
2810         $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
2811         $paths[] = $definition;
2812         $this->add_plugin_structure('gradingform', $definition);
2813         // attach local plugin stucture to $definition element
2814         $this->add_plugin_structure('local', $definition);
2817         if ($userinfo) {
2818             $instance = new restore_path_element('grading_instance',
2819                 '/areas/area/definitions/definition/instances/instance');
2820             $paths[] = $instance;
2821             $this->add_plugin_structure('gradingform', $instance);
2822             // attach local plugin stucture to $intance element
2823             $this->add_plugin_structure('local', $instance);
2824         }
2826         return $paths;
2827     }
2829     /**
2830      * Processes one grading area element
2831      *
2832      * @param array $data element data
2833      */
2834     protected function process_grading_area($data) {
2835         global $DB;
2837         $task = $this->get_task();
2838         $data = (object)$data;
2839         $oldid = $data->id;
2840         $data->component = 'mod_'.$task->get_modulename();
2841         $data->contextid = $task->get_contextid();
2843         $newid = $DB->insert_record('grading_areas', $data);
2844         $this->set_mapping('grading_area', $oldid, $newid);
2845     }
2847     /**
2848      * Processes one grading definition element
2849      *
2850      * @param array $data element data
2851      */
2852     protected function process_grading_definition($data) {
2853         global $DB;
2855         $task = $this->get_task();
2856         $data = (object)$data;
2857         $oldid = $data->id;
2858         $data->areaid = $this->get_new_parentid('grading_area');
2859         $data->copiedfromid = null;
2860         $data->timecreated = time();
2861         $data->usercreated = $task->get_userid();
2862         $data->timemodified = $data->timecreated;
2863         $data->usermodified = $data->usercreated;
2865         $newid = $DB->insert_record('grading_definitions', $data);
2866         $this->set_mapping('grading_definition', $oldid, $newid, true);
2867     }
2869     /**
2870      * Processes one grading form instance element
2871      *
2872      * @param array $data element data
2873      */
2874     protected function process_grading_instance($data) {
2875         global $DB;
2877         $data = (object)$data;
2879         // new form definition id
2880         $newformid = $this->get_new_parentid('grading_definition');
2882         // get the name of the area we are restoring to
2883         $sql = "SELECT ga.areaname
2884                   FROM {grading_definitions} gd
2885                   JOIN {grading_areas} ga ON gd.areaid = ga.id
2886                  WHERE gd.id = ?";
2887         $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
2889         // get the mapped itemid - the activity module is expected to define the mappings
2890         // for each gradable area
2891         $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
2893         $oldid = $data->id;
2894         $data->definitionid = $newformid;
2895         $data->raterid = $this->get_mappingid('user', $data->raterid);
2896         $data->itemid = $newitemid;
2898         $newid = $DB->insert_record('grading_instances', $data);
2899         $this->set_mapping('grading_instance', $oldid, $newid);
2900     }
2902     /**
2903      * Final operations when the database records are inserted
2904      */
2905     protected function after_execute() {
2906         // Add files embedded into the definition description
2907         $this->add_related_files('grading', 'description', 'grading_definition');
2908     }
2912 /**
2913  * This structure step restores the grade items associated with one activity
2914  * All the grade items are made child of the "course" grade item but the original
2915  * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
2916  * the complete gradebook (categories and calculations), that information is
2917  * available there
2918  */
2919 class restore_activity_grades_structure_step extends restore_structure_step {
2921     protected function define_structure() {
2923         $paths = array();
2924         $userinfo = $this->get_setting_value('userinfo');
2926         $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
2927         $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
2928         if ($userinfo) {
2929             $paths[] = new restore_path_element('grade_grade',
2930                            '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
2931         }
2932         return $paths;
2933     }
2935     protected function process_grade_item($data) {
2936         global $DB;
2938         $data = (object)($data);
2939         $oldid       = $data->id;        // We'll need these later
2940         $oldparentid = $data->categoryid;
2941         $courseid = $this->get_courseid();
2943         // make sure top course category exists, all grade items will be associated
2944         // to it. Later, if restoring the whole gradebook, categories will be introduced
2945         $coursecat = grade_category::fetch_course_category($courseid);
2946         $coursecatid = $coursecat->id; // Get the categoryid to be used
2948         $idnumber = null;
2949         if (!empty($data->idnumber)) {
2950             // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
2951             // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
2952             // so the best is to keep the ones already in the gradebook
2953             // Potential problem: duplicates if same items are restored more than once. :-(
2954             // This needs to be fixed in some way (outcomes & activities with multiple items)
2955             // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
2956             // In any case, verify always for uniqueness
2957             $sql = "SELECT cm.id
2958                       FROM {course_modules} cm
2959                      WHERE cm.course = :courseid AND
2960                            cm.idnumber = :idnumber AND
2961                            cm.id <> :cmid";
2962             $params = array(
2963                 'courseid' => $courseid,
2964                 'idnumber' => $data->idnumber,
2965                 'cmid' => $this->task->get_moduleid()
2966             );
2967             if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
2968                 $idnumber = $data->idnumber;
2969             }
2970         }
2972         unset($data->id);
2973         $data->categoryid   = $coursecatid;
2974         $data->courseid     = $this->get_courseid();
2975         $data->iteminstance = $this->task->get_activityid();
2976         $data->idnumber     = $idnumber;
2977         $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
2978         $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
2979         $data->timecreated  = $this->apply_date_offset($data->timecreated);
2980         $data->timemodified = $this->apply_date_offset($data->timemodified);
2982         $gradeitem = new grade_item($data, false);
2983         $gradeitem->insert('restore');
2985         //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
2986         $gradeitem->sortorder = $data->sortorder;
2987         $gradeitem->update('restore');
2989         // Set mapping, saving the original category id into parentitemid
2990         // gradebook restore (final task) will need it to reorganise items
2991         $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
2992     }
2994     protected function process_grade_grade($data) {
2995         $data = (object)($data);
2996         $olduserid = $data->userid;
2997         $oldid = $data->id;
2998         unset($data->id);
3000         $data->itemid = $this->get_new_parentid('grade_item');
3002         $data->userid = $this->get_mappingid('user', $data->userid, null);
3003         if (!empty($data->userid)) {
3004             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3005             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3006             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
3007             $data->overridden = $this->apply_date_offset($data->overridden);
3009             $grade = new grade_grade($data, false);
3010             $grade->insert('restore');
3011             $this->set_mapping('grade_grades', $oldid, $grade->id);
3012         } else {
3013             debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
3014         }
3015     }
3017     /**
3018      * process activity grade_letters. Note that, while these are possible,
3019      * because grade_letters are contextid based, in practice, only course
3020      * context letters can be defined. So we keep here this method knowing
3021      * it won't be executed ever. gradebook restore will restore course letters.
3022      */
3023     protected function process_grade_letter($data) {
3024         global $DB;
3026         $data['contextid'] = $this->task->get_contextid();
3027         $gradeletter = (object)$data;
3029         // Check if it exists before adding it
3030         unset($data['id']);
3031         if (!$DB->record_exists('grade_letters', $data)) {
3032             $newitemid = $DB->insert_record('grade_letters', $gradeletter);
3033         }
3034         // no need to save any grade_letter mapping
3035     }
3037     public function after_restore() {
3038         // Fix grade item's sortorder after restore, as it might have duplicates.
3039         $courseid = $this->get_task()->get_courseid();
3040         grade_item::fix_duplicate_sortorder($courseid);
3041     }
3044 /**
3045  * Step in charge of restoring the grade history of an activity.
3046  *
3047  * This step is added to the task regardless of the setting 'grade_histories'.
3048  * The reason is to allow for a more flexible step in case the logic needs to be
3049  * split accross different settings to control the history of items and/or grades.
3050  */
3051 class restore_activity_grade_history_structure_step extends restore_structure_step {
3053     /**
3054      * This step is executed only if the grade history file is present.
3055      */
3056      protected function execute_condition() {
3057         $fullpath = $this->task->get_taskbasepath();
3058         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3059         if (!file_exists($fullpath)) {
3060             return false;
3061         }
3062         return true;
3063     }
3065     protected function define_structure() {
3066         $paths = array();
3068         // Settings to use.
3069         $userinfo = $this->get_setting_value('userinfo');
3070         $history = $this->get_setting_value('grade_histories');
3072         if ($userinfo && $history) {
3073             $paths[] = new restore_path_element('grade_grade',
3074                '/grade_history/grade_grades/grade_grade');
3075         }
3077         return $paths;
3078     }
3080     protected function process_grade_grade($data) {
3081         global $DB;
3083         $data = (object) $data;
3084         $olduserid = $data->userid;
3085         unset($data->id);
3087         $data->userid = $this->get_mappingid('user', $data->userid, null);
3088         if (!empty($data->userid)) {
3089             // Do not apply the date offsets as this is history.
3090             $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
3091             $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
3092             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3093             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3094             $DB->insert_record('grade_grades_history', $data);
3095         } else {
3096             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
3097             $this->log($message, backup::LOG_DEBUG);
3098         }
3099     }
3103 /**
3104  * This structure steps restores one instance + positions of one block
3105  * Note: Positions corresponding to one existing context are restored
3106  * here, but all the ones having unknown contexts are sent to backup_ids
3107  * for a later chance to be restored at the end (final task)
3108  */
3109 class restore_block_instance_structure_step extends restore_structure_step {
3111     protected function define_structure() {
3113         $paths = array();
3115         $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
3116         $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
3118         return $paths;
3119     }
3121     public function process_block($data) {
3122         global $DB, $CFG;
3124         $data = (object)$data; // Handy
3125         $oldcontextid = $data->contextid;
3126         $oldid        = $data->id;
3127         $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
3129         // Look for the parent contextid
3130         if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
3131             throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
3132         }
3134         // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
3135         // If there is already one block of that type in the parent context
3136         // and the block is not multiple, stop processing
3137         // Use blockslib loader / method executor
3138         if (!$bi = block_instance($data->blockname)) {
3139             return false;
3140         }
3142         if (!$bi->instance_allow_multiple()) {
3143             if ($DB->record_exists_sql("SELECT bi.id
3144                                           FROM {block_instances} bi
3145                                           JOIN {block} b ON b.name = bi.blockname
3146                                          WHERE bi.parentcontextid = ?
3147                                            AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) {
3148                 return false;
3149             }
3150         }
3152         // If there is already one block of that type in the parent context
3153         // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
3154         // stop processing
3155         $params = array(
3156             'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
3157             'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
3158             'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
3159         if ($birecs = $DB->get_records('block_instances', $params)) {
3160             foreach($birecs as $birec) {
3161                 if ($birec->configdata == $data->configdata) {
3162                     return false;
3163                 }
3164             }
3165         }
3167         // Set task old contextid, blockid and blockname once we know them
3168         $this->task->set_old_contextid($oldcontextid);
3169         $this->task->set_old_blockid($oldid);
3170         $this->task->set_blockname($data->blockname);
3172         // Let's look for anything within configdata neededing processing
3173         // (nulls and uses of legacy file.php)
3174         if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
3175             $configdata = (array)unserialize(base64_decode($data->configdata));
3176             foreach ($configdata as $attribute => $value) {
3177                 if (in_array($attribute, $attrstotransform)) {
3178                     $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
3179                 }
3180             }
3181             $data->configdata = base64_encode(serialize((object)$configdata));
3182         }
3184         // Create the block instance
3185         $newitemid = $DB->insert_record('block_instances', $data);
3186         // Save the mapping (with restorefiles support)
3187         $this->set_mapping('block_instance', $oldid, $newitemid, true);
3188         // Create the block context
3189         $newcontextid = context_block::instance($newitemid)->id;
3190         // Save the block contexts mapping and sent it to task
3191         $this->set_mapping('context', $oldcontextid, $newcontextid);
3192         $this->task->set_contextid($newcontextid);
3193         $this->task->set_blockid($newitemid);
3195         // Restore block fileareas if declared
3196         $component = 'block_' . $this->task->get_blockname();
3197         foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
3198             $this->add_related_files($component, $filearea, null);
3199         }
3201         // Process block positions, creating them or accumulating for final step
3202         foreach($positions as $position) {
3203             $position = (object)$position;
3204             $position->blockinstanceid = $newitemid; // The instance is always the restored one
3205             // If position is for one already mapped (known) contextid
3206             // process it now, creating the position
3207             if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
3208                 $position->contextid = $newpositionctxid;
3209                 // Create the block position
3210                 $DB->insert_record('block_positions', $position);
3212             // The position belongs to an unknown context, send it to backup_ids
3213             // to process them as part of the final steps of restore. We send the
3214             // whole $position object there, hence use the low level method.
3215             } else {
3216                 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
3217             }
3218         }
3219     }
3222 /**
3223  * Structure step to restore common course_module information
3224  *
3225  * This step will process the module.xml file for one activity, in order to restore
3226  * the corresponding information to the course_modules table, skipping various bits
3227  * of information based on CFG settings (groupings, completion...) in order to fullfill
3228  * all the reqs to be able to create the context to be used by all the rest of steps
3229  * in the activity restore task
3230  */
3231 class restore_module_structure_step extends restore_structure_step {
3233     protected function define_structure() {
3234         global $CFG;
3236         $paths = array();
3238         $module = new restore_path_element('module', '/module');
3239         $paths[] = $module;
3240         if ($CFG->enableavailability) {
3241             $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
3242             $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
3243         }
3245         // Apply for 'format' plugins optional paths at module level
3246         $this->add_plugin_structure('format', $module);
3248         // Apply for 'plagiarism' plugins optional paths at module level
3249         $this->add_plugin_structure('plagiarism', $module);
3251         // Apply for 'local' plugins optional paths at module level
3252         $this->add_plugin_structure('local', $module);
3254         return $paths;
3255     }
3257     protected function process_module($data) {
3258         global $CFG, $DB;