MDL-10971 question type multianswer: Respect quiz shuffle option
[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         // Update all sections that were restored.
649         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
650         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
651         $sectionsbyid = null;
652         foreach ($rs as $rec) {
653             if (is_null($sectionsbyid)) {
654                 $sectionsbyid = array();
655                 foreach ($modinfo->get_section_info_all() as $section) {
656                     $sectionsbyid[$section->id] = $section;
657                 }
658             }
659             if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
660                 // If the section was not fully restored for some reason
661                 // (e.g. due to an earlier error), skip it.
662                 $this->get_logger()->process('Section not fully restored: id ' .
663                         $rec->newitemid, backup::LOG_WARNING);
664                 continue;
665             }
666             $section = $sectionsbyid[$rec->newitemid];
667             if (!is_null($section->availability)) {
668                 $info = new \core_availability\info_section($section);
669                 $info->update_after_restore($this->get_restoreid(),
670                         $this->get_courseid(), $this->get_logger());
671             }
672         }
673         $rs->close();
675         // Update all modules that were restored.
676         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
677         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
678         foreach ($rs as $rec) {
679             if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
680                 // If the module was not fully restored for some reason
681                 // (e.g. due to an earlier error), skip it.
682                 $this->get_logger()->process('Module not fully restored: id ' .
683                         $rec->newitemid, backup::LOG_WARNING);
684                 continue;
685             }
686             $cm = $modinfo->get_cm($rec->newitemid);
687             if (!is_null($cm->availability)) {
688                 $info = new \core_availability\info_module($cm);
689                 $info->update_after_restore($this->get_restoreid(),
690                         $this->get_courseid(), $this->get_logger());
691             }
692         }
693         $rs->close();
694     }
698 /**
699  * Process legacy module availability records in backup_ids.
700  *
701  * Matches course modules and grade item id once all them have been already restored.
702  * Only if all matchings are satisfied the availability condition will be created.
703  * At the same time, it is required for the site to have that functionality enabled.
704  *
705  * This step is included only to handle legacy backups (2.6 and before). It does not
706  * do anything for newer backups.
707  *
708  * @copyright 2014 The Open University
709  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
710  */
711 class restore_process_course_modules_availability extends restore_execution_step {
713     protected function define_execution() {
714         global $CFG, $DB;
716         // Site hasn't availability enabled
717         if (empty($CFG->enableavailability)) {
718             return;
719         }
721         // Do both modules and sections.
722         foreach (array('module', 'section') as $table) {
723             // Get all the availability objects to process.
724             $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
725             $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
726             // Process availabilities, creating them if everything matches ok.
727             foreach ($rs as $availrec) {
728                 $allmatchesok = true;
729                 // Get the complete legacy availability object.
730                 $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
732                 // Note: This code used to update IDs, but that is now handled by the
733                 // current code (after restore) instead of this legacy code.
735                 // Get showavailability option.
736                 $thingid = ($table === 'module') ? $availability->coursemoduleid :
737                         $availability->coursesectionid;
738                 $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
739                         $table . '_showavailability', $thingid);
740                 if (!$showrec) {
741                     // Should not happen.
742                     throw new coding_exception('No matching showavailability record');
743                 }
744                 $show = $showrec->info->showavailability;
746                 // The $availability object is now in the format used in the old
747                 // system. Interpret this and convert to new system.
748                 $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
749                         array('id' => $thingid), MUST_EXIST);
750                 $newvalue = \core_availability\info::add_legacy_availability_condition(
751                         $currentvalue, $availability, $show);
752                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
753                         array('id' => $thingid));
754             }
755         }
756         $rs->close();
757     }
761 /*
762  * Execution step that, *conditionally* (if there isn't preloaded information)
763  * will load the inforef files for all the included course/section/activity tasks
764  * to backup_temp_ids. They will be stored with "xxxxref" as itemname
765  */
766 class restore_load_included_inforef_records extends restore_execution_step {
768     protected function define_execution() {
770         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
771             return;
772         }
774         // Get all the included tasks
775         $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
776         $progress = $this->task->get_progress();
777         $progress->start_progress($this->get_name(), count($tasks));
778         foreach ($tasks as $task) {
779             // Load the inforef.xml file if exists
780             $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
781             if (file_exists($inforefpath)) {
782                 // Load each inforef file to temp_ids.
783                 restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
784             }
785         }
786         $progress->end_progress();
787     }
790 /*
791  * Execution step that will load all the needed files into backup_files_temp
792  *   - info: contains the whole original object (times, names...)
793  * (all them being original ids as loaded from xml)
794  */
795 class restore_load_included_files extends restore_structure_step {
797     protected function define_structure() {
799         $file = new restore_path_element('file', '/files/file');
801         return array($file);
802     }
804     /**
805      * Process one <file> element from files.xml
806      *
807      * @param array $data the element data
808      */
809     public function process_file($data) {
811         $data = (object)$data; // handy
813         // load it if needed:
814         //   - it it is one of the annotated inforef files (course/section/activity/block)
815         //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
816         // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
817         //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
818         $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
819         $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
820                         $data->component == 'grouping' || $data->component == 'grade' ||
821                         $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
822         if ($isfileref || $iscomponent) {
823             restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
824         }
825     }
828 /**
829  * Execution step that, *conditionally* (if there isn't preloaded information),
830  * will load all the needed roles to backup_temp_ids. They will be stored with
831  * "role" itemname. Also it will perform one automatic mapping to roles existing
832  * in the target site, based in permissions of the user performing the restore,
833  * archetypes and other bits. At the end, each original role will have its associated
834  * target role or 0 if it's going to be skipped. Note we wrap everything over one
835  * restore_dbops method, as far as the same stuff is going to be also executed
836  * by restore prechecks
837  */
838 class restore_load_and_map_roles extends restore_execution_step {
840     protected function define_execution() {
841         if ($this->task->get_preloaded_information()) { // if info is already preloaded
842             return;
843         }
845         $file = $this->get_basepath() . '/roles.xml';
846         // Load needed toles to temp_ids
847         restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
849         // Process roles, mapping/skipping. Any error throws exception
850         // Note we pass controller's info because it can contain role mapping information
851         // about manual mappings performed by UI
852         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);
853     }
856 /**
857  * Execution step that, *conditionally* (if there isn't preloaded information
858  * and users have been selected in settings, will load all the needed users
859  * to backup_temp_ids. They will be stored with "user" itemname and with
860  * their original contextid as paremitemid
861  */
862 class restore_load_included_users extends restore_execution_step {
864     protected function define_execution() {
866         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
867             return;
868         }
869         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
870             return;
871         }
872         $file = $this->get_basepath() . '/users.xml';
873         // Load needed users to temp_ids.
874         restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
875     }
878 /**
879  * Execution step that, *conditionally* (if there isn't preloaded information
880  * and users have been selected in settings, will process all the needed users
881  * in order to decide and perform any action with them (create / map / error)
882  * Note: Any error will cause exception, as far as this is the same processing
883  * than the one into restore prechecks (that should have stopped process earlier)
884  */
885 class restore_process_included_users extends restore_execution_step {
887     protected function define_execution() {
889         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
890             return;
891         }
892         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
893             return;
894         }
895         restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
896                 $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
897     }
900 /**
901  * Execution step that will create all the needed users as calculated
902  * by @restore_process_included_users (those having newiteind = 0)
903  */
904 class restore_create_included_users extends restore_execution_step {
906     protected function define_execution() {
908         restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
909                 $this->task->get_userid(), $this->task->get_progress());
910     }
913 /**
914  * Structure step that will create all the needed groups and groupings
915  * by loading them from the groups.xml file performing the required matches.
916  * Note group members only will be added if restoring user info
917  */
918 class restore_groups_structure_step extends restore_structure_step {
920     protected function define_structure() {
922         $paths = array(); // Add paths here
924         $paths[] = new restore_path_element('group', '/groups/group');
925         $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
926         $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
928         return $paths;
929     }
931     // Processing functions go here
932     public function process_group($data) {
933         global $DB;
935         $data = (object)$data; // handy
936         $data->courseid = $this->get_courseid();
938         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
939         // another a group in the same course
940         $context = context_course::instance($data->courseid);
941         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
942             if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
943                 unset($data->idnumber);
944             }
945         } else {
946             unset($data->idnumber);
947         }
949         $oldid = $data->id;    // need this saved for later
951         $restorefiles = false; // Only if we end creating the group
953         // Search if the group already exists (by name & description) in the target course
954         $description_clause = '';
955         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
956         if (!empty($data->description)) {
957             $description_clause = ' AND ' .
958                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
959            $params['description'] = $data->description;
960         }
961         if (!$groupdb = $DB->get_record_sql("SELECT *
962                                                FROM {groups}
963                                               WHERE courseid = :courseid
964                                                 AND name = :grname $description_clause", $params)) {
965             // group doesn't exist, create
966             $newitemid = $DB->insert_record('groups', $data);
967             $restorefiles = true; // We'll restore the files
968         } else {
969             // group exists, use it
970             $newitemid = $groupdb->id;
971         }
972         // Save the id mapping
973         $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
974         // Invalidate the course group data cache just in case.
975         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
976     }
978     public function process_grouping($data) {
979         global $DB;
981         $data = (object)$data; // handy
982         $data->courseid = $this->get_courseid();
984         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
985         // another a grouping in the same course
986         $context = context_course::instance($data->courseid);
987         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
988             if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
989                 unset($data->idnumber);
990             }
991         } else {
992             unset($data->idnumber);
993         }
995         $oldid = $data->id;    // need this saved for later
996         $restorefiles = false; // Only if we end creating the grouping
998         // Search if the grouping already exists (by name & description) in the target course
999         $description_clause = '';
1000         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1001         if (!empty($data->description)) {
1002             $description_clause = ' AND ' .
1003                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1004            $params['description'] = $data->description;
1005         }
1006         if (!$groupingdb = $DB->get_record_sql("SELECT *
1007                                                   FROM {groupings}
1008                                                  WHERE courseid = :courseid
1009                                                    AND name = :grname $description_clause", $params)) {
1010             // grouping doesn't exist, create
1011             $newitemid = $DB->insert_record('groupings', $data);
1012             $restorefiles = true; // We'll restore the files
1013         } else {
1014             // grouping exists, use it
1015             $newitemid = $groupingdb->id;
1016         }
1017         // Save the id mapping
1018         $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1019         // Invalidate the course group data cache just in case.
1020         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1021     }
1023     public function process_grouping_group($data) {
1024         global $CFG;
1026         require_once($CFG->dirroot.'/group/lib.php');
1028         $data = (object)$data;
1029         groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1030     }
1032     protected function after_execute() {
1033         // Add group related files, matching with "group" mappings
1034         $this->add_related_files('group', 'icon', 'group');
1035         $this->add_related_files('group', 'description', 'group');
1036         // Add grouping related files, matching with "grouping" mappings
1037         $this->add_related_files('grouping', 'description', 'grouping');
1038         // Invalidate the course group data.
1039         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1040     }
1044 /**
1045  * Structure step that will create all the needed group memberships
1046  * by loading them from the groups.xml file performing the required matches.
1047  */
1048 class restore_groups_members_structure_step extends restore_structure_step {
1050     protected $plugins = null;
1052     protected function define_structure() {
1054         $paths = array(); // Add paths here
1056         if ($this->get_setting_value('users')) {
1057             $paths[] = new restore_path_element('group', '/groups/group');
1058             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1059         }
1061         return $paths;
1062     }
1064     public function process_group($data) {
1065         $data = (object)$data; // handy
1067         // HACK ALERT!
1068         // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1069         // Let's fake internal state to make $this->get_new_parentid('group') work.
1071         $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1072     }
1074     public function process_member($data) {
1075         global $DB, $CFG;
1076         require_once("$CFG->dirroot/group/lib.php");
1078         // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1080         $data = (object)$data; // handy
1082         // get parent group->id
1083         $data->groupid = $this->get_new_parentid('group');
1085         // map user newitemid and insert if not member already
1086         if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1087             if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1088                 // Check the component, if any, exists.
1089                 if (empty($data->component)) {
1090                     groups_add_member($data->groupid, $data->userid);
1092                 } else if ((strpos($data->component, 'enrol_') === 0)) {
1093                     // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1094                     // it is possible that enrolment was restored using different plugin type.
1095                     if (!isset($this->plugins)) {
1096                         $this->plugins = enrol_get_plugins(true);
1097                     }
1098                     if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1099                         if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1100                             if (isset($this->plugins[$instance->enrol])) {
1101                                 $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1102                             }
1103                         }
1104                     }
1106                 } else {
1107                     $dir = core_component::get_component_directory($data->component);
1108                     if ($dir and is_dir($dir)) {
1109                         if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1110                             return;
1111                         }
1112                     }
1113                     // Bad luck, plugin could not restore the data, let's add normal membership.
1114                     groups_add_member($data->groupid, $data->userid);
1115                     $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1116                     $this->log($message, backup::LOG_WARNING);
1117                 }
1118             }
1119         }
1120     }
1123 /**
1124  * Structure step that will create all the needed scales
1125  * by loading them from the scales.xml
1126  */
1127 class restore_scales_structure_step extends restore_structure_step {
1129     protected function define_structure() {
1131         $paths = array(); // Add paths here
1132         $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1133         return $paths;
1134     }
1136     protected function process_scale($data) {
1137         global $DB;
1139         $data = (object)$data;
1141         $restorefiles = false; // Only if we end creating the group
1143         $oldid = $data->id;    // need this saved for later
1145         // Look for scale (by 'scale' both in standard (course=0) and current course
1146         // with priority to standard scales (ORDER clause)
1147         // scale is not course unique, use get_record_sql to suppress warning
1148         // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1149         $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1150         $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1151         if (!$scadb = $DB->get_record_sql("SELECT *
1152                                             FROM {scale}
1153                                            WHERE courseid IN (0, :courseid)
1154                                              AND $compare_scale_clause
1155                                         ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1156             // Remap the user if possible, defaut to user performing the restore if not
1157             $userid = $this->get_mappingid('user', $data->userid);
1158             $data->userid = $userid ? $userid : $this->task->get_userid();
1159             // Remap the course if course scale
1160             $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1161             // If global scale (course=0), check the user has perms to create it
1162             // falling to course scale if not
1163             $systemctx = context_system::instance();
1164             if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1165                 $data->courseid = $this->get_courseid();
1166             }
1167             // scale doesn't exist, create
1168             $newitemid = $DB->insert_record('scale', $data);
1169             $restorefiles = true; // We'll restore the files
1170         } else {
1171             // scale exists, use it
1172             $newitemid = $scadb->id;
1173         }
1174         // Save the id mapping (with files support at system context)
1175         $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1176     }
1178     protected function after_execute() {
1179         // Add scales related files, matching with "scale" mappings
1180         $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1181     }
1185 /**
1186  * Structure step that will create all the needed outocomes
1187  * by loading them from the outcomes.xml
1188  */
1189 class restore_outcomes_structure_step extends restore_structure_step {
1191     protected function define_structure() {
1193         $paths = array(); // Add paths here
1194         $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1195         return $paths;
1196     }
1198     protected function process_outcome($data) {
1199         global $DB;
1201         $data = (object)$data;
1203         $restorefiles = false; // Only if we end creating the group
1205         $oldid = $data->id;    // need this saved for later
1207         // Look for outcome (by shortname both in standard (courseid=null) and current course
1208         // with priority to standard outcomes (ORDER clause)
1209         // outcome is not course unique, use get_record_sql to suppress warning
1210         $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1211         if (!$outdb = $DB->get_record_sql('SELECT *
1212                                              FROM {grade_outcomes}
1213                                             WHERE shortname = :shortname
1214                                               AND (courseid = :courseid OR courseid IS NULL)
1215                                          ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1216             // Remap the user
1217             $userid = $this->get_mappingid('user', $data->usermodified);
1218             $data->usermodified = $userid ? $userid : $this->task->get_userid();
1219             // Remap the scale
1220             $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1221             // Remap the course if course outcome
1222             $data->courseid = $data->courseid ? $this->get_courseid() : null;
1223             // If global outcome (course=null), check the user has perms to create it
1224             // falling to course outcome if not
1225             $systemctx = context_system::instance();
1226             if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1227                 $data->courseid = $this->get_courseid();
1228             }
1229             // outcome doesn't exist, create
1230             $newitemid = $DB->insert_record('grade_outcomes', $data);
1231             $restorefiles = true; // We'll restore the files
1232         } else {
1233             // scale exists, use it
1234             $newitemid = $outdb->id;
1235         }
1236         // Set the corresponding grade_outcomes_courses record
1237         $outcourserec = new stdclass();
1238         $outcourserec->courseid  = $this->get_courseid();
1239         $outcourserec->outcomeid = $newitemid;
1240         if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1241             $DB->insert_record('grade_outcomes_courses', $outcourserec);
1242         }
1243         // Save the id mapping (with files support at system context)
1244         $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1245     }
1247     protected function after_execute() {
1248         // Add outcomes related files, matching with "outcome" mappings
1249         $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1250     }
1253 /**
1254  * Execution step that, *conditionally* (if there isn't preloaded information
1255  * will load all the question categories and questions (header info only)
1256  * to backup_temp_ids. They will be stored with "question_category" and
1257  * "question" itemnames and with their original contextid and question category
1258  * id as paremitemids
1259  */
1260 class restore_load_categories_and_questions extends restore_execution_step {
1262     protected function define_execution() {
1264         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1265             return;
1266         }
1267         $file = $this->get_basepath() . '/questions.xml';
1268         restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1269     }
1272 /**
1273  * Execution step that, *conditionally* (if there isn't preloaded information)
1274  * will process all the needed categories and questions
1275  * in order to decide and perform any action with them (create / map / error)
1276  * Note: Any error will cause exception, as far as this is the same processing
1277  * than the one into restore prechecks (that should have stopped process earlier)
1278  */
1279 class restore_process_categories_and_questions extends restore_execution_step {
1281     protected function define_execution() {
1283         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1284             return;
1285         }
1286         restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1287     }
1290 /**
1291  * Structure step that will read the section.xml creating/updating sections
1292  * as needed, rebuilding course cache and other friends
1293  */
1294 class restore_section_structure_step extends restore_structure_step {
1296     protected function define_structure() {
1297         global $CFG;
1299         $paths = array();
1301         $section = new restore_path_element('section', '/section');
1302         $paths[] = $section;
1303         if ($CFG->enableavailability) {
1304             $paths[] = new restore_path_element('availability', '/section/availability');
1305             $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1306         }
1307         $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1309         // Apply for 'format' plugins optional paths at section level
1310         $this->add_plugin_structure('format', $section);
1312         // Apply for 'local' plugins optional paths at section level
1313         $this->add_plugin_structure('local', $section);
1315         return $paths;
1316     }
1318     public function process_section($data) {
1319         global $CFG, $DB;
1320         $data = (object)$data;
1321         $oldid = $data->id; // We'll need this later
1323         $restorefiles = false;
1325         // Look for the section
1326         $section = new stdclass();
1327         $section->course  = $this->get_courseid();
1328         $section->section = $data->number;
1329         // Section doesn't exist, create it with all the info from backup
1330         if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
1331             $section->name = $data->name;
1332             $section->summary = $data->summary;
1333             $section->summaryformat = $data->summaryformat;
1334             $section->sequence = '';
1335             $section->visible = $data->visible;
1336             if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1337                 $section->availability = null;
1338             } else {
1339                 $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1340                 // Include legacy [<2.7] availability data if provided.
1341                 if (is_null($section->availability)) {
1342                     $section->availability = \core_availability\info::convert_legacy_fields(
1343                             $data, true);
1344                 }
1345             }
1346             $newitemid = $DB->insert_record('course_sections', $section);
1347             $restorefiles = true;
1349         // Section exists, update non-empty information
1350         } else {
1351             $section->id = $secrec->id;
1352             if ((string)$secrec->name === '') {
1353                 $section->name = $data->name;
1354             }
1355             if (empty($secrec->summary)) {
1356                 $section->summary = $data->summary;
1357                 $section->summaryformat = $data->summaryformat;
1358                 $restorefiles = true;
1359             }
1361             // Don't update availability (I didn't see a useful way to define
1362             // whether existing or new one should take precedence).
1364             $DB->update_record('course_sections', $section);
1365             $newitemid = $secrec->id;
1366         }
1368         // Annotate the section mapping, with restorefiles option if needed
1369         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1371         // set the new course_section id in the task
1372         $this->task->set_sectionid($newitemid);
1374         // If there is the legacy showavailability data, store this for later use.
1375         // (This data is not present when restoring 'new' backups.)
1376         if (isset($data->showavailability)) {
1377             // Cache the showavailability flag using the backup_ids data field.
1378             restore_dbops::set_backup_ids_record($this->get_restoreid(),
1379                     'section_showavailability', $newitemid, 0, null,
1380                     (object)array('showavailability' => $data->showavailability));
1381         }
1383         // Commented out. We never modify course->numsections as far as that is used
1384         // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1385         // Note: We keep the code here, to know about and because of the possibility of making this
1386         // optional based on some setting/attribute in the future
1387         // If needed, adjust course->numsections
1388         //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1389         //    if ($numsections < $section->section) {
1390         //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1391         //    }
1392         //}
1393     }
1395     /**
1396      * Process the legacy availability table record. This table does not exist
1397      * in Moodle 2.7+ but we still support restore.
1398      *
1399      * @param stdClass $data Record data
1400      */
1401     public function process_availability($data) {
1402         $data = (object)$data;
1403         // Simply going to store the whole availability record now, we'll process
1404         // all them later in the final task (once all activities have been restored)
1405         // Let's call the low level one to be able to store the whole object.
1406         $data->coursesectionid = $this->task->get_sectionid();
1407         restore_dbops::set_backup_ids_record($this->get_restoreid(),
1408                 'section_availability', $data->id, 0, null, $data);
1409     }
1411     /**
1412      * Process the legacy availability fields table record. This table does not
1413      * exist in Moodle 2.7+ but we still support restore.
1414      *
1415      * @param stdClass $data Record data
1416      */
1417     public function process_availability_field($data) {
1418         global $DB;
1419         $data = (object)$data;
1420         // Mark it is as passed by default
1421         $passed = true;
1422         $customfieldid = null;
1424         // If a customfield has been used in order to pass we must be able to match an existing
1425         // customfield by name (data->customfield) and type (data->customfieldtype)
1426         if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1427             // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1428             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1429             $passed = false;
1430         } else if (!is_null($data->customfield)) {
1431             $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1432             $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1433             $passed = ($customfieldid !== false);
1434         }
1436         if ($passed) {
1437             // Create the object to insert into the database
1438             $availfield = new stdClass();
1439             $availfield->coursesectionid = $this->task->get_sectionid();
1440             $availfield->userfield = $data->userfield;
1441             $availfield->customfieldid = $customfieldid;
1442             $availfield->operator = $data->operator;
1443             $availfield->value = $data->value;
1445             // Get showavailability option.
1446             $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1447                     'section_showavailability', $availfield->coursesectionid);
1448             if (!$showrec) {
1449                 // Should not happen.
1450                 throw new coding_exception('No matching showavailability record');
1451             }
1452             $show = $showrec->info->showavailability;
1454             // The $availfield object is now in the format used in the old
1455             // system. Interpret this and convert to new system.
1456             $currentvalue = $DB->get_field('course_sections', 'availability',
1457                     array('id' => $availfield->coursesectionid), MUST_EXIST);
1458             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1459                     $currentvalue, $availfield, $show);
1460             $DB->set_field('course_sections', 'availability', $newvalue,
1461                     array('id' => $availfield->coursesectionid));
1462         }
1463     }
1465     public function process_course_format_options($data) {
1466         global $DB;
1467         $data = (object)$data;
1468         $oldid = $data->id;
1469         unset($data->id);
1470         $data->sectionid = $this->task->get_sectionid();
1471         $data->courseid = $this->get_courseid();
1472         $newid = $DB->insert_record('course_format_options', $data);
1473         $this->set_mapping('course_format_options', $oldid, $newid);
1474     }
1476     protected function after_execute() {
1477         // Add section related files, with 'course_section' itemid to match
1478         $this->add_related_files('course', 'section', 'course_section');
1479     }
1482 /**
1483  * Structure step that will read the course.xml file, loading it and performing
1484  * various actions depending of the site/restore settings. Note that target
1485  * course always exist before arriving here so this step will be updating
1486  * the course record (never inserting)
1487  */
1488 class restore_course_structure_step extends restore_structure_step {
1489     /**
1490      * @var bool this gets set to true by {@link process_course()} if we are
1491      * restoring an old coures that used the legacy 'module security' feature.
1492      * If so, we have to do more work in {@link after_execute()}.
1493      */
1494     protected $legacyrestrictmodules = false;
1496     /**
1497      * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1498      * array with array keys the module names ('forum', 'quiz', etc.). These are
1499      * the modules that are allowed according to the data in the backup file.
1500      * In {@link after_execute()} we then have to prevent adding of all the other
1501      * types of activity.
1502      */
1503     protected $legacyallowedmodules = array();
1505     protected function define_structure() {
1507         $course = new restore_path_element('course', '/course');
1508         $category = new restore_path_element('category', '/course/category');
1509         $tag = new restore_path_element('tag', '/course/tags/tag');
1510         $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1512         // Apply for 'format' plugins optional paths at course level
1513         $this->add_plugin_structure('format', $course);
1515         // Apply for 'theme' plugins optional paths at course level
1516         $this->add_plugin_structure('theme', $course);
1518         // Apply for 'report' plugins optional paths at course level
1519         $this->add_plugin_structure('report', $course);
1521         // Apply for 'course report' plugins optional paths at course level
1522         $this->add_plugin_structure('coursereport', $course);
1524         // Apply for plagiarism plugins optional paths at course level
1525         $this->add_plugin_structure('plagiarism', $course);
1527         // Apply for local plugins optional paths at course level
1528         $this->add_plugin_structure('local', $course);
1530         return array($course, $category, $tag, $allowed_module);
1531     }
1533     /**
1534      * Processing functions go here
1535      *
1536      * @global moodledatabase $DB
1537      * @param stdClass $data
1538      */
1539     public function process_course($data) {
1540         global $CFG, $DB;
1542         $data = (object)$data;
1544         $fullname  = $this->get_setting_value('course_fullname');
1545         $shortname = $this->get_setting_value('course_shortname');
1546         $startdate = $this->get_setting_value('course_startdate');
1548         // Calculate final course names, to avoid dupes
1549         list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
1551         // Need to change some fields before updating the course record
1552         $data->id = $this->get_courseid();
1553         $data->fullname = $fullname;
1554         $data->shortname= $shortname;
1556         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1557         // another course on this site.
1558         $context = context::instance_by_id($this->task->get_contextid());
1559         if (!empty($data->idnumber) && has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid()) &&
1560                 $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1561             // Do not reset idnumber.
1562         } else {
1563             $data->idnumber = '';
1564         }
1566         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1567         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1568         if (empty($data->hiddensections)) {
1569             $data->hiddensections = 0;
1570         }
1572         // Set legacyrestrictmodules to true if the course was resticting modules. If so
1573         // then we will need to process restricted modules after execution.
1574         $this->legacyrestrictmodules = !empty($data->restrictmodules);
1576         $data->startdate= $this->apply_date_offset($data->startdate);
1577         if ($data->defaultgroupingid) {
1578             $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1579         }
1580         if (empty($CFG->enablecompletion)) {
1581             $data->enablecompletion = 0;
1582             $data->completionstartonenrol = 0;
1583             $data->completionnotify = 0;
1584         }
1585         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1586         if (!array_key_exists($data->lang, $languages)) {
1587             $data->lang = '';
1588         }
1590         $themes = get_list_of_themes(); // Get themes for quick search later
1591         if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1592             $data->theme = '';
1593         }
1595         // Check if this is an old SCORM course format.
1596         if ($data->format == 'scorm') {
1597             $data->format = 'singleactivity';
1598             $data->activitytype = 'scorm';
1599         }
1601         // Course record ready, update it
1602         $DB->update_record('course', $data);
1604         course_get_format($data)->update_course_format_options($data);
1606         // Role name aliases
1607         restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1608     }
1610     public function process_category($data) {
1611         // Nothing to do with the category. UI sets it before restore starts
1612     }
1614     public function process_tag($data) {
1615         global $CFG, $DB;
1617         $data = (object)$data;
1619         if (!empty($CFG->usetags)) { // if enabled in server
1620             // TODO: This is highly inneficient. Each time we add one tag
1621             // we fetch all the existing because tag_set() deletes them
1622             // so everything must be reinserted on each call
1623             $tags = array();
1624             $existingtags = tag_get_tags('course', $this->get_courseid());
1625             // Re-add all the existitng tags
1626             foreach ($existingtags as $existingtag) {
1627                 $tags[] = $existingtag->rawname;
1628             }
1629             // Add the one being restored
1630             $tags[] = $data->rawname;
1631             // Send all the tags back to the course
1632             tag_set('course', $this->get_courseid(), $tags, 'core',
1633                 context_course::instance($this->get_courseid())->id);
1634         }
1635     }
1637     public function process_allowed_module($data) {
1638         $data = (object)$data;
1640         // Backwards compatiblity support for the data that used to be in the
1641         // course_allowed_modules table.
1642         if ($this->legacyrestrictmodules) {
1643             $this->legacyallowedmodules[$data->modulename] = 1;
1644         }
1645     }
1647     protected function after_execute() {
1648         global $DB;
1650         // Add course related files, without itemid to match
1651         $this->add_related_files('course', 'summary', null);
1652         $this->add_related_files('course', 'overviewfiles', null);
1654         // Deal with legacy allowed modules.
1655         if ($this->legacyrestrictmodules) {
1656             $context = context_course::instance($this->get_courseid());
1658             list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1659             list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1660             foreach ($managerroleids as $roleid) {
1661                 unset($roleids[$roleid]);
1662             }
1664             foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1665                 if (isset($this->legacyallowedmodules[$modname])) {
1666                     // Module is allowed, no worries.
1667                     continue;
1668                 }
1670                 $capability = 'mod/' . $modname . ':addinstance';
1671                 foreach ($roleids as $roleid) {
1672                     assign_capability($capability, CAP_PREVENT, $roleid, $context);
1673                 }
1674             }
1675         }
1676     }
1679 /**
1680  * Execution step that will migrate legacy files if present.
1681  */
1682 class restore_course_legacy_files_step extends restore_execution_step {
1683     public function define_execution() {
1684         global $DB;
1686         // Do a check for legacy files and skip if there are none.
1687         $sql = 'SELECT count(*)
1688                   FROM {backup_files_temp}
1689                  WHERE backupid = ?
1690                    AND contextid = ?
1691                    AND component = ?
1692                    AND filearea  = ?';
1693         $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
1695         if ($DB->count_records_sql($sql, $params)) {
1696             $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
1697             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
1698                 'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
1699         }
1700     }
1703 /*
1704  * Structure step that will read the roles.xml file (at course/activity/block levels)
1705  * containing all the role_assignments and overrides for that context. If corresponding to
1706  * one mapped role, they will be applied to target context. Will observe the role_assignments
1707  * setting to decide if ras are restored.
1708  *
1709  * Note: this needs to be executed after all users are enrolled.
1710  */
1711 class restore_ras_and_caps_structure_step extends restore_structure_step {
1712     protected $plugins = null;
1714     protected function define_structure() {
1716         $paths = array();
1718         // Observe the role_assignments setting
1719         if ($this->get_setting_value('role_assignments')) {
1720             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
1721         }
1722         $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
1724         return $paths;
1725     }
1727     /**
1728      * Assign roles
1729      *
1730      * This has to be called after enrolments processing.
1731      *
1732      * @param mixed $data
1733      * @return void
1734      */
1735     public function process_assignment($data) {
1736         global $DB;
1738         $data = (object)$data;
1740         // Check roleid, userid are one of the mapped ones
1741         if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
1742             return;
1743         }
1744         if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
1745             return;
1746         }
1747         if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
1748             // Only assign roles to not deleted users
1749             return;
1750         }
1751         if (!$contextid = $this->task->get_contextid()) {
1752             return;
1753         }
1755         if (empty($data->component)) {
1756             // assign standard manual roles
1757             // TODO: role_assign() needs one userid param to be able to specify our restore userid
1758             role_assign($newroleid, $newuserid, $contextid);
1760         } else if ((strpos($data->component, 'enrol_') === 0)) {
1761             // Deal with enrolment roles - ignore the component and just find out the instance via new id,
1762             // it is possible that enrolment was restored using different plugin type.
1763             if (!isset($this->plugins)) {
1764                 $this->plugins = enrol_get_plugins(true);
1765             }
1766             if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1767                 if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1768                     if (isset($this->plugins[$instance->enrol])) {
1769                         $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
1770                     }
1771                 }
1772             }
1774         } else {
1775             $data->roleid    = $newroleid;
1776             $data->userid    = $newuserid;
1777             $data->contextid = $contextid;
1778             $dir = core_component::get_component_directory($data->component);
1779             if ($dir and is_dir($dir)) {
1780                 if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
1781                     return;
1782                 }
1783             }
1784             // Bad luck, plugin could not restore the data, let's add normal membership.
1785             role_assign($data->roleid, $data->userid, $data->contextid);
1786             $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
1787             $this->log($message, backup::LOG_WARNING);
1788         }
1789     }
1791     public function process_override($data) {
1792         $data = (object)$data;
1794         // Check roleid is one of the mapped ones
1795         $newroleid = $this->get_mappingid('role', $data->roleid);
1796         // If newroleid and context are valid assign it via API (it handles dupes and so on)
1797         if ($newroleid && $this->task->get_contextid()) {
1798             // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1799             // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1800             assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1801         }
1802     }
1805 /**
1806  * If no instances yet add default enrol methods the same way as when creating new course in UI.
1807  */
1808 class restore_default_enrolments_step extends restore_execution_step {
1809     public function define_execution() {
1810         global $DB;
1812         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
1814         if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
1815             // Something already added instances, do not add default instances.
1816             $plugins = enrol_get_plugins(true);
1817             foreach ($plugins as $plugin) {
1818                 $plugin->restore_sync_course($course);
1819             }
1821         } else {
1822             // Looks like a newly created course.
1823             enrol_course_updated(true, $course, null);
1824         }
1825     }
1828 /**
1829  * This structure steps restores the enrol plugins and their underlying
1830  * enrolments, performing all the mappings and/or movements required
1831  */
1832 class restore_enrolments_structure_step extends restore_structure_step {
1833     protected $enrolsynced = false;
1834     protected $plugins = null;
1835     protected $originalstatus = array();
1837     /**
1838      * Conditionally decide if this step should be executed.
1839      *
1840      * This function checks the following parameter:
1841      *
1842      *   1. the course/enrolments.xml file exists
1843      *
1844      * @return bool true is safe to execute, false otherwise
1845      */
1846     protected function execute_condition() {
1848         // Check it is included in the backup
1849         $fullpath = $this->task->get_taskbasepath();
1850         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
1851         if (!file_exists($fullpath)) {
1852             // Not found, can't restore enrolments info
1853             return false;
1854         }
1856         return true;
1857     }
1859     protected function define_structure() {
1861         $paths = array();
1863         $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
1864         $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
1866         return $paths;
1867     }
1869     /**
1870      * Create enrolment instances.
1871      *
1872      * This has to be called after creation of roles
1873      * and before adding of role assignments.
1874      *
1875      * @param mixed $data
1876      * @return void
1877      */
1878     public function process_enrol($data) {
1879         global $DB;
1881         $data = (object)$data;
1882         $oldid = $data->id; // We'll need this later.
1883         unset($data->id);
1885         $this->originalstatus[$oldid] = $data->status;
1887         if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
1888             $this->set_mapping('enrol', $oldid, 0);
1889             return;
1890         }
1892         if (!isset($this->plugins)) {
1893             $this->plugins = enrol_get_plugins(true);
1894         }
1896         if (!$this->enrolsynced) {
1897             // Make sure that all plugin may create instances and enrolments automatically
1898             // before the first instance restore - this is suitable especially for plugins
1899             // that synchronise data automatically using course->idnumber or by course categories.
1900             foreach ($this->plugins as $plugin) {
1901                 $plugin->restore_sync_course($courserec);
1902             }
1903             $this->enrolsynced = true;
1904         }
1906         // Map standard fields - plugin has to process custom fields manually.
1907         $data->roleid   = $this->get_mappingid('role', $data->roleid);
1908         $data->courseid = $courserec->id;
1910         if ($this->get_setting_value('enrol_migratetomanual')) {
1911             unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
1912             if (!enrol_is_enabled('manual')) {
1913                 $this->set_mapping('enrol', $oldid, 0);
1914                 return;
1915             }
1916             if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
1917                 $instance = reset($instances);
1918                 $this->set_mapping('enrol', $oldid, $instance->id);
1919             } else {
1920                 if ($data->enrol === 'manual') {
1921                     $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
1922                 } else {
1923                     $instanceid = $this->plugins['manual']->add_default_instance($courserec);
1924                 }
1925                 $this->set_mapping('enrol', $oldid, $instanceid);
1926             }
1928         } else {
1929             if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
1930                 $this->set_mapping('enrol', $oldid, 0);
1931                 $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
1932                 $this->log($message, backup::LOG_WARNING);
1933                 return;
1934             }
1935             if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
1936                 // Let's keep the sortorder in old backups.
1937             } else {
1938                 // Prevent problems with colliding sortorders in old backups,
1939                 // new 2.4 backups do not need sortorder because xml elements are ordered properly.
1940                 unset($data->sortorder);
1941             }
1942             // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
1943             $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
1944         }
1945     }
1947     /**
1948      * Create user enrolments.
1949      *
1950      * This has to be called after creation of enrolment instances
1951      * and before adding of role assignments.
1952      *
1953      * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
1954      *
1955      * @param mixed $data
1956      * @return void
1957      */
1958     public function process_enrolment($data) {
1959         global $DB;
1961         if (!isset($this->plugins)) {
1962             $this->plugins = enrol_get_plugins(true);
1963         }
1965         $data = (object)$data;
1967         // Process only if parent instance have been mapped.
1968         if ($enrolid = $this->get_new_parentid('enrol')) {
1969             $oldinstancestatus = ENROL_INSTANCE_ENABLED;
1970             $oldenrolid = $this->get_old_parentid('enrol');
1971             if (isset($this->originalstatus[$oldenrolid])) {
1972                 $oldinstancestatus = $this->originalstatus[$oldenrolid];
1973             }
1974             if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1975                 // And only if user is a mapped one.
1976                 if ($userid = $this->get_mappingid('user', $data->userid)) {
1977                     if (isset($this->plugins[$instance->enrol])) {
1978                         $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
1979                     }
1980                 }
1981             }
1982         }
1983     }
1987 /**
1988  * Make sure the user restoring the course can actually access it.
1989  */
1990 class restore_fix_restorer_access_step extends restore_execution_step {
1991     protected function define_execution() {
1992         global $CFG, $DB;
1994         if (!$userid = $this->task->get_userid()) {
1995             return;
1996         }
1998         if (empty($CFG->restorernewroleid)) {
1999             // Bad luck, no fallback role for restorers specified
2000             return;
2001         }
2003         $courseid = $this->get_courseid();
2004         $context = context_course::instance($courseid);
2006         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2007             // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2008             return;
2009         }
2011         // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2012         role_assign($CFG->restorernewroleid, $userid, $context);
2014         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2015             // Extra role is enough, yay!
2016             return;
2017         }
2019         // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2020         // hopefully admin selected suitable $CFG->restorernewroleid ...
2021         if (!enrol_is_enabled('manual')) {
2022             return;
2023         }
2024         if (!$enrol = enrol_get_plugin('manual')) {
2025             return;
2026         }
2027         if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2028             $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2029             $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2030             $enrol->add_instance($course, $fields);
2031         }
2033         enrol_try_internal_enrol($courseid, $userid);
2034     }
2038 /**
2039  * This structure steps restores the filters and their configs
2040  */
2041 class restore_filters_structure_step extends restore_structure_step {
2043     protected function define_structure() {
2045         $paths = array();
2047         $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2048         $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2050         return $paths;
2051     }
2053     public function process_active($data) {
2055         $data = (object)$data;
2057         if (strpos($data->filter, 'filter/') === 0) {
2058             $data->filter = substr($data->filter, 7);
2060         } else if (strpos($data->filter, '/') !== false) {
2061             // Unsupported old filter.
2062             return;
2063         }
2065         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2066             return;
2067         }
2068         filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2069     }
2071     public function process_config($data) {
2073         $data = (object)$data;
2075         if (strpos($data->filter, 'filter/') === 0) {
2076             $data->filter = substr($data->filter, 7);
2078         } else if (strpos($data->filter, '/') !== false) {
2079             // Unsupported old filter.
2080             return;
2081         }
2083         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2084             return;
2085         }
2086         filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2087     }
2091 /**
2092  * This structure steps restores the comments
2093  * Note: Cannot use the comments API because defaults to USER->id.
2094  * That should change allowing to pass $userid
2095  */
2096 class restore_comments_structure_step extends restore_structure_step {
2098     protected function define_structure() {
2100         $paths = array();
2102         $paths[] = new restore_path_element('comment', '/comments/comment');
2104         return $paths;
2105     }
2107     public function process_comment($data) {
2108         global $DB;
2110         $data = (object)$data;
2112         // First of all, if the comment has some itemid, ask to the task what to map
2113         $mapping = false;
2114         if ($data->itemid) {
2115             $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2116             $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2117         }
2118         // Only restore the comment if has no mapping OR we have found the matching mapping
2119         if (!$mapping || $data->itemid) {
2120             // Only if user mapping and context
2121             $data->userid = $this->get_mappingid('user', $data->userid);
2122             if ($data->userid && $this->task->get_contextid()) {
2123                 $data->contextid = $this->task->get_contextid();
2124                 // Only if there is another comment with same context/user/timecreated
2125                 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2126                 if (!$DB->record_exists('comments', $params)) {
2127                     $DB->insert_record('comments', $data);
2128                 }
2129             }
2130         }
2131     }
2134 /**
2135  * This structure steps restores the badges and their configs
2136  */
2137 class restore_badges_structure_step extends restore_structure_step {
2139     /**
2140      * Conditionally decide if this step should be executed.
2141      *
2142      * This function checks the following parameters:
2143      *
2144      *   1. Badges and course badges are enabled on the site.
2145      *   2. The course/badges.xml file exists.
2146      *   3. All modules are restorable.
2147      *   4. All modules are marked for restore.
2148      *
2149      * @return bool True is safe to execute, false otherwise
2150      */
2151     protected function execute_condition() {
2152         global $CFG;
2154         // First check is badges and course level badges are enabled on this site.
2155         if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2156             // Disabled, don't restore course badges.
2157             return false;
2158         }
2160         // Check if badges.xml is included in the backup.
2161         $fullpath = $this->task->get_taskbasepath();
2162         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2163         if (!file_exists($fullpath)) {
2164             // Not found, can't restore course badges.
2165             return false;
2166         }
2168         // Check we are able to restore all backed up modules.
2169         if ($this->task->is_missing_modules()) {
2170             return false;
2171         }
2173         // Finally check all modules within the backup are being restored.
2174         if ($this->task->is_excluding_activities()) {
2175             return false;
2176         }
2178         return true;
2179     }
2181     protected function define_structure() {
2182         $paths = array();
2183         $paths[] = new restore_path_element('badge', '/badges/badge');
2184         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2185         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2186         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2188         return $paths;
2189     }
2191     public function process_badge($data) {
2192         global $DB, $CFG;
2194         require_once($CFG->libdir . '/badgeslib.php');
2196         $data = (object)$data;
2197         $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2198         if (empty($data->usercreated)) {
2199             $data->usercreated = $this->task->get_userid();
2200         }
2201         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2202         if (empty($data->usermodified)) {
2203             $data->usermodified = $this->task->get_userid();
2204         }
2206         // We'll restore the badge image.
2207         $restorefiles = true;
2209         $courseid = $this->get_courseid();
2211         $params = array(
2212                 'name'           => $data->name,
2213                 'description'    => $data->description,
2214                 'timecreated'    => $this->apply_date_offset($data->timecreated),
2215                 'timemodified'   => $this->apply_date_offset($data->timemodified),
2216                 'usercreated'    => $data->usercreated,
2217                 'usermodified'   => $data->usermodified,
2218                 'issuername'     => $data->issuername,
2219                 'issuerurl'      => $data->issuerurl,
2220                 'issuercontact'  => $data->issuercontact,
2221                 'expiredate'     => $this->apply_date_offset($data->expiredate),
2222                 'expireperiod'   => $data->expireperiod,
2223                 'type'           => BADGE_TYPE_COURSE,
2224                 'courseid'       => $courseid,
2225                 'message'        => $data->message,
2226                 'messagesubject' => $data->messagesubject,
2227                 'attachment'     => $data->attachment,
2228                 'notification'   => $data->notification,
2229                 'status'         => BADGE_STATUS_INACTIVE,
2230                 'nextcron'       => $this->apply_date_offset($data->nextcron)
2231         );
2233         $newid = $DB->insert_record('badge', $params);
2234         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2235     }
2237     public function process_criterion($data) {
2238         global $DB;
2240         $data = (object)$data;
2242         $params = array(
2243                 'badgeid'      => $this->get_new_parentid('badge'),
2244                 'criteriatype' => $data->criteriatype,
2245                 'method'       => $data->method
2246         );
2247         $newid = $DB->insert_record('badge_criteria', $params);
2248         $this->set_mapping('criterion', $data->id, $newid);
2249     }
2251     public function process_parameter($data) {
2252         global $DB, $CFG;
2254         require_once($CFG->libdir . '/badgeslib.php');
2256         $data = (object)$data;
2257         $criteriaid = $this->get_new_parentid('criterion');
2259         // Parameter array that will go to database.
2260         $params = array();
2261         $params['critid'] = $criteriaid;
2263         $oldparam = explode('_', $data->name);
2265         if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2266             $module = $this->get_mappingid('course_module', $oldparam[1]);
2267             $params['name'] = $oldparam[0] . '_' . $module;
2268             $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2269         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2270             $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2271             $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2272         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2273             $role = $this->get_mappingid('role', $data->value);
2274             if (!empty($role)) {
2275                 $params['name'] = 'role_' . $role;
2276                 $params['value'] = $role;
2277             } else {
2278                 return;
2279             }
2280         }
2282         if (!$DB->record_exists('badge_criteria_param', $params)) {
2283             $DB->insert_record('badge_criteria_param', $params);
2284         }
2285     }
2287     public function process_manual_award($data) {
2288         global $DB;
2290         $data = (object)$data;
2291         $role = $this->get_mappingid('role', $data->issuerrole);
2293         if (!empty($role)) {
2294             $award = array(
2295                 'badgeid'     => $this->get_new_parentid('badge'),
2296                 'recipientid' => $this->get_mappingid('user', $data->recipientid),
2297                 'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2298                 'issuerrole'  => $role,
2299                 'datemet'     => $this->apply_date_offset($data->datemet)
2300             );
2302             // Skip the manual award if recipient or issuer can not be mapped to.
2303             if (empty($award['recipientid']) || empty($award['issuerid'])) {
2304                 return;
2305             }
2307             $DB->insert_record('badge_manual_award', $award);
2308         }
2309     }
2311     protected function after_execute() {
2312         // Add related files.
2313         $this->add_related_files('badges', 'badgeimage', 'badge');
2314     }
2317 /**
2318  * This structure steps restores the calendar events
2319  */
2320 class restore_calendarevents_structure_step extends restore_structure_step {
2322     protected function define_structure() {
2324         $paths = array();
2326         $paths[] = new restore_path_element('calendarevents', '/events/event');
2328         return $paths;
2329     }
2331     public function process_calendarevents($data) {
2332         global $DB, $SITE;
2334         $data = (object)$data;
2335         $oldid = $data->id;
2336         $restorefiles = true; // We'll restore the files
2337         // Find the userid and the groupid associated with the event. Return if not found.
2338         $data->userid = $this->get_mappingid('user', $data->userid);
2339         if ($data->userid === false) {
2340             return;
2341         }
2342         if (!empty($data->groupid)) {
2343             $data->groupid = $this->get_mappingid('group', $data->groupid);
2344             if ($data->groupid === false) {
2345                 return;
2346             }
2347         }
2348         // Handle events with empty eventtype //MDL-32827
2349         if(empty($data->eventtype)) {
2350             if ($data->courseid == $SITE->id) {                                // Site event
2351                 $data->eventtype = "site";
2352             } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2353                 // Course assingment event
2354                 $data->eventtype = "due";
2355             } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2356                 $data->eventtype = "course";
2357             } else if ($data->groupid) {                                      // Group event
2358                 $data->eventtype = "group";
2359             } else if ($data->userid) {                                       // User event
2360                 $data->eventtype = "user";
2361             } else {
2362                 return;
2363             }
2364         }
2366         $params = array(
2367                 'name'           => $data->name,
2368                 'description'    => $data->description,
2369                 'format'         => $data->format,
2370                 'courseid'       => $this->get_courseid(),
2371                 'groupid'        => $data->groupid,
2372                 'userid'         => $data->userid,
2373                 'repeatid'       => $data->repeatid,
2374                 'modulename'     => $data->modulename,
2375                 'eventtype'      => $data->eventtype,
2376                 'timestart'      => $this->apply_date_offset($data->timestart),
2377                 'timeduration'   => $data->timeduration,
2378                 'visible'        => $data->visible,
2379                 'uuid'           => $data->uuid,
2380                 'sequence'       => $data->sequence,
2381                 'timemodified'    => $this->apply_date_offset($data->timemodified));
2382         if ($this->name == 'activity_calendar') {
2383             $params['instance'] = $this->task->get_activityid();
2384         } else {
2385             $params['instance'] = 0;
2386         }
2387         $sql = "SELECT id
2388                   FROM {event}
2389                  WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2390                    AND courseid = ?
2391                    AND repeatid = ?
2392                    AND modulename = ?
2393                    AND timestart = ?
2394                    AND timeduration = ?
2395                    AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2396         $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
2397         $result = $DB->record_exists_sql($sql, $arg);
2398         if (empty($result)) {
2399             $newitemid = $DB->insert_record('event', $params);
2400             $this->set_mapping('event', $oldid, $newitemid);
2401             $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2402         }
2404     }
2405     protected function after_execute() {
2406         // Add related files
2407         $this->add_related_files('calendar', 'event_description', 'event_description');
2408     }
2411 class restore_course_completion_structure_step extends restore_structure_step {
2413     /**
2414      * Conditionally decide if this step should be executed.
2415      *
2416      * This function checks parameters that are not immediate settings to ensure
2417      * that the enviroment is suitable for the restore of course completion info.
2418      *
2419      * This function checks the following four parameters:
2420      *
2421      *   1. Course completion is enabled on the site
2422      *   2. The backup includes course completion information
2423      *   3. All modules are restorable
2424      *   4. All modules are marked for restore.
2425      *
2426      * @return bool True is safe to execute, false otherwise
2427      */
2428     protected function execute_condition() {
2429         global $CFG;
2431         // First check course completion is enabled on this site
2432         if (empty($CFG->enablecompletion)) {
2433             // Disabled, don't restore course completion
2434             return false;
2435         }
2437         // Check it is included in the backup
2438         $fullpath = $this->task->get_taskbasepath();
2439         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2440         if (!file_exists($fullpath)) {
2441             // Not found, can't restore course completion
2442             return false;
2443         }
2445         // Check we are able to restore all backed up modules
2446         if ($this->task->is_missing_modules()) {
2447             return false;
2448         }
2450         // Finally check all modules within the backup are being restored.
2451         if ($this->task->is_excluding_activities()) {
2452             return false;
2453         }
2455         return true;
2456     }
2458     /**
2459      * Define the course completion structure
2460      *
2461      * @return array Array of restore_path_element
2462      */
2463     protected function define_structure() {
2465         // To know if we are including user completion info
2466         $userinfo = $this->get_setting_value('userscompletion');
2468         $paths = array();
2469         $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
2470         $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
2472         if ($userinfo) {
2473             $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
2474             $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
2475         }
2477         return $paths;
2479     }
2481     /**
2482      * Process course completion criteria
2483      *
2484      * @global moodle_database $DB
2485      * @param stdClass $data
2486      */
2487     public function process_course_completion_criteria($data) {
2488         global $DB;
2490         $data = (object)$data;
2491         $data->course = $this->get_courseid();
2493         // Apply the date offset to the time end field
2494         $data->timeend = $this->apply_date_offset($data->timeend);
2496         // Map the role from the criteria
2497         if (!empty($data->role)) {
2498             $data->role = $this->get_mappingid('role', $data->role);
2499         }
2501         $skipcriteria = false;
2503         // If the completion criteria is for a module we need to map the module instance
2504         // to the new module id.
2505         if (!empty($data->moduleinstance) && !empty($data->module)) {
2506             $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
2507             if (empty($data->moduleinstance)) {
2508                 $skipcriteria = true;
2509             }
2510         } else {
2511             $data->module = null;
2512             $data->moduleinstance = null;
2513         }
2515         // We backup the course shortname rather than the ID so that we can match back to the course
2516         if (!empty($data->courseinstanceshortname)) {
2517             $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
2518             if (!$courseinstanceid) {
2519                 $skipcriteria = true;
2520             }
2521         } else {
2522             $courseinstanceid = null;
2523         }
2524         $data->courseinstance = $courseinstanceid;
2526         if (!$skipcriteria) {
2527             $params = array(
2528                 'course'         => $data->course,
2529                 'criteriatype'   => $data->criteriatype,
2530                 'enrolperiod'    => $data->enrolperiod,
2531                 'courseinstance' => $data->courseinstance,
2532                 'module'         => $data->module,
2533                 'moduleinstance' => $data->moduleinstance,
2534                 'timeend'        => $data->timeend,
2535                 'gradepass'      => $data->gradepass,
2536                 'role'           => $data->role
2537             );
2538             $newid = $DB->insert_record('course_completion_criteria', $params);
2539             $this->set_mapping('course_completion_criteria', $data->id, $newid);
2540         }
2541     }
2543     /**
2544      * Processes course compltion criteria complete records
2545      *
2546      * @global moodle_database $DB
2547      * @param stdClass $data
2548      */
2549     public function process_course_completion_crit_compl($data) {
2550         global $DB;
2552         $data = (object)$data;
2554         // This may be empty if criteria could not be restored
2555         $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
2557         $data->course = $this->get_courseid();
2558         $data->userid = $this->get_mappingid('user', $data->userid);
2560         if (!empty($data->criteriaid) && !empty($data->userid)) {
2561             $params = array(
2562                 'userid' => $data->userid,
2563                 'course' => $data->course,
2564                 'criteriaid' => $data->criteriaid,
2565                 'timecompleted' => $this->apply_date_offset($data->timecompleted)
2566             );
2567             if (isset($data->gradefinal)) {
2568                 $params['gradefinal'] = $data->gradefinal;
2569             }
2570             if (isset($data->unenroled)) {
2571                 $params['unenroled'] = $data->unenroled;
2572             }
2573             $DB->insert_record('course_completion_crit_compl', $params);
2574         }
2575     }
2577     /**
2578      * Process course completions
2579      *
2580      * @global moodle_database $DB
2581      * @param stdClass $data
2582      */
2583     public function process_course_completions($data) {
2584         global $DB;
2586         $data = (object)$data;
2588         $data->course = $this->get_courseid();
2589         $data->userid = $this->get_mappingid('user', $data->userid);
2591         if (!empty($data->userid)) {
2592             $params = array(
2593                 'userid' => $data->userid,
2594                 'course' => $data->course,
2595                 'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
2596                 'timestarted' => $this->apply_date_offset($data->timestarted),
2597                 'timecompleted' => $this->apply_date_offset($data->timecompleted),
2598                 'reaggregate' => $data->reaggregate
2599             );
2600             $DB->insert_record('course_completions', $params);
2601         }
2602     }
2604     /**
2605      * Process course completion aggregate methods
2606      *
2607      * @global moodle_database $DB
2608      * @param stdClass $data
2609      */
2610     public function process_course_completion_aggr_methd($data) {
2611         global $DB;
2613         $data = (object)$data;
2615         $data->course = $this->get_courseid();
2617         // Only create the course_completion_aggr_methd records if
2618         // the target course has not them defined. MDL-28180
2619         if (!$DB->record_exists('course_completion_aggr_methd', array(
2620                     'course' => $data->course,
2621                     'criteriatype' => $data->criteriatype))) {
2622             $params = array(
2623                 'course' => $data->course,
2624                 'criteriatype' => $data->criteriatype,
2625                 'method' => $data->method,
2626                 'value' => $data->value,
2627             );
2628             $DB->insert_record('course_completion_aggr_methd', $params);
2629         }
2630     }
2634 /**
2635  * This structure step restores course logs (cmid = 0), delegating
2636  * the hard work to the corresponding {@link restore_logs_processor} passing the
2637  * collection of {@link restore_log_rule} rules to be observed as they are defined
2638  * by the task. Note this is only executed based in the 'logs' setting.
2639  *
2640  * NOTE: This is executed by final task, to have all the activities already restored
2641  *
2642  * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
2643  * records are. There are others like 'calendar' and 'upload' that will be handled
2644  * later.
2645  *
2646  * NOTE: All the missing actions (not able to be restored) are sent to logs for
2647  * debugging purposes
2648  */
2649 class restore_course_logs_structure_step extends restore_structure_step {
2651     /**
2652      * Conditionally decide if this step should be executed.
2653      *
2654      * This function checks the following parameter:
2655      *
2656      *   1. the course/logs.xml file exists
2657      *
2658      * @return bool true is safe to execute, false otherwise
2659      */
2660     protected function execute_condition() {
2662         // Check it is included in the backup
2663         $fullpath = $this->task->get_taskbasepath();
2664         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2665         if (!file_exists($fullpath)) {
2666             // Not found, can't restore course logs
2667             return false;
2668         }
2670         return true;
2671     }
2673     protected function define_structure() {
2675         $paths = array();
2677         // Simple, one plain level of information contains them
2678         $paths[] = new restore_path_element('log', '/logs/log');
2680         return $paths;
2681     }
2683     protected function process_log($data) {
2684         global $DB;
2686         $data = (object)($data);
2688         $data->time = $this->apply_date_offset($data->time);
2689         $data->userid = $this->get_mappingid('user', $data->userid);
2690         $data->course = $this->get_courseid();
2691         $data->cmid = 0;
2693         // For any reason user wasn't remapped ok, stop processing this
2694         if (empty($data->userid)) {
2695             return;
2696         }
2698         // Everything ready, let's delegate to the restore_logs_processor
2700         // Set some fixed values that will save tons of DB requests
2701         $values = array(
2702             'course' => $this->get_courseid());
2703         // Get instance and process log record
2704         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2706         // If we have data, insert it, else something went wrong in the restore_logs_processor
2707         if ($data) {
2708             if (empty($data->url)) {
2709                 $data->url = '';
2710             }
2711             if (empty($data->info)) {
2712                 $data->info = '';
2713             }
2714             // Store the data in the legacy log table if we are still using it.
2715             $manager = get_log_manager();
2716             if (method_exists($manager, 'legacy_add_to_log')) {
2717                 $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2718                     $data->info, $data->cmid, $data->userid);
2719             }
2720         }
2721     }
2724 /**
2725  * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
2726  * sharing its same structure but modifying the way records are handled
2727  */
2728 class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
2730     protected function process_log($data) {
2731         global $DB;
2733         $data = (object)($data);
2735         $data->time = $this->apply_date_offset($data->time);
2736         $data->userid = $this->get_mappingid('user', $data->userid);
2737         $data->course = $this->get_courseid();
2738         $data->cmid = $this->task->get_moduleid();
2740         // For any reason user wasn't remapped ok, stop processing this
2741         if (empty($data->userid)) {
2742             return;
2743         }
2745         // Everything ready, let's delegate to the restore_logs_processor
2747         // Set some fixed values that will save tons of DB requests
2748         $values = array(
2749             'course' => $this->get_courseid(),
2750             'course_module' => $this->task->get_moduleid(),
2751             $this->task->get_modulename() => $this->task->get_activityid());
2752         // Get instance and process log record
2753         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2755         // If we have data, insert it, else something went wrong in the restore_logs_processor
2756         if ($data) {
2757             if (empty($data->url)) {
2758                 $data->url = '';
2759             }
2760             if (empty($data->info)) {
2761                 $data->info = '';
2762             }
2763             // Store the data in the legacy log table if we are still using it.
2764             $manager = get_log_manager();
2765             if (method_exists($manager, 'legacy_add_to_log')) {
2766                 $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2767                     $data->info, $data->cmid, $data->userid);
2768             }
2769         }
2770     }
2774 /**
2775  * Defines the restore step for advanced grading methods attached to the activity module
2776  */
2777 class restore_activity_grading_structure_step extends restore_structure_step {
2779     /**
2780      * This step is executed only if the grading file is present
2781      */
2782      protected function execute_condition() {
2784         $fullpath = $this->task->get_taskbasepath();
2785         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2786         if (!file_exists($fullpath)) {
2787             return false;
2788         }
2790         return true;
2791     }
2794     /**
2795      * Declares paths in the grading.xml file we are interested in
2796      */
2797     protected function define_structure() {
2799         $paths = array();
2800         $userinfo = $this->get_setting_value('userinfo');
2802         $area = new restore_path_element('grading_area', '/areas/area');
2803         $paths[] = $area;
2804         // attach local plugin stucture to $area element
2805         $this->add_plugin_structure('local', $area);
2807         $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
2808         $paths[] = $definition;
2809         $this->add_plugin_structure('gradingform', $definition);
2810         // attach local plugin stucture to $definition element
2811         $this->add_plugin_structure('local', $definition);
2814         if ($userinfo) {
2815             $instance = new restore_path_element('grading_instance',
2816                 '/areas/area/definitions/definition/instances/instance');
2817             $paths[] = $instance;
2818             $this->add_plugin_structure('gradingform', $instance);
2819             // attach local plugin stucture to $intance element
2820             $this->add_plugin_structure('local', $instance);
2821         }
2823         return $paths;
2824     }
2826     /**
2827      * Processes one grading area element
2828      *
2829      * @param array $data element data
2830      */
2831     protected function process_grading_area($data) {
2832         global $DB;
2834         $task = $this->get_task();
2835         $data = (object)$data;
2836         $oldid = $data->id;
2837         $data->component = 'mod_'.$task->get_modulename();
2838         $data->contextid = $task->get_contextid();
2840         $newid = $DB->insert_record('grading_areas', $data);
2841         $this->set_mapping('grading_area', $oldid, $newid);
2842     }
2844     /**
2845      * Processes one grading definition element
2846      *
2847      * @param array $data element data
2848      */
2849     protected function process_grading_definition($data) {
2850         global $DB;
2852         $task = $this->get_task();
2853         $data = (object)$data;
2854         $oldid = $data->id;
2855         $data->areaid = $this->get_new_parentid('grading_area');
2856         $data->copiedfromid = null;
2857         $data->timecreated = time();
2858         $data->usercreated = $task->get_userid();
2859         $data->timemodified = $data->timecreated;
2860         $data->usermodified = $data->usercreated;
2862         $newid = $DB->insert_record('grading_definitions', $data);
2863         $this->set_mapping('grading_definition', $oldid, $newid, true);
2864     }
2866     /**
2867      * Processes one grading form instance element
2868      *
2869      * @param array $data element data
2870      */
2871     protected function process_grading_instance($data) {
2872         global $DB;
2874         $data = (object)$data;
2876         // new form definition id
2877         $newformid = $this->get_new_parentid('grading_definition');
2879         // get the name of the area we are restoring to
2880         $sql = "SELECT ga.areaname
2881                   FROM {grading_definitions} gd
2882                   JOIN {grading_areas} ga ON gd.areaid = ga.id
2883                  WHERE gd.id = ?";
2884         $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
2886         // get the mapped itemid - the activity module is expected to define the mappings
2887         // for each gradable area
2888         $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
2890         $oldid = $data->id;
2891         $data->definitionid = $newformid;
2892         $data->raterid = $this->get_mappingid('user', $data->raterid);
2893         $data->itemid = $newitemid;
2895         $newid = $DB->insert_record('grading_instances', $data);
2896         $this->set_mapping('grading_instance', $oldid, $newid);
2897     }
2899     /**
2900      * Final operations when the database records are inserted
2901      */
2902     protected function after_execute() {
2903         // Add files embedded into the definition description
2904         $this->add_related_files('grading', 'description', 'grading_definition');
2905     }
2909 /**
2910  * This structure step restores the grade items associated with one activity
2911  * All the grade items are made child of the "course" grade item but the original
2912  * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
2913  * the complete gradebook (categories and calculations), that information is
2914  * available there
2915  */
2916 class restore_activity_grades_structure_step extends restore_structure_step {
2918     protected function define_structure() {
2920         $paths = array();
2921         $userinfo = $this->get_setting_value('userinfo');
2923         $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
2924         $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
2925         if ($userinfo) {
2926             $paths[] = new restore_path_element('grade_grade',
2927                            '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
2928         }
2929         return $paths;
2930     }
2932     protected function process_grade_item($data) {
2933         global $DB;
2935         $data = (object)($data);
2936         $oldid       = $data->id;        // We'll need these later
2937         $oldparentid = $data->categoryid;
2938         $courseid = $this->get_courseid();
2940         // make sure top course category exists, all grade items will be associated
2941         // to it. Later, if restoring the whole gradebook, categories will be introduced
2942         $coursecat = grade_category::fetch_course_category($courseid);
2943         $coursecatid = $coursecat->id; // Get the categoryid to be used
2945         $idnumber = null;
2946         if (!empty($data->idnumber)) {
2947             // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
2948             // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
2949             // so the best is to keep the ones already in the gradebook
2950             // Potential problem: duplicates if same items are restored more than once. :-(
2951             // This needs to be fixed in some way (outcomes & activities with multiple items)
2952             // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
2953             // In any case, verify always for uniqueness
2954             $sql = "SELECT cm.id
2955                       FROM {course_modules} cm
2956                      WHERE cm.course = :courseid AND
2957                            cm.idnumber = :idnumber AND
2958                            cm.id <> :cmid";
2959             $params = array(
2960                 'courseid' => $courseid,
2961                 'idnumber' => $data->idnumber,
2962                 'cmid' => $this->task->get_moduleid()
2963             );
2964             if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
2965                 $idnumber = $data->idnumber;
2966             }
2967         }
2969         unset($data->id);
2970         $data->categoryid   = $coursecatid;
2971         $data->courseid     = $this->get_courseid();
2972         $data->iteminstance = $this->task->get_activityid();
2973         $data->idnumber     = $idnumber;
2974         $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
2975         $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
2976         $data->timecreated  = $this->apply_date_offset($data->timecreated);
2977         $data->timemodified = $this->apply_date_offset($data->timemodified);
2979         $gradeitem = new grade_item($data, false);
2980         $gradeitem->insert('restore');
2982         //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
2983         $gradeitem->sortorder = $data->sortorder;
2984         $gradeitem->update('restore');
2986         // Set mapping, saving the original category id into parentitemid
2987         // gradebook restore (final task) will need it to reorganise items
2988         $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
2989     }
2991     protected function process_grade_grade($data) {
2992         $data = (object)($data);
2993         $olduserid = $data->userid;
2994         $oldid = $data->id;
2995         unset($data->id);
2997         $data->itemid = $this->get_new_parentid('grade_item');
2999         $data->userid = $this->get_mappingid('user', $data->userid, null);
3000         if (!empty($data->userid)) {
3001             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3002             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3003             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
3004             $data->overridden = $this->apply_date_offset($data->overridden);
3006             $grade = new grade_grade($data, false);
3007             $grade->insert('restore');
3008             $this->set_mapping('grade_grades', $oldid, $grade->id);
3009         } else {
3010             debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
3011         }
3012     }
3014     /**
3015      * process activity grade_letters. Note that, while these are possible,
3016      * because grade_letters are contextid based, in practice, only course
3017      * context letters can be defined. So we keep here this method knowing
3018      * it won't be executed ever. gradebook restore will restore course letters.
3019      */
3020     protected function process_grade_letter($data) {
3021         global $DB;
3023         $data['contextid'] = $this->task->get_contextid();
3024         $gradeletter = (object)$data;
3026         // Check if it exists before adding it
3027         unset($data['id']);
3028         if (!$DB->record_exists('grade_letters', $data)) {
3029             $newitemid = $DB->insert_record('grade_letters', $gradeletter);
3030         }
3031         // no need to save any grade_letter mapping
3032     }
3034     public function after_restore() {
3035         // Fix grade item's sortorder after restore, as it might have duplicates.
3036         $courseid = $this->get_task()->get_courseid();
3037         grade_item::fix_duplicate_sortorder($courseid);
3038     }
3041 /**
3042  * Step in charge of restoring the grade history of an activity.
3043  *
3044  * This step is added to the task regardless of the setting 'grade_histories'.
3045  * The reason is to allow for a more flexible step in case the logic needs to be
3046  * split accross different settings to control the history of items and/or grades.
3047  */
3048 class restore_activity_grade_history_structure_step extends restore_structure_step {
3050     /**
3051      * This step is executed only if the grade history file is present.
3052      */
3053      protected function execute_condition() {
3054         $fullpath = $this->task->get_taskbasepath();
3055         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3056         if (!file_exists($fullpath)) {
3057             return false;
3058         }
3059         return true;
3060     }
3062     protected function define_structure() {
3063         $paths = array();
3065         // Settings to use.
3066         $userinfo = $this->get_setting_value('userinfo');
3067         $history = $this->get_setting_value('grade_histories');
3069         if ($userinfo && $history) {
3070             $paths[] = new restore_path_element('grade_grade',
3071                '/grade_history/grade_grades/grade_grade');
3072         }
3074         return $paths;
3075     }
3077     protected function process_grade_grade($data) {
3078         global $DB;
3080         $data = (object) $data;
3081         $olduserid = $data->userid;
3082         unset($data->id);
3084         $data->userid = $this->get_mappingid('user', $data->userid, null);
3085         if (!empty($data->userid)) {
3086             // Do not apply the date offsets as this is history.
3087             $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
3088             $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
3089             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3090             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3091             $DB->insert_record('grade_grades_history', $data);
3092         } else {
3093             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
3094             $this->log($message, backup::LOG_DEBUG);
3095         }
3096     }
3100 /**
3101  * This structure steps restores one instance + positions of one block
3102  * Note: Positions corresponding to one existing context are restored
3103  * here, but all the ones having unknown contexts are sent to backup_ids
3104  * for a later chance to be restored at the end (final task)
3105  */
3106 class restore_block_instance_structure_step extends restore_structure_step {
3108     protected function define_structure() {
3110         $paths = array();
3112         $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
3113         $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
3115         return $paths;
3116     }
3118     public function process_block($data) {
3119         global $DB, $CFG;
3121         $data = (object)$data; // Handy
3122         $oldcontextid = $data->contextid;
3123         $oldid        = $data->id;
3124         $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
3126         // Look for the parent contextid
3127         if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
3128             throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
3129         }
3131         // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
3132         // If there is already one block of that type in the parent context
3133         // and the block is not multiple, stop processing
3134         // Use blockslib loader / method executor
3135         if (!$bi = block_instance($data->blockname)) {
3136             return false;
3137         }
3139         if (!$bi->instance_allow_multiple()) {
3140             if ($DB->record_exists_sql("SELECT bi.id
3141                                           FROM {block_instances} bi
3142                                           JOIN {block} b ON b.name = bi.blockname
3143                                          WHERE bi.parentcontextid = ?
3144                                            AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) {
3145                 return false;
3146             }
3147         }
3149         // If there is already one block of that type in the parent context
3150         // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
3151         // stop processing
3152         $params = array(
3153             'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
3154             'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
3155             'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
3156         if ($birecs = $DB->get_records('block_instances', $params)) {
3157             foreach($birecs as $birec) {
3158                 if ($birec->configdata == $data->configdata) {
3159                     return false;
3160                 }
3161             }
3162         }
3164         // Set task old contextid, blockid and blockname once we know them
3165         $this->task->set_old_contextid($oldcontextid);
3166         $this->task->set_old_blockid($oldid);
3167         $this->task->set_blockname($data->blockname);
3169         // Let's look for anything within configdata neededing processing
3170         // (nulls and uses of legacy file.php)
3171         if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
3172             $configdata = (array)unserialize(base64_decode($data->configdata));
3173             foreach ($configdata as $attribute => $value) {
3174                 if (in_array($attribute, $attrstotransform)) {
3175                     $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
3176                 }
3177             }
3178             $data->configdata = base64_encode(serialize((object)$configdata));
3179         }
3181         // Create the block instance
3182         $newitemid = $DB->insert_record('block_instances', $data);
3183         // Save the mapping (with restorefiles support)
3184         $this->set_mapping('block_instance', $oldid, $newitemid, true);
3185         // Create the block context
3186         $newcontextid = context_block::instance($newitemid)->id;
3187         // Save the block contexts mapping and sent it to task
3188         $this->set_mapping('context', $oldcontextid, $newcontextid);
3189         $this->task->set_contextid($newcontextid);
3190         $this->task->set_blockid($newitemid);
3192         // Restore block fileareas if declared
3193         $component = 'block_' . $this->task->get_blockname();
3194         foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
3195             $this->add_related_files($component, $filearea, null);
3196         }
3198         // Process block positions, creating them or accumulating for final step
3199         foreach($positions as $position) {
3200             $position = (object)$position;
3201             $position->blockinstanceid = $newitemid; // The instance is always the restored one
3202             // If position is for one already mapped (known) contextid
3203             // process it now, creating the position
3204             if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
3205                 $position->contextid = $newpositionctxid;
3206                 // Create the block position
3207                 $DB->insert_record('block_positions', $position);
3209             // The position belongs to an unknown context, send it to backup_ids
3210             // to process them as part of the final steps of restore. We send the
3211             // whole $position object there, hence use the low level method.
3212             } else {
3213                 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
3214             }
3215         }
3216     }
3219 /**
3220  * Structure step to restore common course_module information
3221  *
3222  * This step will process the module.xml file for one activity, in order to restore
3223  * the corresponding information to the course_modules table, skipping various bits
3224  * of information based on CFG settings (groupings, completion...) in order to fullfill
3225  * all the reqs to be able to create the context to be used by all the rest of steps
3226  * in the activity restore task
3227  */
3228 class restore_module_structure_step extends restore_structure_step {
3230     protected function define_structure() {
3231         global $CFG;
3233         $paths = array();
3235         $module = new restore_path_element('module', '/module');
3236         $paths[] = $module;
3237         if ($CFG->enableavailability) {
3238             $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
3239             $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
3240         }
3242         // Apply for 'format' plugins optional paths at module level
3243         $this->add_plugin_structure('format', $module);
3245         // Apply for 'plagiarism' plugins optional paths at module level
3246         $this->add_plugin_structure('plagiarism', $module);
3248         // Apply for 'local' plugins optional paths at module level
3249         $this->add_plugin_structure('local', $module);
3251         return $paths;
3252     }
3254     protected function process_module($data) {
3255         global $CFG, $DB;
3257         $data = (object)$data;
3258         $oldid = $data->id;
3259