faff785a0c71b857df30cc16afd7f6152420468c
[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(time() - (4 * 60 * 60), $progress);              // Delete > 4 hours 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         $olduserid = $data->userid;
229         $data->itemid = $this->get_new_parentid('grade_item');
231         $data->userid = $this->get_mappingid('user', $data->userid, null);
232         if (!empty($data->userid)) {
233             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
234             $data->locktime     = $this->apply_date_offset($data->locktime);
235             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
236             $data->overridden = $this->apply_date_offset($data->overridden);
237             $data->timecreated  = $this->apply_date_offset($data->timecreated);
238             $data->timemodified = $this->apply_date_offset($data->timemodified);
240             $newitemid = $DB->insert_record('grade_grades', $data);
241         } else {
242             $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
243             $this->log($message, backup::LOG_DEBUG);
244         }
245     }
247     protected function process_grade_category($data) {
248         global $DB;
250         $data = (object)$data;
251         $oldid = $data->id;
253         $data->course = $this->get_courseid();
254         $data->courseid = $data->course;
256         $data->timecreated  = $this->apply_date_offset($data->timecreated);
257         $data->timemodified = $this->apply_date_offset($data->timemodified);
259         $newitemid = null;
260         //no parent means a course level grade category. That may have been created when the course was created
261         if(empty($data->parent)) {
262             //parent was being saved as 0 when it should be null
263             $data->parent = null;
265             //get the already created course level grade category
266             $category = new stdclass();
267             $category->courseid = $this->get_courseid();
268             $category->parent = null;
270             $coursecategory = $DB->get_record('grade_categories', (array)$category);
271             if (!empty($coursecategory)) {
272                 $data->id = $newitemid = $coursecategory->id;
273                 $DB->update_record('grade_categories', $data);
274             }
275         }
277         //need to insert a course category
278         if (empty($newitemid)) {
279             $newitemid = $DB->insert_record('grade_categories', $data);
280         }
281         $this->set_mapping('grade_category', $oldid, $newitemid);
282     }
283     protected function process_grade_letter($data) {
284         global $DB;
286         $data = (object)$data;
287         $oldid = $data->id;
289         $data->contextid = context_course::instance($this->get_courseid())->id;
291         $gradeletter = (array)$data;
292         unset($gradeletter['id']);
293         if (!$DB->record_exists('grade_letters', $gradeletter)) {
294             $newitemid = $DB->insert_record('grade_letters', $data);
295         } else {
296             $newitemid = $data->id;
297         }
299         $this->set_mapping('grade_letter', $oldid, $newitemid);
300     }
301     protected function process_grade_setting($data) {
302         global $DB;
304         $data = (object)$data;
305         $oldid = $data->id;
307         $data->courseid = $this->get_courseid();
309         if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
310             $newitemid = $DB->insert_record('grade_settings', $data);
311         } else {
312             $newitemid = $data->id;
313         }
315         $this->set_mapping('grade_setting', $oldid, $newitemid);
316     }
318     /**
319      * put all activity grade items in the correct grade category and mark all for recalculation
320      */
321     protected function after_execute() {
322         global $DB;
324         $conditions = array(
325             'backupid' => $this->get_restoreid(),
326             'itemname' => 'grade_item'//,
327             //'itemid'   => $itemid
328         );
329         $rs = $DB->get_recordset('backup_ids_temp', $conditions);
331         // We need this for calculation magic later on.
332         $mappings = array();
334         if (!empty($rs)) {
335             foreach($rs as $grade_item_backup) {
337                 // Store the oldid with the new id.
338                 $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
340                 $updateobj = new stdclass();
341                 $updateobj->id = $grade_item_backup->newitemid;
343                 //if this is an activity grade item that needs to be put back in its correct category
344                 if (!empty($grade_item_backup->parentitemid)) {
345                     $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
346                     if (!is_null($oldcategoryid)) {
347                         $updateobj->categoryid = $oldcategoryid;
348                         $DB->update_record('grade_items', $updateobj);
349                     }
350                 } else {
351                     //mark course and category items as needing to be recalculated
352                     $updateobj->needsupdate=1;
353                     $DB->update_record('grade_items', $updateobj);
354                 }
355             }
356         }
357         $rs->close();
359         // We need to update the calculations for calculated grade items that may reference old
360         // grade item ids using ##gi\d+##.
361         // $mappings can be empty, use 0 if so (won't match ever)
362         list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
363         $sql = "SELECT gi.id, gi.calculation
364                   FROM {grade_items} gi
365                  WHERE gi.id {$sql} AND
366                        calculation IS NOT NULL";
367         $rs = $DB->get_recordset_sql($sql, $params);
368         foreach ($rs as $gradeitem) {
369             // Collect all of the used grade item id references
370             if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
371                 // This calculation doesn't reference any other grade items... EASY!
372                 continue;
373             }
374             // For this next bit we are going to do the replacement of id's in two steps:
375             // 1. We will replace all old id references with a special mapping reference.
376             // 2. We will replace all mapping references with id's
377             // Why do we do this?
378             // Because there potentially there will be an overlap of ids within the query and we
379             // we substitute the wrong id.. safest way around this is the two step system
380             $calculationmap = array();
381             $mapcount = 0;
382             foreach ($matches[1] as $match) {
383                 // Check that the old id is known to us, if not it was broken to begin with and will
384                 // continue to be broken.
385                 if (!array_key_exists($match, $mappings)) {
386                     continue;
387                 }
388                 // Our special mapping key
389                 $mapping = '##MAPPING'.$mapcount.'##';
390                 // The old id that exists within the calculation now
391                 $oldid = '##gi'.$match.'##';
392                 // The new id that we want to replace the old one with.
393                 $newid = '##gi'.$mappings[$match].'##';
394                 // Replace in the special mapping key
395                 $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
396                 // And record the mapping
397                 $calculationmap[$mapping] = $newid;
398                 $mapcount++;
399             }
400             // Iterate all special mappings for this calculation and replace in the new id's
401             foreach ($calculationmap as $mapping => $newid) {
402                 $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
403             }
404             // Update the calculation now that its being remapped
405             $DB->update_record('grade_items', $gradeitem);
406         }
407         $rs->close();
409         // Need to correct the grade category path and parent
410         $conditions = array(
411             'courseid' => $this->get_courseid()
412         );
414         $rs = $DB->get_recordset('grade_categories', $conditions);
415         // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
416         foreach ($rs as $gc) {
417             if (!empty($gc->parent)) {
418                 $grade_category = new stdClass();
419                 $grade_category->id = $gc->id;
420                 $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
421                 $DB->update_record('grade_categories', $grade_category);
422             }
423         }
424         $rs->close();
426         // Now we can rebuild all the paths
427         $rs = $DB->get_recordset('grade_categories', $conditions);
428         foreach ($rs as $gc) {
429             $grade_category = new stdClass();
430             $grade_category->id = $gc->id;
431             $grade_category->path = grade_category::build_path($gc);
432             $grade_category->depth = substr_count($grade_category->path, '/') - 1;
433             $DB->update_record('grade_categories', $grade_category);
434         }
435         $rs->close();
437         // Restore marks items as needing update. Update everything now.
438         grade_regrade_final_grades($this->get_courseid());
439     }
442 /**
443  * decode all the interlinks present in restored content
444  * relying 100% in the restore_decode_processor that handles
445  * both the contents to modify and the rules to be applied
446  */
447 class restore_decode_interlinks extends restore_execution_step {
449     protected function define_execution() {
450         // Get the decoder (from the plan)
451         $decoder = $this->task->get_decoder();
452         restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
453         // And launch it, everything will be processed
454         $decoder->execute();
455     }
458 /**
459  * first, ensure that we have no gaps in section numbers
460  * and then, rebuid the course cache
461  */
462 class restore_rebuild_course_cache extends restore_execution_step {
464     protected function define_execution() {
465         global $DB;
467         // Although there is some sort of auto-recovery of missing sections
468         // present in course/formats... here we check that all the sections
469         // from 0 to MAX(section->section) exist, creating them if necessary
470         $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
471         // Iterate over all sections
472         for ($i = 0; $i <= $maxsection; $i++) {
473             // If the section $i doesn't exist, create it
474             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
475                 $sectionrec = array(
476                     'course' => $this->get_courseid(),
477                     'section' => $i);
478                 $DB->insert_record('course_sections', $sectionrec); // missing section created
479             }
480         }
482         // Rebuild cache now that all sections are in place
483         rebuild_course_cache($this->get_courseid());
484         cache_helper::purge_by_event('changesincourse');
485         cache_helper::purge_by_event('changesincoursecat');
486     }
489 /**
490  * Review all the tasks having one after_restore method
491  * executing it to perform some final adjustments of information
492  * not available when the task was executed.
493  */
494 class restore_execute_after_restore extends restore_execution_step {
496     protected function define_execution() {
498         // Simply call to the execute_after_restore() method of the task
499         // that always is the restore_final_task
500         $this->task->launch_execute_after_restore();
501     }
505 /**
506  * Review all the (pending) block positions in backup_ids, matching by
507  * contextid, creating positions as needed. This is executed by the
508  * final task, once all the contexts have been created
509  */
510 class restore_review_pending_block_positions extends restore_execution_step {
512     protected function define_execution() {
513         global $DB;
515         // Get all the block_position objects pending to match
516         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
517         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
518         // Process block positions, creating them or accumulating for final step
519         foreach($rs as $posrec) {
520             // Get the complete position object out of the info field.
521             $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
522             // If position is for one already mapped (known) contextid
523             // process it now, creating the position, else nothing to
524             // do, position finally discarded
525             if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
526                 $position->contextid = $newctx->newitemid;
527                 // Create the block position
528                 $DB->insert_record('block_positions', $position);
529             }
530         }
531         $rs->close();
532     }
536 /**
537  * Updates the availability data for course modules and sections.
538  *
539  * Runs after the restore of all course modules, sections, and grade items has
540  * completed. This is necessary in order to update IDs that have changed during
541  * restore.
542  *
543  * @package core_backup
544  * @copyright 2014 The Open University
545  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
546  */
547 class restore_update_availability extends restore_execution_step {
549     protected function define_execution() {
550         global $CFG, $DB;
552         // Note: This code runs even if availability is disabled when restoring.
553         // That will ensure that if you later turn availability on for the site,
554         // there will be no incorrect IDs. (It doesn't take long if the restored
555         // data does not contain any availability information.)
557         // Get modinfo with all data after resetting cache.
558         rebuild_course_cache($this->get_courseid(), true);
559         $modinfo = get_fast_modinfo($this->get_courseid());
561         // Update all sections that were restored.
562         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
563         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
564         $sectionsbyid = null;
565         foreach ($rs as $rec) {
566             if (is_null($sectionsbyid)) {
567                 $sectionsbyid = array();
568                 foreach ($modinfo->get_section_info_all() as $section) {
569                     $sectionsbyid[$section->id] = $section;
570                 }
571             }
572             if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
573                 // If the section was not fully restored for some reason
574                 // (e.g. due to an earlier error), skip it.
575                 $this->get_logger()->process('Section not fully restored: id ' .
576                         $rec->newitemid, backup::LOG_WARNING);
577                 continue;
578             }
579             $section = $sectionsbyid[$rec->newitemid];
580             if (!is_null($section->availability)) {
581                 $info = new \core_availability\info_section($section);
582                 $info->update_after_restore($this->get_restoreid(),
583                         $this->get_courseid(), $this->get_logger());
584             }
585         }
586         $rs->close();
588         // Update all modules that were restored.
589         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
590         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
591         foreach ($rs as $rec) {
592             if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
593                 // If the module was not fully restored for some reason
594                 // (e.g. due to an earlier error), skip it.
595                 $this->get_logger()->process('Module not fully restored: id ' .
596                         $rec->newitemid, backup::LOG_WARNING);
597                 continue;
598             }
599             $cm = $modinfo->get_cm($rec->newitemid);
600             if (!is_null($cm->availability)) {
601                 $info = new \core_availability\info_module($cm);
602                 $info->update_after_restore($this->get_restoreid(),
603                         $this->get_courseid(), $this->get_logger());
604             }
605         }
606         $rs->close();
607     }
611 /**
612  * Process legacy module availability records in backup_ids.
613  *
614  * Matches course modules and grade item id once all them have been already restored.
615  * Only if all matchings are satisfied the availability condition will be created.
616  * At the same time, it is required for the site to have that functionality enabled.
617  *
618  * This step is included only to handle legacy backups (2.6 and before). It does not
619  * do anything for newer backups.
620  *
621  * @copyright 2014 The Open University
622  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
623  */
624 class restore_process_course_modules_availability extends restore_execution_step {
626     protected function define_execution() {
627         global $CFG, $DB;
629         // Site hasn't availability enabled
630         if (empty($CFG->enableavailability)) {
631             return;
632         }
634         // Do both modules and sections.
635         foreach (array('module', 'section') as $table) {
636             // Get all the availability objects to process.
637             $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
638             $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
639             // Process availabilities, creating them if everything matches ok.
640             foreach ($rs as $availrec) {
641                 $allmatchesok = true;
642                 // Get the complete legacy availability object.
643                 $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
645                 // Note: This code used to update IDs, but that is now handled by the
646                 // current code (after restore) instead of this legacy code.
648                 // Get showavailability option.
649                 $thingid = ($table === 'module') ? $availability->coursemoduleid :
650                         $availability->coursesectionid;
651                 $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
652                         $table . '_showavailability', $thingid);
653                 if (!$showrec) {
654                     // Should not happen.
655                     throw new coding_exception('No matching showavailability record');
656                 }
657                 $show = $showrec->info->showavailability;
659                 // The $availability object is now in the format used in the old
660                 // system. Interpret this and convert to new system.
661                 $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
662                         array('id' => $thingid), MUST_EXIST);
663                 $newvalue = \core_availability\info::add_legacy_availability_condition(
664                         $currentvalue, $availability, $show);
665                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
666                         array('id' => $thingid));
667             }
668         }
669         $rs->close();
670     }
674 /*
675  * Execution step that, *conditionally* (if there isn't preloaded information)
676  * will load the inforef files for all the included course/section/activity tasks
677  * to backup_temp_ids. They will be stored with "xxxxref" as itemname
678  */
679 class restore_load_included_inforef_records extends restore_execution_step {
681     protected function define_execution() {
683         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
684             return;
685         }
687         // Get all the included tasks
688         $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
689         $progress = $this->task->get_progress();
690         $progress->start_progress($this->get_name(), count($tasks));
691         foreach ($tasks as $task) {
692             // Load the inforef.xml file if exists
693             $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
694             if (file_exists($inforefpath)) {
695                 // Load each inforef file to temp_ids.
696                 restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
697             }
698         }
699         $progress->end_progress();
700     }
703 /*
704  * Execution step that will load all the needed files into backup_files_temp
705  *   - info: contains the whole original object (times, names...)
706  * (all them being original ids as loaded from xml)
707  */
708 class restore_load_included_files extends restore_structure_step {
710     protected function define_structure() {
712         $file = new restore_path_element('file', '/files/file');
714         return array($file);
715     }
717     /**
718      * Process one <file> element from files.xml
719      *
720      * @param array $data the element data
721      */
722     public function process_file($data) {
724         $data = (object)$data; // handy
726         // load it if needed:
727         //   - it it is one of the annotated inforef files (course/section/activity/block)
728         //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
729         // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
730         //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
731         $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
732         $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
733                         $data->component == 'grouping' || $data->component == 'grade' ||
734                         $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
735         if ($isfileref || $iscomponent) {
736             restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
737         }
738     }
741 /**
742  * Execution step that, *conditionally* (if there isn't preloaded information),
743  * will load all the needed roles to backup_temp_ids. They will be stored with
744  * "role" itemname. Also it will perform one automatic mapping to roles existing
745  * in the target site, based in permissions of the user performing the restore,
746  * archetypes and other bits. At the end, each original role will have its associated
747  * target role or 0 if it's going to be skipped. Note we wrap everything over one
748  * restore_dbops method, as far as the same stuff is going to be also executed
749  * by restore prechecks
750  */
751 class restore_load_and_map_roles extends restore_execution_step {
753     protected function define_execution() {
754         if ($this->task->get_preloaded_information()) { // if info is already preloaded
755             return;
756         }
758         $file = $this->get_basepath() . '/roles.xml';
759         // Load needed toles to temp_ids
760         restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
762         // Process roles, mapping/skipping. Any error throws exception
763         // Note we pass controller's info because it can contain role mapping information
764         // about manual mappings performed by UI
765         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);
766     }
769 /**
770  * Execution step that, *conditionally* (if there isn't preloaded information
771  * and users have been selected in settings, will load all the needed users
772  * to backup_temp_ids. They will be stored with "user" itemname and with
773  * their original contextid as paremitemid
774  */
775 class restore_load_included_users extends restore_execution_step {
777     protected function define_execution() {
779         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
780             return;
781         }
782         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
783             return;
784         }
785         $file = $this->get_basepath() . '/users.xml';
786         // Load needed users to temp_ids.
787         restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
788     }
791 /**
792  * Execution step that, *conditionally* (if there isn't preloaded information
793  * and users have been selected in settings, will process all the needed users
794  * in order to decide and perform any action with them (create / map / error)
795  * Note: Any error will cause exception, as far as this is the same processing
796  * than the one into restore prechecks (that should have stopped process earlier)
797  */
798 class restore_process_included_users extends restore_execution_step {
800     protected function define_execution() {
802         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
803             return;
804         }
805         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
806             return;
807         }
808         restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
809                 $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
810     }
813 /**
814  * Execution step that will create all the needed users as calculated
815  * by @restore_process_included_users (those having newiteind = 0)
816  */
817 class restore_create_included_users extends restore_execution_step {
819     protected function define_execution() {
821         restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
822                 $this->task->get_userid(), $this->task->get_progress());
823     }
826 /**
827  * Structure step that will create all the needed groups and groupings
828  * by loading them from the groups.xml file performing the required matches.
829  * Note group members only will be added if restoring user info
830  */
831 class restore_groups_structure_step extends restore_structure_step {
833     protected function define_structure() {
835         $paths = array(); // Add paths here
837         $paths[] = new restore_path_element('group', '/groups/group');
838         $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
839         $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
841         return $paths;
842     }
844     // Processing functions go here
845     public function process_group($data) {
846         global $DB;
848         $data = (object)$data; // handy
849         $data->courseid = $this->get_courseid();
851         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
852         // another a group in the same course
853         $context = context_course::instance($data->courseid);
854         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
855             if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
856                 unset($data->idnumber);
857             }
858         } else {
859             unset($data->idnumber);
860         }
862         $oldid = $data->id;    // need this saved for later
864         $restorefiles = false; // Only if we end creating the group
866         // Search if the group already exists (by name & description) in the target course
867         $description_clause = '';
868         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
869         if (!empty($data->description)) {
870             $description_clause = ' AND ' .
871                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
872            $params['description'] = $data->description;
873         }
874         if (!$groupdb = $DB->get_record_sql("SELECT *
875                                                FROM {groups}
876                                               WHERE courseid = :courseid
877                                                 AND name = :grname $description_clause", $params)) {
878             // group doesn't exist, create
879             $newitemid = $DB->insert_record('groups', $data);
880             $restorefiles = true; // We'll restore the files
881         } else {
882             // group exists, use it
883             $newitemid = $groupdb->id;
884         }
885         // Save the id mapping
886         $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
887         // Invalidate the course group data cache just in case.
888         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
889     }
891     public function process_grouping($data) {
892         global $DB;
894         $data = (object)$data; // handy
895         $data->courseid = $this->get_courseid();
897         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
898         // another a grouping in the same course
899         $context = context_course::instance($data->courseid);
900         if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
901             if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
902                 unset($data->idnumber);
903             }
904         } else {
905             unset($data->idnumber);
906         }
908         $oldid = $data->id;    // need this saved for later
909         $restorefiles = false; // Only if we end creating the grouping
911         // Search if the grouping already exists (by name & description) in the target course
912         $description_clause = '';
913         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
914         if (!empty($data->description)) {
915             $description_clause = ' AND ' .
916                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
917            $params['description'] = $data->description;
918         }
919         if (!$groupingdb = $DB->get_record_sql("SELECT *
920                                                   FROM {groupings}
921                                                  WHERE courseid = :courseid
922                                                    AND name = :grname $description_clause", $params)) {
923             // grouping doesn't exist, create
924             $newitemid = $DB->insert_record('groupings', $data);
925             $restorefiles = true; // We'll restore the files
926         } else {
927             // grouping exists, use it
928             $newitemid = $groupingdb->id;
929         }
930         // Save the id mapping
931         $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
932         // Invalidate the course group data cache just in case.
933         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
934     }
936     public function process_grouping_group($data) {
937         global $CFG;
939         require_once($CFG->dirroot.'/group/lib.php');
941         $data = (object)$data;
942         groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
943     }
945     protected function after_execute() {
946         // Add group related files, matching with "group" mappings
947         $this->add_related_files('group', 'icon', 'group');
948         $this->add_related_files('group', 'description', 'group');
949         // Add grouping related files, matching with "grouping" mappings
950         $this->add_related_files('grouping', 'description', 'grouping');
951         // Invalidate the course group data.
952         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
953     }
957 /**
958  * Structure step that will create all the needed group memberships
959  * by loading them from the groups.xml file performing the required matches.
960  */
961 class restore_groups_members_structure_step extends restore_structure_step {
963     protected $plugins = null;
965     protected function define_structure() {
967         $paths = array(); // Add paths here
969         if ($this->get_setting_value('users')) {
970             $paths[] = new restore_path_element('group', '/groups/group');
971             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
972         }
974         return $paths;
975     }
977     public function process_group($data) {
978         $data = (object)$data; // handy
980         // HACK ALERT!
981         // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
982         // Let's fake internal state to make $this->get_new_parentid('group') work.
984         $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
985     }
987     public function process_member($data) {
988         global $DB, $CFG;
989         require_once("$CFG->dirroot/group/lib.php");
991         // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
993         $data = (object)$data; // handy
995         // get parent group->id
996         $data->groupid = $this->get_new_parentid('group');
998         // map user newitemid and insert if not member already
999         if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1000             if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1001                 // Check the component, if any, exists.
1002                 if (empty($data->component)) {
1003                     groups_add_member($data->groupid, $data->userid);
1005                 } else if ((strpos($data->component, 'enrol_') === 0)) {
1006                     // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1007                     // it is possible that enrolment was restored using different plugin type.
1008                     if (!isset($this->plugins)) {
1009                         $this->plugins = enrol_get_plugins(true);
1010                     }
1011                     if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1012                         if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1013                             if (isset($this->plugins[$instance->enrol])) {
1014                                 $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1015                             }
1016                         }
1017                     }
1019                 } else {
1020                     $dir = core_component::get_component_directory($data->component);
1021                     if ($dir and is_dir($dir)) {
1022                         if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1023                             return;
1024                         }
1025                     }
1026                     // Bad luck, plugin could not restore the data, let's add normal membership.
1027                     groups_add_member($data->groupid, $data->userid);
1028                     $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1029                     $this->log($message, backup::LOG_WARNING);
1030                 }
1031             }
1032         }
1033     }
1036 /**
1037  * Structure step that will create all the needed scales
1038  * by loading them from the scales.xml
1039  */
1040 class restore_scales_structure_step extends restore_structure_step {
1042     protected function define_structure() {
1044         $paths = array(); // Add paths here
1045         $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1046         return $paths;
1047     }
1049     protected function process_scale($data) {
1050         global $DB;
1052         $data = (object)$data;
1054         $restorefiles = false; // Only if we end creating the group
1056         $oldid = $data->id;    // need this saved for later
1058         // Look for scale (by 'scale' both in standard (course=0) and current course
1059         // with priority to standard scales (ORDER clause)
1060         // scale is not course unique, use get_record_sql to suppress warning
1061         // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1062         $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1063         $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1064         if (!$scadb = $DB->get_record_sql("SELECT *
1065                                             FROM {scale}
1066                                            WHERE courseid IN (0, :courseid)
1067                                              AND $compare_scale_clause
1068                                         ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1069             // Remap the user if possible, defaut to user performing the restore if not
1070             $userid = $this->get_mappingid('user', $data->userid);
1071             $data->userid = $userid ? $userid : $this->task->get_userid();
1072             // Remap the course if course scale
1073             $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1074             // If global scale (course=0), check the user has perms to create it
1075             // falling to course scale if not
1076             $systemctx = context_system::instance();
1077             if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1078                 $data->courseid = $this->get_courseid();
1079             }
1080             // scale doesn't exist, create
1081             $newitemid = $DB->insert_record('scale', $data);
1082             $restorefiles = true; // We'll restore the files
1083         } else {
1084             // scale exists, use it
1085             $newitemid = $scadb->id;
1086         }
1087         // Save the id mapping (with files support at system context)
1088         $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1089     }
1091     protected function after_execute() {
1092         // Add scales related files, matching with "scale" mappings
1093         $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1094     }
1098 /**
1099  * Structure step that will create all the needed outocomes
1100  * by loading them from the outcomes.xml
1101  */
1102 class restore_outcomes_structure_step extends restore_structure_step {
1104     protected function define_structure() {
1106         $paths = array(); // Add paths here
1107         $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1108         return $paths;
1109     }
1111     protected function process_outcome($data) {
1112         global $DB;
1114         $data = (object)$data;
1116         $restorefiles = false; // Only if we end creating the group
1118         $oldid = $data->id;    // need this saved for later
1120         // Look for outcome (by shortname both in standard (courseid=null) and current course
1121         // with priority to standard outcomes (ORDER clause)
1122         // outcome is not course unique, use get_record_sql to suppress warning
1123         $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1124         if (!$outdb = $DB->get_record_sql('SELECT *
1125                                              FROM {grade_outcomes}
1126                                             WHERE shortname = :shortname
1127                                               AND (courseid = :courseid OR courseid IS NULL)
1128                                          ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1129             // Remap the user
1130             $userid = $this->get_mappingid('user', $data->usermodified);
1131             $data->usermodified = $userid ? $userid : $this->task->get_userid();
1132             // Remap the scale
1133             $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1134             // Remap the course if course outcome
1135             $data->courseid = $data->courseid ? $this->get_courseid() : null;
1136             // If global outcome (course=null), check the user has perms to create it
1137             // falling to course outcome if not
1138             $systemctx = context_system::instance();
1139             if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1140                 $data->courseid = $this->get_courseid();
1141             }
1142             // outcome doesn't exist, create
1143             $newitemid = $DB->insert_record('grade_outcomes', $data);
1144             $restorefiles = true; // We'll restore the files
1145         } else {
1146             // scale exists, use it
1147             $newitemid = $outdb->id;
1148         }
1149         // Set the corresponding grade_outcomes_courses record
1150         $outcourserec = new stdclass();
1151         $outcourserec->courseid  = $this->get_courseid();
1152         $outcourserec->outcomeid = $newitemid;
1153         if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1154             $DB->insert_record('grade_outcomes_courses', $outcourserec);
1155         }
1156         // Save the id mapping (with files support at system context)
1157         $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1158     }
1160     protected function after_execute() {
1161         // Add outcomes related files, matching with "outcome" mappings
1162         $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1163     }
1166 /**
1167  * Execution step that, *conditionally* (if there isn't preloaded information
1168  * will load all the question categories and questions (header info only)
1169  * to backup_temp_ids. They will be stored with "question_category" and
1170  * "question" itemnames and with their original contextid and question category
1171  * id as paremitemids
1172  */
1173 class restore_load_categories_and_questions extends restore_execution_step {
1175     protected function define_execution() {
1177         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1178             return;
1179         }
1180         $file = $this->get_basepath() . '/questions.xml';
1181         restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1182     }
1185 /**
1186  * Execution step that, *conditionally* (if there isn't preloaded information)
1187  * will process all the needed categories and questions
1188  * in order to decide and perform any action with them (create / map / error)
1189  * Note: Any error will cause exception, as far as this is the same processing
1190  * than the one into restore prechecks (that should have stopped process earlier)
1191  */
1192 class restore_process_categories_and_questions extends restore_execution_step {
1194     protected function define_execution() {
1196         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1197             return;
1198         }
1199         restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1200     }
1203 /**
1204  * Structure step that will read the section.xml creating/updating sections
1205  * as needed, rebuilding course cache and other friends
1206  */
1207 class restore_section_structure_step extends restore_structure_step {
1209     protected function define_structure() {
1210         global $CFG;
1212         $paths = array();
1214         $section = new restore_path_element('section', '/section');
1215         $paths[] = $section;
1216         if ($CFG->enableavailability) {
1217             $paths[] = new restore_path_element('availability', '/section/availability');
1218             $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1219         }
1220         $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1222         // Apply for 'format' plugins optional paths at section level
1223         $this->add_plugin_structure('format', $section);
1225         // Apply for 'local' plugins optional paths at section level
1226         $this->add_plugin_structure('local', $section);
1228         return $paths;
1229     }
1231     public function process_section($data) {
1232         global $CFG, $DB;
1233         $data = (object)$data;
1234         $oldid = $data->id; // We'll need this later
1236         $restorefiles = false;
1238         // Look for the section
1239         $section = new stdclass();
1240         $section->course  = $this->get_courseid();
1241         $section->section = $data->number;
1242         // Section doesn't exist, create it with all the info from backup
1243         if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
1244             $section->name = $data->name;
1245             $section->summary = $data->summary;
1246             $section->summaryformat = $data->summaryformat;
1247             $section->sequence = '';
1248             $section->visible = $data->visible;
1249             if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1250                 $section->availability = null;
1251             } else {
1252                 $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1253                 // Include legacy [<2.7] availability data if provided.
1254                 if (is_null($section->availability)) {
1255                     $section->availability = \core_availability\info::convert_legacy_fields(
1256                             $data, true);
1257                 }
1258             }
1259             $newitemid = $DB->insert_record('course_sections', $section);
1260             $restorefiles = true;
1262         // Section exists, update non-empty information
1263         } else {
1264             $section->id = $secrec->id;
1265             if ((string)$secrec->name === '') {
1266                 $section->name = $data->name;
1267             }
1268             if (empty($secrec->summary)) {
1269                 $section->summary = $data->summary;
1270                 $section->summaryformat = $data->summaryformat;
1271                 $restorefiles = true;
1272             }
1274             // Don't update availability (I didn't see a useful way to define
1275             // whether existing or new one should take precedence).
1277             $DB->update_record('course_sections', $section);
1278             $newitemid = $secrec->id;
1279         }
1281         // Annotate the section mapping, with restorefiles option if needed
1282         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1284         // set the new course_section id in the task
1285         $this->task->set_sectionid($newitemid);
1287         // If there is the legacy showavailability data, store this for later use.
1288         // (This data is not present when restoring 'new' backups.)
1289         if (isset($data->showavailability)) {
1290             // Cache the showavailability flag using the backup_ids data field.
1291             restore_dbops::set_backup_ids_record($this->get_restoreid(),
1292                     'section_showavailability', $newitemid, 0, null,
1293                     (object)array('showavailability' => $data->showavailability));
1294         }
1296         // Commented out. We never modify course->numsections as far as that is used
1297         // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1298         // Note: We keep the code here, to know about and because of the possibility of making this
1299         // optional based on some setting/attribute in the future
1300         // If needed, adjust course->numsections
1301         //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1302         //    if ($numsections < $section->section) {
1303         //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1304         //    }
1305         //}
1306     }
1308     /**
1309      * Process the legacy availability table record. This table does not exist
1310      * in Moodle 2.7+ but we still support restore.
1311      *
1312      * @param stdClass $data Record data
1313      */
1314     public function process_availability($data) {
1315         $data = (object)$data;
1316         // Simply going to store the whole availability record now, we'll process
1317         // all them later in the final task (once all activities have been restored)
1318         // Let's call the low level one to be able to store the whole object.
1319         $data->coursesectionid = $this->task->get_sectionid();
1320         restore_dbops::set_backup_ids_record($this->get_restoreid(),
1321                 'section_availability', $data->id, 0, null, $data);
1322     }
1324     /**
1325      * Process the legacy availability fields table record. This table does not
1326      * exist in Moodle 2.7+ but we still support restore.
1327      *
1328      * @param stdClass $data Record data
1329      */
1330     public function process_availability_field($data) {
1331         global $DB;
1332         $data = (object)$data;
1333         // Mark it is as passed by default
1334         $passed = true;
1335         $customfieldid = null;
1337         // If a customfield has been used in order to pass we must be able to match an existing
1338         // customfield by name (data->customfield) and type (data->customfieldtype)
1339         if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1340             // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1341             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1342             $passed = false;
1343         } else if (!is_null($data->customfield)) {
1344             $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1345             $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1346             $passed = ($customfieldid !== false);
1347         }
1349         if ($passed) {
1350             // Create the object to insert into the database
1351             $availfield = new stdClass();
1352             $availfield->coursesectionid = $this->task->get_sectionid();
1353             $availfield->userfield = $data->userfield;
1354             $availfield->customfieldid = $customfieldid;
1355             $availfield->operator = $data->operator;
1356             $availfield->value = $data->value;
1358             // Get showavailability option.
1359             $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1360                     'section_showavailability', $availfield->coursesectionid);
1361             if (!$showrec) {
1362                 // Should not happen.
1363                 throw new coding_exception('No matching showavailability record');
1364             }
1365             $show = $showrec->info->showavailability;
1367             // The $availfield object is now in the format used in the old
1368             // system. Interpret this and convert to new system.
1369             $currentvalue = $DB->get_field('course_sections', 'availability',
1370                     array('id' => $availfield->coursesectionid), MUST_EXIST);
1371             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1372                     $currentvalue, $availfield, $show);
1373             $DB->set_field('course_sections', 'availability', $newvalue,
1374                     array('id' => $availfield->coursesectionid));
1375         }
1376     }
1378     public function process_course_format_options($data) {
1379         global $DB;
1380         $data = (object)$data;
1381         $oldid = $data->id;
1382         unset($data->id);
1383         $data->sectionid = $this->task->get_sectionid();
1384         $data->courseid = $this->get_courseid();
1385         $newid = $DB->insert_record('course_format_options', $data);
1386         $this->set_mapping('course_format_options', $oldid, $newid);
1387     }
1389     protected function after_execute() {
1390         // Add section related files, with 'course_section' itemid to match
1391         $this->add_related_files('course', 'section', 'course_section');
1392     }
1395 /**
1396  * Structure step that will read the course.xml file, loading it and performing
1397  * various actions depending of the site/restore settings. Note that target
1398  * course always exist before arriving here so this step will be updating
1399  * the course record (never inserting)
1400  */
1401 class restore_course_structure_step extends restore_structure_step {
1402     /**
1403      * @var bool this gets set to true by {@link process_course()} if we are
1404      * restoring an old coures that used the legacy 'module security' feature.
1405      * If so, we have to do more work in {@link after_execute()}.
1406      */
1407     protected $legacyrestrictmodules = false;
1409     /**
1410      * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1411      * array with array keys the module names ('forum', 'quiz', etc.). These are
1412      * the modules that are allowed according to the data in the backup file.
1413      * In {@link after_execute()} we then have to prevent adding of all the other
1414      * types of activity.
1415      */
1416     protected $legacyallowedmodules = array();
1418     protected function define_structure() {
1420         $course = new restore_path_element('course', '/course');
1421         $category = new restore_path_element('category', '/course/category');
1422         $tag = new restore_path_element('tag', '/course/tags/tag');
1423         $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1425         // Apply for 'format' plugins optional paths at course level
1426         $this->add_plugin_structure('format', $course);
1428         // Apply for 'theme' plugins optional paths at course level
1429         $this->add_plugin_structure('theme', $course);
1431         // Apply for 'report' plugins optional paths at course level
1432         $this->add_plugin_structure('report', $course);
1434         // Apply for 'course report' plugins optional paths at course level
1435         $this->add_plugin_structure('coursereport', $course);
1437         // Apply for plagiarism plugins optional paths at course level
1438         $this->add_plugin_structure('plagiarism', $course);
1440         // Apply for local plugins optional paths at course level
1441         $this->add_plugin_structure('local', $course);
1443         return array($course, $category, $tag, $allowed_module);
1444     }
1446     /**
1447      * Processing functions go here
1448      *
1449      * @global moodledatabase $DB
1450      * @param stdClass $data
1451      */
1452     public function process_course($data) {
1453         global $CFG, $DB;
1455         $data = (object)$data;
1457         $fullname  = $this->get_setting_value('course_fullname');
1458         $shortname = $this->get_setting_value('course_shortname');
1459         $startdate = $this->get_setting_value('course_startdate');
1461         // Calculate final course names, to avoid dupes
1462         list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
1464         // Need to change some fields before updating the course record
1465         $data->id = $this->get_courseid();
1466         $data->fullname = $fullname;
1467         $data->shortname= $shortname;
1469         $context = context::instance_by_id($this->task->get_contextid());
1470         if (has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1471             $data->idnumber = '';
1472         } else {
1473             unset($data->idnumber);
1474         }
1476         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1477         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1478         if (empty($data->hiddensections)) {
1479             $data->hiddensections = 0;
1480         }
1482         // Set legacyrestrictmodules to true if the course was resticting modules. If so
1483         // then we will need to process restricted modules after execution.
1484         $this->legacyrestrictmodules = !empty($data->restrictmodules);
1486         $data->startdate= $this->apply_date_offset($data->startdate);
1487         if ($data->defaultgroupingid) {
1488             $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1489         }
1490         if (empty($CFG->enablecompletion)) {
1491             $data->enablecompletion = 0;
1492             $data->completionstartonenrol = 0;
1493             $data->completionnotify = 0;
1494         }
1495         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1496         if (!array_key_exists($data->lang, $languages)) {
1497             $data->lang = '';
1498         }
1500         $themes = get_list_of_themes(); // Get themes for quick search later
1501         if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1502             $data->theme = '';
1503         }
1505         // Check if this is an old SCORM course format.
1506         if ($data->format == 'scorm') {
1507             $data->format = 'singleactivity';
1508             $data->activitytype = 'scorm';
1509         }
1511         // Course record ready, update it
1512         $DB->update_record('course', $data);
1514         course_get_format($data)->update_course_format_options($data);
1516         // Role name aliases
1517         restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1518     }
1520     public function process_category($data) {
1521         // Nothing to do with the category. UI sets it before restore starts
1522     }
1524     public function process_tag($data) {
1525         global $CFG, $DB;
1527         $data = (object)$data;
1529         if (!empty($CFG->usetags)) { // if enabled in server
1530             // TODO: This is highly inneficient. Each time we add one tag
1531             // we fetch all the existing because tag_set() deletes them
1532             // so everything must be reinserted on each call
1533             $tags = array();
1534             $existingtags = tag_get_tags('course', $this->get_courseid());
1535             // Re-add all the existitng tags
1536             foreach ($existingtags as $existingtag) {
1537                 $tags[] = $existingtag->rawname;
1538             }
1539             // Add the one being restored
1540             $tags[] = $data->rawname;
1541             // Send all the tags back to the course
1542             tag_set('course', $this->get_courseid(), $tags, 'core',
1543                 context_course::instance($this->get_courseid())->id);
1544         }
1545     }
1547     public function process_allowed_module($data) {
1548         $data = (object)$data;
1550         // Backwards compatiblity support for the data that used to be in the
1551         // course_allowed_modules table.
1552         if ($this->legacyrestrictmodules) {
1553             $this->legacyallowedmodules[$data->modulename] = 1;
1554         }
1555     }
1557     protected function after_execute() {
1558         global $DB;
1560         // Add course related files, without itemid to match
1561         $this->add_related_files('course', 'summary', null);
1562         $this->add_related_files('course', 'overviewfiles', null);
1564         // Deal with legacy allowed modules.
1565         if ($this->legacyrestrictmodules) {
1566             $context = context_course::instance($this->get_courseid());
1568             list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1569             list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1570             foreach ($managerroleids as $roleid) {
1571                 unset($roleids[$roleid]);
1572             }
1574             foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1575                 if (isset($this->legacyallowedmodules[$modname])) {
1576                     // Module is allowed, no worries.
1577                     continue;
1578                 }
1580                 $capability = 'mod/' . $modname . ':addinstance';
1581                 foreach ($roleids as $roleid) {
1582                     assign_capability($capability, CAP_PREVENT, $roleid, $context);
1583                 }
1584             }
1585         }
1586     }
1589 /**
1590  * Execution step that will migrate legacy files if present.
1591  */
1592 class restore_course_legacy_files_step extends restore_execution_step {
1593     public function define_execution() {
1594         global $DB;
1596         // Do a check for legacy files and skip if there are none.
1597         $sql = 'SELECT count(*)
1598                   FROM {backup_files_temp}
1599                  WHERE backupid = ?
1600                    AND contextid = ?
1601                    AND component = ?
1602                    AND filearea  = ?';
1603         $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
1605         if ($DB->count_records_sql($sql, $params)) {
1606             $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
1607             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
1608                 'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
1609         }
1610     }
1613 /*
1614  * Structure step that will read the roles.xml file (at course/activity/block levels)
1615  * containing all the role_assignments and overrides for that context. If corresponding to
1616  * one mapped role, they will be applied to target context. Will observe the role_assignments
1617  * setting to decide if ras are restored.
1618  *
1619  * Note: this needs to be executed after all users are enrolled.
1620  */
1621 class restore_ras_and_caps_structure_step extends restore_structure_step {
1622     protected $plugins = null;
1624     protected function define_structure() {
1626         $paths = array();
1628         // Observe the role_assignments setting
1629         if ($this->get_setting_value('role_assignments')) {
1630             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
1631         }
1632         $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
1634         return $paths;
1635     }
1637     /**
1638      * Assign roles
1639      *
1640      * This has to be called after enrolments processing.
1641      *
1642      * @param mixed $data
1643      * @return void
1644      */
1645     public function process_assignment($data) {
1646         global $DB;
1648         $data = (object)$data;
1650         // Check roleid, userid are one of the mapped ones
1651         if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
1652             return;
1653         }
1654         if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
1655             return;
1656         }
1657         if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
1658             // Only assign roles to not deleted users
1659             return;
1660         }
1661         if (!$contextid = $this->task->get_contextid()) {
1662             return;
1663         }
1665         if (empty($data->component)) {
1666             // assign standard manual roles
1667             // TODO: role_assign() needs one userid param to be able to specify our restore userid
1668             role_assign($newroleid, $newuserid, $contextid);
1670         } else if ((strpos($data->component, 'enrol_') === 0)) {
1671             // Deal with enrolment roles - ignore the component and just find out the instance via new id,
1672             // it is possible that enrolment was restored using different plugin type.
1673             if (!isset($this->plugins)) {
1674                 $this->plugins = enrol_get_plugins(true);
1675             }
1676             if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1677                 if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1678                     if (isset($this->plugins[$instance->enrol])) {
1679                         $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
1680                     }
1681                 }
1682             }
1684         } else {
1685             $data->roleid    = $newroleid;
1686             $data->userid    = $newuserid;
1687             $data->contextid = $contextid;
1688             $dir = core_component::get_component_directory($data->component);
1689             if ($dir and is_dir($dir)) {
1690                 if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
1691                     return;
1692                 }
1693             }
1694             // Bad luck, plugin could not restore the data, let's add normal membership.
1695             role_assign($data->roleid, $data->userid, $data->contextid);
1696             $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
1697             $this->log($message, backup::LOG_WARNING);
1698         }
1699     }
1701     public function process_override($data) {
1702         $data = (object)$data;
1704         // Check roleid is one of the mapped ones
1705         $newroleid = $this->get_mappingid('role', $data->roleid);
1706         // If newroleid and context are valid assign it via API (it handles dupes and so on)
1707         if ($newroleid && $this->task->get_contextid()) {
1708             // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1709             // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1710             assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1711         }
1712     }
1715 /**
1716  * If no instances yet add default enrol methods the same way as when creating new course in UI.
1717  */
1718 class restore_default_enrolments_step extends restore_execution_step {
1719     public function define_execution() {
1720         global $DB;
1722         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
1724         if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
1725             // Something already added instances, do not add default instances.
1726             $plugins = enrol_get_plugins(true);
1727             foreach ($plugins as $plugin) {
1728                 $plugin->restore_sync_course($course);
1729             }
1731         } else {
1732             // Looks like a newly created course.
1733             enrol_course_updated(true, $course, null);
1734         }
1735     }
1738 /**
1739  * This structure steps restores the enrol plugins and their underlying
1740  * enrolments, performing all the mappings and/or movements required
1741  */
1742 class restore_enrolments_structure_step extends restore_structure_step {
1743     protected $enrolsynced = false;
1744     protected $plugins = null;
1745     protected $originalstatus = array();
1747     /**
1748      * Conditionally decide if this step should be executed.
1749      *
1750      * This function checks the following parameter:
1751      *
1752      *   1. the course/enrolments.xml file exists
1753      *
1754      * @return bool true is safe to execute, false otherwise
1755      */
1756     protected function execute_condition() {
1758         // Check it is included in the backup
1759         $fullpath = $this->task->get_taskbasepath();
1760         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
1761         if (!file_exists($fullpath)) {
1762             // Not found, can't restore enrolments info
1763             return false;
1764         }
1766         return true;
1767     }
1769     protected function define_structure() {
1771         $paths = array();
1773         $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
1774         $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
1776         return $paths;
1777     }
1779     /**
1780      * Create enrolment instances.
1781      *
1782      * This has to be called after creation of roles
1783      * and before adding of role assignments.
1784      *
1785      * @param mixed $data
1786      * @return void
1787      */
1788     public function process_enrol($data) {
1789         global $DB;
1791         $data = (object)$data;
1792         $oldid = $data->id; // We'll need this later.
1793         unset($data->id);
1795         $this->originalstatus[$oldid] = $data->status;
1797         if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
1798             $this->set_mapping('enrol', $oldid, 0);
1799             return;
1800         }
1802         if (!isset($this->plugins)) {
1803             $this->plugins = enrol_get_plugins(true);
1804         }
1806         if (!$this->enrolsynced) {
1807             // Make sure that all plugin may create instances and enrolments automatically
1808             // before the first instance restore - this is suitable especially for plugins
1809             // that synchronise data automatically using course->idnumber or by course categories.
1810             foreach ($this->plugins as $plugin) {
1811                 $plugin->restore_sync_course($courserec);
1812             }
1813             $this->enrolsynced = true;
1814         }
1816         // Map standard fields - plugin has to process custom fields manually.
1817         $data->roleid   = $this->get_mappingid('role', $data->roleid);
1818         $data->courseid = $courserec->id;
1820         if ($this->get_setting_value('enrol_migratetomanual')) {
1821             unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
1822             if (!enrol_is_enabled('manual')) {
1823                 $this->set_mapping('enrol', $oldid, 0);
1824                 return;
1825             }
1826             if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
1827                 $instance = reset($instances);
1828                 $this->set_mapping('enrol', $oldid, $instance->id);
1829             } else {
1830                 if ($data->enrol === 'manual') {
1831                     $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
1832                 } else {
1833                     $instanceid = $this->plugins['manual']->add_default_instance($courserec);
1834                 }
1835                 $this->set_mapping('enrol', $oldid, $instanceid);
1836             }
1838         } else {
1839             if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
1840                 $this->set_mapping('enrol', $oldid, 0);
1841                 $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
1842                 $this->log($message, backup::LOG_WARNING);
1843                 return;
1844             }
1845             if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
1846                 // Let's keep the sortorder in old backups.
1847             } else {
1848                 // Prevent problems with colliding sortorders in old backups,
1849                 // new 2.4 backups do not need sortorder because xml elements are ordered properly.
1850                 unset($data->sortorder);
1851             }
1852             // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
1853             $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
1854         }
1855     }
1857     /**
1858      * Create user enrolments.
1859      *
1860      * This has to be called after creation of enrolment instances
1861      * and before adding of role assignments.
1862      *
1863      * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
1864      *
1865      * @param mixed $data
1866      * @return void
1867      */
1868     public function process_enrolment($data) {
1869         global $DB;
1871         if (!isset($this->plugins)) {
1872             $this->plugins = enrol_get_plugins(true);
1873         }
1875         $data = (object)$data;
1877         // Process only if parent instance have been mapped.
1878         if ($enrolid = $this->get_new_parentid('enrol')) {
1879             $oldinstancestatus = ENROL_INSTANCE_ENABLED;
1880             $oldenrolid = $this->get_old_parentid('enrol');
1881             if (isset($this->originalstatus[$oldenrolid])) {
1882                 $oldinstancestatus = $this->originalstatus[$oldenrolid];
1883             }
1884             if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1885                 // And only if user is a mapped one.
1886                 if ($userid = $this->get_mappingid('user', $data->userid)) {
1887                     if (isset($this->plugins[$instance->enrol])) {
1888                         $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
1889                     }
1890                 }
1891             }
1892         }
1893     }
1897 /**
1898  * Make sure the user restoring the course can actually access it.
1899  */
1900 class restore_fix_restorer_access_step extends restore_execution_step {
1901     protected function define_execution() {
1902         global $CFG, $DB;
1904         if (!$userid = $this->task->get_userid()) {
1905             return;
1906         }
1908         if (empty($CFG->restorernewroleid)) {
1909             // Bad luck, no fallback role for restorers specified
1910             return;
1911         }
1913         $courseid = $this->get_courseid();
1914         $context = context_course::instance($courseid);
1916         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
1917             // Current user may access the course (admin, category manager or restored teacher enrolment usually)
1918             return;
1919         }
1921         // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
1922         role_assign($CFG->restorernewroleid, $userid, $context);
1924         if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
1925             // Extra role is enough, yay!
1926             return;
1927         }
1929         // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
1930         // hopefully admin selected suitable $CFG->restorernewroleid ...
1931         if (!enrol_is_enabled('manual')) {
1932             return;
1933         }
1934         if (!$enrol = enrol_get_plugin('manual')) {
1935             return;
1936         }
1937         if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
1938             $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
1939             $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
1940             $enrol->add_instance($course, $fields);
1941         }
1943         enrol_try_internal_enrol($courseid, $userid);
1944     }
1948 /**
1949  * This structure steps restores the filters and their configs
1950  */
1951 class restore_filters_structure_step extends restore_structure_step {
1953     protected function define_structure() {
1955         $paths = array();
1957         $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
1958         $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
1960         return $paths;
1961     }
1963     public function process_active($data) {
1965         $data = (object)$data;
1967         if (strpos($data->filter, 'filter/') === 0) {
1968             $data->filter = substr($data->filter, 7);
1970         } else if (strpos($data->filter, '/') !== false) {
1971             // Unsupported old filter.
1972             return;
1973         }
1975         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
1976             return;
1977         }
1978         filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
1979     }
1981     public function process_config($data) {
1983         $data = (object)$data;
1985         if (strpos($data->filter, 'filter/') === 0) {
1986             $data->filter = substr($data->filter, 7);
1988         } else if (strpos($data->filter, '/') !== false) {
1989             // Unsupported old filter.
1990             return;
1991         }
1993         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
1994             return;
1995         }
1996         filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
1997     }
2001 /**
2002  * This structure steps restores the comments
2003  * Note: Cannot use the comments API because defaults to USER->id.
2004  * That should change allowing to pass $userid
2005  */
2006 class restore_comments_structure_step extends restore_structure_step {
2008     protected function define_structure() {
2010         $paths = array();
2012         $paths[] = new restore_path_element('comment', '/comments/comment');
2014         return $paths;
2015     }
2017     public function process_comment($data) {
2018         global $DB;
2020         $data = (object)$data;
2022         // First of all, if the comment has some itemid, ask to the task what to map
2023         $mapping = false;
2024         if ($data->itemid) {
2025             $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2026             $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2027         }
2028         // Only restore the comment if has no mapping OR we have found the matching mapping
2029         if (!$mapping || $data->itemid) {
2030             // Only if user mapping and context
2031             $data->userid = $this->get_mappingid('user', $data->userid);
2032             if ($data->userid && $this->task->get_contextid()) {
2033                 $data->contextid = $this->task->get_contextid();
2034                 // Only if there is another comment with same context/user/timecreated
2035                 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2036                 if (!$DB->record_exists('comments', $params)) {
2037                     $DB->insert_record('comments', $data);
2038                 }
2039             }
2040         }
2041     }
2044 /**
2045  * This structure steps restores the badges and their configs
2046  */
2047 class restore_badges_structure_step extends restore_structure_step {
2049     /**
2050      * Conditionally decide if this step should be executed.
2051      *
2052      * This function checks the following parameters:
2053      *
2054      *   1. Badges and course badges are enabled on the site.
2055      *   2. The course/badges.xml file exists.
2056      *   3. All modules are restorable.
2057      *   4. All modules are marked for restore.
2058      *
2059      * @return bool True is safe to execute, false otherwise
2060      */
2061     protected function execute_condition() {
2062         global $CFG;
2064         // First check is badges and course level badges are enabled on this site.
2065         if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2066             // Disabled, don't restore course badges.
2067             return false;
2068         }
2070         // Check if badges.xml is included in the backup.
2071         $fullpath = $this->task->get_taskbasepath();
2072         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2073         if (!file_exists($fullpath)) {
2074             // Not found, can't restore course badges.
2075             return false;
2076         }
2078         // Check we are able to restore all backed up modules.
2079         if ($this->task->is_missing_modules()) {
2080             return false;
2081         }
2083         // Finally check all modules within the backup are being restored.
2084         if ($this->task->is_excluding_activities()) {
2085             return false;
2086         }
2088         return true;
2089     }
2091     protected function define_structure() {
2092         $paths = array();
2093         $paths[] = new restore_path_element('badge', '/badges/badge');
2094         $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2095         $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2096         $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2098         return $paths;
2099     }
2101     public function process_badge($data) {
2102         global $DB, $CFG;
2104         require_once($CFG->libdir . '/badgeslib.php');
2106         $data = (object)$data;
2107         $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2108         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2110         // We'll restore the badge image.
2111         $restorefiles = true;
2113         $courseid = $this->get_courseid();
2115         $params = array(
2116                 'name'           => $data->name,
2117                 'description'    => $data->description,
2118                 'timecreated'    => $this->apply_date_offset($data->timecreated),
2119                 'timemodified'   => $this->apply_date_offset($data->timemodified),
2120                 'usercreated'    => $data->usercreated,
2121                 'usermodified'   => $data->usermodified,
2122                 'issuername'     => $data->issuername,
2123                 'issuerurl'      => $data->issuerurl,
2124                 'issuercontact'  => $data->issuercontact,
2125                 'expiredate'     => $this->apply_date_offset($data->expiredate),
2126                 'expireperiod'   => $data->expireperiod,
2127                 'type'           => BADGE_TYPE_COURSE,
2128                 'courseid'       => $courseid,
2129                 'message'        => $data->message,
2130                 'messagesubject' => $data->messagesubject,
2131                 'attachment'     => $data->attachment,
2132                 'notification'   => $data->notification,
2133                 'status'         => BADGE_STATUS_INACTIVE,
2134                 'nextcron'       => $this->apply_date_offset($data->nextcron)
2135         );
2137         $newid = $DB->insert_record('badge', $params);
2138         $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2139     }
2141     public function process_criterion($data) {
2142         global $DB;
2144         $data = (object)$data;
2146         $params = array(
2147                 'badgeid'      => $this->get_new_parentid('badge'),
2148                 'criteriatype' => $data->criteriatype,
2149                 'method'       => $data->method
2150         );
2151         $newid = $DB->insert_record('badge_criteria', $params);
2152         $this->set_mapping('criterion', $data->id, $newid);
2153     }
2155     public function process_parameter($data) {
2156         global $DB, $CFG;
2158         require_once($CFG->libdir . '/badgeslib.php');
2160         $data = (object)$data;
2161         $criteriaid = $this->get_new_parentid('criterion');
2163         // Parameter array that will go to database.
2164         $params = array();
2165         $params['critid'] = $criteriaid;
2167         $oldparam = explode('_', $data->name);
2169         if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2170             $module = $this->get_mappingid('course_module', $oldparam[1]);
2171             $params['name'] = $oldparam[0] . '_' . $module;
2172             $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2173         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2174             $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2175             $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2176         } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2177             $role = $this->get_mappingid('role', $data->value);
2178             if (!empty($role)) {
2179                 $params['name'] = 'role_' . $role;
2180                 $params['value'] = $role;
2181             } else {
2182                 return;
2183             }
2184         }
2186         if (!$DB->record_exists('badge_criteria_param', $params)) {
2187             $DB->insert_record('badge_criteria_param', $params);
2188         }
2189     }
2191     public function process_manual_award($data) {
2192         global $DB;
2194         $data = (object)$data;
2195         $role = $this->get_mappingid('role', $data->issuerrole);
2197         if (!empty($role)) {
2198             $award = array(
2199                 'badgeid'     => $this->get_new_parentid('badge'),
2200                 'recipientid' => $this->get_mappingid('user', $data->recipientid),
2201                 'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2202                 'issuerrole'  => $role,
2203                 'datemet'     => $this->apply_date_offset($data->datemet)
2204             );
2205             $DB->insert_record('badge_manual_award', $award);
2206         }
2207     }
2209     protected function after_execute() {
2210         // Add related files.
2211         $this->add_related_files('badges', 'badgeimage', 'badge');
2212     }
2215 /**
2216  * This structure steps restores the calendar events
2217  */
2218 class restore_calendarevents_structure_step extends restore_structure_step {
2220     protected function define_structure() {
2222         $paths = array();
2224         $paths[] = new restore_path_element('calendarevents', '/events/event');
2226         return $paths;
2227     }
2229     public function process_calendarevents($data) {
2230         global $DB, $SITE;
2232         $data = (object)$data;
2233         $oldid = $data->id;
2234         $restorefiles = true; // We'll restore the files
2235         // Find the userid and the groupid associated with the event. Return if not found.
2236         $data->userid = $this->get_mappingid('user', $data->userid);
2237         if ($data->userid === false) {
2238             return;
2239         }
2240         if (!empty($data->groupid)) {
2241             $data->groupid = $this->get_mappingid('group', $data->groupid);
2242             if ($data->groupid === false) {
2243                 return;
2244             }
2245         }
2246         // Handle events with empty eventtype //MDL-32827
2247         if(empty($data->eventtype)) {
2248             if ($data->courseid == $SITE->id) {                                // Site event
2249                 $data->eventtype = "site";
2250             } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2251                 // Course assingment event
2252                 $data->eventtype = "due";
2253             } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2254                 $data->eventtype = "course";
2255             } else if ($data->groupid) {                                      // Group event
2256                 $data->eventtype = "group";
2257             } else if ($data->userid) {                                       // User event
2258                 $data->eventtype = "user";
2259             } else {
2260                 return;
2261             }
2262         }
2264         $params = array(
2265                 'name'           => $data->name,
2266                 'description'    => $data->description,
2267                 'format'         => $data->format,
2268                 'courseid'       => $this->get_courseid(),
2269                 'groupid'        => $data->groupid,
2270                 'userid'         => $data->userid,
2271                 'repeatid'       => $data->repeatid,
2272                 'modulename'     => $data->modulename,
2273                 'eventtype'      => $data->eventtype,
2274                 'timestart'      => $this->apply_date_offset($data->timestart),
2275                 'timeduration'   => $data->timeduration,
2276                 'visible'        => $data->visible,
2277                 'uuid'           => $data->uuid,
2278                 'sequence'       => $data->sequence,
2279                 'timemodified'    => $this->apply_date_offset($data->timemodified));
2280         if ($this->name == 'activity_calendar') {
2281             $params['instance'] = $this->task->get_activityid();
2282         } else {
2283             $params['instance'] = 0;
2284         }
2285         $sql = "SELECT id
2286                   FROM {event}
2287                  WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2288                    AND courseid = ?
2289                    AND repeatid = ?
2290                    AND modulename = ?
2291                    AND timestart = ?
2292                    AND timeduration = ?
2293                    AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2294         $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
2295         $result = $DB->record_exists_sql($sql, $arg);
2296         if (empty($result)) {
2297             $newitemid = $DB->insert_record('event', $params);
2298             $this->set_mapping('event', $oldid, $newitemid);
2299             $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2300         }
2302     }
2303     protected function after_execute() {
2304         // Add related files
2305         $this->add_related_files('calendar', 'event_description', 'event_description');
2306     }
2309 class restore_course_completion_structure_step extends restore_structure_step {
2311     /**
2312      * Conditionally decide if this step should be executed.
2313      *
2314      * This function checks parameters that are not immediate settings to ensure
2315      * that the enviroment is suitable for the restore of course completion info.
2316      *
2317      * This function checks the following four parameters:
2318      *
2319      *   1. Course completion is enabled on the site
2320      *   2. The backup includes course completion information
2321      *   3. All modules are restorable
2322      *   4. All modules are marked for restore.
2323      *
2324      * @return bool True is safe to execute, false otherwise
2325      */
2326     protected function execute_condition() {
2327         global $CFG;
2329         // First check course completion is enabled on this site
2330         if (empty($CFG->enablecompletion)) {
2331             // Disabled, don't restore course completion
2332             return false;
2333         }
2335         // Check it is included in the backup
2336         $fullpath = $this->task->get_taskbasepath();
2337         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2338         if (!file_exists($fullpath)) {
2339             // Not found, can't restore course completion
2340             return false;
2341         }
2343         // Check we are able to restore all backed up modules
2344         if ($this->task->is_missing_modules()) {
2345             return false;
2346         }
2348         // Finally check all modules within the backup are being restored.
2349         if ($this->task->is_excluding_activities()) {
2350             return false;
2351         }
2353         return true;
2354     }
2356     /**
2357      * Define the course completion structure
2358      *
2359      * @return array Array of restore_path_element
2360      */
2361     protected function define_structure() {
2363         // To know if we are including user completion info
2364         $userinfo = $this->get_setting_value('userscompletion');
2366         $paths = array();
2367         $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
2368         $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
2370         if ($userinfo) {
2371             $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
2372             $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
2373         }
2375         return $paths;
2377     }
2379     /**
2380      * Process course completion criteria
2381      *
2382      * @global moodle_database $DB
2383      * @param stdClass $data
2384      */
2385     public function process_course_completion_criteria($data) {
2386         global $DB;
2388         $data = (object)$data;
2389         $data->course = $this->get_courseid();
2391         // Apply the date offset to the time end field
2392         $data->timeend = $this->apply_date_offset($data->timeend);
2394         // Map the role from the criteria
2395         if (!empty($data->role)) {
2396             $data->role = $this->get_mappingid('role', $data->role);
2397         }
2399         $skipcriteria = false;
2401         // If the completion criteria is for a module we need to map the module instance
2402         // to the new module id.
2403         if (!empty($data->moduleinstance) && !empty($data->module)) {
2404             $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
2405             if (empty($data->moduleinstance)) {
2406                 $skipcriteria = true;
2407             }
2408         } else {
2409             $data->module = null;
2410             $data->moduleinstance = null;
2411         }
2413         // We backup the course shortname rather than the ID so that we can match back to the course
2414         if (!empty($data->courseinstanceshortname)) {
2415             $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
2416             if (!$courseinstanceid) {
2417                 $skipcriteria = true;
2418             }
2419         } else {
2420             $courseinstanceid = null;
2421         }
2422         $data->courseinstance = $courseinstanceid;
2424         if (!$skipcriteria) {
2425             $params = array(
2426                 'course'         => $data->course,
2427                 'criteriatype'   => $data->criteriatype,
2428                 'enrolperiod'    => $data->enrolperiod,
2429                 'courseinstance' => $data->courseinstance,
2430                 'module'         => $data->module,
2431                 'moduleinstance' => $data->moduleinstance,
2432                 'timeend'        => $data->timeend,
2433                 'gradepass'      => $data->gradepass,
2434                 'role'           => $data->role
2435             );
2436             $newid = $DB->insert_record('course_completion_criteria', $params);
2437             $this->set_mapping('course_completion_criteria', $data->id, $newid);
2438         }
2439     }
2441     /**
2442      * Processes course compltion criteria complete records
2443      *
2444      * @global moodle_database $DB
2445      * @param stdClass $data
2446      */
2447     public function process_course_completion_crit_compl($data) {
2448         global $DB;
2450         $data = (object)$data;
2452         // This may be empty if criteria could not be restored
2453         $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
2455         $data->course = $this->get_courseid();
2456         $data->userid = $this->get_mappingid('user', $data->userid);
2458         if (!empty($data->criteriaid) && !empty($data->userid)) {
2459             $params = array(
2460                 'userid' => $data->userid,
2461                 'course' => $data->course,
2462                 'criteriaid' => $data->criteriaid,
2463                 'timecompleted' => $this->apply_date_offset($data->timecompleted)
2464             );
2465             if (isset($data->gradefinal)) {
2466                 $params['gradefinal'] = $data->gradefinal;
2467             }
2468             if (isset($data->unenroled)) {
2469                 $params['unenroled'] = $data->unenroled;
2470             }
2471             $DB->insert_record('course_completion_crit_compl', $params);
2472         }
2473     }
2475     /**
2476      * Process course completions
2477      *
2478      * @global moodle_database $DB
2479      * @param stdClass $data
2480      */
2481     public function process_course_completions($data) {
2482         global $DB;
2484         $data = (object)$data;
2486         $data->course = $this->get_courseid();
2487         $data->userid = $this->get_mappingid('user', $data->userid);
2489         if (!empty($data->userid)) {
2490             $params = array(
2491                 'userid' => $data->userid,
2492                 'course' => $data->course,
2493                 'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
2494                 'timestarted' => $this->apply_date_offset($data->timestarted),
2495                 'timecompleted' => $this->apply_date_offset($data->timecompleted),
2496                 'reaggregate' => $data->reaggregate
2497             );
2498             $DB->insert_record('course_completions', $params);
2499         }
2500     }
2502     /**
2503      * Process course completion aggregate methods
2504      *
2505      * @global moodle_database $DB
2506      * @param stdClass $data
2507      */
2508     public function process_course_completion_aggr_methd($data) {
2509         global $DB;
2511         $data = (object)$data;
2513         $data->course = $this->get_courseid();
2515         // Only create the course_completion_aggr_methd records if
2516         // the target course has not them defined. MDL-28180
2517         if (!$DB->record_exists('course_completion_aggr_methd', array(
2518                     'course' => $data->course,
2519                     'criteriatype' => $data->criteriatype))) {
2520             $params = array(
2521                 'course' => $data->course,
2522                 'criteriatype' => $data->criteriatype,
2523                 'method' => $data->method,
2524                 'value' => $data->value,
2525             );
2526             $DB->insert_record('course_completion_aggr_methd', $params);
2527         }
2528     }
2532 /**
2533  * This structure step restores course logs (cmid = 0), delegating
2534  * the hard work to the corresponding {@link restore_logs_processor} passing the
2535  * collection of {@link restore_log_rule} rules to be observed as they are defined
2536  * by the task. Note this is only executed based in the 'logs' setting.
2537  *
2538  * NOTE: This is executed by final task, to have all the activities already restored
2539  *
2540  * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
2541  * records are. There are others like 'calendar' and 'upload' that will be handled
2542  * later.
2543  *
2544  * NOTE: All the missing actions (not able to be restored) are sent to logs for
2545  * debugging purposes
2546  */
2547 class restore_course_logs_structure_step extends restore_structure_step {
2549     /**
2550      * Conditionally decide if this step should be executed.
2551      *
2552      * This function checks the following parameter:
2553      *
2554      *   1. the course/logs.xml file exists
2555      *
2556      * @return bool true is safe to execute, false otherwise
2557      */
2558     protected function execute_condition() {
2560         // Check it is included in the backup
2561         $fullpath = $this->task->get_taskbasepath();
2562         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2563         if (!file_exists($fullpath)) {
2564             // Not found, can't restore course logs
2565             return false;
2566         }
2568         return true;
2569     }
2571     protected function define_structure() {
2573         $paths = array();
2575         // Simple, one plain level of information contains them
2576         $paths[] = new restore_path_element('log', '/logs/log');
2578         return $paths;
2579     }
2581     protected function process_log($data) {
2582         global $DB;
2584         $data = (object)($data);
2586         $data->time = $this->apply_date_offset($data->time);
2587         $data->userid = $this->get_mappingid('user', $data->userid);
2588         $data->course = $this->get_courseid();
2589         $data->cmid = 0;
2591         // For any reason user wasn't remapped ok, stop processing this
2592         if (empty($data->userid)) {
2593             return;
2594         }
2596         // Everything ready, let's delegate to the restore_logs_processor
2598         // Set some fixed values that will save tons of DB requests
2599         $values = array(
2600             'course' => $this->get_courseid());
2601         // Get instance and process log record
2602         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2604         // If we have data, insert it, else something went wrong in the restore_logs_processor
2605         if ($data) {
2606             $DB->insert_record('log', $data);
2607         }
2608     }
2611 /**
2612  * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
2613  * sharing its same structure but modifying the way records are handled
2614  */
2615 class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
2617     protected function process_log($data) {
2618         global $DB;
2620         $data = (object)($data);
2622         $data->time = $this->apply_date_offset($data->time);
2623         $data->userid = $this->get_mappingid('user', $data->userid);
2624         $data->course = $this->get_courseid();
2625         $data->cmid = $this->task->get_moduleid();
2627         // For any reason user wasn't remapped ok, stop processing this
2628         if (empty($data->userid)) {
2629             return;
2630         }
2632         // Everything ready, let's delegate to the restore_logs_processor
2634         // Set some fixed values that will save tons of DB requests
2635         $values = array(
2636             'course' => $this->get_courseid(),
2637             'course_module' => $this->task->get_moduleid(),
2638             $this->task->get_modulename() => $this->task->get_activityid());
2639         // Get instance and process log record
2640         $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2642         // If we have data, insert it, else something went wrong in the restore_logs_processor
2643         if ($data) {
2644             $DB->insert_record('log', $data);
2645         }
2646     }
2650 /**
2651  * Defines the restore step for advanced grading methods attached to the activity module
2652  */
2653 class restore_activity_grading_structure_step extends restore_structure_step {
2655     /**
2656      * This step is executed only if the grading file is present
2657      */
2658      protected function execute_condition() {
2660         $fullpath = $this->task->get_taskbasepath();
2661         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2662         if (!file_exists($fullpath)) {
2663             return false;
2664         }
2666         return true;
2667     }
2670     /**
2671      * Declares paths in the grading.xml file we are interested in
2672      */
2673     protected function define_structure() {
2675         $paths = array();
2676         $userinfo = $this->get_setting_value('userinfo');
2678         $area = new restore_path_element('grading_area', '/areas/area');
2679         $paths[] = $area;
2680         // attach local plugin stucture to $area element
2681         $this->add_plugin_structure('local', $area);
2683         $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
2684         $paths[] = $definition;
2685         $this->add_plugin_structure('gradingform', $definition);
2686         // attach local plugin stucture to $definition element
2687         $this->add_plugin_structure('local', $definition);
2690         if ($userinfo) {
2691             $instance = new restore_path_element('grading_instance',
2692                 '/areas/area/definitions/definition/instances/instance');
2693             $paths[] = $instance;
2694             $this->add_plugin_structure('gradingform', $instance);
2695             // attach local plugin stucture to $intance element
2696             $this->add_plugin_structure('local', $instance);
2697         }
2699         return $paths;
2700     }
2702     /**
2703      * Processes one grading area element
2704      *
2705      * @param array $data element data
2706      */
2707     protected function process_grading_area($data) {
2708         global $DB;
2710         $task = $this->get_task();
2711         $data = (object)$data;
2712         $oldid = $data->id;
2713         $data->component = 'mod_'.$task->get_modulename();
2714         $data->contextid = $task->get_contextid();
2716         $newid = $DB->insert_record('grading_areas', $data);
2717         $this->set_mapping('grading_area', $oldid, $newid);
2718     }
2720     /**
2721      * Processes one grading definition element
2722      *
2723      * @param array $data element data
2724      */
2725     protected function process_grading_definition($data) {
2726         global $DB;
2728         $task = $this->get_task();
2729         $data = (object)$data;
2730         $oldid = $data->id;
2731         $data->areaid = $this->get_new_parentid('grading_area');
2732         $data->copiedfromid = null;
2733         $data->timecreated = time();
2734         $data->usercreated = $task->get_userid();
2735         $data->timemodified = $data->timecreated;
2736         $data->usermodified = $data->usercreated;
2738         $newid = $DB->insert_record('grading_definitions', $data);
2739         $this->set_mapping('grading_definition', $oldid, $newid, true);
2740     }
2742     /**
2743      * Processes one grading form instance element
2744      *
2745      * @param array $data element data
2746      */
2747     protected function process_grading_instance($data) {
2748         global $DB;
2750         $data = (object)$data;
2752         // new form definition id
2753         $newformid = $this->get_new_parentid('grading_definition');
2755         // get the name of the area we are restoring to
2756         $sql = "SELECT ga.areaname
2757                   FROM {grading_definitions} gd
2758                   JOIN {grading_areas} ga ON gd.areaid = ga.id
2759                  WHERE gd.id = ?";
2760         $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
2762         // get the mapped itemid - the activity module is expected to define the mappings
2763         // for each gradable area
2764         $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
2766         $oldid = $data->id;
2767         $data->definitionid = $newformid;
2768         $data->raterid = $this->get_mappingid('user', $data->raterid);
2769         $data->itemid = $newitemid;
2771         $newid = $DB->insert_record('grading_instances', $data);
2772         $this->set_mapping('grading_instance', $oldid, $newid);
2773     }
2775     /**
2776      * Final operations when the database records are inserted
2777      */
2778     protected function after_execute() {
2779         // Add files embedded into the definition description
2780         $this->add_related_files('grading', 'description', 'grading_definition');
2781     }
2785 /**
2786  * This structure step restores the grade items associated with one activity
2787  * All the grade items are made child of the "course" grade item but the original
2788  * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
2789  * the complete gradebook (categories and calculations), that information is
2790  * available there
2791  */
2792 class restore_activity_grades_structure_step extends restore_structure_step {
2794     protected function define_structure() {
2796         $paths = array();
2797         $userinfo = $this->get_setting_value('userinfo');
2799         $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
2800         $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
2801         if ($userinfo) {
2802             $paths[] = new restore_path_element('grade_grade',
2803                            '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
2804         }
2805         return $paths;
2806     }
2808     protected function process_grade_item($data) {
2809         global $DB;
2811         $data = (object)($data);
2812         $oldid       = $data->id;        // We'll need these later
2813         $oldparentid = $data->categoryid;
2814         $courseid = $this->get_courseid();
2816         // make sure top course category exists, all grade items will be associated
2817         // to it. Later, if restoring the whole gradebook, categories will be introduced
2818         $coursecat = grade_category::fetch_course_category($courseid);
2819         $coursecatid = $coursecat->id; // Get the categoryid to be used
2821         $idnumber = null;
2822         if (!empty($data->idnumber)) {
2823             // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
2824             // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
2825             // so the best is to keep the ones already in the gradebook
2826             // Potential problem: duplicates if same items are restored more than once. :-(
2827             // This needs to be fixed in some way (outcomes & activities with multiple items)
2828             // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
2829             // In any case, verify always for uniqueness
2830             $sql = "SELECT cm.id
2831                       FROM {course_modules} cm
2832                      WHERE cm.course = :courseid AND
2833                            cm.idnumber = :idnumber AND
2834                            cm.id <> :cmid";
2835             $params = array(
2836                 'courseid' => $courseid,
2837                 'idnumber' => $data->idnumber,
2838                 'cmid' => $this->task->get_moduleid()
2839             );
2840             if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
2841                 $idnumber = $data->idnumber;
2842             }
2843         }
2845         unset($data->id);
2846         $data->categoryid   = $coursecatid;
2847         $data->courseid     = $this->get_courseid();
2848         $data->iteminstance = $this->task->get_activityid();
2849         $data->idnumber     = $idnumber;
2850         $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
2851         $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
2852         $data->timecreated  = $this->apply_date_offset($data->timecreated);
2853         $data->timemodified = $this->apply_date_offset($data->timemodified);
2855         $gradeitem = new grade_item($data, false);
2856         $gradeitem->insert('restore');
2858         //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
2859         $gradeitem->sortorder = $data->sortorder;
2860         $gradeitem->update('restore');
2862         // Set mapping, saving the original category id into parentitemid
2863         // gradebook restore (final task) will need it to reorganise items
2864         $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
2865     }
2867     protected function process_grade_grade($data) {
2868         $data = (object)($data);
2869         $olduserid = $data->userid;
2870         unset($data->id);
2872         $data->itemid = $this->get_new_parentid('grade_item');
2874         $data->userid = $this->get_mappingid('user', $data->userid, null);
2875         if (!empty($data->userid)) {
2876             $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
2877             $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
2878             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
2879             $data->overridden = $this->apply_date_offset($data->overridden);
2881             $grade = new grade_grade($data, false);
2882             $grade->insert('restore');
2883             // no need to save any grade_grade mapping
2884         } else {
2885             debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
2886         }
2887     }
2889     /**
2890      * process activity grade_letters. Note that, while these are possible,
2891      * because grade_letters are contextid based, in practice, only course
2892      * context letters can be defined. So we keep here this method knowing
2893      * it won't be executed ever. gradebook restore will restore course letters.
2894      */
2895     protected function process_grade_letter($data) {
2896         global $DB;
2898         $data['contextid'] = $this->task->get_contextid();
2899         $gradeletter = (object)$data;
2901         // Check if it exists before adding it
2902         unset($data['id']);
2903         if (!$DB->record_exists('grade_letters', $data)) {
2904             $newitemid = $DB->insert_record('grade_letters', $gradeletter);
2905         }
2906         // no need to save any grade_letter mapping
2907     }
2909     public function after_restore() {
2910         // Fix grade item's sortorder after restore, as it might have duplicates.
2911         $courseid = $this->get_task()->get_courseid();
2912         grade_item::fix_duplicate_sortorder($courseid);
2913     }
2917 /**
2918  * This structure steps restores one instance + positions of one block
2919  * Note: Positions corresponding to one existing context are restored
2920  * here, but all the ones having unknown contexts are sent to backup_ids
2921  * for a later chance to be restored at the end (final task)
2922  */
2923 class restore_block_instance_structure_step extends restore_structure_step {
2925     protected function define_structure() {
2927         $paths = array();
2929         $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
2930         $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
2932         return $paths;
2933     }
2935     public function process_block($data) {
2936         global $DB, $CFG;
2938         $data = (object)$data; // Handy
2939         $oldcontextid = $data->contextid;
2940         $oldid        = $data->id;
2941         $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
2943         // Look for the parent contextid
2944         if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
2945             throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
2946         }
2948         // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
2949         // If there is already one block of that type in the parent context
2950         // and the block is not multiple, stop processing
2951         // Use blockslib loader / method executor
2952         if (!$bi = block_instance($data->blockname)) {
2953             return false;
2954         }
2956         if (!$bi->instance_allow_multiple()) {
2957             if ($DB->record_exists_sql("SELECT bi.id
2958                                           FROM {block_instances} bi
2959                                           JOIN {block} b ON b.name = bi.blockname
2960                                          WHERE bi.parentcontextid = ?
2961                                            AND bi.blockname = ?", array($data->parentcontextid, $data->blockname))) {
2962                 return false;
2963             }
2964         }
2966         // If there is already one block of that type in the parent context
2967         // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
2968         // stop processing
2969         $params = array(
2970             'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
2971             'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
2972             'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
2973         if ($birecs = $DB->get_records('block_instances', $params)) {
2974             foreach($birecs as $birec) {
2975                 if ($birec->configdata == $data->configdata) {
2976                     return false;
2977                 }
2978             }
2979         }
2981         // Set task old contextid, blockid and blockname once we know them
2982         $this->task->set_old_contextid($oldcontextid);
2983         $this->task->set_old_blockid($oldid);
2984         $this->task->set_blockname($data->blockname);
2986         // Let's look for anything within configdata neededing processing
2987         // (nulls and uses of legacy file.php)
2988         if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
2989             $configdata = (array)unserialize(base64_decode($data->configdata));
2990             foreach ($configdata as $attribute => $value) {
2991                 if (in_array($attribute, $attrstotransform)) {
2992                     $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
2993                 }
2994             }
2995             $data->configdata = base64_encode(serialize((object)$configdata));
2996         }
2998         // Create the block instance
2999         $newitemid = $DB->insert_record('block_instances', $data);
3000         // Save the mapping (with restorefiles support)
3001         $this->set_mapping('block_instance', $oldid, $newitemid, true);
3002         // Create the block context
3003         $newcontextid = context_block::instance($newitemid)->id;
3004         // Save the block contexts mapping and sent it to task
3005         $this->set_mapping('context', $oldcontextid, $newcontextid);
3006         $this->task->set_contextid($newcontextid);
3007         $this->task->set_blockid($newitemid);
3009         // Restore block fileareas if declared
3010         $component = 'block_' . $this->task->get_blockname();
3011         foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
3012             $this->add_related_files($component, $filearea, null);
3013         }
3015         // Process block positions, creating them or accumulating for final step
3016         foreach($positions as $position) {
3017             $position = (object)$position;
3018             $position->blockinstanceid = $newitemid; // The instance is always the restored one
3019             // If position is for one already mapped (known) contextid
3020             // process it now, creating the position
3021             if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
3022                 $position->contextid = $newpositionctxid;
3023                 // Create the block position
3024                 $DB->insert_record('block_positions', $position);
3026             // The position belongs to an unknown context, send it to backup_ids
3027             // to process them as part of the final steps of restore. We send the
3028             // whole $position object there, hence use the low level method.
3029             } else {
3030                 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
3031             }
3032         }
3033     }
3036 /**
3037  * Structure step to restore common course_module information
3038  *
3039  * This step will process the module.xml file for one activity, in order to restore
3040  * the corresponding information to the course_modules table, skipping various bits
3041  * of information based on CFG settings (groupings, completion...) in order to fullfill
3042  * all the reqs to be able to create the context to be used by all the rest of steps
3043  * in the activity restore task
3044  */
3045 class restore_module_structure_step extends restore_structure_step {
3047     protected function define_structure() {
3048         global $CFG;
3050         $paths = array();
3052         $module = new restore_path_element('module', '/module');
3053         $paths[] = $module;
3054         if ($CFG->enableavailability) {
3055             $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
3056             $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
3057         }
3059         // Apply for 'format' plugins optional paths at module level
3060         $this->add_plugin_structure('format', $module);
3062         // Apply for 'plagiarism' plugins optional paths at module level
3063         $this->add_plugin_structure('plagiarism', $module);
3065         // Apply for 'local' plugins optional paths at module level
3066         $this->add_plugin_structure('local', $module);
3068         return $paths;
3069     }
3071     protected function process_module($data) {
3072         global $CFG, $DB;
3074         $data = (object)$data;
3075         $oldid = $data->id;
3076         $this->task->set_old_moduleversion($data->version);
3078         $data->course = $this->task->get_courseid();
3079         $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
3080         // Map section (first try by course_section mapping match. Useful in course and section restores)
3081         $data->section = $this->get_mappingid('course_section', $data->sectionid);
3082         if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
3083             $params = array(
3084                 'course' => $this->get_courseid(),
3085                 'section' => $data->sectionnumber);
3086             $data->section = $DB->get_field('course_sections', 'id', $params);
3087         }
3088         if (!$data->section) { // sectionnumber failed, try to get first section in course
3089             $params = array(
3090                 'course' => $this->get_courseid());
3091             $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
3092         }
3093         if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
3094             $sectionrec = array(
3095                 'course' => $this->get_courseid(),
3096                 'section' => 0);
3097             $DB->insert_record('course_sections', $sectionrec); // section 0
3098             $sectionrec = array(
3099                 'course' => $this->get_courseid(),
3100                 'section' => 1);
3101             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
3102         }
3103         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
3104         if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
3105             $data->idnumber = '';
3106         }
3107         if (empty($CFG->enablecompletion)) { // completion
3108             $data->completion = 0;
3109             $data->completiongradeitemnumber = null;
3110             $data->completionview = 0;
3111             $data->completionexpected = 0;
3112         } else {
3113             $data->completionexpected = $this->apply_date_offset($data->completionexpected);
3114         }
3115         if (empty($CFG->enableavailability)) {
3116             $data->availability = null;
3117         }
3118         // Backups that did not include showdescription, set it to default 0
3119         // (this is not totally necessary as it has a db default, but just to
3120         // be explicit).
3121         if (!isset($data->showdescription)) {
3122             $data->showdescription = 0;
3123         }
3124         $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
3126         // If there are legacy availablility data fields (and no new format data),
3127         // convert the old fields.
3128         if (empty($data->availability)) {
3129             // If groupmembersonly is disabled on this system, convert the
3130             // groupmembersonly option into the new API. Otherwise don't.
3131             $data->availability = \core_availability\info::convert_legacy_fields(
3132                     $data, false, !$CFG->enablegroupmembersonly);
3133         }
3135         // course_module record ready, insert it
3136         $newitemid = $DB->insert_record('course_modules', $data);
3137         // save mapping
3138         $this->set_mapping('course_module', $oldid, $newitemid);
3139         // set the new course_module id in the task
3140         $this->task->set_moduleid($newitemid);
3141         // we can now create the context safely
3142         $ctxid = context_module::instance($newitemid)->id;
3143         // set the new context id in the task
3144         $this->task->set_contextid($ctxid);
3145         // update sequence field in course_section
3146         if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
3147             $sequence .= ',' . $newitemid;
3148         } else {
3149             $sequence = $newitemid;
3150         }
3151         $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
3153         // If there is the legacy showavailability data, store this for later use.
3154         // (This data is not present when restoring 'new' backups.)
3155         if (isset($data->showavailability)) {
3156             // Cache the showavailability flag using the backup_ids data field.
3157             restore_dbops::set_backup_ids_record($this->get_restoreid(),
3158                     'module_showavailability', $newitemid, 0, null,
3159                     (object)array('showavailability' => $data->showavailability));
3160         }
3161     }
3163     /**
3164      * Process the legacy availability table record. This table does not exist
3165      * in Moodle 2.7+ but we still support restore.
3166      *
3167      * @param stdClass $data Record data
3168      */
3169     protected function process_availability($data) {
3170         $data = (object)$data;
3171         // Simply going to store the whole availability record now, we'll process
3172         // all them later in the final task (once all activities have been restored)
3173         // Let's call the low level one to be able to store the whole object
3174         $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
3175         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
3176     }
3178     /**
3179      * Process the legacy availability fields table record. This table does not
3180      * exist in Moodle 2.7+ but we still support restore.
3181      *
3182      * @param stdClass $data Record data
3183      */
3184     protected function process_availability_field($data) {
3185         global $DB;
3186         $data = (object)$data;
3187         // Mark it is as passed by default
3188         $passed = true;
3189         $customfieldid = null;
3191         // If a customfield has been used in order to pass we must be able to match an existing
3192         // customfield by name (data->customfield) and type (data->customfieldtype)
3193         if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
3194             // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
3195             // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
3196             $passed = false;
3197         } else if (!empty($data->customfield)) {
3198             $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
3199             $customfieldid = $DB->get_field('user_info_field', 'id', $params);
3200             $passed = ($customfieldid !== false);
3201         }
3203         if ($passed) {
3204             // Create the object to insert into the database
3205             $availfield = new stdClass();
3206             $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
3207             $availfield->userfield = $data->userfield;
3208             $availfield->customfieldid = $customfieldid;
3209             $availfield->operator = $data->operator;
3210             $availfield->value = $data->value;
3212             // Get showavailability option.
3213             $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
3214                     'module_showavailability', $availfield->coursemoduleid);
3215             if (!$showrec) {
3216                 // Should not happen.
3217                 throw new coding_exception('No matching showavailability record');
3218             }
3219             $show = $showrec->info->showavailability;
3221             // The $availfieldobject is now in the format used in the old
3222             // system. Interpret this and convert to new system.
3223             $currentvalue = $DB->get_field('course_modules', 'availability',
3224                     array('id' => $availfield->coursemoduleid), MUST_EXIST);
3225             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
3226                     $currentvalue, $availfield, $show);
3227             $DB->set_field('course_modules', 'availability', $newvalue,
3228                     array('id' => $availfield->coursemoduleid));
3229         }
3230     }
3233 /**
3234  * Structure step that will process the user activity completion
3235  * information if all these conditions are met:
3236  *  - Target site has completion enabled ($CFG->enablecompletion)
3237  *  - Activity includes completion info (file_exists)
3238  */
3239 class restore_userscompletion_structure_step extends restore_structure_step {
3240     /**
3241      * To conditionally decide if this step must be executed
3242      * Note the "settings" conditions are evaluated in the
3243      * corresponding task. Here we check for other conditions
3244      * not being restore settings (files, site settings...)
3245      */
3246      protected function execute_condition() {
3247          global $CFG;
3249          // Completion disabled in this site, don't execute
3250          if (empty($CFG->enablecompletion)) {
3251              return false;
3252          }
3254          // No user completion info found, don't execute
3255         $fullpath = $this->task->get_taskbasepath();
3256         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3257          if (!file_exists($fullpath)) {
3258              return false;
3259          }
3261          // Arrived here, execute the step
3262          return true;
3263      }
3265      protected function define_structure() {
3267         $paths = array();
3269         $paths[] = new restore_path_element('completion', '/completions/completion');
3271         return $paths;
3272     }
3274     protected function process_completion($data) {
3275         global $DB;
3277         $data = (object)$data;
3279         $data->coursemoduleid = $this->task->get_moduleid();
3280         $data->userid = $this->get_mappingid('user', $data->userid);
3281         $data->timemodified = $this->apply_date_offset($data->timemodified);
3283         // Find the existing record
3284         $existing = $DB->get_record('course_modules_completion', array(
3285                 'coursemoduleid' => $data->coursemoduleid,
3286                 'userid' => $data->userid), 'id, timemodified');
3287         // Check we didn't already insert one for this cmid and userid
3288         // (there aren't supposed to be duplicates in that field, but
3289         // it was possible until MDL-28021 was fixed).
3290         if ($existing) {
3291             // Update it to these new values, but only if the time is newer
3292             if ($existing->timemodified < $data->timemodified) {
3293                 $data->id = $existing->id;
3294                 $DB->update_record('course_modules_completion', $data);
3295             }
3296         } else {
3297             // Normal entry where it doesn't exist already
3298             $DB->insert_record('course_modules_completion', $data);
3299         }
3300     }
3303 /**
3304  * Abstract structure step, parent of all the activity structure steps. Used to suuport
3305  * the main <activity ...> tag and process it. Also provides subplugin support for
3306  * activities.
3307  */
3308 abstract class restore_activity_structure_step extends restore_structure_step {
3310     protected function add_subplugin_structure($subplugintype, $element) {
3312         global $CFG;
3314         // Check the requested subplugintype is a valid one
3315         $subpluginsfile = $CFG->dirroot . '/mod/' . $this->task->get_modulename() . '/db/subplugins.php';
3316         if (!file_exists($subpluginsfile)) {
3317              throw new restore_step_exception('activity_missing_subplugins_php_file', $this->task->get_modulename());
3318         }
3319         include($subpluginsfile);
3320         if (!array_key_exists($subplugintype, $subplugins)) {
3321              throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
3322         }
3323         // Get all the restore path elements, looking across all the subplugin dirs
3324         $subpluginsdirs = core_component::get_plugin_list($subplugintype);
3325         foreach ($subpluginsdirs as $name => $subpluginsdir) {
3326             $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
3327             $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
3328             if (file_exists($restorefile)) {
3329                 require_once($restorefile);
3330                 $restoresubplugin = new $classname($subplugintype, $name, $this);
3331                 // Add subplugin paths to the step
3332                 $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
3333             }
3334         }
3335     }
3337     /**
3338      * As far as activity restore steps are implementing restore_subplugin stuff, they need to
3339      * have the parent task available for wrapping purposes (get course/context....)
3340      * @return restore_task
3341      */
3342     public function get_task() {
3343         return $this->task;
3344     }
3346     /**
3347      * Adds support for the 'activity' path that is common to all the activities
3348      * and will be processed globally here
3349      */
3350     protected function prepare_activity_structure($paths) {
3352         $paths[] = new restore_path_element('activity', '/activity');
3354         return $paths;
3355     }
3357     /**
3358      * Process the activity path, informing the task about various ids, needed later
3359      */
3360     protected function process_activity($data) {
3361         $data = (object)$data;
3362         $this->task->set_old_contextid($data->contextid); // Save old contextid in task
3363         $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
3364         $this->task->set_old_activityid($data->id); // Save old activityid in task
3365     }
3367     /**
3368      * This must be invoked immediately after creating the "module" activity record (forum, choice...)
3369      * and will adjust the new activity id (the instance) in various places
3370      */
3371     protected function apply_activity_instance($newitemid) {
3372         global $DB;
3374         $this->task->set_activityid($newitemid); // Save activity id in task
3375         // Apply the id to course_sections->instanceid
3376         $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
3377         // Do the mapping for modulename, preparing it for files by oldcontext
3378         $modulename = $this->task->get_modulename();
3379         $oldid = $this->task->get_old_activityid();
3380         $this->set_mapping($modulename, $oldid, $newitemid, true);
3381     }
3384 /**
3385  * Structure step in charge of creating/mapping all the qcats and qs
3386  * by parsing the questions.xml file and checking it against the
3387  * results calculated by {@link restore_process_categories_and_questions}
3388  * and stored in backup_ids_temp
3389  */
3390 class restore_create_categories_and_questions extends restore_structure_step {
3392     /** @var array $cachecategory store a question category */
3393     protected $cachedcategory = null;
3395     protected function define_structure() {
3397         $category = new restore_path_element('question_category', '/question_categories/question_category');
3398         $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
3399         $hint = new restore_path_element('question_hint',
3400                 '/question_categories/question_category/questions/question/question_hints/question_hint');
3402         $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
3404         // Apply for 'qtype' plugins optional paths at question level
3405         $this->add_plugin_structure('qtype', $question);
3407         // Apply for 'local' plugins optional paths at question level
3408         $this->add_plugin_structure('local', $question);
3410         return array($category, $question, $hint, $tag);
3411     }
3413     protected function process_question_category($data) {
3414         global $DB;
3416         $data = (object)$data;
3417         $oldid = $data->id;
3419         // Check we have one mapping for this category
3420         if (!$mapping = $this->get_mapping('question_category', $oldid)) {
3421             return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
3422         }
3424         // Check we have to create the category (newitemid = 0)
3425         if ($mapping->newitemid) {
3426             return; // newitemid != 0, this category is going to be mapped. Nothing to do
3427         }
3429         // Arrived here, newitemid = 0, we need to create the category
3430         // we'll do it at parentitemid context, but for CONTEXT_MODULE
3431         // categories, that will be created at CONTEXT_COURSE and moved
3432         // to module context later when the activity is created
3433         if ($mapping->info->contextlevel == CONTEXT_MODULE) {
3434             $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
3435         }
3436         $data->contextid = $mapping->parentitemid;
3438         // Let's create the question_category and save mapping
3439         $newitemid = $DB->insert_record('question_categories', $data);
3440         $this->set_mapping('question_category', $oldid, $newitemid);
3441         // Also annotate them as question_category_created, we need
3442         // that later when remapping parents
3443         $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
3444     }
3446     protected function process_question($data) {
3447         global $DB;
3449         $data = (object)$data;
3450         $oldid = $data->id;
3452         // Check we have one mapping for this question
3453         if (!$questionmapping = $this->get_mapping('question', $oldid)) {
3454             return; // No mapping = this question doesn't need to be created/mapped
3455         }
3457         // Get the mapped category (cannot use get_new_parentid() because not
3458         // all the categories have been created, so it is not always available
3459         // Instead we get the mapping for the question->parentitemid because
3460         // we have loaded qcatids there for all parsed questions
3461         $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
3463         // In the past, there were some very sloppy values of penalty. Fix them.
3464         if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
3465             $data->penalty = 0.3333333;
3466         }
3467         if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
3468             $data->penalty = 0.6666667;
3469         }
3470         if ($data->penalty >= 1) {
3471             $data->penalty = 1;
3472         }
3474         $userid = $this->get_mappingid('user', $data->createdby);
3475         $data->createdby = $userid ? $userid : $this->task->get_userid();
3477         $userid = $this->get_mappingid('user', $data->modifiedby);
3478         $data->modifiedby = $userid ? $userid : $this->task->get_userid();
3480         // With newitemid = 0, let's create the question
3481         if (!$questionmapping->newitemid) {
3482             $newitemid = $DB->insert_record('question', $data);
3483             $this->set_mapping('question', $oldid, $newitemid);
3484             // Also annotate them as question_created, we need
3485             // that later when remapping parents (keeping the old categoryid as parentid)
3486             $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
3487         } else {
3488             // By performing this set_mapping() we make get_old/new_parentid() to work for all the
3489             // children elements of the 'question' one (so qtype plugins will know the question they belong to)
3490             $this->set_mapping('question', $oldid, $questionmapping->newitemid);
3491         }
3493         // Note, we don't restore any question files yet
3494         // as far as the CONTEXT_MODULE categories still
3495         // haven't their contexts to be restored to
3496         // The {@link restore_create_question_files}, executed in the final step
3497         // step will be in charge of restoring all the question files
3498     }
3500     protected function process_question_hint($data) {
3501         global $DB;
3503         $data = (object)$data;
3504         $oldid = $data->id;
3506         // Detect if the question is created or mapped
3507         $oldquestionid   = $this->get_old_parentid('question');
3508         $newquestionid   = $this->get_new_parentid('question');
3509         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
3511         // If the question has been created by restore, we need to create its question_answers too
3512         if ($questioncreated) {
3513             // Adjust some columns
3514             $data->questionid = $newquestionid;
3515             // Insert record
3516             $newitemid = $DB->insert_record('question_hints', $data);
3518         // The question existed, we need to map the existing question_hints
3519         } else {
3520             // Look in question_hints by hint text matching
3521             $sql = 'SELECT id
3522                       FROM {question_hints}
3523                      WHERE questionid = ?
3524                        AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
3525             $params = array($newquestionid, $data->hint);
3526             $newitemid = $DB->get_field_sql($sql, $params);
3528             // Not able to find the hint, let's try cleaning the hint text
3529             // of all the question's hints in DB as slower fallback. MDL-33863.
3530             if (!$newitemid) {
3531                 $potentialhints = $DB->get_records('question_hints',
3532                         array('questionid' => $newquestionid), '', 'id, hint');
3533                 foreach ($potentialhints as $potentialhint) {
3534                     // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
3535                     $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
3536                     $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
3537                     if ($cleanhint === $data->hint) {
3538                         $newitemid = $data->id;
3539                     }
3540                 }
3541             }
3543             // If we haven't found the newitemid, something has gone really wrong, question in DB
3544             // is missing hints, exception
3545             if (!$newitemid) {
3546                 $info = new stdClass();
3547                 $info->filequestionid = $oldquestionid;
3548                 $info->dbquestionid   = $newquestionid;
3549                 $info->hint           = $data->hint;
3550                 throw new restore_step_exception('error_question_hint_missing_in_db', $info);
3551             }
3552         }
3553         // Create mapping (I'm not sure if this is really needed?)
3554         $this->set_mapping('question_hint', $oldid, $newitemid);
3555     }
3557     protected function process_tag($data) {
3558         global $CFG, $DB;
3560         $data = (object)$data;
3561         $newquestion = $this->get_new_parentid('question');
3563         if (!empty($CFG->usetags)) { // if enabled in server
3564             // TODO: This is highly inefficient. Each time we add one tag
3565             // we fetch all the existing because tag_set() deletes them
3566             // so everything must be reinserted on each call
3567             $tags = array();
3568             $existingtags = tag_get_tags('question', $newquestion);
3569             // Re-add all the existitng tags
3570             foreach ($existingtags as $existingtag) {
3571                 $tags[] = $existingtag->rawname;
3572             }
3573             // Add the one being restored
3574             $tags[] = $data->rawname;
3575             // Get the category, so we can then later get the context.
3576             $categoryid = $this->get_new_parentid('question_category');
3577             if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
3578                 $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
3579             }
3580             // Send all the tags back to the question
3581             tag_set('question', $newquestion, $tags, 'core_question', $this->cachedcategory->contextid);
3582         }
3583     }
3585     protected function after_execute() {
3586         global $DB;
3588         // First of all, recode all the created question_categories->parent fields
3589         $qcats = $DB->get_records('backup_ids_temp', array(
3590                      'backupid' => $this->get_restoreid(),
3591                      'itemname' => 'question_category_created'));
3592         foreach ($qcats as $qcat) {
3593             $newparent = 0;
3594             $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
3595             // Get new parent (mapped or created, so we look in quesiton_category mappings)
3596             if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
3597                                  'backupid' => $this->get_restoreid(),
3598                                  'itemname' => 'question_category',
3599                                  'itemid'   => $dbcat->parent))) {
3600                 // contextids must match always, as far as we always include complete qbanks, just check it
3601                 $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
3602                 if ($dbcat->contextid == $newparentctxid) {
3603                     $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
3604                 } else {
3605                     $newparent = 0; // No ctx match for both cats, no parent relationship
3606                 }
3607             }
3608             // Here with $newparent empty, problem with contexts or remapping, set it to top cat
3609             if (!$newparent) {
3610                 $DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id));
3611             }
3612         }
3614         // Now, recode all the created question->parent fields
3615         $qs = $DB->get_records('backup_ids_temp', array(
3616                   'backupid' => $this->get_restoreid(),
3617                   'itemname' => 'question_created'));
3618         foreach ($qs as $q) {
3619             $newparent = 0;
3620             $dbq = $DB->get_record('question', array('id' => $q->newitemid));
3621             // Get new parent (mapped or created, so we look in question mappings)
3622             if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
3623                                  'backupid' => $this->get_restoreid(),
3624                                  'itemname' => 'question',
3625                                  'itemid'   => $dbq->parent))) {
3626                 $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
3627             }
3628         }
3630         // Note, we don't restore any question files yet
3631         // as far as the CONTEXT_MODULE categories still
3632         // haven't their contexts to be restored to
3633         // The {@link restore_create_question_files}, executed in the final step
3634         // step will be in charge of restoring all the question files
3635     }
3638 /**
3639  * Execution step that will move all the CONTEXT_MODULE question categories
3640  * created at early stages of restore in course context (because modules weren't
3641  * created yet) to their target module (matching by old-new-contextid mapping)
3642  */
3643 class restore_move_module_questions_categories extends restore_execution_step {
3645     protected function define_execution() {
3646         global $DB;
3648         $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
3649         foreach ($contexts as $contextid => $contextlevel) {
3650             // Only if context mapping exists (i.e. the module has been restored)
3651             if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
3652                 // Update all the qcats having their parentitemid set to the original contextid
3653                 $modulecats = $DB->get_records_sql("SELECT itemid, newitemid
3654                                                       FROM {backup_ids_temp}
3655                                                      WHERE backupid = ?
3656                                                        AND itemname = 'question_category'
3657                                                        AND parentitemid = ?", array($this->get_restoreid(), $contextid));
3658                 foreach ($modulecats as $modulecat) {
3659                     $DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid));
3660                     // And set new contextid also in question_category mapping (will be
3661                     // used by {@link restore_create_question_files} later
3662                     restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid);
3663                 }
3664             }
3665         }
3666     }
3669 /**
3670  * Execution step that will create all the question/answers/qtype-specific files for the restored
3671  * questions. It must be executed after {@link restore_move_module_questions_categories}
3672  * because only then each question is in its final category and only then the
3673  * context can be determined
3674  *
3675  * TODO: Improve this. Instead of looping over each question, it can be reduced to
3676  *       be done by contexts (this will save a huge ammount of queries)
3677  */
3678 class restore_create_question_files extends restore_execution_step {
3680     protected function define_execution() {
3681         global $DB;
3683         // Track progress, as this task can take a long time.
3684         $progress = $this->task->get_progress();
3685         $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
3687         // Let's process only created questions
3688         $questionsrs = $DB->get_recordset_sql("SELECT bi.itemid, bi.newitemid, bi.parentitemid, q.qtype
3689                                                FROM {backup_ids_temp} bi
3690                                                JOIN {question} q ON q.id = bi.newitemid
3691                                               WHERE bi.backupid = ?
3692                                                 AND bi.itemname = 'question_created'", array($this->get_restoreid()));
3693         foreach ($questionsrs as $question) {
3694             // Report progress for each question.
3695             $progress->progress();
3697             // Get question_category mapping, it contains the target context for the question
3698             if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'question_category', $question->parentitemid)) {
3699                 // Something went really wrong, cannot find the question_category for the question
3700                 debugging('Error fetching target context for question', DEBUG_DEVELOPER);
3701                 continue;
3702             }
3703             // Calculate source and target contexts
3704             $oldctxid = $qcatmapping->info->contextid;
3705             $newctxid = $qcatmapping->parentitemid;
3707             // Add common question files (question and question_answer ones)
3708             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
3709                     $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true, $progress);
3710             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
3711                     $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true, $progress);
3712             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
3713                     $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
3714             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
3715                     $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
3716             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
3717                     $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
3718             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
3719                     $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true, $progress);
3720             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
3721                     $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true, $progress);
3722             restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
3723                     $oldctxid, $this->task->get_userid(), 'question_created', $question->itemid, $newctxid, true, $progress);
3725             // Add qtype dependent files
3726             $components = backup_qtype_plugin::get_components_and_fileareas($question->qtype);
3727             foreach ($components as $component => $fileareas) {
3728                 foreach ($fileareas as $filearea => $mapping) {
3729                     // Use itemid only if mapping is question_created
3730                     $itemid = ($mapping == 'question_created') ? $question->itemid : null;
3731                     restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,