MDL-23362 backup - allow set_mapping() to specify one parentitemid to be saved
[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  * @package moodlecore
20  * @subpackage backup-moodle2
21  * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 /**
26  * Define all the restore steps that will be used by common tasks in restore
27  */
29 /**
30  * delete old directories and conditionally create backup_temp_ids table
31  */
32 class restore_create_and_clean_temp_stuff extends restore_execution_step {
34     protected function define_execution() {
35         $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
36         // If the table already exists, it's because restore_prechecks have been executed in the same
37         // request (without problems) and it already contains a bunch of preloaded information (users...)
38         // that we aren't going to execute again
39         if ($exists) { // Inform plan about preloaded information
40             $this->task->set_preloaded_information();
41         }
42         // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
43         $itemid = $this->task->get_old_contextid();
44         $newitemid = get_context_instance(CONTEXT_COURSE, $this->get_courseid())->id;
45         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
46         // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
47         $itemid = $this->task->get_old_system_contextid();
48         $newitemid = get_context_instance(CONTEXT_SYSTEM)->id;
49         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
50     }
51 }
53 /**
54  * delete the temp dir used by backup/restore (conditionally),
55  * delete old directories and drop temp ids table
56  */
57 class restore_drop_and_clean_temp_stuff extends restore_execution_step {
59     protected function define_execution() {
60         global $CFG;
61         restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
62         backup_helper::delete_old_backup_dirs(time() - (4 * 60 * 60));              // Delete > 4 hours temp dirs
63         if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
64             backup_helper::delete_backup_dir($this->task->get_tempdir()); // Empty restore dir
65         }
66     }
67 }
69 /**
70  * Restore calculated grade items, grade categories etc
71  */
72 class restore_gradebook_step extends restore_structure_step {
74     /**
75      * To conditionally decide if this step must be executed
76      * Note the "settings" conditions are evaluated in the
77      * corresponding task. Here we check for other conditions
78      * not being restore settings (files, site settings...)
79      */
80      protected function execute_condition() {
81         global $CFG;
83         // No gradebook info found, don't execute
84         $fullpath = $this->task->get_taskbasepath();
85         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
86         if (!file_exists($fullpath)) {
87             return false;
88         }
90         // Arrived here, execute the step
91         return true;
92      }
94     protected function define_structure() {
95         $paths = array();
96         $userinfo = $this->task->get_setting_value('users');
98         $paths[] = new restore_path_element('gradebook', '/gradebook');
99         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
100         $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
101         if ($userinfo) {
102             $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
103         }
104         $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
106         return $paths;
107     }
109     protected function process_gradebook($data) {
110     }
112     protected function process_grade_item($data) {
113         global $DB;
115         $data = (object)$data;
117         $oldid = $data->id;
118         $data->course = $this->get_courseid();
120         $data->courseid = $this->get_courseid();
122         //manual grade items store category id in categoryid
123         if ($data->itemtype=='manual') {
124             $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid);
125         } //course and category grade items store their category id in iteminstance
126         else if ($data->itemtype=='course' || $data->itemtype=='category') {
127             $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance);
128         }
130         $data->scaleid   = $this->get_mappingid('scale', $data->scaleid);
131         $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid);
133         $data->locktime     = $this->apply_date_offset($data->locktime);
134         $data->timecreated  = $this->apply_date_offset($data->timecreated);
135         $data->timemodified = $this->apply_date_offset($data->timemodified);
137         //course grade item should already exist so updating instead of inserting
138         if($data->itemtype=='course') {
140             //get the ID of the already created grade item
141             $gi = new stdclass();
142             $gi->courseid  = $this->get_courseid();
144             $gi->itemtype  = $data->itemtype;
145             if ($data->itemtype=='course') {
146                 //need to get the id of the grade_category that was automatically created for the course
147                 $category = new stdclass();
148                 $category->courseid  = $this->get_courseid();
149                 $category->parent  = null;
150                 $category->fullname  = '?';
152                 $coursecategory = $DB->get_record('grade_categories', (array)$category);
153                 $gi->iteminstance = $coursecategory->id;
154             }
156             $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
157             $newitemid = $existinggradeitem->id;
159             $data->id = $newitemid;
160             $DB->update_record('grade_items', $data);
161         } else { //insert manual grade items
162             $newitemid = $DB->insert_record('grade_items', $data);
163         }
164         $this->set_mapping('grade_item', $oldid, $newitemid);
165     }
167     protected function process_grade_grade($data) {
168         global $DB;
170         $data = (object)$data;
171         $oldid = $data->id;
173         $data->itemid = $this->get_new_parentid('grade_item');
175         $data->userid = $this->get_mappingid('user', $data->userid);
176         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
177         $data->locktime     = $this->apply_date_offset($data->locktime);
178         $data->timecreated  = $this->apply_date_offset($data->timecreated);
179         $data->timemodified = $this->apply_date_offset($data->timemodified);
181         $newitemid = $DB->insert_record('grade_grades', $data);
182         $this->set_mapping('grade_grade', $oldid, $newitemid);
183     }
184     protected function process_grade_category($data) {
185         global $DB;
187         $data = (object)$data;
188         $oldid = $data->id;
190         $data->course = $this->get_courseid();
191         $data->courseid = $data->course;
193         $data->timecreated  = $this->apply_date_offset($data->timecreated);
194         $data->timemodified = $this->apply_date_offset($data->timemodified);
196         //no parent means a course level grade category. That should have been created when the course was created
197         if(empty($data->parent)) {
198             //get the already created course level grade category
199             $category = new stdclass();
200             $category->courseid  = $this->get_courseid();
202             $coursecategory = $DB->get_record('grade_categories', (array)$category);
203             $newitemid = $coursecategory->id;
204             $data->id = $newitemid;
205             
206             //parent was being saved as 0 when it should be null
207             $data->parent = null;
209             $DB->update_record('grade_categories', $data);
210         } else {
211             $data->parent = $this->get_mappingid('grade_category', $data->parent);
212             $newitemid = $DB->insert_record('grade_categories', $data);
213         }
214         $this->set_mapping('grade_category', $oldid, $newitemid);
216         //need to correct the path as its a string that contains grade category IDs
217         $grade_category = new stdclass();
218         $grade_category->parent = $data->parent;
219         $grade_category->id = $newitemid;
220         $grade_category->path = grade_category::build_path($grade_category);
221         $DB->update_record('grade_categories', $grade_category);
222     }
223     protected function process_grade_letter($data) {
224         global $DB;
226         $data = (object)$data;
227         $oldid = $data->id;
229         $data->contextid = $this->get_mappingid('context', $data->contextid);
231         $newitemid = $DB->insert_record('grade_letters', $data);
232         $this->set_mapping('grade_letter', $oldid, $newitemid);
233     }
234     protected function after_execute() {
235         global $DB;
237         //put all activity grade items in the correct grade category
239         $conditions = array(
240             'backupid' => $this->get_restoreid(),
241             'itemname' => 'grade_item'//,
242             //'itemid'   => $itemid
243         );
244         $rs = $DB->get_recordset('backup_ids_temp', $conditions);
246         if (!empty($rs)) {
247             foreach($rs as $grade_item_backup) {
248                 // Get the complete grade item object (stored as info)
249                 echo '<hr />grade_item_backup==';
250                 var_dump($grade_item_backup);
252                 //get the grade_item from the backup to find out the old categoryid
253                 //cant use the grade_item in the DB as the category IDs have been defaulted to the course grade_item
254                 var_dump('trying to get grade_item from backup with id=='.$grade_item_backup->itemid);
256                 //this isnt working so tried to avoid a second trip to the DB to see if that would help
257                 //$grade_item = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'grade_item', $grade_item_backup->itemid)->info;
258                 $grade_item = null;
259                 if ($grade_item_backup->info != null) {
260                     $grade_item = unserialize(base64_decode($grade_item_backup->info));
261                 }
263                 var_dump('grade_item object from backup==');
264                 //$grade_item = $DB->get_record('grade_items', array('id'=>$grade_item_backup->newitemid));
265                 //var_dump('grade_item object from db==');
266                 var_dump($grade_item);
268                 if (!empty($grade_item)) {
269                     //exclude the grade items handled as part of the gradebook backup/restore process
270                     if ($grade_item->itemtype!='course' && $grade_item->itemtype!='category' && $grade_item->itemtype!='manual') {
271                         var_dump('got a grade item we need to deal with. was put in category '.$grade_item->categoryid);
273                         //find the category the grade item is meant to be in
274                         /*$conditions = array(
275                             'backupid' => $this->get_restoreid(),
276                             'itemname' => 'grade_category',
277                             'itemid'   => $grade_item->categoryid
278                         );
279                         $grade_category_backup = $DB->get_record('backup_ids_temp', $conditions);
280                         echo 'grade_category_backup==';
281                         var_dump($grade_category_backup);
282                         */
283                         $updateobj = new stdclass();
284                         //$updateobj->id = $grade_item->id;
285                         $updateobj->id = $grade_item_backup->newitemid;
286                         //$updateobj->categoryid = $grade_category_backup->newitemid;
287                         $updateobj->categoryid = $this->get_mappingid('grade_category', $grade_item->categoryid);
288                         var_dump('updateobj==');
289                         var_dump($updateobj->categoryid);
290                         $DB->update_record('grade_items', $updateobj);
291                     }
292                 } else {
293                     mtrace('backup_ids_temp contains a grade_item that doesnt have the serialized grade_item object in ->info');
294                 }
295             }
296         }
297         $rs->close();
298     }
301 /**
302  * decode all the interlinks present in restored content
303  * relying 100% in the restore_decode_processor that handles
304  * both the contents to modify and the rules to be applied
305  */
306 class restore_decode_interlinks extends restore_execution_step {
308     protected function define_execution() {
309         // Just that
310         $this->task->get_decoder()->execute();
311     }
314 /**
315  * rebuid the course cache
316  */
317 class restore_rebuild_course_cache extends restore_execution_step {
319     protected function define_execution() {
320         // Just that
321         rebuild_course_cache($this->get_courseid());
322     }
326 /**
327  * Review all the (pending) block positions in backup_ids, matching by
328  * contextid, creating positions as needed. This is executed by the
329  * final task, once all the contexts have been created
330  */
331 class restore_review_pending_block_positions extends restore_execution_step {
333     protected function define_execution() {
334         global $DB;
336         // Get all the block_position objects pending to match
337         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
338         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
339         // Process block positions, creating them or accumulating for final step
340         foreach($rs as $posrec) {
341             // Get the complete position object (stored as info)
342             $position = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'block_position', $posrec->itemid)->info;
343             // If position is for one already mapped (known) contextid
344             // process it now, creating the position, else nothing to
345             // do, position finally discarded
346             if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
347                 $position->contextid = $newctx->newitemid;
348                 // Create the block position
349                 $DB->insert_record('block_positions', $position);
350             }
351         }
352         $rs->close();
353     }
356 /**
357  * Process all the saved module availability records in backup_ids, matching
358  * course modules and grade item id once all them have been already restored.
359  * only if all matchings are satisfied the availability condition will be created.
360  * At the same time, it is required for the site to have that functionality enabled.
361  */
362 class restore_process_course_modules_availability extends restore_execution_step {
364     protected function define_execution() {
365         global $CFG, $DB;
367         // Site hasn't availability enabled
368         if (empty($CFG->enableavailability)) {
369             return;
370         }
372         // Get all the module_availability objects to process
373         $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'module_availability');
374         $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid');
375         // Process availabilities, creating them if everything matches ok
376         foreach($rs as $availrec) {
377             $allmatchesok = true;
378             // Get the complete availabilityobject
379             $availability = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_availability', $availrec->itemid)->info;
380             // Map the sourcecmid if needed and possible
381             if (!empty($availability->sourcecmid)) {
382                 $newcm = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'course_module', $availability->sourcecmid);
383                 if ($newcm) {
384                     $availability->sourcecmid = $newcm->newitemid;
385                 } else {
386                     $allmatchesok = false; // Failed matching, we won't create this availability rule
387                 }
388             }
389             // Map the gradeitemid if needed and possible
390             if (!empty($availability->gradeitemid)) {
391                 $newgi = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'grade_item', $availability->gradeitemid);
392                 if ($newgi) {
393                     $availability->gradeitemid = $newgi->newitemid;
394                 } else {
395                     $allmatchesok = false; // Failed matching, we won't create this availability rule
396                 }
397             }
398             if ($allmatchesok) { // Everything ok, create the availability rule
399                 $DB->insert_record('course_modules_availability', $availability);
400             }
401         }
402         $rs->close();
403     }
407 /*
408  * Execution step that, *conditionally* (if there isn't preloaded information)
409  * will load the inforef files for all the included course/section/activity tasks
410  * to backup_temp_ids. They will be stored with "xxxxref" as itemname
411  */
412 class restore_load_included_inforef_records extends restore_execution_step {
414     protected function define_execution() {
416         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
417             return;
418         }
420         // Get all the included inforef files
421         $files = restore_dbops::get_needed_inforef_files($this->get_restoreid());
422         foreach ($files as $file) {
423             restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $file); // Load each inforef file to temp_ids
424         }
425     }
428 /*
429  * Execution step that will load all the needed files into backup_files_temp
430  *   - info: contains the whole original object (times, names...)
431  * (all them being original ids as loaded from xml)
432  */
433 class restore_load_included_files extends restore_structure_step {
435     protected function define_structure() {
437         $file = new restore_path_element('file', '/files/file');
439         return array($file);
440     }
442     // Processing functions go here
443     public function process_file($data) {
445         $data = (object)$data; // handy
447         // load it if needed:
448         //   - it it is one of the annotated inforef files (course/section/activity/block)
449         //   - it is one "user", "group", "grouping" or "grade" component file (that aren't sent to inforef ever)
450         $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
451         $iscomponent = ($data->component == 'user' || $data->component == 'group' ||
452                         $data->component == 'grouping' || $data->component == 'grade');
453         if ($isfileref || $iscomponent) {
454             restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
455         }
456     }
459 /**
460  * Execution step that, *conditionally* (if there isn't preloaded information),
461  * will load all the needed roles to backup_temp_ids. They will be stored with
462  * "role" itemname. Also it will perform one automatic mapping to roles existing
463  * in the target site, based in permissions of the user performing the restore,
464  * archetypes and other bits. At the end, each original role will have its associated
465  * target role or 0 if it's going to be skipped. Note we wrap everything over one
466  * restore_dbops method, as far as the same stuff is going to be also executed
467  * by restore prechecks
468  */
469 class restore_load_and_map_roles extends restore_execution_step {
471     protected function define_execution() {
472         if ($this->task->get_preloaded_information()) { // if info is already preloaded
473             return;
474         }
476         $file = $this->get_basepath() . '/roles.xml';
477         // Load needed toles to temp_ids
478         restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
480         // Process roles, mapping/skipping. Any error throws exception
481         // Note we pass controller's info because it can contain role mapping information
482         // about manual mappings performed by UI
483         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);
484     }
487 /**
488  * Execution step that, *conditionally* (if there isn't preloaded information
489  * and users have been selected in settings, will load all the needed users
490  * to backup_temp_ids. They will be stored with "user" itemname and with
491  * their original contextid as paremitemid
492  */
493 class restore_load_included_users extends restore_execution_step {
495     protected function define_execution() {
497         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
498             return;
499         }
500         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
501             return;
502         }
503         $file = $this->get_basepath() . '/users.xml';
504         restore_dbops::load_users_to_tempids($this->get_restoreid(), $file); // Load needed users to temp_ids
505     }
508 /**
509  * Execution step that, *conditionally* (if there isn't preloaded information
510  * and users have been selected in settings, will process all the needed users
511  * in order to decide and perform any action with them (create / map / error)
512  * Note: Any error will cause exception, as far as this is the same processing
513  * than the one into restore prechecks (that should have stopped process earlier)
514  */
515 class restore_process_included_users extends restore_execution_step {
517     protected function define_execution() {
519         if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
520             return;
521         }
522         if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
523             return;
524         }
525         restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
526     }
529 /**
530  * Execution step that will create all the needed users as calculated
531  * by @restore_process_included_users (those having newiteind = 0)
532  */
533 class restore_create_included_users extends restore_execution_step {
535     protected function define_execution() {
537         restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(), $this->get_setting_value('user_files'));
538     }
541 /**
542  * Structure step that will create all the needed groups and groupings
543  * by loading them from the groups.xml file performing the required matches.
544  * Note group members only will be added if restoring user info
545  */
546 class restore_groups_structure_step extends restore_structure_step {
548     protected function define_structure() {
550         $paths = array(); // Add paths here
552         $paths[] = new restore_path_element('group', '/groups/group');
553         if ($this->get_setting_value('users')) {
554             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
555         }
556         $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
557         $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
559         return $paths;
560     }
562     // Processing functions go here
563     public function process_group($data) {
564         global $DB;
566         $data = (object)$data; // handy
567         $data->courseid = $this->get_courseid();
569         $oldid = $data->id;    // need this saved for later
571         $restorefiles = false; // Only if we end creating the group
573         // Search if the group already exists (by name & description) in the target course
574         $description_clause = '';
575         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
576         if (!empty($data->description)) {
577             $description_clause = ' AND ' .
578                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':desc');
579            $params['desc'] = $data->description;
580         }
581         if (!$groupdb = $DB->get_record_sql("SELECT *
582                                                FROM {groups}
583                                               WHERE courseid = :courseid
584                                                 AND name = :grname $description_clause", $params)) {
585             // group doesn't exist, create
586             $newitemid = $DB->insert_record('groups', $data);
587             $restorefiles = true; // We'll restore the files
588         } else {
589             // group exists, use it
590             $newitemid = $groupdb->id;
591         }
592         // Save the id mapping
593         $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
594     }
596     public function process_member($data) {
597         global $DB;
599         $data = (object)$data; // handy
601         // get parent group->id
602         $data->groupid = $this->get_new_parentid('group');
604         // map user newitemid and insert if not member already
605         if ($data->userid = $this->get_mappingid('user', $data->userid)) {
606             if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
607                 $DB->insert_record('groups_members', $data);
608             }
609         }
610     }
612     public function process_grouping($data) {
613         global $DB;
615         $data = (object)$data; // handy
616         $data->courseid = $this->get_courseid();
618         $oldid = $data->id;    // need this saved for later
619         $restorefiles = false; // Only if we end creating the grouping
621         // Search if the grouping already exists (by name & description) in the target course
622         $description_clause = '';
623         $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
624         if (!empty($data->description)) {
625             $description_clause = ' AND ' .
626                                   $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':desc');
627            $params['desc'] = $data->description;
628         }
629         if (!$groupingdb = $DB->get_record_sql("SELECT *
630                                                   FROM {groupings}
631                                                  WHERE courseid = :courseid
632                                                    AND name = :grname $description_clause", $params)) {
633             // grouping doesn't exist, create
634             $newitemid = $DB->insert_record('groupings', $data);
635             $restorefiles = true; // We'll restore the files
636         } else {
637             // grouping exists, use it
638             $newitemid = $groupingdb->id;
639         }
640         // Save the id mapping
641         $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
642     }
644     public function process_grouping_group($data) {
645         global $DB;
647         $data = (object)$data;
649         $data->groupingid = $this->get_new_parentid('grouping'); // Use new parentid
650         $data->groupid    = $this->get_mappingid('group', $data->groupid); // Get from mappings
651         $DB->insert_record('groupings_groups', $data);  // No need to set this mapping (no child info nor files)
652     }
654     protected function after_execute() {
655         // Add group related files, matching with "group" mappings
656         $this->add_related_files('group', 'icon', 'group');
657         $this->add_related_files('group', 'description', 'group');
658         // Add grouping related files, matching with "grouping" mappings
659         $this->add_related_files('grouping', 'description', 'grouping');
660     }
664 /**
665  * Structure step that will create all the needed scales
666  * by loading them from the scales.xml
667  */
668 class restore_scales_structure_step extends restore_structure_step {
670     protected function define_structure() {
672         $paths = array(); // Add paths here
673         $paths[] = new restore_path_element('scale', '/scales_definition/scale');
674         return $paths;
675     }
677     protected function process_scale($data) {
678         global $DB;
680         $data = (object)$data;
682         $restorefiles = false; // Only if we end creating the group
684         $oldid = $data->id;    // need this saved for later
686         // Look for scale (by 'scale' both in standard (course=0) and current course
687         // with priority to standard scales (ORDER clause)
688         // scale is not course unique, use get_record_sql to suppress warning
689         // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
690         $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
691         $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
692         if (!$scadb = $DB->get_record_sql("SELECT *
693                                             FROM {scale}
694                                            WHERE courseid IN (0, :courseid)
695                                              AND $compare_scale_clause
696                                         ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
697             // Remap the user if possible, defaut to user performing the restore if not
698             $userid = $this->get_mappingid('user', $data->userid);
699             $data->userid = $userid ? $userid : $this->task->get_userid();
700             // Remap the course if course scale
701             $data->courseid = $data->courseid ? $this->get_courseid() : 0;
702             // If global scale (course=0), check the user has perms to create it
703             // falling to course scale if not
704             $systemctx = get_context_instance(CONTEXT_SYSTEM);
705             if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
706                 $data->courseid = $this->get_courseid();
707             }
708             // scale doesn't exist, create
709             $newitemid = $DB->insert_record('scale', $data);
710             $restorefiles = true; // We'll restore the files
711         } else {
712             // scale exists, use it
713             $newitemid = $scadb->id;
714         }
715         // Save the id mapping (with files support at system context)
716         $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
717     }
719     protected function after_execute() {
720         // Add scales related files, matching with "scale" mappings
721         $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
722     }
726 /**
727  * Structure step that will create all the needed outocomes
728  * by loading them from the outcomes.xml
729  */
730 class restore_outcomes_structure_step extends restore_structure_step {
732     protected function define_structure() {
734         $paths = array(); // Add paths here
735         $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
736         return $paths;
737     }
739     protected function process_outcome($data) {
740         global $DB;
742         $data = (object)$data;
744         $restorefiles = false; // Only if we end creating the group
746         $oldid = $data->id;    // need this saved for later
748         // Look for outcome (by shortname both in standard (courseid=null) and current course
749         // with priority to standard outcomes (ORDER clause)
750         // outcome is not course unique, use get_record_sql to suppress warning
751         $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
752         if (!$outdb = $DB->get_record_sql('SELECT *
753                                              FROM {grade_outcomes}
754                                             WHERE shortname = :shortname
755                                               AND (courseid = :courseid OR courseid IS NULL)
756                                          ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
757             // Remap the user
758             $userid = $this->get_mappingid('user', $data->usermodified);
759             $data->usermodified = $userid ? $userid : $this->task->get_userid();
760             // Remap the scale
761             $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
762             // Remap the course if course outcome
763             $data->courseid = $data->courseid ? $this->get_courseid() : null;
764             // If global outcome (course=null), check the user has perms to create it
765             // falling to course outcome if not
766             $systemctx = get_context_instance(CONTEXT_SYSTEM);
767             if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
768                 $data->courseid = $this->get_courseid();
769             }
770             // outcome doesn't exist, create
771             $newitemid = $DB->insert_record('grade_outcomes', $data);
772             $restorefiles = true; // We'll restore the files
773         } else {
774             // scale exists, use it
775             $newitemid = $outdb->id;
776         }
777         // Set the corresponding grade_outcomes_courses record
778         $outcourserec = new stdclass();
779         $outcourserec->courseid  = $this->get_courseid();
780         $outcourserec->outcomeid = $newitemid;
781         if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
782             $DB->insert_record('grade_outcomes_courses', $outcourserec);
783         }
784         // Save the id mapping (with files support at system context)
785         $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
786     }
788     protected function after_execute() {
789         // Add outcomes related files, matching with "outcome" mappings
790         $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
791     }
794 /**
795  * Structure step that will read the section.xml creating/updating sections
796  * as needed, rebuilding course cache and other friends
797  */
798 class restore_section_structure_step extends restore_structure_step {
800     protected function define_structure() {
801         return array(new restore_path_element('section', '/section'));
802     }
804     public function process_section($data) {
805         global $DB;
806         $data = (object)$data;
807         $oldid = $data->id; // We'll need this later
809         $restorefiles = false;
811         // Look for the section
812         $section = new stdclass();
813         $section->course  = $this->get_courseid();
814         $section->section = $data->number;
815         // Section doesn't exist, create it with all the info from backup
816         if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
817             $section->name = $data->name;
818             $section->summary = $data->summary;
819             $section->summaryformat = $data->summaryformat;
820             $section->sequence = '';
821             $section->visible = $data->visible;
822             $newitemid = $DB->insert_record('course_sections', $section);
823             $restorefiles = true;
825         // Section exists, update non-empty information
826         } else {
827             $section->id = $secrec->id;
828             if (empty($secrec->name)) {
829                 $section->name = $data->name;
830             }
831             if (empty($secrec->summary)) {
832                 $section->summary = $data->summary;
833                 $section->summaryformat = $data->summaryformat;
834                 $restorefiles = true;
835             }
836             $DB->update_record('course_sections', $section);
837             $newitemid = $secrec->id;
838         }
840         // Annotate the section mapping, with restorefiles option if needed
841         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
843         // If needed, adjust course->numsections
844         if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
845             if ($numsections < $section->section) {
846                 $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
847             }
848         }
849     }
851     protected function after_execute() {
852         // Add section related files, with 'course_section' itemid to match
853         $this->add_related_files('course', 'section', 'course_section');
854     }
858 /**
859  * Structure step that will read the course.xml file, loading it and performing
860  * various actions depending of the site/restore settings. Note that target
861  * course always exist before arriving here so this step will be updating
862  * the course record (never inserting)
863  */
864 class restore_course_structure_step extends restore_structure_step {
866     protected function define_structure() {
868         $course = new restore_path_element('course', '/course', true); // Grouped
869         $category = new restore_path_element('category', '/course/category');
870         $tag = new restore_path_element('tag', '/course/tags/tag');
871         $allowed = new restore_path_element('allowed', '/course/allowed_modules/module');
873         return array($course, $category, $tag, $allowed);
874     }
876     // Processing functions go here
877     public function process_course($data) {
878         global $CFG, $DB;
880         $data = (object)$data;
881         $coursetags = isset($data->tags['tag']) ? $data->tags['tag'] : array();
882         $coursemodules = isset($data->allowed_modules['module']) ? $data->allowed_modules['module'] : array();
883         $oldid = $data->id; // We'll need this later
885         $fullname  = $this->get_setting_value('course_fullname');
886         $shortname = $this->get_setting_value('course_shortname');
887         $startdate = $this->get_setting_value('course_startdate');
889         // Calculate final course names, to avoid dupes
890         list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
892         // Need to change some fields before updating the course record
893         $data->id = $this->get_courseid();
894         $data->fullname = $fullname;
895         $data->shortname= $shortname;
896         $data->idnumber = '';
897         // TODO: Set category from the UI, its not a setting just a param
898         $data->category = get_course_category()->id;
899         $data->startdate= $this->apply_date_offset($data->startdate);
900         if ($data->defaultgroupingid) {
901             $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
902         }
903         if (empty($CFG->enablecompletion)) {
904             $data->enablecompletion = 0;
905             $data->completionstartonenrol = 0;
906             $data->completionnotify = 0;
907         }
908         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
909         if (!array_key_exists($data->lang, $languages)) {
910             $data->lang = '';
911         }
912         $themes = get_list_of_themes(); // Get themes for quick search later
913         if (!in_array($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
914             $data->theme = '';
915         }
917         // Course record ready, update it
918         $DB->update_record('course', $data);
920         // Set course mapping
921         $this->set_mapping('course', $oldid, $data->id);
923         // Course tags
924         if (!empty($CFG->usetags) && isset($coursetags)) { // if enabled in server and present in backup
925             $tags = array();
926             foreach ($coursetags as $coursetag) {
927                 $coursetag = (object)$coursetag;
928                 $tags[] = $coursetag->rawname;
929             }
930             tag_set('course', $this->get_courseid(), $tags);
931         }
932         // Course allowed modules
933         if (!empty($data->restrictmodules) && !empty($coursemodules)) {
934             $available = get_plugin_list('mod');
935             foreach ($coursemodules as $coursemodule) {
936                 $mname = $coursemodule['modulename'];
937                 if (array_key_exists($mname, $available)) {
938                     if ($module = $DB->get_record('modules', array('name' => $mname, 'visible' => 1))) {
939                         $rec = new stdclass();
940                         $rec->course = $this->get_courseid();
941                         $rec->module = $module->id;
942                         if (!$DB->record_exists('course_allowed_modules', (array)$rec)) {
943                             $DB->insert_record('course_allowed_modules', $rec);
944                         }
945                     }
946                 }
947             }
948         }
949         // Role name aliases
950         restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
951     }
953     protected function after_execute() {
954         // Add course related files, without itemid to match
955         $this->add_related_files('course', 'summary', null);
956         $this->add_related_files('course', 'legacy', null);
957     }
961 /*
962  * Structure step that will read the roles.xml file (at course/activity/block levels)
963  * containig all the role_assignments and overrides for that context. If corresponding to
964  * one mapped role, they will be applied to target context. Will observe the role_assignments
965  * setting to decide if ras are restored.
966  * Note: only ras with component == null are restored as far as the any ra with component
967  * is handled by one enrolment plugin, hence it will createt the ras later
968  */
969 class restore_ras_and_caps_structure_step extends restore_structure_step {
971     protected function define_structure() {
973         $paths = array();
975         // Observe the role_assignments setting
976         if ($this->get_setting_value('role_assignments')) {
977             $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
978         }
979         $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
981         return $paths;
982     }
984     public function process_assignment($data) {
985         global $DB;
987         $data = (object)$data;
989         // Check roleid, userid are one of the mapped ones
990         $newroleid = $this->get_mappingid('role', $data->roleid);
991         $newuserid = $this->get_mappingid('user', $data->userid);
992         // If newroleid and newuserid and component is empty and context valid assign via API (handles dupes and friends)
993         if ($newroleid && $newuserid && empty($data->component) && $this->task->get_contextid()) {
994             // Only assign roles to not deleted users
995             if ($DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
996                 // TODO: role_assign() needs one userid param to be able to specify our restore userid
997                 role_assign($newroleid, $newuserid, $this->task->get_contextid());
998             }
999         }
1000     }
1002     public function process_override($data) {
1003         $data = (object)$data;
1005         // Check roleid is one of the mapped ones
1006         $newroleid = $this->get_mappingid('role', $data->roleid);
1007         // If newroleid and context are valid assign it via API (it handles dupes and so on)
1008         if ($newroleid && $this->task->get_contextid()) {
1009             // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1010             // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1011             assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1012         }
1013     }
1016 /**
1017  * This structure steps restores the enrol plugins and their underlying
1018  * enrolments, performing all the mappings and/or movements required
1019  */
1020 class restore_enrolments_structure_step extends restore_structure_step {
1022     protected function define_structure() {
1024         $paths = array();
1026         $paths[] = new restore_path_element('enrol', '/enrolments/enrols/enrol');
1027         $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
1029         return $paths;
1030     }
1032     public function process_enrol($data) {
1033         global $DB;
1035         $data = (object)$data;
1036         $oldid = $data->id; // We'll need this later
1038         // TODO: Just one quick process of manual enrol_plugin. Add the rest (complex ones) and fix this
1039         if ($data->enrol !== 'manual') {
1040             debugging("Skipping '{$data->enrol}' enrolment plugin. Must be implemented", DEBUG_DEVELOPER);
1041             return;
1042         }
1044         // Perform various checks to decide what to do with the enrol plugin
1045         $installed = array_key_exists($data->enrol, enrol_get_plugins(false));
1046         $enabled   = enrol_is_enabled($data->enrol);
1047         $exists    = 0;
1048         $roleid    = $this->get_mappingid('role', $data->roleid);
1049         if ($rec = $DB->get_record('enrol', array('courseid' => $this->get_courseid(), 'enrol' => $data->enrol))) {
1050             $exists = $rec->id;
1051         }
1052         // If installed and enabled, continue processing
1053         if ($installed && $enabled) {
1054             // If not exists in course and we have a target role mapping
1055             if (!$exists && $roleid) {
1056                 $data->roleid = $roleid;
1057                 $enrol = enrol_get_plugin($data->enrol);
1058                 $courserec = $DB->get_record('course', array('id' => $this->get_courseid())); // Requires object, uses only id!!
1059                 $newitemid = $enrol->add_instance($courserec, array($data));
1061             // Already exists, user it for enrolments
1062             } else {
1063                 $newitemid = $exists;
1064             }
1066         // Not installed and enabled, map to 0
1067         } else {
1068             $newitemid = 0;
1069         }
1070         // Perform the simple mapping and done
1071         $this->set_mapping('enrol', $oldid, $newitemid);
1072     }
1074     public function process_enrolment($data) {
1075         global $DB;
1077         $data = (object)$data;
1079         // Process only if parent instance have been mapped
1080         if ($enrolid = $this->get_new_parentid('enrol')) {
1081             // And only if user is a mapped one
1082             if ($userid = $this->get_mappingid('user', $data->userid)) {
1083                 // TODO: Surely need to use API (enrol_user) here, instead of the current low-level impl
1084                 // TODO: Note enrol_user() sticks to $USER->id (need to add userid param)
1085                 $enrolment = new stdclass();
1086                 $enrolment->enrolid = $enrolid;
1087                 $enrolment->userid  = $userid;
1088                 if (!$DB->record_exists('user_enrolments', (array)$enrolment)) {
1089                     $enrolment->status = $data->status;
1090                     $enrolment->timestart = $data->timestart;
1091                     $enrolment->timeend = $data->timeend;
1092                     $enrolment->modifierid = $this->task->get_userid();
1093                     $enrolment->timecreated = time();
1094                     $enrolment->timemodified = 0;
1095                     $DB->insert_record('user_enrolments', $enrolment);
1096                 }
1097             }
1098         }
1099     }
1103 /**
1104  * This structure steps restores the filters and their configs
1105  */
1106 class restore_filters_structure_step extends restore_structure_step {
1108     protected function define_structure() {
1110         $paths = array();
1112         $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
1113         $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
1115         return $paths;
1116     }
1118     public function process_active($data) {
1120         $data = (object)$data;
1122         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
1123             return;
1124         }
1125         filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
1126     }
1128     public function process_config($data) {
1130         $data = (object)$data;
1132         if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
1133             return;
1134         }
1135         filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
1136     }
1140 /**
1141  * This structure steps restores the comments
1142  * Note: Cannot use the comments API because defaults to USER->id.
1143  * That should change allowing to pass $userid
1144  */
1145 class restore_comments_structure_step extends restore_structure_step {
1147     protected function define_structure() {
1149         $paths = array();
1151         $paths[] = new restore_path_element('comment', '/comments/comment');
1153         return $paths;
1154     }
1156     public function process_comment($data) {
1157         global $DB;
1159         $data = (object)$data;
1161         // First of all, if the comment has some itemid, ask to the task what to map
1162         $mapping = false;
1163         if ($data->itemid) {
1164             $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
1165             $data->itemid = $this->get_mappingid($mapping, $data->itemid);
1166         }
1167         // Only restore the comment if has no mapping OR we have found the matching mapping
1168         if (!$mapping || $data->itemid) {
1169             // Only if user mapping and context
1170             $data->userid = $this->get_mappingid('user', $data->userid);
1171             if ($data->userid && $this->task->get_contextid()) {
1172                 $data->contextid = $this->task->get_contextid();
1173                 // Only if there is another comment with same context/user/timecreated
1174                 $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
1175                 if (!$DB->record_exists('comments', $params)) {
1176                     $DB->insert_record('comments', $data);
1177                 }
1178             }
1179         }
1180     }
1183 /**
1184  * This structure step restores the grade items associated with one activity
1185  * All the grade items are made child of the "course" grade item but the original
1186  * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
1187  * the complete gradebook (categories and calculations), that information is
1188  * available there
1189  */
1190 class restore_activity_grades_structure_step extends restore_structure_step {
1192     protected function define_structure() {
1194         $paths = array();
1195         $userinfo = $this->get_setting_value('userinfo');
1197         $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
1198         $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
1199         if ($userinfo) {
1200             $paths[] = new restore_path_element('grade_grade',
1201                            '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
1202         }
1203         return $paths;
1204     }
1206     protected function process_grade_item($data) {
1208         $data = (object)($data);
1209         $oldid       = $data->id;        // We'll need these later
1210         $oldparentid = $data->categoryid;
1212         // make sure top course category exists, all grade items will be associated
1213         // to it. Later, if restoring the whole gradebook, categories will be introduced
1214         $coursecat = grade_category::fetch_course_category($this->get_courseid());
1215         $coursecatid = $coursecat->id; // Get the categoryid to be used
1217         unset($data->id);
1218         $data->categoryid   = $coursecatid;
1219         $data->courseid     = $this->get_courseid();
1220         $data->iteminstance = $this->task->get_activityid();
1221         // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
1222         // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
1223         // so the best is to keep the ones already in the gradebook
1224         // Potential problem: duplicates if same items are restored more than once. :-(
1225         // This needs to be fixed in some way (outcomes & activities with multiple items)
1226         // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
1227         // In any case, verify always for uniqueness
1228         $data->idnumber = grade_verify_idnumber($data->idnumber, $this->get_courseid()) ? $data->idnumber : null;
1229         $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
1230         $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
1231         $data->timecreated  = $this->apply_date_offset($data->timecreated);
1232         $data->timemodified = $this->apply_date_offset($data->timemodified);
1234         $gradeitem = new grade_item($data);
1235         $gradeitem->insert('restore');
1236         $this->set_mapping('grade_item', $oldid, $gradeitem->id, $oldparentid);
1237     }
1239     protected function process_grade_grade($data) {
1240         $data = (object)($data);
1242         unset($data->id);
1243         $data->itemid = $this->get_new_parentid('grade_item');
1244         $data->userid = $this->get_mappingid('user', $data->userid);
1245         $data->usermodified = $this->get_mappingid('user', $data->usermodified);
1246         $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
1247         // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
1248         $data->overridden = $this->apply_date_offset($data->overridden);
1250         $grade = new grade_grade($data);
1251         $grade->insert('restore');
1252         // no need to save any grade_grade mapping
1253     }
1255     /**
1256      * process activity grade_letters. Note that, while these are possible,
1257      * because grade_letters are contextid based, in proctice, only course
1258      * context letters can be defined. So we keep here this method knowing
1259      * it won't be executed ever. gradebook restore will restore course letters.
1260      */
1261     protected function process_grade_letter($data) {
1262         global $DB;
1264         $data = (object)$data;
1266         $data->contextid = $this->task->get_contextid();
1267         $newitemid = $DB->insert_record('grade_letters', $data);
1268         // no need to save any grade_letter mapping
1269     }
1273 /**
1274  * This structure steps restores one instance + positions of one block
1275  * Note: Positions corresponding to one existing context are restored
1276  * here, but all the ones having unknown contexts are sent to backup_ids
1277  * for a later chance to be restored at the end (final task)
1278  */
1279 class restore_block_instance_structure_step extends restore_structure_step {
1281     protected function define_structure() {
1283         $paths = array();
1285         $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
1286         $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
1288         return $paths;
1289     }
1291     public function process_block($data) {
1292         global $DB;
1294         $data = (object)$data; // Handy
1295         $oldcontextid = $data->contextid;
1296         $oldid        = $data->id;
1297         $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
1299         // Look for the parent contextid
1300         if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
1301             throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
1302         }
1304         // If there is already one block of that type in the parent context
1305         // and the block is not multiple, stop processing
1306         if ($DB->record_exists_sql("SELECT bi.id
1307                                       FROM {block_instances} bi
1308                                       JOIN {block} b ON b.name = bi.blockname
1309                                      WHERE bi.parentcontextid = ?
1310                                        AND bi.blockname = ?
1311                                        AND b.multiple = 0", array($data->parentcontextid, $data->blockname))) {
1312             return false;
1313         }
1315         // If there is already one block of that type in the parent context
1316         // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
1317         // stop processing
1318         $params = array(
1319             'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
1320             'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
1321             'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
1322         if ($birecs = $DB->get_records('block_instances', $params)) {
1323             foreach($birecs as $birec) {
1324                 if ($birec->configdata == $data->configdata) {
1325                     return false;
1326                 }
1327             }
1328         }
1330         // Set task old contextid, blockid and blockname once we know them
1331         $this->task->set_old_contextid($oldcontextid);
1332         $this->task->set_old_blockid($oldid);
1333         $this->task->set_blockname($data->blockname);
1335         // Let's look for anything within configdata neededing processing
1336         // (nulls and uses of legacy file.php)
1337         if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
1338             $configdata = (array)unserialize(base64_decode($data->configdata));
1339             foreach ($configdata as $attribute => $value) {
1340                 if (in_array($attribute, $attrstotransform)) {
1341                     $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
1342                 }
1343             }
1344             $data->configdata = base64_encode(serialize((object)$configdata));
1345         }
1347         // Create the block instance
1348         $newitemid = $DB->insert_record('block_instances', $data);
1349         // Save the mapping (with restorefiles support)
1350         $this->set_mapping('block_instance', $oldid, $newitemid, true);
1351         // Create the block context
1352         $newcontextid = get_context_instance(CONTEXT_BLOCK, $newitemid)->id;
1353         // Save the block contexts mapping and sent it to task
1354         $this->set_mapping('context', $oldcontextid, $newcontextid);
1355         $this->task->set_contextid($newcontextid);
1356         $this->task->set_blockid($newitemid);
1358         // Restore block fileareas if declared
1359         $component = 'block_' . $this->task->get_blockname();
1360         foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
1361             $this->add_related_files($component, $filearea, null);
1362         }
1364         // Process block positions, creating them or accumulating for final step
1365         foreach($positions as $position) {
1366             $position = (object)$position;
1367             $position->blockinstanceid = $newitemid; // The instance is always the restored one
1368             // If position is for one already mapped (known) contextid
1369             // process it now, creating the position
1370             if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
1371                 $position->contextid = $newpositionctxid;
1372                 // Create the block position
1373                 $DB->insert_record('block_positions', $position);
1375             // The position belongs to an unknown context, send it to backup_ids
1376             // to process them as part of the final steps of restore. We send the
1377             // whole $position object there, hence use the low level method.
1378             } else {
1379                 restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
1380             }
1381         }
1382     }
1385 /**
1386  * Structure step to restore common course_module information
1387  *
1388  * This step will process the module.xml file for one activity, in order to restore
1389  * the corresponding information to the course_modules table, skipping various bits
1390  * of information based on CFG settings (groupings, completion...) in order to fullfill
1391  * all the reqs to be able to create the context to be used by all the rest of steps
1392  * in the activity restore task
1393  */
1394 class restore_module_structure_step extends restore_structure_step {
1396     protected function define_structure() {
1397         global $CFG;
1399         $paths = array();
1401         $paths[] = new restore_path_element('module', '/module');
1402         if ($CFG->enableavailability) {
1403             $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
1404         }
1406         return $paths;
1407     }
1409     protected function process_module($data) {
1410         global $CFG, $DB;
1412         $data = (object)$data;
1413         $oldid = $data->id;
1415         $data->course = $this->task->get_courseid();
1416         $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
1417         // Map section (first try by course_section mapping match. Useful in course and section restores)
1418         $data->section = $this->get_mappingid('course_section', $data->sectionid);
1419         if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
1420             $params = array(
1421                 'course' => $this->get_courseid(),
1422                 'section' => $data->sectionnumber);
1423             $data->section = $DB->get_field('course_sections', 'id', $params);
1424         }
1425         if (!$data->section) { // sectionnumber failed, try to get first section in course
1426             $params = array(
1427                 'course' => $this->get_courseid());
1428             $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
1429         }
1430         if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
1431             $sectionrec = array(
1432                 'course' => $this->get_courseid(),
1433                 'section' => 0);
1434             $DB->insert_record('course_sections', $sectionrec); // section 0
1435             $sectionrec = array(
1436                 'course' => $this->get_courseid(),
1437                 'section' => 1);
1438             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
1439         }
1440         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
1441         if (!$CFG->enablegroupmembersonly) {                                         // observe groupsmemberonly
1442             $data->groupmembersonly = 0;
1443         }
1444         if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
1445             $data->idnumber = '';
1446         }
1447         if (empty($CFG->enablecompletion)) { // completion
1448             $data->completion = 0;
1449             $data->completiongradeitemnumber = null;
1450             $data->completionview = 0;
1451             $data->completionexpected = 0;
1452         } else {
1453             $data->completionexpected = $this->apply_date_offset($data->completionexpected);
1454         }
1455         if (empty($CFG->enableavailability)) {
1456             $data->availablefrom = 0;
1457             $data->availableuntil = 0;
1458             $data->showavailability = 0;
1459         } else {
1460             $data->availablefrom = $this->apply_date_offset($data->availablefrom);
1461             $data->availableuntil= $this->apply_date_offset($data->availableuntil);
1462         }
1463         $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
1465         // course_module record ready, insert it
1466         $newitemid = $DB->insert_record('course_modules', $data);
1467         // save mapping
1468         $this->set_mapping('course_module', $oldid, $newitemid);
1469         // set the new course_module id in the task
1470         $this->task->set_moduleid($newitemid);
1471         // we can now create the context safely
1472         $ctxid = get_context_instance(CONTEXT_MODULE, $newitemid)->id;
1473         // set the new context id in the task
1474         $this->task->set_contextid($ctxid);
1475         // update sequence field in course_section
1476         if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
1477             $sequence .= ',' . $newitemid;
1478         } else {
1479             $sequence = $newitemid;
1480         }
1481         $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
1482     }
1485     protected function process_availability($data) {
1486         $data = (object)$data;
1487         // Simply going to store the whole availability record now, we'll process
1488         // all them later in the final task (once all actvivities have been restored)
1489         // Let's call the low level one to be able to store the whole object
1490         $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
1491         restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
1492     }
1495 /**
1496  * Structure step that will process the user activity completion
1497  * information if all these conditions are met:
1498  *  - Target site has completion enabled ($CFG->enablecompletion)
1499  *  - Activity includes completion info (file_exists)
1500  */
1501 class restore_userscompletion_structure_step extends restore_structure_step {
1503     /**
1504      * To conditionally decide if this step must be executed
1505      * Note the "settings" conditions are evaluated in the
1506      * corresponding task. Here we check for other conditions
1507      * not being restore settings (files, site settings...)
1508      */
1509      protected function execute_condition() {
1510          global $CFG;
1512          // Completion disabled in this site, don't execute
1513          if (empty($CFG->enablecompletion)) {
1514              return false;
1515          }
1517          // No user completion info found, don't execute
1518         $fullpath = $this->task->get_taskbasepath();
1519         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
1520          if (!file_exists($fullpath)) {
1521              return false;
1522          }
1524          // Arrived here, execute the step
1525          return true;
1526      }
1528      protected function define_structure() {
1530         $paths = array();
1532         $paths[] = new restore_path_element('completion', '/completions/completion');
1534         return $paths;
1535     }
1537     protected function process_completion($data) {
1538         global $DB;
1540         $data = (object)$data;
1542         $data->coursemoduleid = $this->task->get_moduleid();
1543         $data->userid = $this->get_mappingid('user', $data->userid);
1544         $data->timemodified = $this->apply_date_offset($data->timemodified);
1546         $DB->insert_record('course_modules_completion', $data);
1547     }
1550 /**
1551  * Abstract structure step, parent of all the activity structure steps. Used to suuport
1552  * the main <activity ...> tag and process it. Also provides subplugin support for
1553  * activities.
1554  */
1555 abstract class restore_activity_structure_step extends restore_structure_step {
1557     protected function add_subplugin_structure($subplugintype, $element) {
1559         global $CFG;
1561         // Check the requested subplugintype is a valid one
1562         $subpluginsfile = $CFG->dirroot . '/mod/' . $this->task->get_modulename() . '/db/subplugins.php';
1563         if (!file_exists($subpluginsfile)) {
1564              throw new restore_step_exception('activity_missing_subplugins_php_file', $this->task->get_modulename());
1565         }
1566         include($subpluginsfile);
1567         if (!array_key_exists($subplugintype, $subplugins)) {
1568              throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
1569         }
1570         // Get all the restore path elements, looking across all the subplugin dirs
1571         $subpluginsdirs = get_plugin_list($subplugintype);
1572         foreach ($subpluginsdirs as $name => $subpluginsdir) {
1573             $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
1574             $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
1575             if (file_exists($restorefile)) {
1576                 require_once($restorefile);
1577                 $restoresubplugin = new $classname($subplugintype, $name, $this);
1578                 // Add subplugin paths to the step
1579                 $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
1580             }
1581         }
1582     }
1584     /**
1585      * As far as activity restore steps are implementing restore_subplugin stuff, they need to
1586      * have the parent task available for wrapping purposes (get course/context....)
1587      */
1588     public function get_task() {
1589         return $this->task;
1590     }
1592     /**
1593      * Adds support for the 'activity' path that is common to all the activities
1594      * and will be processed globally here
1595      */
1596     protected function prepare_activity_structure($paths) {
1598         $paths[] = new restore_path_element('activity', '/activity');
1600         return $paths;
1601     }
1603     /**
1604      * Process the activity path, informing the task about various ids, needed later
1605      */
1606     protected function process_activity($data) {
1607         $data = (object)$data;
1608         $this->task->set_old_contextid($data->contextid); // Save old contextid in task
1609         $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
1610         $this->task->set_old_activityid($data->id); // Save old activityid in task
1611     }
1613     /**
1614      * This must be invoked inmediately after creating the "module" activity record (forum, choice...)
1615      * and will adjust the new activity id (the instance) in various places
1616      */
1617     protected function apply_activity_instance($newitemid) {
1618         global $DB;
1620         $this->task->set_activityid($newitemid); // Save activity id in task
1621         // Apply the id to course_sections->instanceid
1622         $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
1623         // Do the mapping for modulename, preparing it for files by oldcontext
1624         $modulename = $this->task->get_modulename();
1625         $oldid = $this->task->get_old_activityid();
1626         $this->set_mapping($modulename, $oldid, $newitemid, true);
1627     }