source: moodle/trunk/fuentes/backup/moodle2/restore_stepslib.php @ 136

Last change on this file since 136 was 136, checked in by mabarracus, 3 years ago

Ported code to xenial

File size: 196.3 KB
Line 
1<?php
2
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/>.
17
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 */
27
28defined('MOODLE_INTERNAL') || die();
29
30/**
31 * delete old directories and conditionally create backup_temp_ids table
32 */
33class restore_create_and_clean_temp_stuff extends restore_execution_step {
34
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);
55
56    }
57}
58
59/**
60 * delete the temp dir used by backup/restore (conditionally),
61 * delete old directories and drop temp ids table
62 */
63class restore_drop_and_clean_temp_stuff extends restore_execution_step {
64
65    protected function define_execution() {
66        global $CFG;
67        restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
68        $progress = $this->task->get_progress();
69        $progress->start_progress('Deleting backup dir');
70        backup_helper::delete_old_backup_dirs(strtotime('-1 week'), $progress);      // Delete > 1 week old temp dirs.
71        if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
72            backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
73        }
74        $progress->end_progress();
75    }
76}
77
78/**
79 * Restore calculated grade items, grade categories etc
80 */
81class restore_gradebook_structure_step extends restore_structure_step {
82
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;
91
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        }
98
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        }
104
105        // Some activity has been excluded to be restored, don't execute
106        if ($this->task->is_excluding_activities()) {
107            return false;
108        }
109
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        }
119
120        // Arrived here, execute the step
121        return true;
122     }
123
124    protected function define_structure() {
125        $paths = array();
126        $userinfo = $this->task->get_setting_value('users');
127
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');
136
137        return $paths;
138    }
139
140    protected function process_gradebook($data) {
141    }
142
143    protected function process_grade_item($data) {
144        global $DB;
145
146        $data = (object)$data;
147
148        $oldid = $data->id;
149        $data->course = $this->get_courseid();
150
151        $data->courseid = $this->get_courseid();
152
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        }
171
172        $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
173        $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
174
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);
178
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;
186
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;
195
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        }
211
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            }
217
218            $newitemid = $DB->insert_record('grade_items', $data);
219        }
220        $this->set_mapping('grade_item', $oldid, $newitemid);
221    }
222
223    protected function process_grade_grade($data) {
224        global $DB;
225
226        $data = (object)$data;
227        $oldid = $data->id;
228        $olduserid = $data->userid;
229
230        $data->itemid = $this->get_new_parentid('grade_item');
231
232        $data->userid = $this->get_mappingid('user', $data->userid, null);
233        if (!empty($data->userid)) {
234            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
235            $data->locktime     = $this->apply_date_offset($data->locktime);
236            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
237            $data->overridden = $this->apply_date_offset($data->overridden);
238            $data->timecreated  = $this->apply_date_offset($data->timecreated);
239            $data->timemodified = $this->apply_date_offset($data->timemodified);
240
241            $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
242            if ($gradeexists) {
243                $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
244                $this->log($message, backup::LOG_DEBUG);
245            } else {
246                $newitemid = $DB->insert_record('grade_grades', $data);
247                $this->set_mapping('grade_grades', $oldid, $newitemid);
248            }
249        } else {
250            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
251            $this->log($message, backup::LOG_DEBUG);
252        }
253    }
254
255    protected function process_grade_category($data) {
256        global $DB;
257
258        $data = (object)$data;
259        $oldid = $data->id;
260
261        $data->course = $this->get_courseid();
262        $data->courseid = $data->course;
263
264        $data->timecreated  = $this->apply_date_offset($data->timecreated);
265        $data->timemodified = $this->apply_date_offset($data->timemodified);
266
267        $newitemid = null;
268        //no parent means a course level grade category. That may have been created when the course was created
269        if(empty($data->parent)) {
270            //parent was being saved as 0 when it should be null
271            $data->parent = null;
272
273            //get the already created course level grade category
274            $category = new stdclass();
275            $category->courseid = $this->get_courseid();
276            $category->parent = null;
277
278            $coursecategory = $DB->get_record('grade_categories', (array)$category);
279            if (!empty($coursecategory)) {
280                $data->id = $newitemid = $coursecategory->id;
281                $DB->update_record('grade_categories', $data);
282            }
283        }
284
285        // Add a warning about a removed setting.
286        if (!empty($data->aggregatesubcats)) {
287            set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
288        }
289
290        //need to insert a course category
291        if (empty($newitemid)) {
292            $newitemid = $DB->insert_record('grade_categories', $data);
293        }
294        $this->set_mapping('grade_category', $oldid, $newitemid);
295    }
296    protected function process_grade_letter($data) {
297        global $DB;
298
299        $data = (object)$data;
300        $oldid = $data->id;
301
302        $data->contextid = context_course::instance($this->get_courseid())->id;
303
304        $gradeletter = (array)$data;
305        unset($gradeletter['id']);
306        if (!$DB->record_exists('grade_letters', $gradeletter)) {
307            $newitemid = $DB->insert_record('grade_letters', $data);
308        } else {
309            $newitemid = $data->id;
310        }
311
312        $this->set_mapping('grade_letter', $oldid, $newitemid);
313    }
314    protected function process_grade_setting($data) {
315        global $DB;
316
317        $data = (object)$data;
318        $oldid = $data->id;
319
320        $data->courseid = $this->get_courseid();
321
322        if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
323            $newitemid = $DB->insert_record('grade_settings', $data);
324        } else {
325            $newitemid = $data->id;
326        }
327
328        $this->set_mapping('grade_setting', $oldid, $newitemid);
329    }
330
331    /**
332     * put all activity grade items in the correct grade category and mark all for recalculation
333     */
334    protected function after_execute() {
335        global $DB;
336
337        $conditions = array(
338            'backupid' => $this->get_restoreid(),
339            'itemname' => 'grade_item'//,
340            //'itemid'   => $itemid
341        );
342        $rs = $DB->get_recordset('backup_ids_temp', $conditions);
343
344        // We need this for calculation magic later on.
345        $mappings = array();
346
347        if (!empty($rs)) {
348            foreach($rs as $grade_item_backup) {
349
350                // Store the oldid with the new id.
351                $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
352
353                $updateobj = new stdclass();
354                $updateobj->id = $grade_item_backup->newitemid;
355
356                //if this is an activity grade item that needs to be put back in its correct category
357                if (!empty($grade_item_backup->parentitemid)) {
358                    $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
359                    if (!is_null($oldcategoryid)) {
360                        $updateobj->categoryid = $oldcategoryid;
361                        $DB->update_record('grade_items', $updateobj);
362                    }
363                } else {
364                    //mark course and category items as needing to be recalculated
365                    $updateobj->needsupdate=1;
366                    $DB->update_record('grade_items', $updateobj);
367                }
368            }
369        }
370        $rs->close();
371
372        // We need to update the calculations for calculated grade items that may reference old
373        // grade item ids using ##gi\d+##.
374        // $mappings can be empty, use 0 if so (won't match ever)
375        list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
376        $sql = "SELECT gi.id, gi.calculation
377                  FROM {grade_items} gi
378                 WHERE gi.id {$sql} AND
379                       calculation IS NOT NULL";
380        $rs = $DB->get_recordset_sql($sql, $params);
381        foreach ($rs as $gradeitem) {
382            // Collect all of the used grade item id references
383            if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
384                // This calculation doesn't reference any other grade items... EASY!
385                continue;
386            }
387            // For this next bit we are going to do the replacement of id's in two steps:
388            // 1. We will replace all old id references with a special mapping reference.
389            // 2. We will replace all mapping references with id's
390            // Why do we do this?
391            // Because there potentially there will be an overlap of ids within the query and we
392            // we substitute the wrong id.. safest way around this is the two step system
393            $calculationmap = array();
394            $mapcount = 0;
395            foreach ($matches[1] as $match) {
396                // Check that the old id is known to us, if not it was broken to begin with and will
397                // continue to be broken.
398                if (!array_key_exists($match, $mappings)) {
399                    continue;
400                }
401                // Our special mapping key
402                $mapping = '##MAPPING'.$mapcount.'##';
403                // The old id that exists within the calculation now
404                $oldid = '##gi'.$match.'##';
405                // The new id that we want to replace the old one with.
406                $newid = '##gi'.$mappings[$match].'##';
407                // Replace in the special mapping key
408                $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
409                // And record the mapping
410                $calculationmap[$mapping] = $newid;
411                $mapcount++;
412            }
413            // Iterate all special mappings for this calculation and replace in the new id's
414            foreach ($calculationmap as $mapping => $newid) {
415                $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
416            }
417            // Update the calculation now that its being remapped
418            $DB->update_record('grade_items', $gradeitem);
419        }
420        $rs->close();
421
422        // Need to correct the grade category path and parent
423        $conditions = array(
424            'courseid' => $this->get_courseid()
425        );
426
427        $rs = $DB->get_recordset('grade_categories', $conditions);
428        // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
429        foreach ($rs as $gc) {
430            if (!empty($gc->parent)) {
431                $grade_category = new stdClass();
432                $grade_category->id = $gc->id;
433                $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
434                $DB->update_record('grade_categories', $grade_category);
435            }
436        }
437        $rs->close();
438
439        // Now we can rebuild all the paths
440        $rs = $DB->get_recordset('grade_categories', $conditions);
441        foreach ($rs as $gc) {
442            $grade_category = new stdClass();
443            $grade_category->id = $gc->id;
444            $grade_category->path = grade_category::build_path($gc);
445            $grade_category->depth = substr_count($grade_category->path, '/') - 1;
446            $DB->update_record('grade_categories', $grade_category);
447        }
448        $rs->close();
449
450        // Restore marks items as needing update. Update everything now.
451        grade_regrade_final_grades($this->get_courseid());
452    }
453}
454
455/**
456 * Step in charge of restoring the grade history of a course.
457 *
458 * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
459 * we do not want to restore the history if the gradebook and its content has not been
460 * restored. At least for now.
461 */
462class restore_grade_history_structure_step extends restore_structure_step {
463
464     protected function execute_condition() {
465        global $CFG, $DB;
466
467        // No gradebook info found, don't execute.
468        $fullpath = $this->task->get_taskbasepath();
469        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
470        if (!file_exists($fullpath)) {
471            return false;
472        }
473
474        // Some module present in backup file isn't available to restore in this site, don't execute.
475        if ($this->task->is_missing_modules()) {
476            return false;
477        }
478
479        // Some activity has been excluded to be restored, don't execute.
480        if ($this->task->is_excluding_activities()) {
481            return false;
482        }
483
484        // There should only be one grade category (the 1 associated with the course itself).
485        $category = new stdclass();
486        $category->courseid  = $this->get_courseid();
487        $catcount = $DB->count_records('grade_categories', (array)$category);
488        if ($catcount > 1) {
489            return false;
490        }
491
492        // Arrived here, execute the step.
493        return true;
494     }
495
496    protected function define_structure() {
497        $paths = array();
498
499        // Settings to use.
500        $userinfo = $this->get_setting_value('users');
501        $history = $this->get_setting_value('grade_histories');
502
503        if ($userinfo && $history) {
504            $paths[] = new restore_path_element('grade_grade',
505               '/grade_history/grade_grades/grade_grade');
506        }
507
508        return $paths;
509    }
510
511    protected function process_grade_grade($data) {
512        global $DB;
513
514        $data = (object)($data);
515        $olduserid = $data->userid;
516        unset($data->id);
517
518        $data->userid = $this->get_mappingid('user', $data->userid, null);
519        if (!empty($data->userid)) {
520            // Do not apply the date offsets as this is history.
521            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
522            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
523            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
524            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
525            $DB->insert_record('grade_grades_history', $data);
526        } else {
527            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
528            $this->log($message, backup::LOG_DEBUG);
529        }
530    }
531
532}
533
534/**
535 * decode all the interlinks present in restored content
536 * relying 100% in the restore_decode_processor that handles
537 * both the contents to modify and the rules to be applied
538 */
539class restore_decode_interlinks extends restore_execution_step {
540
541    protected function define_execution() {
542        // Get the decoder (from the plan)
543        $decoder = $this->task->get_decoder();
544        restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
545        // And launch it, everything will be processed
546        $decoder->execute();
547    }
548}
549
550/**
551 * first, ensure that we have no gaps in section numbers
552 * and then, rebuid the course cache
553 */
554class restore_rebuild_course_cache extends restore_execution_step {
555
556    protected function define_execution() {
557        global $DB;
558
559        // Although there is some sort of auto-recovery of missing sections
560        // present in course/formats... here we check that all the sections
561        // from 0 to MAX(section->section) exist, creating them if necessary
562        $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
563        // Iterate over all sections
564        for ($i = 0; $i <= $maxsection; $i++) {
565            // If the section $i doesn't exist, create it
566            if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
567                $sectionrec = array(
568                    'course' => $this->get_courseid(),
569                    'section' => $i);
570                $DB->insert_record('course_sections', $sectionrec); // missing section created
571            }
572        }
573
574        // Rebuild cache now that all sections are in place
575        rebuild_course_cache($this->get_courseid());
576        cache_helper::purge_by_event('changesincourse');
577        cache_helper::purge_by_event('changesincoursecat');
578    }
579}
580
581/**
582 * Review all the tasks having one after_restore method
583 * executing it to perform some final adjustments of information
584 * not available when the task was executed.
585 */
586class restore_execute_after_restore extends restore_execution_step {
587
588    protected function define_execution() {
589
590        // Simply call to the execute_after_restore() method of the task
591        // that always is the restore_final_task
592        $this->task->launch_execute_after_restore();
593    }
594}
595
596
597/**
598 * Review all the (pending) block positions in backup_ids, matching by
599 * contextid, creating positions as needed. This is executed by the
600 * final task, once all the contexts have been created
601 */
602class restore_review_pending_block_positions extends restore_execution_step {
603
604    protected function define_execution() {
605        global $DB;
606
607        // Get all the block_position objects pending to match
608        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
609        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
610        // Process block positions, creating them or accumulating for final step
611        foreach($rs as $posrec) {
612            // Get the complete position object out of the info field.
613            $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
614            // If position is for one already mapped (known) contextid
615            // process it now, creating the position, else nothing to
616            // do, position finally discarded
617            if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
618                $position->contextid = $newctx->newitemid;
619                // Create the block position
620                $DB->insert_record('block_positions', $position);
621            }
622        }
623        $rs->close();
624    }
625}
626
627
628/**
629 * Updates the availability data for course modules and sections.
630 *
631 * Runs after the restore of all course modules, sections, and grade items has
632 * completed. This is necessary in order to update IDs that have changed during
633 * restore.
634 *
635 * @package core_backup
636 * @copyright 2014 The Open University
637 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
638 */
639class restore_update_availability extends restore_execution_step {
640
641    protected function define_execution() {
642        global $CFG, $DB;
643
644        // Note: This code runs even if availability is disabled when restoring.
645        // That will ensure that if you later turn availability on for the site,
646        // there will be no incorrect IDs. (It doesn't take long if the restored
647        // data does not contain any availability information.)
648
649        // Get modinfo with all data after resetting cache.
650        rebuild_course_cache($this->get_courseid(), true);
651        $modinfo = get_fast_modinfo($this->get_courseid());
652
653        // Get the date offset for this restore.
654        $dateoffset = $this->apply_date_offset(1) - 1;
655
656        // Update all sections that were restored.
657        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
658        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
659        $sectionsbyid = null;
660        foreach ($rs as $rec) {
661            if (is_null($sectionsbyid)) {
662                $sectionsbyid = array();
663                foreach ($modinfo->get_section_info_all() as $section) {
664                    $sectionsbyid[$section->id] = $section;
665                }
666            }
667            if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
668                // If the section was not fully restored for some reason
669                // (e.g. due to an earlier error), skip it.
670                $this->get_logger()->process('Section not fully restored: id ' .
671                        $rec->newitemid, backup::LOG_WARNING);
672                continue;
673            }
674            $section = $sectionsbyid[$rec->newitemid];
675            if (!is_null($section->availability)) {
676                $info = new \core_availability\info_section($section);
677                $info->update_after_restore($this->get_restoreid(),
678                        $this->get_courseid(), $this->get_logger(), $dateoffset);
679            }
680        }
681        $rs->close();
682
683        // Update all modules that were restored.
684        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
685        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
686        foreach ($rs as $rec) {
687            if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
688                // If the module was not fully restored for some reason
689                // (e.g. due to an earlier error), skip it.
690                $this->get_logger()->process('Module not fully restored: id ' .
691                        $rec->newitemid, backup::LOG_WARNING);
692                continue;
693            }
694            $cm = $modinfo->get_cm($rec->newitemid);
695            if (!is_null($cm->availability)) {
696                $info = new \core_availability\info_module($cm);
697                $info->update_after_restore($this->get_restoreid(),
698                        $this->get_courseid(), $this->get_logger(), $dateoffset);
699            }
700        }
701        $rs->close();
702    }
703}
704
705
706/**
707 * Process legacy module availability records in backup_ids.
708 *
709 * Matches course modules and grade item id once all them have been already restored.
710 * Only if all matchings are satisfied the availability condition will be created.
711 * At the same time, it is required for the site to have that functionality enabled.
712 *
713 * This step is included only to handle legacy backups (2.6 and before). It does not
714 * do anything for newer backups.
715 *
716 * @copyright 2014 The Open University
717 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
718 */
719class restore_process_course_modules_availability extends restore_execution_step {
720
721    protected function define_execution() {
722        global $CFG, $DB;
723
724        // Site hasn't availability enabled
725        if (empty($CFG->enableavailability)) {
726            return;
727        }
728
729        // Do both modules and sections.
730        foreach (array('module', 'section') as $table) {
731            // Get all the availability objects to process.
732            $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
733            $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
734            // Process availabilities, creating them if everything matches ok.
735            foreach ($rs as $availrec) {
736                $allmatchesok = true;
737                // Get the complete legacy availability object.
738                $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
739
740                // Note: This code used to update IDs, but that is now handled by the
741                // current code (after restore) instead of this legacy code.
742
743                // Get showavailability option.
744                $thingid = ($table === 'module') ? $availability->coursemoduleid :
745                        $availability->coursesectionid;
746                $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
747                        $table . '_showavailability', $thingid);
748                if (!$showrec) {
749                    // Should not happen.
750                    throw new coding_exception('No matching showavailability record');
751                }
752                $show = $showrec->info->showavailability;
753
754                // The $availability object is now in the format used in the old
755                // system. Interpret this and convert to new system.
756                $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
757                        array('id' => $thingid), MUST_EXIST);
758                $newvalue = \core_availability\info::add_legacy_availability_condition(
759                        $currentvalue, $availability, $show);
760                $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
761                        array('id' => $thingid));
762            }
763        }
764        $rs->close();
765    }
766}
767
768
769/*
770 * Execution step that, *conditionally* (if there isn't preloaded information)
771 * will load the inforef files for all the included course/section/activity tasks
772 * to backup_temp_ids. They will be stored with "xxxxref" as itemname
773 */
774class restore_load_included_inforef_records extends restore_execution_step {
775
776    protected function define_execution() {
777
778        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
779            return;
780        }
781
782        // Get all the included tasks
783        $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
784        $progress = $this->task->get_progress();
785        $progress->start_progress($this->get_name(), count($tasks));
786        foreach ($tasks as $task) {
787            // Load the inforef.xml file if exists
788            $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
789            if (file_exists($inforefpath)) {
790                // Load each inforef file to temp_ids.
791                restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
792            }
793        }
794        $progress->end_progress();
795    }
796}
797
798/*
799 * Execution step that will load all the needed files into backup_files_temp
800 *   - info: contains the whole original object (times, names...)
801 * (all them being original ids as loaded from xml)
802 */
803class restore_load_included_files extends restore_structure_step {
804
805    protected function define_structure() {
806
807        $file = new restore_path_element('file', '/files/file');
808
809        return array($file);
810    }
811
812    /**
813     * Process one <file> element from files.xml
814     *
815     * @param array $data the element data
816     */
817    public function process_file($data) {
818
819        $data = (object)$data; // handy
820
821        // load it if needed:
822        //   - it it is one of the annotated inforef files (course/section/activity/block)
823        //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
824        // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
825        //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
826        $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
827        $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
828                        $data->component == 'grouping' || $data->component == 'grade' ||
829                        $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
830        if ($isfileref || $iscomponent) {
831            restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
832        }
833    }
834}
835
836/**
837 * Execution step that, *conditionally* (if there isn't preloaded information),
838 * will load all the needed roles to backup_temp_ids. They will be stored with
839 * "role" itemname. Also it will perform one automatic mapping to roles existing
840 * in the target site, based in permissions of the user performing the restore,
841 * archetypes and other bits. At the end, each original role will have its associated
842 * target role or 0 if it's going to be skipped. Note we wrap everything over one
843 * restore_dbops method, as far as the same stuff is going to be also executed
844 * by restore prechecks
845 */
846class restore_load_and_map_roles extends restore_execution_step {
847
848    protected function define_execution() {
849        if ($this->task->get_preloaded_information()) { // if info is already preloaded
850            return;
851        }
852
853        $file = $this->get_basepath() . '/roles.xml';
854        // Load needed toles to temp_ids
855        restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
856
857        // Process roles, mapping/skipping. Any error throws exception
858        // Note we pass controller's info because it can contain role mapping information
859        // about manual mappings performed by UI
860        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);
861    }
862}
863
864/**
865 * Execution step that, *conditionally* (if there isn't preloaded information
866 * and users have been selected in settings, will load all the needed users
867 * to backup_temp_ids. They will be stored with "user" itemname and with
868 * their original contextid as paremitemid
869 */
870class restore_load_included_users extends restore_execution_step {
871
872    protected function define_execution() {
873
874        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
875            return;
876        }
877        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
878            return;
879        }
880        $file = $this->get_basepath() . '/users.xml';
881        // Load needed users to temp_ids.
882        restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
883    }
884}
885
886/**
887 * Execution step that, *conditionally* (if there isn't preloaded information
888 * and users have been selected in settings, will process all the needed users
889 * in order to decide and perform any action with them (create / map / error)
890 * Note: Any error will cause exception, as far as this is the same processing
891 * than the one into restore prechecks (that should have stopped process earlier)
892 */
893class restore_process_included_users extends restore_execution_step {
894
895    protected function define_execution() {
896
897        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
898            return;
899        }
900        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
901            return;
902        }
903        restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
904                $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
905    }
906}
907
908/**
909 * Execution step that will create all the needed users as calculated
910 * by @restore_process_included_users (those having newiteind = 0)
911 */
912class restore_create_included_users extends restore_execution_step {
913
914    protected function define_execution() {
915
916        restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
917                $this->task->get_userid(), $this->task->get_progress());
918    }
919}
920
921/**
922 * Structure step that will create all the needed groups and groupings
923 * by loading them from the groups.xml file performing the required matches.
924 * Note group members only will be added if restoring user info
925 */
926class restore_groups_structure_step extends restore_structure_step {
927
928    protected function define_structure() {
929
930        $paths = array(); // Add paths here
931
932        $paths[] = new restore_path_element('group', '/groups/group');
933        $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
934        $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
935
936        return $paths;
937    }
938
939    // Processing functions go here
940    public function process_group($data) {
941        global $DB;
942
943        $data = (object)$data; // handy
944        $data->courseid = $this->get_courseid();
945
946        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
947        // another a group in the same course
948        $context = context_course::instance($data->courseid);
949        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
950            if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
951                unset($data->idnumber);
952            }
953        } else {
954            unset($data->idnumber);
955        }
956
957        $oldid = $data->id;    // need this saved for later
958
959        $restorefiles = false; // Only if we end creating the group
960
961        // Search if the group already exists (by name & description) in the target course
962        $description_clause = '';
963        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
964        if (!empty($data->description)) {
965            $description_clause = ' AND ' .
966                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
967           $params['description'] = $data->description;
968        }
969        if (!$groupdb = $DB->get_record_sql("SELECT *
970                                               FROM {groups}
971                                              WHERE courseid = :courseid
972                                                AND name = :grname $description_clause", $params)) {
973            // group doesn't exist, create
974            $newitemid = $DB->insert_record('groups', $data);
975            $restorefiles = true; // We'll restore the files
976        } else {
977            // group exists, use it
978            $newitemid = $groupdb->id;
979        }
980        // Save the id mapping
981        $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
982        // Invalidate the course group data cache just in case.
983        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
984    }
985
986    public function process_grouping($data) {
987        global $DB;
988
989        $data = (object)$data; // handy
990        $data->courseid = $this->get_courseid();
991
992        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
993        // another a grouping in the same course
994        $context = context_course::instance($data->courseid);
995        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
996            if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
997                unset($data->idnumber);
998            }
999        } else {
1000            unset($data->idnumber);
1001        }
1002
1003        $oldid = $data->id;    // need this saved for later
1004        $restorefiles = false; // Only if we end creating the grouping
1005
1006        // Search if the grouping already exists (by name & description) in the target course
1007        $description_clause = '';
1008        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1009        if (!empty($data->description)) {
1010            $description_clause = ' AND ' .
1011                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1012           $params['description'] = $data->description;
1013        }
1014        if (!$groupingdb = $DB->get_record_sql("SELECT *
1015                                                  FROM {groupings}
1016                                                 WHERE courseid = :courseid
1017                                                   AND name = :grname $description_clause", $params)) {
1018            // grouping doesn't exist, create
1019            $newitemid = $DB->insert_record('groupings', $data);
1020            $restorefiles = true; // We'll restore the files
1021        } else {
1022            // grouping exists, use it
1023            $newitemid = $groupingdb->id;
1024        }
1025        // Save the id mapping
1026        $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1027        // Invalidate the course group data cache just in case.
1028        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1029    }
1030
1031    public function process_grouping_group($data) {
1032        global $CFG;
1033
1034        require_once($CFG->dirroot.'/group/lib.php');
1035
1036        $data = (object)$data;
1037        groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1038    }
1039
1040    protected function after_execute() {
1041        // Add group related files, matching with "group" mappings
1042        $this->add_related_files('group', 'icon', 'group');
1043        $this->add_related_files('group', 'description', 'group');
1044        // Add grouping related files, matching with "grouping" mappings
1045        $this->add_related_files('grouping', 'description', 'grouping');
1046        // Invalidate the course group data.
1047        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1048    }
1049
1050}
1051
1052/**
1053 * Structure step that will create all the needed group memberships
1054 * by loading them from the groups.xml file performing the required matches.
1055 */
1056class restore_groups_members_structure_step extends restore_structure_step {
1057
1058    protected $plugins = null;
1059
1060    protected function define_structure() {
1061
1062        $paths = array(); // Add paths here
1063
1064        if ($this->get_setting_value('users')) {
1065            $paths[] = new restore_path_element('group', '/groups/group');
1066            $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1067        }
1068
1069        return $paths;
1070    }
1071
1072    public function process_group($data) {
1073        $data = (object)$data; // handy
1074
1075        // HACK ALERT!
1076        // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1077        // Let's fake internal state to make $this->get_new_parentid('group') work.
1078
1079        $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1080    }
1081
1082    public function process_member($data) {
1083        global $DB, $CFG;
1084        require_once("$CFG->dirroot/group/lib.php");
1085
1086        // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1087
1088        $data = (object)$data; // handy
1089
1090        // get parent group->id
1091        $data->groupid = $this->get_new_parentid('group');
1092
1093        // map user newitemid and insert if not member already
1094        if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1095            if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1096                // Check the component, if any, exists.
1097                if (empty($data->component)) {
1098                    groups_add_member($data->groupid, $data->userid);
1099
1100                } else if ((strpos($data->component, 'enrol_') === 0)) {
1101                    // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1102                    // it is possible that enrolment was restored using different plugin type.
1103                    if (!isset($this->plugins)) {
1104                        $this->plugins = enrol_get_plugins(true);
1105                    }
1106                    if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1107                        if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1108                            if (isset($this->plugins[$instance->enrol])) {
1109                                $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1110                            }
1111                        }
1112                    }
1113
1114                } else {
1115                    $dir = core_component::get_component_directory($data->component);
1116                    if ($dir and is_dir($dir)) {
1117                        if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1118                            return;
1119                        }
1120                    }
1121                    // Bad luck, plugin could not restore the data, let's add normal membership.
1122                    groups_add_member($data->groupid, $data->userid);
1123                    $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1124                    $this->log($message, backup::LOG_WARNING);
1125                }
1126            }
1127        }
1128    }
1129}
1130
1131/**
1132 * Structure step that will create all the needed scales
1133 * by loading them from the scales.xml
1134 */
1135class restore_scales_structure_step extends restore_structure_step {
1136
1137    protected function define_structure() {
1138
1139        $paths = array(); // Add paths here
1140        $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1141        return $paths;
1142    }
1143
1144    protected function process_scale($data) {
1145        global $DB;
1146
1147        $data = (object)$data;
1148
1149        $restorefiles = false; // Only if we end creating the group
1150
1151        $oldid = $data->id;    // need this saved for later
1152
1153        // Look for scale (by 'scale' both in standard (course=0) and current course
1154        // with priority to standard scales (ORDER clause)
1155        // scale is not course unique, use get_record_sql to suppress warning
1156        // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1157        $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1158        $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1159        if (!$scadb = $DB->get_record_sql("SELECT *
1160                                            FROM {scale}
1161                                           WHERE courseid IN (0, :courseid)
1162                                             AND $compare_scale_clause
1163                                        ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1164            // Remap the user if possible, defaut to user performing the restore if not
1165            $userid = $this->get_mappingid('user', $data->userid);
1166            $data->userid = $userid ? $userid : $this->task->get_userid();
1167            // Remap the course if course scale
1168            $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1169            // If global scale (course=0), check the user has perms to create it
1170            // falling to course scale if not
1171            $systemctx = context_system::instance();
1172            if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1173                $data->courseid = $this->get_courseid();
1174            }
1175            // scale doesn't exist, create
1176            $newitemid = $DB->insert_record('scale', $data);
1177            $restorefiles = true; // We'll restore the files
1178        } else {
1179            // scale exists, use it
1180            $newitemid = $scadb->id;
1181        }
1182        // Save the id mapping (with files support at system context)
1183        $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1184    }
1185
1186    protected function after_execute() {
1187        // Add scales related files, matching with "scale" mappings
1188        $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1189    }
1190}
1191
1192
1193/**
1194 * Structure step that will create all the needed outocomes
1195 * by loading them from the outcomes.xml
1196 */
1197class restore_outcomes_structure_step extends restore_structure_step {
1198
1199    protected function define_structure() {
1200
1201        $paths = array(); // Add paths here
1202        $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1203        return $paths;
1204    }
1205
1206    protected function process_outcome($data) {
1207        global $DB;
1208
1209        $data = (object)$data;
1210
1211        $restorefiles = false; // Only if we end creating the group
1212
1213        $oldid = $data->id;    // need this saved for later
1214
1215        // Look for outcome (by shortname both in standard (courseid=null) and current course
1216        // with priority to standard outcomes (ORDER clause)
1217        // outcome is not course unique, use get_record_sql to suppress warning
1218        $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1219        if (!$outdb = $DB->get_record_sql('SELECT *
1220                                             FROM {grade_outcomes}
1221                                            WHERE shortname = :shortname
1222                                              AND (courseid = :courseid OR courseid IS NULL)
1223                                         ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1224            // Remap the user
1225            $userid = $this->get_mappingid('user', $data->usermodified);
1226            $data->usermodified = $userid ? $userid : $this->task->get_userid();
1227            // Remap the scale
1228            $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1229            // Remap the course if course outcome
1230            $data->courseid = $data->courseid ? $this->get_courseid() : null;
1231            // If global outcome (course=null), check the user has perms to create it
1232            // falling to course outcome if not
1233            $systemctx = context_system::instance();
1234            if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1235                $data->courseid = $this->get_courseid();
1236            }
1237            // outcome doesn't exist, create
1238            $newitemid = $DB->insert_record('grade_outcomes', $data);
1239            $restorefiles = true; // We'll restore the files
1240        } else {
1241            // scale exists, use it
1242            $newitemid = $outdb->id;
1243        }
1244        // Set the corresponding grade_outcomes_courses record
1245        $outcourserec = new stdclass();
1246        $outcourserec->courseid  = $this->get_courseid();
1247        $outcourserec->outcomeid = $newitemid;
1248        if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1249            $DB->insert_record('grade_outcomes_courses', $outcourserec);
1250        }
1251        // Save the id mapping (with files support at system context)
1252        $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1253    }
1254
1255    protected function after_execute() {
1256        // Add outcomes related files, matching with "outcome" mappings
1257        $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1258    }
1259}
1260
1261/**
1262 * Execution step that, *conditionally* (if there isn't preloaded information
1263 * will load all the question categories and questions (header info only)
1264 * to backup_temp_ids. They will be stored with "question_category" and
1265 * "question" itemnames and with their original contextid and question category
1266 * id as paremitemids
1267 */
1268class restore_load_categories_and_questions extends restore_execution_step {
1269
1270    protected function define_execution() {
1271
1272        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1273            return;
1274        }
1275        $file = $this->get_basepath() . '/questions.xml';
1276        restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1277    }
1278}
1279
1280/**
1281 * Execution step that, *conditionally* (if there isn't preloaded information)
1282 * will process all the needed categories and questions
1283 * in order to decide and perform any action with them (create / map / error)
1284 * Note: Any error will cause exception, as far as this is the same processing
1285 * than the one into restore prechecks (that should have stopped process earlier)
1286 */
1287class restore_process_categories_and_questions extends restore_execution_step {
1288
1289    protected function define_execution() {
1290
1291        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1292            return;
1293        }
1294        restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1295    }
1296}
1297
1298/**
1299 * Structure step that will read the section.xml creating/updating sections
1300 * as needed, rebuilding course cache and other friends
1301 */
1302class restore_section_structure_step extends restore_structure_step {
1303
1304    protected function define_structure() {
1305        global $CFG;
1306
1307        $paths = array();
1308
1309        $section = new restore_path_element('section', '/section');
1310        $paths[] = $section;
1311        if ($CFG->enableavailability) {
1312            $paths[] = new restore_path_element('availability', '/section/availability');
1313            $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1314        }
1315        $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1316
1317        // Apply for 'format' plugins optional paths at section level
1318        $this->add_plugin_structure('format', $section);
1319
1320        // Apply for 'local' plugins optional paths at section level
1321        $this->add_plugin_structure('local', $section);
1322
1323        return $paths;
1324    }
1325
1326    public function process_section($data) {
1327        global $CFG, $DB;
1328        $data = (object)$data;
1329        $oldid = $data->id; // We'll need this later
1330
1331        $restorefiles = false;
1332
1333        // Look for the section
1334        $section = new stdclass();
1335        $section->course  = $this->get_courseid();
1336        $section->section = $data->number;
1337        // Section doesn't exist, create it with all the info from backup
1338        if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
1339            $section->name = $data->name;
1340            $section->summary = $data->summary;
1341            $section->summaryformat = $data->summaryformat;
1342            $section->sequence = '';
1343            $section->visible = $data->visible;
1344            if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1345                $section->availability = null;
1346            } else {
1347                $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1348                // Include legacy [<2.7] availability data if provided.
1349                if (is_null($section->availability)) {
1350                    $section->availability = \core_availability\info::convert_legacy_fields(
1351                            $data, true);
1352                }
1353            }
1354            $newitemid = $DB->insert_record('course_sections', $section);
1355            $restorefiles = true;
1356
1357        // Section exists, update non-empty information
1358        } else {
1359            $section->id = $secrec->id;
1360            if ((string)$secrec->name === '') {
1361                $section->name = $data->name;
1362            }
1363            if (empty($secrec->summary)) {
1364                $section->summary = $data->summary;
1365                $section->summaryformat = $data->summaryformat;
1366                $restorefiles = true;
1367            }
1368
1369            // Don't update availability (I didn't see a useful way to define
1370            // whether existing or new one should take precedence).
1371
1372            $DB->update_record('course_sections', $section);
1373            $newitemid = $secrec->id;
1374        }
1375
1376        // Annotate the section mapping, with restorefiles option if needed
1377        $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1378
1379        // set the new course_section id in the task
1380        $this->task->set_sectionid($newitemid);
1381
1382        // If there is the legacy showavailability data, store this for later use.
1383        // (This data is not present when restoring 'new' backups.)
1384        if (isset($data->showavailability)) {
1385            // Cache the showavailability flag using the backup_ids data field.
1386            restore_dbops::set_backup_ids_record($this->get_restoreid(),
1387                    'section_showavailability', $newitemid, 0, null,
1388                    (object)array('showavailability' => $data->showavailability));
1389        }
1390
1391        // Commented out. We never modify course->numsections as far as that is used
1392        // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1393        // Note: We keep the code here, to know about and because of the possibility of making this
1394        // optional based on some setting/attribute in the future
1395        // If needed, adjust course->numsections
1396        //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1397        //    if ($numsections < $section->section) {
1398        //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1399        //    }
1400        //}
1401    }
1402
1403    /**
1404     * Process the legacy availability table record. This table does not exist
1405     * in Moodle 2.7+ but we still support restore.
1406     *
1407     * @param stdClass $data Record data
1408     */
1409    public function process_availability($data) {
1410        $data = (object)$data;
1411        // Simply going to store the whole availability record now, we'll process
1412        // all them later in the final task (once all activities have been restored)
1413        // Let's call the low level one to be able to store the whole object.
1414        $data->coursesectionid = $this->task->get_sectionid();
1415        restore_dbops::set_backup_ids_record($this->get_restoreid(),
1416                'section_availability', $data->id, 0, null, $data);
1417    }
1418
1419    /**
1420     * Process the legacy availability fields table record. This table does not
1421     * exist in Moodle 2.7+ but we still support restore.
1422     *
1423     * @param stdClass $data Record data
1424     */
1425    public function process_availability_field($data) {
1426        global $DB;
1427        $data = (object)$data;
1428        // Mark it is as passed by default
1429        $passed = true;
1430        $customfieldid = null;
1431
1432        // If a customfield has been used in order to pass we must be able to match an existing
1433        // customfield by name (data->customfield) and type (data->customfieldtype)
1434        if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1435            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1436            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1437            $passed = false;
1438        } else if (!is_null($data->customfield)) {
1439            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
1440            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
1441            $passed = ($customfieldid !== false);
1442        }
1443
1444        if ($passed) {
1445            // Create the object to insert into the database
1446            $availfield = new stdClass();
1447            $availfield->coursesectionid = $this->task->get_sectionid();
1448            $availfield->userfield = $data->userfield;
1449            $availfield->customfieldid = $customfieldid;
1450            $availfield->operator = $data->operator;
1451            $availfield->value = $data->value;
1452
1453            // Get showavailability option.
1454            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1455                    'section_showavailability', $availfield->coursesectionid);
1456            if (!$showrec) {
1457                // Should not happen.
1458                throw new coding_exception('No matching showavailability record');
1459            }
1460            $show = $showrec->info->showavailability;
1461
1462            // The $availfield object is now in the format used in the old
1463            // system. Interpret this and convert to new system.
1464            $currentvalue = $DB->get_field('course_sections', 'availability',
1465                    array('id' => $availfield->coursesectionid), MUST_EXIST);
1466            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1467                    $currentvalue, $availfield, $show);
1468            $DB->set_field('course_sections', 'availability', $newvalue,
1469                    array('id' => $availfield->coursesectionid));
1470        }
1471    }
1472
1473    public function process_course_format_options($data) {
1474        global $DB;
1475        $data = (object)$data;
1476        $oldid = $data->id;
1477        unset($data->id);
1478        $data->sectionid = $this->task->get_sectionid();
1479        $data->courseid = $this->get_courseid();
1480        $newid = $DB->insert_record('course_format_options', $data);
1481        $this->set_mapping('course_format_options', $oldid, $newid);
1482    }
1483
1484    protected function after_execute() {
1485        // Add section related files, with 'course_section' itemid to match
1486        $this->add_related_files('course', 'section', 'course_section');
1487    }
1488}
1489
1490/**
1491 * Structure step that will read the course.xml file, loading it and performing
1492 * various actions depending of the site/restore settings. Note that target
1493 * course always exist before arriving here so this step will be updating
1494 * the course record (never inserting)
1495 */
1496class restore_course_structure_step extends restore_structure_step {
1497    /**
1498     * @var bool this gets set to true by {@link process_course()} if we are
1499     * restoring an old coures that used the legacy 'module security' feature.
1500     * If so, we have to do more work in {@link after_execute()}.
1501     */
1502    protected $legacyrestrictmodules = false;
1503
1504    /**
1505     * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1506     * array with array keys the module names ('forum', 'quiz', etc.). These are
1507     * the modules that are allowed according to the data in the backup file.
1508     * In {@link after_execute()} we then have to prevent adding of all the other
1509     * types of activity.
1510     */
1511    protected $legacyallowedmodules = array();
1512
1513    protected function define_structure() {
1514
1515        $course = new restore_path_element('course', '/course');
1516        $category = new restore_path_element('category', '/course/category');
1517        $tag = new restore_path_element('tag', '/course/tags/tag');
1518        $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1519
1520        // Apply for 'format' plugins optional paths at course level
1521        $this->add_plugin_structure('format', $course);
1522
1523        // Apply for 'theme' plugins optional paths at course level
1524        $this->add_plugin_structure('theme', $course);
1525
1526        // Apply for 'report' plugins optional paths at course level
1527        $this->add_plugin_structure('report', $course);
1528
1529        // Apply for 'course report' plugins optional paths at course level
1530        $this->add_plugin_structure('coursereport', $course);
1531
1532        // Apply for plagiarism plugins optional paths at course level
1533        $this->add_plugin_structure('plagiarism', $course);
1534
1535        // Apply for local plugins optional paths at course level
1536        $this->add_plugin_structure('local', $course);
1537
1538        return array($course, $category, $tag, $allowed_module);
1539    }
1540
1541    /**
1542     * Processing functions go here
1543     *
1544     * @global moodledatabase $DB
1545     * @param stdClass $data
1546     */
1547    public function process_course($data) {
1548        global $CFG, $DB;
1549
1550        $data = (object)$data;
1551
1552        $fullname  = $this->get_setting_value('course_fullname');
1553        $shortname = $this->get_setting_value('course_shortname');
1554        $startdate = $this->get_setting_value('course_startdate');
1555
1556        // Calculate final course names, to avoid dupes
1557        list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
1558
1559        // Need to change some fields before updating the course record
1560        $data->id = $this->get_courseid();
1561        $data->fullname = $fullname;
1562        $data->shortname= $shortname;
1563
1564        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1565        // another course on this site.
1566        $context = context::instance_by_id($this->task->get_contextid());
1567        if (!empty($data->idnumber) && has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid()) &&
1568                $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
1569            // Do not reset idnumber.
1570        } else {
1571            $data->idnumber = '';
1572        }
1573
1574        // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
1575        // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
1576        if (empty($data->hiddensections)) {
1577            $data->hiddensections = 0;
1578        }
1579
1580        // Set legacyrestrictmodules to true if the course was resticting modules. If so
1581        // then we will need to process restricted modules after execution.
1582        $this->legacyrestrictmodules = !empty($data->restrictmodules);
1583
1584        $data->startdate= $this->apply_date_offset($data->startdate);
1585        if ($data->defaultgroupingid) {
1586            $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
1587        }
1588        if (empty($CFG->enablecompletion)) {
1589            $data->enablecompletion = 0;
1590            $data->completionstartonenrol = 0;
1591            $data->completionnotify = 0;
1592        }
1593        $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
1594        if (!array_key_exists($data->lang, $languages)) {
1595            $data->lang = '';
1596        }
1597
1598        $themes = get_list_of_themes(); // Get themes for quick search later
1599        if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
1600            $data->theme = '';
1601        }
1602
1603        // Check if this is an old SCORM course format.
1604        if ($data->format == 'scorm') {
1605            $data->format = 'singleactivity';
1606            $data->activitytype = 'scorm';
1607        }
1608
1609        // Course record ready, update it
1610        $DB->update_record('course', $data);
1611
1612        course_get_format($data)->update_course_format_options($data);
1613
1614        // Role name aliases
1615        restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
1616    }
1617
1618    public function process_category($data) {
1619        // Nothing to do with the category. UI sets it before restore starts
1620    }
1621
1622    public function process_tag($data) {
1623        global $CFG, $DB;
1624
1625        $data = (object)$data;
1626
1627        if (!empty($CFG->usetags)) { // if enabled in server
1628            // TODO: This is highly inneficient. Each time we add one tag
1629            // we fetch all the existing because tag_set() deletes them
1630            // so everything must be reinserted on each call
1631            $tags = array();
1632            $existingtags = tag_get_tags('course', $this->get_courseid());
1633            // Re-add all the existitng tags
1634            foreach ($existingtags as $existingtag) {
1635                $tags[] = $existingtag->rawname;
1636            }
1637            // Add the one being restored
1638            $tags[] = $data->rawname;
1639            // Send all the tags back to the course
1640            tag_set('course', $this->get_courseid(), $tags, 'core',
1641                context_course::instance($this->get_courseid())->id);
1642        }
1643    }
1644
1645    public function process_allowed_module($data) {
1646        $data = (object)$data;
1647
1648        // Backwards compatiblity support for the data that used to be in the
1649        // course_allowed_modules table.
1650        if ($this->legacyrestrictmodules) {
1651            $this->legacyallowedmodules[$data->modulename] = 1;
1652        }
1653    }
1654
1655    protected function after_execute() {
1656        global $DB;
1657
1658        // Add course related files, without itemid to match
1659        $this->add_related_files('course', 'summary', null);
1660        $this->add_related_files('course', 'overviewfiles', null);
1661
1662        // Deal with legacy allowed modules.
1663        if ($this->legacyrestrictmodules) {
1664            $context = context_course::instance($this->get_courseid());
1665
1666            list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
1667            list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
1668            foreach ($managerroleids as $roleid) {
1669                unset($roleids[$roleid]);
1670            }
1671
1672            foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
1673                if (isset($this->legacyallowedmodules[$modname])) {
1674                    // Module is allowed, no worries.
1675                    continue;
1676                }
1677
1678                $capability = 'mod/' . $modname . ':addinstance';
1679                foreach ($roleids as $roleid) {
1680                    assign_capability($capability, CAP_PREVENT, $roleid, $context);
1681                }
1682            }
1683        }
1684    }
1685}
1686
1687/**
1688 * Execution step that will migrate legacy files if present.
1689 */
1690class restore_course_legacy_files_step extends restore_execution_step {
1691    public function define_execution() {
1692        global $DB;
1693
1694        // Do a check for legacy files and skip if there are none.
1695        $sql = 'SELECT count(*)
1696                  FROM {backup_files_temp}
1697                 WHERE backupid = ?
1698                   AND contextid = ?
1699                   AND component = ?
1700                   AND filearea  = ?';
1701        $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
1702
1703        if ($DB->count_records_sql($sql, $params)) {
1704            $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
1705            restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
1706                'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
1707        }
1708    }
1709}
1710
1711/*
1712 * Structure step that will read the roles.xml file (at course/activity/block levels)
1713 * containing all the role_assignments and overrides for that context. If corresponding to
1714 * one mapped role, they will be applied to target context. Will observe the role_assignments
1715 * setting to decide if ras are restored.
1716 *
1717 * Note: this needs to be executed after all users are enrolled.
1718 */
1719class restore_ras_and_caps_structure_step extends restore_structure_step {
1720    protected $plugins = null;
1721
1722    protected function define_structure() {
1723
1724        $paths = array();
1725
1726        // Observe the role_assignments setting
1727        if ($this->get_setting_value('role_assignments')) {
1728            $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
1729        }
1730        $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
1731
1732        return $paths;
1733    }
1734
1735    /**
1736     * Assign roles
1737     *
1738     * This has to be called after enrolments processing.
1739     *
1740     * @param mixed $data
1741     * @return void
1742     */
1743    public function process_assignment($data) {
1744        global $DB;
1745
1746        $data = (object)$data;
1747
1748        // Check roleid, userid are one of the mapped ones
1749        if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
1750            return;
1751        }
1752        if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
1753            return;
1754        }
1755        if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
1756            // Only assign roles to not deleted users
1757            return;
1758        }
1759        if (!$contextid = $this->task->get_contextid()) {
1760            return;
1761        }
1762
1763        if (empty($data->component)) {
1764            // assign standard manual roles
1765            // TODO: role_assign() needs one userid param to be able to specify our restore userid
1766            role_assign($newroleid, $newuserid, $contextid);
1767
1768        } else if ((strpos($data->component, 'enrol_') === 0)) {
1769            // Deal with enrolment roles - ignore the component and just find out the instance via new id,
1770            // it is possible that enrolment was restored using different plugin type.
1771            if (!isset($this->plugins)) {
1772                $this->plugins = enrol_get_plugins(true);
1773            }
1774            if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1775                if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1776                    if (isset($this->plugins[$instance->enrol])) {
1777                        $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
1778                    }
1779                }
1780            }
1781
1782        } else {
1783            $data->roleid    = $newroleid;
1784            $data->userid    = $newuserid;
1785            $data->contextid = $contextid;
1786            $dir = core_component::get_component_directory($data->component);
1787            if ($dir and is_dir($dir)) {
1788                if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
1789                    return;
1790                }
1791            }
1792            // Bad luck, plugin could not restore the data, let's add normal membership.
1793            role_assign($data->roleid, $data->userid, $data->contextid);
1794            $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
1795            $this->log($message, backup::LOG_WARNING);
1796        }
1797    }
1798
1799    public function process_override($data) {
1800        $data = (object)$data;
1801
1802        // Check roleid is one of the mapped ones
1803        $newroleid = $this->get_mappingid('role', $data->roleid);
1804        // If newroleid and context are valid assign it via API (it handles dupes and so on)
1805        if ($newroleid && $this->task->get_contextid()) {
1806            // TODO: assign_capability() needs one userid param to be able to specify our restore userid
1807            // TODO: it seems that assign_capability() doesn't check for valid capabilities at all ???
1808            assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
1809        }
1810    }
1811}
1812
1813/**
1814 * If no instances yet add default enrol methods the same way as when creating new course in UI.
1815 */
1816class restore_default_enrolments_step extends restore_execution_step {
1817    public function define_execution() {
1818        global $DB;
1819
1820        $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
1821
1822        if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
1823            // Something already added instances, do not add default instances.
1824            $plugins = enrol_get_plugins(true);
1825            foreach ($plugins as $plugin) {
1826                $plugin->restore_sync_course($course);
1827            }
1828
1829        } else {
1830            // Looks like a newly created course.
1831            enrol_course_updated(true, $course, null);
1832        }
1833    }
1834}
1835
1836/**
1837 * This structure steps restores the enrol plugins and their underlying
1838 * enrolments, performing all the mappings and/or movements required
1839 */
1840class restore_enrolments_structure_step extends restore_structure_step {
1841    protected $enrolsynced = false;
1842    protected $plugins = null;
1843    protected $originalstatus = array();
1844
1845    /**
1846     * Conditionally decide if this step should be executed.
1847     *
1848     * This function checks the following parameter:
1849     *
1850     *   1. the course/enrolments.xml file exists
1851     *
1852     * @return bool true is safe to execute, false otherwise
1853     */
1854    protected function execute_condition() {
1855
1856        // Check it is included in the backup
1857        $fullpath = $this->task->get_taskbasepath();
1858        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
1859        if (!file_exists($fullpath)) {
1860            // Not found, can't restore enrolments info
1861            return false;
1862        }
1863
1864        return true;
1865    }
1866
1867    protected function define_structure() {
1868
1869        $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
1870        $enrolment = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
1871        // Attach local plugin stucture to enrol element.
1872        $this->add_plugin_structure('enrol', $enrol);
1873
1874        return array($enrol, $enrolment);
1875    }
1876
1877    /**
1878     * Create enrolment instances.
1879     *
1880     * This has to be called after creation of roles
1881     * and before adding of role assignments.
1882     *
1883     * @param mixed $data
1884     * @return void
1885     */
1886    public function process_enrol($data) {
1887        global $DB;
1888
1889        $data = (object)$data;
1890        $oldid = $data->id; // We'll need this later.
1891        unset($data->id);
1892
1893        $this->originalstatus[$oldid] = $data->status;
1894
1895        if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
1896            $this->set_mapping('enrol', $oldid, 0);
1897            return;
1898        }
1899
1900        if (!isset($this->plugins)) {
1901            $this->plugins = enrol_get_plugins(true);
1902        }
1903
1904        if (!$this->enrolsynced) {
1905            // Make sure that all plugin may create instances and enrolments automatically
1906            // before the first instance restore - this is suitable especially for plugins
1907            // that synchronise data automatically using course->idnumber or by course categories.
1908            foreach ($this->plugins as $plugin) {
1909                $plugin->restore_sync_course($courserec);
1910            }
1911            $this->enrolsynced = true;
1912        }
1913
1914        // Map standard fields - plugin has to process custom fields manually.
1915        $data->roleid   = $this->get_mappingid('role', $data->roleid);
1916        $data->courseid = $courserec->id;
1917
1918        if ($this->get_setting_value('enrol_migratetomanual')) {
1919            unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
1920            if (!enrol_is_enabled('manual')) {
1921                $this->set_mapping('enrol', $oldid, 0);
1922                return;
1923            }
1924            if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
1925                $instance = reset($instances);
1926                $this->set_mapping('enrol', $oldid, $instance->id);
1927            } else {
1928                if ($data->enrol === 'manual') {
1929                    $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
1930                } else {
1931                    $instanceid = $this->plugins['manual']->add_default_instance($courserec);
1932                }
1933                $this->set_mapping('enrol', $oldid, $instanceid);
1934            }
1935
1936        } else {
1937            if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
1938                $this->set_mapping('enrol', $oldid, 0);
1939                $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, use migration to manual enrolments";
1940                $this->log($message, backup::LOG_WARNING);
1941                return;
1942            }
1943            if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
1944                // Let's keep the sortorder in old backups.
1945            } else {
1946                // Prevent problems with colliding sortorders in old backups,
1947                // new 2.4 backups do not need sortorder because xml elements are ordered properly.
1948                unset($data->sortorder);
1949            }
1950            // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
1951            $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
1952        }
1953    }
1954
1955    /**
1956     * Create user enrolments.
1957     *
1958     * This has to be called after creation of enrolment instances
1959     * and before adding of role assignments.
1960     *
1961     * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
1962     *
1963     * @param mixed $data
1964     * @return void
1965     */
1966    public function process_enrolment($data) {
1967        global $DB;
1968
1969        if (!isset($this->plugins)) {
1970            $this->plugins = enrol_get_plugins(true);
1971        }
1972
1973        $data = (object)$data;
1974
1975        // Process only if parent instance have been mapped.
1976        if ($enrolid = $this->get_new_parentid('enrol')) {
1977            $oldinstancestatus = ENROL_INSTANCE_ENABLED;
1978            $oldenrolid = $this->get_old_parentid('enrol');
1979            if (isset($this->originalstatus[$oldenrolid])) {
1980                $oldinstancestatus = $this->originalstatus[$oldenrolid];
1981            }
1982            if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1983                // And only if user is a mapped one.
1984                if ($userid = $this->get_mappingid('user', $data->userid)) {
1985                    if (isset($this->plugins[$instance->enrol])) {
1986                        $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
1987                    }
1988                }
1989            }
1990        }
1991    }
1992}
1993
1994
1995/**
1996 * Make sure the user restoring the course can actually access it.
1997 */
1998class restore_fix_restorer_access_step extends restore_execution_step {
1999    protected function define_execution() {
2000        global $CFG, $DB;
2001
2002        if (!$userid = $this->task->get_userid()) {
2003            return;
2004        }
2005
2006        if (empty($CFG->restorernewroleid)) {
2007            // Bad luck, no fallback role for restorers specified
2008            return;
2009        }
2010
2011        $courseid = $this->get_courseid();
2012        $context = context_course::instance($courseid);
2013
2014        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2015            // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2016            return;
2017        }
2018
2019        // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2020        role_assign($CFG->restorernewroleid, $userid, $context);
2021
2022        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2023            // Extra role is enough, yay!
2024            return;
2025        }
2026
2027        // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2028        // hopefully admin selected suitable $CFG->restorernewroleid ...
2029        if (!enrol_is_enabled('manual')) {
2030            return;
2031        }
2032        if (!$enrol = enrol_get_plugin('manual')) {
2033            return;
2034        }
2035        if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2036            $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2037            $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2038            $enrol->add_instance($course, $fields);
2039        }
2040
2041        enrol_try_internal_enrol($courseid, $userid);
2042    }
2043}
2044
2045
2046/**
2047 * This structure steps restores the filters and their configs
2048 */
2049class restore_filters_structure_step extends restore_structure_step {
2050
2051    protected function define_structure() {
2052
2053        $paths = array();
2054
2055        $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2056        $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2057
2058        return $paths;
2059    }
2060
2061    public function process_active($data) {
2062
2063        $data = (object)$data;
2064
2065        if (strpos($data->filter, 'filter/') === 0) {
2066            $data->filter = substr($data->filter, 7);
2067
2068        } else if (strpos($data->filter, '/') !== false) {
2069            // Unsupported old filter.
2070            return;
2071        }
2072
2073        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2074            return;
2075        }
2076        filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2077    }
2078
2079    public function process_config($data) {
2080
2081        $data = (object)$data;
2082
2083        if (strpos($data->filter, 'filter/') === 0) {
2084            $data->filter = substr($data->filter, 7);
2085
2086        } else if (strpos($data->filter, '/') !== false) {
2087            // Unsupported old filter.
2088            return;
2089        }
2090
2091        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2092            return;
2093        }
2094        filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2095    }
2096}
2097
2098
2099/**
2100 * This structure steps restores the comments
2101 * Note: Cannot use the comments API because defaults to USER->id.
2102 * That should change allowing to pass $userid
2103 */
2104class restore_comments_structure_step extends restore_structure_step {
2105
2106    protected function define_structure() {
2107
2108        $paths = array();
2109
2110        $paths[] = new restore_path_element('comment', '/comments/comment');
2111
2112        return $paths;
2113    }
2114
2115    public function process_comment($data) {
2116        global $DB;
2117
2118        $data = (object)$data;
2119
2120        // First of all, if the comment has some itemid, ask to the task what to map
2121        $mapping = false;
2122        if ($data->itemid) {
2123            $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2124            $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2125        }
2126        // Only restore the comment if has no mapping OR we have found the matching mapping
2127        if (!$mapping || $data->itemid) {
2128            // Only if user mapping and context
2129            $data->userid = $this->get_mappingid('user', $data->userid);
2130            if ($data->userid && $this->task->get_contextid()) {
2131                $data->contextid = $this->task->get_contextid();
2132                // Only if there is another comment with same context/user/timecreated
2133                $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2134                if (!$DB->record_exists('comments', $params)) {
2135                    $DB->insert_record('comments', $data);
2136                }
2137            }
2138        }
2139    }
2140}
2141
2142/**
2143 * This structure steps restores the badges and their configs
2144 */
2145class restore_badges_structure_step extends restore_structure_step {
2146
2147    /**
2148     * Conditionally decide if this step should be executed.
2149     *
2150     * This function checks the following parameters:
2151     *
2152     *   1. Badges and course badges are enabled on the site.
2153     *   2. The course/badges.xml file exists.
2154     *   3. All modules are restorable.
2155     *   4. All modules are marked for restore.
2156     *
2157     * @return bool True is safe to execute, false otherwise
2158     */
2159    protected function execute_condition() {
2160        global $CFG;
2161
2162        // First check is badges and course level badges are enabled on this site.
2163        if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2164            // Disabled, don't restore course badges.
2165            return false;
2166        }
2167
2168        // Check if badges.xml is included in the backup.
2169        $fullpath = $this->task->get_taskbasepath();
2170        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2171        if (!file_exists($fullpath)) {
2172            // Not found, can't restore course badges.
2173            return false;
2174        }
2175
2176        // Check we are able to restore all backed up modules.
2177        if ($this->task->is_missing_modules()) {
2178            return false;
2179        }
2180
2181        // Finally check all modules within the backup are being restored.
2182        if ($this->task->is_excluding_activities()) {
2183            return false;
2184        }
2185
2186        return true;
2187    }
2188
2189    protected function define_structure() {
2190        $paths = array();
2191        $paths[] = new restore_path_element('badge', '/badges/badge');
2192        $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2193        $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2194        $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2195
2196        return $paths;
2197    }
2198
2199    public function process_badge($data) {
2200        global $DB, $CFG;
2201
2202        require_once($CFG->libdir . '/badgeslib.php');
2203
2204        $data = (object)$data;
2205        $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2206        if (empty($data->usercreated)) {
2207            $data->usercreated = $this->task->get_userid();
2208        }
2209        $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2210        if (empty($data->usermodified)) {
2211            $data->usermodified = $this->task->get_userid();
2212        }
2213
2214        // We'll restore the badge image.
2215        $restorefiles = true;
2216
2217        $courseid = $this->get_courseid();
2218
2219        $params = array(
2220                'name'           => $data->name,
2221                'description'    => $data->description,
2222                'timecreated'    => $this->apply_date_offset($data->timecreated),
2223                'timemodified'   => $this->apply_date_offset($data->timemodified),
2224                'usercreated'    => $data->usercreated,
2225                'usermodified'   => $data->usermodified,
2226                'issuername'     => $data->issuername,
2227                'issuerurl'      => $data->issuerurl,
2228                'issuercontact'  => $data->issuercontact,
2229                'expiredate'     => $this->apply_date_offset($data->expiredate),
2230                'expireperiod'   => $data->expireperiod,
2231                'type'           => BADGE_TYPE_COURSE,
2232                'courseid'       => $courseid,
2233                'message'        => $data->message,
2234                'messagesubject' => $data->messagesubject,
2235                'attachment'     => $data->attachment,
2236                'notification'   => $data->notification,
2237                'status'         => BADGE_STATUS_INACTIVE,
2238                'nextcron'       => $this->apply_date_offset($data->nextcron)
2239        );
2240
2241        $newid = $DB->insert_record('badge', $params);
2242        $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2243    }
2244
2245    public function process_criterion($data) {
2246        global $DB;
2247
2248        $data = (object)$data;
2249
2250        $params = array(
2251                'badgeid'      => $this->get_new_parentid('badge'),
2252                'criteriatype' => $data->criteriatype,
2253                'method'       => $data->method
2254        );
2255        $newid = $DB->insert_record('badge_criteria', $params);
2256        $this->set_mapping('criterion', $data->id, $newid);
2257    }
2258
2259    public function process_parameter($data) {
2260        global $DB, $CFG;
2261
2262        require_once($CFG->libdir . '/badgeslib.php');
2263
2264        $data = (object)$data;
2265        $criteriaid = $this->get_new_parentid('criterion');
2266
2267        // Parameter array that will go to database.
2268        $params = array();
2269        $params['critid'] = $criteriaid;
2270
2271        $oldparam = explode('_', $data->name);
2272
2273        if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2274            $module = $this->get_mappingid('course_module', $oldparam[1]);
2275            $params['name'] = $oldparam[0] . '_' . $module;
2276            $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2277        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2278            $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2279            $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2280        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2281            $role = $this->get_mappingid('role', $data->value);
2282            if (!empty($role)) {
2283                $params['name'] = 'role_' . $role;
2284                $params['value'] = $role;
2285            } else {
2286                return;
2287            }
2288        }
2289
2290        if (!$DB->record_exists('badge_criteria_param', $params)) {
2291            $DB->insert_record('badge_criteria_param', $params);
2292        }
2293    }
2294
2295    public function process_manual_award($data) {
2296        global $DB;
2297
2298        $data = (object)$data;
2299        $role = $this->get_mappingid('role', $data->issuerrole);
2300
2301        if (!empty($role)) {
2302            $award = array(
2303                'badgeid'     => $this->get_new_parentid('badge'),
2304                'recipientid' => $this->get_mappingid('user', $data->recipientid),
2305                'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2306                'issuerrole'  => $role,
2307                'datemet'     => $this->apply_date_offset($data->datemet)
2308            );
2309
2310            // Skip the manual award if recipient or issuer can not be mapped to.
2311            if (empty($award['recipientid']) || empty($award['issuerid'])) {
2312                return;
2313            }
2314
2315            $DB->insert_record('badge_manual_award', $award);
2316        }
2317    }
2318
2319    protected function after_execute() {
2320        // Add related files.
2321        $this->add_related_files('badges', 'badgeimage', 'badge');
2322    }
2323}
2324
2325/**
2326 * This structure steps restores the calendar events
2327 */
2328class restore_calendarevents_structure_step extends restore_structure_step {
2329
2330    protected function define_structure() {
2331
2332        $paths = array();
2333
2334        $paths[] = new restore_path_element('calendarevents', '/events/event');
2335
2336        return $paths;
2337    }
2338
2339    public function process_calendarevents($data) {
2340        global $DB, $SITE, $USER;
2341
2342        $data = (object)$data;
2343        $oldid = $data->id;
2344        $restorefiles = true; // We'll restore the files
2345        // Find the userid and the groupid associated with the event.
2346        $data->userid = $this->get_mappingid('user', $data->userid);
2347        if ($data->userid === false) {
2348            // Blank user ID means that we are dealing with module generated events such as quiz starting times.
2349            // Use the current user ID for these events.
2350            $data->userid = $USER->id;
2351        }
2352        if (!empty($data->groupid)) {
2353            $data->groupid = $this->get_mappingid('group', $data->groupid);
2354            if ($data->groupid === false) {
2355                return;
2356            }
2357        }
2358        // Handle events with empty eventtype //MDL-32827
2359        if(empty($data->eventtype)) {
2360            if ($data->courseid == $SITE->id) {                                // Site event
2361                $data->eventtype = "site";
2362            } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
2363                // Course assingment event
2364                $data->eventtype = "due";
2365            } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
2366                $data->eventtype = "course";
2367            } else if ($data->groupid) {                                      // Group event
2368                $data->eventtype = "group";
2369            } else if ($data->userid) {                                       // User event
2370                $data->eventtype = "user";
2371            } else {
2372                return;
2373            }
2374        }
2375
2376        $params = array(
2377                'name'           => $data->name,
2378                'description'    => $data->description,
2379                'format'         => $data->format,
2380                'courseid'       => $this->get_courseid(),
2381                'groupid'        => $data->groupid,
2382                'userid'         => $data->userid,
2383                'repeatid'       => $data->repeatid,
2384                'modulename'     => $data->modulename,
2385                'eventtype'      => $data->eventtype,
2386                'timestart'      => $this->apply_date_offset($data->timestart),
2387                'timeduration'   => $data->timeduration,
2388                'visible'        => $data->visible,
2389                'uuid'           => $data->uuid,
2390                'sequence'       => $data->sequence,
2391                'timemodified'    => $this->apply_date_offset($data->timemodified));
2392        if ($this->name == 'activity_calendar') {
2393            $params['instance'] = $this->task->get_activityid();
2394        } else {
2395            $params['instance'] = 0;
2396        }
2397        $sql = "SELECT id
2398                  FROM {event}
2399                 WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
2400                   AND courseid = ?
2401                   AND repeatid = ?
2402                   AND modulename = ?
2403                   AND timestart = ?
2404                   AND timeduration = ?
2405                   AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
2406        $arg = array ($params['name'], $params['courseid'], $params['repeatid'], $params['modulename'], $params['timestart'], $params['timeduration'], $params['description']);
2407        $result = $DB->record_exists_sql($sql, $arg);
2408        if (empty($result)) {
2409            $newitemid = $DB->insert_record('event', $params);
2410            $this->set_mapping('event', $oldid, $newitemid);
2411            $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
2412        }
2413
2414    }
2415    protected function after_execute() {
2416        // Add related files
2417        $this->add_related_files('calendar', 'event_description', 'event_description');
2418    }
2419}
2420
2421class restore_course_completion_structure_step extends restore_structure_step {
2422
2423    /**
2424     * Conditionally decide if this step should be executed.
2425     *
2426     * This function checks parameters that are not immediate settings to ensure
2427     * that the enviroment is suitable for the restore of course completion info.
2428     *
2429     * This function checks the following four parameters:
2430     *
2431     *   1. Course completion is enabled on the site
2432     *   2. The backup includes course completion information
2433     *   3. All modules are restorable
2434     *   4. All modules are marked for restore.
2435     *
2436     * @return bool True is safe to execute, false otherwise
2437     */
2438    protected function execute_condition() {
2439        global $CFG;
2440
2441        // First check course completion is enabled on this site
2442        if (empty($CFG->enablecompletion)) {
2443            // Disabled, don't restore course completion
2444            return false;
2445        }
2446
2447        // Check it is included in the backup
2448        $fullpath = $this->task->get_taskbasepath();
2449        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2450        if (!file_exists($fullpath)) {
2451            // Not found, can't restore course completion
2452            return false;
2453        }
2454
2455        // Check we are able to restore all backed up modules
2456        if ($this->task->is_missing_modules()) {
2457            return false;
2458        }
2459
2460        // Finally check all modules within the backup are being restored.
2461        if ($this->task->is_excluding_activities()) {
2462            return false;
2463        }
2464
2465        return true;
2466    }
2467
2468    /**
2469     * Define the course completion structure
2470     *
2471     * @return array Array of restore_path_element
2472     */
2473    protected function define_structure() {
2474
2475        // To know if we are including user completion info
2476        $userinfo = $this->get_setting_value('userscompletion');
2477
2478        $paths = array();
2479        $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
2480        $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
2481
2482        if ($userinfo) {
2483            $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
2484            $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
2485        }
2486
2487        return $paths;
2488
2489    }
2490
2491    /**
2492     * Process course completion criteria
2493     *
2494     * @global moodle_database $DB
2495     * @param stdClass $data
2496     */
2497    public function process_course_completion_criteria($data) {
2498        global $DB;
2499
2500        $data = (object)$data;
2501        $data->course = $this->get_courseid();
2502
2503        // Apply the date offset to the time end field
2504        $data->timeend = $this->apply_date_offset($data->timeend);
2505
2506        // Map the role from the criteria
2507        if (!empty($data->role)) {
2508            $data->role = $this->get_mappingid('role', $data->role);
2509        }
2510
2511        $skipcriteria = false;
2512
2513        // If the completion criteria is for a module we need to map the module instance
2514        // to the new module id.
2515        if (!empty($data->moduleinstance) && !empty($data->module)) {
2516            $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
2517            if (empty($data->moduleinstance)) {
2518                $skipcriteria = true;
2519            }
2520        } else {
2521            $data->module = null;
2522            $data->moduleinstance = null;
2523        }
2524
2525        // We backup the course shortname rather than the ID so that we can match back to the course
2526        if (!empty($data->courseinstanceshortname)) {
2527            $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
2528            if (!$courseinstanceid) {
2529                $skipcriteria = true;
2530            }
2531        } else {
2532            $courseinstanceid = null;
2533        }
2534        $data->courseinstance = $courseinstanceid;
2535
2536        if (!$skipcriteria) {
2537            $params = array(
2538                'course'         => $data->course,
2539                'criteriatype'   => $data->criteriatype,
2540                'enrolperiod'    => $data->enrolperiod,
2541                'courseinstance' => $data->courseinstance,
2542                'module'         => $data->module,
2543                'moduleinstance' => $data->moduleinstance,
2544                'timeend'        => $data->timeend,
2545                'gradepass'      => $data->gradepass,
2546                'role'           => $data->role
2547            );
2548            $newid = $DB->insert_record('course_completion_criteria', $params);
2549            $this->set_mapping('course_completion_criteria', $data->id, $newid);
2550        }
2551    }
2552
2553    /**
2554     * Processes course compltion criteria complete records
2555     *
2556     * @global moodle_database $DB
2557     * @param stdClass $data
2558     */
2559    public function process_course_completion_crit_compl($data) {
2560        global $DB;
2561
2562        $data = (object)$data;
2563
2564        // This may be empty if criteria could not be restored
2565        $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
2566
2567        $data->course = $this->get_courseid();
2568        $data->userid = $this->get_mappingid('user', $data->userid);
2569
2570        if (!empty($data->criteriaid) && !empty($data->userid)) {
2571            $params = array(
2572                'userid' => $data->userid,
2573                'course' => $data->course,
2574                'criteriaid' => $data->criteriaid,
2575                'timecompleted' => $this->apply_date_offset($data->timecompleted)
2576            );
2577            if (isset($data->gradefinal)) {
2578                $params['gradefinal'] = $data->gradefinal;
2579            }
2580            if (isset($data->unenroled)) {
2581                $params['unenroled'] = $data->unenroled;
2582            }
2583            $DB->insert_record('course_completion_crit_compl', $params);
2584        }
2585    }
2586
2587    /**
2588     * Process course completions
2589     *
2590     * @global moodle_database $DB
2591     * @param stdClass $data
2592     */
2593    public function process_course_completions($data) {
2594        global $DB;
2595
2596        $data = (object)$data;
2597
2598        $data->course = $this->get_courseid();
2599        $data->userid = $this->get_mappingid('user', $data->userid);
2600
2601        if (!empty($data->userid)) {
2602            $params = array(
2603                'userid' => $data->userid,
2604                'course' => $data->course,
2605                'timeenrolled' => $this->apply_date_offset($data->timeenrolled),
2606                'timestarted' => $this->apply_date_offset($data->timestarted),
2607                'timecompleted' => $this->apply_date_offset($data->timecompleted),
2608                'reaggregate' => $data->reaggregate
2609            );
2610            $DB->insert_record('course_completions', $params);
2611        }
2612    }
2613
2614    /**
2615     * Process course completion aggregate methods
2616     *
2617     * @global moodle_database $DB
2618     * @param stdClass $data
2619     */
2620    public function process_course_completion_aggr_methd($data) {
2621        global $DB;
2622
2623        $data = (object)$data;
2624
2625        $data->course = $this->get_courseid();
2626
2627        // Only create the course_completion_aggr_methd records if
2628        // the target course has not them defined. MDL-28180
2629        if (!$DB->record_exists('course_completion_aggr_methd', array(
2630                    'course' => $data->course,
2631                    'criteriatype' => $data->criteriatype))) {
2632            $params = array(
2633                'course' => $data->course,
2634                'criteriatype' => $data->criteriatype,
2635                'method' => $data->method,
2636                'value' => $data->value,
2637            );
2638            $DB->insert_record('course_completion_aggr_methd', $params);
2639        }
2640    }
2641}
2642
2643
2644/**
2645 * This structure step restores course logs (cmid = 0), delegating
2646 * the hard work to the corresponding {@link restore_logs_processor} passing the
2647 * collection of {@link restore_log_rule} rules to be observed as they are defined
2648 * by the task. Note this is only executed based in the 'logs' setting.
2649 *
2650 * NOTE: This is executed by final task, to have all the activities already restored
2651 *
2652 * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
2653 * records are. There are others like 'calendar' and 'upload' that will be handled
2654 * later.
2655 *
2656 * NOTE: All the missing actions (not able to be restored) are sent to logs for
2657 * debugging purposes
2658 */
2659class restore_course_logs_structure_step extends restore_structure_step {
2660
2661    /**
2662     * Conditionally decide if this step should be executed.
2663     *
2664     * This function checks the following parameter:
2665     *
2666     *   1. the course/logs.xml file exists
2667     *
2668     * @return bool true is safe to execute, false otherwise
2669     */
2670    protected function execute_condition() {
2671
2672        // Check it is included in the backup
2673        $fullpath = $this->task->get_taskbasepath();
2674        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2675        if (!file_exists($fullpath)) {
2676            // Not found, can't restore course logs
2677            return false;
2678        }
2679
2680        return true;
2681    }
2682
2683    protected function define_structure() {
2684
2685        $paths = array();
2686
2687        // Simple, one plain level of information contains them
2688        $paths[] = new restore_path_element('log', '/logs/log');
2689
2690        return $paths;
2691    }
2692
2693    protected function process_log($data) {
2694        global $DB;
2695
2696        $data = (object)($data);
2697
2698        $data->time = $this->apply_date_offset($data->time);
2699        $data->userid = $this->get_mappingid('user', $data->userid);
2700        $data->course = $this->get_courseid();
2701        $data->cmid = 0;
2702
2703        // For any reason user wasn't remapped ok, stop processing this
2704        if (empty($data->userid)) {
2705            return;
2706        }
2707
2708        // Everything ready, let's delegate to the restore_logs_processor
2709
2710        // Set some fixed values that will save tons of DB requests
2711        $values = array(
2712            'course' => $this->get_courseid());
2713        // Get instance and process log record
2714        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2715
2716        // If we have data, insert it, else something went wrong in the restore_logs_processor
2717        if ($data) {
2718            if (empty($data->url)) {
2719                $data->url = '';
2720            }
2721            if (empty($data->info)) {
2722                $data->info = '';
2723            }
2724            // Store the data in the legacy log table if we are still using it.
2725            $manager = get_log_manager();
2726            if (method_exists($manager, 'legacy_add_to_log')) {
2727                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2728                    $data->info, $data->cmid, $data->userid);
2729            }
2730        }
2731    }
2732}
2733
2734/**
2735 * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
2736 * sharing its same structure but modifying the way records are handled
2737 */
2738class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
2739
2740    protected function process_log($data) {
2741        global $DB;
2742
2743        $data = (object)($data);
2744
2745        $data->time = $this->apply_date_offset($data->time);
2746        $data->userid = $this->get_mappingid('user', $data->userid);
2747        $data->course = $this->get_courseid();
2748        $data->cmid = $this->task->get_moduleid();
2749
2750        // For any reason user wasn't remapped ok, stop processing this
2751        if (empty($data->userid)) {
2752            return;
2753        }
2754
2755        // Everything ready, let's delegate to the restore_logs_processor
2756
2757        // Set some fixed values that will save tons of DB requests
2758        $values = array(
2759            'course' => $this->get_courseid(),
2760            'course_module' => $this->task->get_moduleid(),
2761            $this->task->get_modulename() => $this->task->get_activityid());
2762        // Get instance and process log record
2763        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
2764
2765        // If we have data, insert it, else something went wrong in the restore_logs_processor
2766        if ($data) {
2767            if (empty($data->url)) {
2768                $data->url = '';
2769            }
2770            if (empty($data->info)) {
2771                $data->info = '';
2772            }
2773            // Store the data in the legacy log table if we are still using it.
2774            $manager = get_log_manager();
2775            if (method_exists($manager, 'legacy_add_to_log')) {
2776                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
2777                    $data->info, $data->cmid, $data->userid);
2778            }
2779        }
2780    }
2781}
2782
2783
2784/**
2785 * Defines the restore step for advanced grading methods attached to the activity module
2786 */
2787class restore_activity_grading_structure_step extends restore_structure_step {
2788
2789    /**
2790     * This step is executed only if the grading file is present
2791     */
2792     protected function execute_condition() {
2793
2794        $fullpath = $this->task->get_taskbasepath();
2795        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2796        if (!file_exists($fullpath)) {
2797            return false;
2798        }
2799
2800        return true;
2801    }
2802
2803
2804    /**
2805     * Declares paths in the grading.xml file we are interested in
2806     */
2807    protected function define_structure() {
2808
2809        $paths = array();
2810        $userinfo = $this->get_setting_value('userinfo');
2811
2812        $area = new restore_path_element('grading_area', '/areas/area');
2813        $paths[] = $area;
2814        // attach local plugin stucture to $area element
2815        $this->add_plugin_structure('local', $area);
2816
2817        $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
2818        $paths[] = $definition;
2819        $this->add_plugin_structure('gradingform', $definition);
2820        // attach local plugin stucture to $definition element
2821        $this->add_plugin_structure('local', $definition);
2822
2823
2824        if ($userinfo) {
2825            $instance = new restore_path_element('grading_instance',
2826                '/areas/area/definitions/definition/instances/instance');
2827            $paths[] = $instance;
2828            $this->add_plugin_structure('gradingform', $instance);
2829            // attach local plugin stucture to $intance element
2830            $this->add_plugin_structure('local', $instance);
2831        }
2832
2833        return $paths;
2834    }
2835
2836    /**
2837     * Processes one grading area element
2838     *
2839     * @param array $data element data
2840     */
2841    protected function process_grading_area($data) {
2842        global $DB;
2843
2844        $task = $this->get_task();
2845        $data = (object)$data;
2846        $oldid = $data->id;
2847        $data->component = 'mod_'.$task->get_modulename();
2848        $data->contextid = $task->get_contextid();
2849
2850        $newid = $DB->insert_record('grading_areas', $data);
2851        $this->set_mapping('grading_area', $oldid, $newid);
2852    }
2853
2854    /**
2855     * Processes one grading definition element
2856     *
2857     * @param array $data element data
2858     */
2859    protected function process_grading_definition($data) {
2860        global $DB;
2861
2862        $task = $this->get_task();
2863        $data = (object)$data;
2864        $oldid = $data->id;
2865        $data->areaid = $this->get_new_parentid('grading_area');
2866        $data->copiedfromid = null;
2867        $data->timecreated = time();
2868        $data->usercreated = $task->get_userid();
2869        $data->timemodified = $data->timecreated;
2870        $data->usermodified = $data->usercreated;
2871
2872        $newid = $DB->insert_record('grading_definitions', $data);
2873        $this->set_mapping('grading_definition', $oldid, $newid, true);
2874    }
2875
2876    /**
2877     * Processes one grading form instance element
2878     *
2879     * @param array $data element data
2880     */
2881    protected function process_grading_instance($data) {
2882        global $DB;
2883
2884        $data = (object)$data;
2885
2886        // new form definition id
2887        $newformid = $this->get_new_parentid('grading_definition');
2888
2889        // get the name of the area we are restoring to
2890        $sql = "SELECT ga.areaname
2891                  FROM {grading_definitions} gd
2892                  JOIN {grading_areas} ga ON gd.areaid = ga.id
2893                 WHERE gd.id = ?";
2894        $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
2895
2896        // get the mapped itemid - the activity module is expected to define the mappings
2897        // for each gradable area
2898        $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
2899
2900        $oldid = $data->id;
2901        $data->definitionid = $newformid;
2902        $data->raterid = $this->get_mappingid('user', $data->raterid);
2903        $data->itemid = $newitemid;
2904
2905        $newid = $DB->insert_record('grading_instances', $data);
2906        $this->set_mapping('grading_instance', $oldid, $newid);
2907    }
2908
2909    /**
2910     * Final operations when the database records are inserted
2911     */
2912    protected function after_execute() {
2913        // Add files embedded into the definition description
2914        $this->add_related_files('grading', 'description', 'grading_definition');
2915    }
2916}
2917
2918
2919/**
2920 * This structure step restores the grade items associated with one activity
2921 * All the grade items are made child of the "course" grade item but the original
2922 * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
2923 * the complete gradebook (categories and calculations), that information is
2924 * available there
2925 */
2926class restore_activity_grades_structure_step extends restore_structure_step {
2927
2928    protected function define_structure() {
2929
2930        $paths = array();
2931        $userinfo = $this->get_setting_value('userinfo');
2932
2933        $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
2934        $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
2935        if ($userinfo) {
2936            $paths[] = new restore_path_element('grade_grade',
2937                           '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
2938        }
2939        return $paths;
2940    }
2941
2942    protected function process_grade_item($data) {
2943        global $DB;
2944
2945        $data = (object)($data);
2946        $oldid       = $data->id;        // We'll need these later
2947        $oldparentid = $data->categoryid;
2948        $courseid = $this->get_courseid();
2949
2950        // make sure top course category exists, all grade items will be associated
2951        // to it. Later, if restoring the whole gradebook, categories will be introduced
2952        $coursecat = grade_category::fetch_course_category($courseid);
2953        $coursecatid = $coursecat->id; // Get the categoryid to be used
2954
2955        $idnumber = null;
2956        if (!empty($data->idnumber)) {
2957            // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
2958            // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
2959            // so the best is to keep the ones already in the gradebook
2960            // Potential problem: duplicates if same items are restored more than once. :-(
2961            // This needs to be fixed in some way (outcomes & activities with multiple items)
2962            // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
2963            // In any case, verify always for uniqueness
2964            $sql = "SELECT cm.id
2965                      FROM {course_modules} cm
2966                     WHERE cm.course = :courseid AND
2967                           cm.idnumber = :idnumber AND
2968                           cm.id <> :cmid";
2969            $params = array(
2970                'courseid' => $courseid,
2971                'idnumber' => $data->idnumber,
2972                'cmid' => $this->task->get_moduleid()
2973            );
2974            if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
2975                $idnumber = $data->idnumber;
2976            }
2977        }
2978
2979        unset($data->id);
2980        $data->categoryid   = $coursecatid;
2981        $data->courseid     = $this->get_courseid();
2982        $data->iteminstance = $this->task->get_activityid();
2983        $data->idnumber     = $idnumber;
2984        $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
2985        $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
2986        $data->timecreated  = $this->apply_date_offset($data->timecreated);
2987        $data->timemodified = $this->apply_date_offset($data->timemodified);
2988
2989        $gradeitem = new grade_item($data, false);
2990        $gradeitem->insert('restore');
2991
2992        //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
2993        $gradeitem->sortorder = $data->sortorder;
2994        $gradeitem->update('restore');
2995
2996        // Set mapping, saving the original category id into parentitemid
2997        // gradebook restore (final task) will need it to reorganise items
2998        $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
2999    }
3000
3001    protected function process_grade_grade($data) {
3002        $data = (object)($data);
3003        $olduserid = $data->userid;
3004        $oldid = $data->id;
3005        unset($data->id);
3006
3007        $data->itemid = $this->get_new_parentid('grade_item');
3008
3009        $data->userid = $this->get_mappingid('user', $data->userid, null);
3010        if (!empty($data->userid)) {
3011            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3012            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3013            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
3014            $data->overridden = $this->apply_date_offset($data->overridden);
3015
3016            $grade = new grade_grade($data, false);
3017            $grade->insert('restore');
3018            $this->set_mapping('grade_grades', $oldid, $grade->id);
3019        } else {
3020            debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
3021        }
3022    }
3023
3024    /**
3025     * process activity grade_letters. Note that, while these are possible,
3026     * because grade_letters are contextid based, in practice, only course
3027     * context letters can be defined. So we keep here this method knowing
3028     * it won't be executed ever. gradebook restore will restore course letters.
3029     */
3030    protected function process_grade_letter($data) {
3031        global $DB;
3032
3033        $data['contextid'] = $this->task->get_contextid();
3034        $gradeletter = (object)$data;
3035
3036        // Check if it exists before adding it
3037        unset($data['id']);
3038        if (!$DB->record_exists('grade_letters', $data)) {
3039            $newitemid = $DB->insert_record('grade_letters', $gradeletter);
3040        }
3041        // no need to save any grade_letter mapping
3042    }
3043
3044    public function after_restore() {
3045        // Fix grade item's sortorder after restore, as it might have duplicates.
3046        $courseid = $this->get_task()->get_courseid();
3047        grade_item::fix_duplicate_sortorder($courseid);
3048    }
3049}
3050
3051/**
3052 * Step in charge of restoring the grade history of an activity.
3053 *
3054 * This step is added to the task regardless of the setting 'grade_histories'.
3055 * The reason is to allow for a more flexible step in case the logic needs to be
3056 * split accross different settings to control the history of items and/or grades.
3057 */
3058class restore_activity_grade_history_structure_step extends restore_structure_step {
3059
3060    /**
3061     * This step is executed only if the grade history file is present.
3062     */
3063     protected function execute_condition() {
3064        $fullpath = $this->task->get_taskbasepath();
3065        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3066        if (!file_exists($fullpath)) {
3067            return false;
3068        }
3069        return true;
3070    }
3071
3072    protected function define_structure() {
3073        $paths = array();
3074
3075        // Settings to use.
3076        $userinfo = $this->get_setting_value('userinfo');
3077        $history = $this->get_setting_value('grade_histories');
3078
3079        if ($userinfo && $history) {
3080            $paths[] = new restore_path_element('grade_grade',
3081               '/grade_history/grade_grades/grade_grade');
3082        }
3083
3084        return $paths;
3085    }
3086
3087    protected function process_grade_grade($data) {
3088        global $DB;
3089
3090        $data = (object) $data;
3091        $olduserid = $data->userid;
3092        unset($data->id);
3093
3094        $data->userid = $this->get_mappingid('user', $data->userid, null);
3095        if (!empty($data->userid)) {
3096            // Do not apply the date offsets as this is history.
3097            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
3098            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
3099            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
3100            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
3101            $DB->insert_record('grade_grades_history', $data);
3102        } else {
3103            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
3104            $this->log($message, backup::LOG_DEBUG);
3105        }
3106    }
3107
3108}
3109
3110/**
3111 * This structure steps restores one instance + positions of one block
3112 * Note: Positions corresponding to one existing context are restored
3113 * here, but all the ones having unknown contexts are sent to backup_ids
3114 * for a later chance to be restored at the end (final task)
3115 */
3116class restore_block_instance_structure_step extends restore_structure_step {
3117
3118    protected function define_structure() {
3119
3120        $paths = array();
3121
3122        $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
3123        $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
3124
3125        return $paths;
3126    }
3127
3128    public function process_block($data) {
3129        global $DB, $CFG;
3130
3131        $data = (object)$data; // Handy
3132        $oldcontextid = $data->contextid;
3133        $oldid        = $data->id;
3134        $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
3135
3136        // Look for the parent contextid
3137        if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
3138            throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
3139        }
3140
3141        // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
3142        // If there is already one block of that type in the parent context
3143        // and the block is not multiple, stop processing
3144        // Use blockslib loader / method executor
3145        if (!$bi = block_instance($data->blockname)) {
3146            return false;
3147        }
3148
3149        if (!$bi->instance_allow_multiple()) {
3150            // The block cannot be added twice, so we will check if the same block is already being
3151            // displayed on the same page. For this, rather than mocking a page and using the block_manager
3152            // we use a similar query to the one in block_manager::load_blocks(), this will give us
3153            // a very good idea of the blocks already displayed in the context.
3154            $params =  array(
3155                'blockname' => $data->blockname
3156            );
3157
3158            // Context matching test.
3159            $context = context::instance_by_id($data->parentcontextid);
3160            $contextsql = 'bi.parentcontextid = :contextid';
3161            $params['contextid'] = $context->id;
3162
3163            $parentcontextids = $context->get_parent_context_ids();
3164            if ($parentcontextids) {
3165                list($parentcontextsql, $parentcontextparams) =
3166                        $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
3167                $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
3168                $params = array_merge($params, $parentcontextparams);
3169            }
3170
3171            // Page type pattern test.
3172            $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
3173            list($pagetypepatternsql, $pagetypepatternparams) =
3174                $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
3175            $params = array_merge($params, $pagetypepatternparams);
3176
3177            // Sub page pattern test.
3178            $subpagepatternsql = 'bi.subpagepattern IS NULL';
3179            if ($data->subpagepattern !== null) {
3180                $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
3181                $params['subpagepattern'] = $data->subpagepattern;
3182            }
3183
3184            $exists = $DB->record_exists_sql("SELECT bi.id
3185                                                FROM {block_instances} bi
3186                                                JOIN {block} b ON b.name = bi.blockname
3187                                               WHERE bi.blockname = :blockname
3188                                                 AND $contextsql
3189                                                 AND bi.pagetypepattern $pagetypepatternsql
3190                                                 AND $subpagepatternsql", $params);
3191            if ($exists) {
3192                // There is at least one very similar block visible on the page where we
3193                // are trying to restore the block. In these circumstances the block API
3194                // would not allow the user to add another instance of the block, so we
3195                // apply the same rule here.
3196                return false;
3197            }
3198        }
3199
3200        // If there is already one block of that type in the parent context
3201        // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
3202        // stop processing
3203        $params = array(
3204            'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
3205            'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
3206            'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
3207        if ($birecs = $DB->get_records('block_instances', $params)) {
3208            foreach($birecs as $birec) {
3209                if ($birec->configdata == $data->configdata) {
3210                    return false;
3211                }
3212            }
3213        }
3214
3215        // Set task old contextid, blockid and blockname once we know them
3216        $this->task->set_old_contextid($oldcontextid);
3217        $this->task->set_old_blockid($oldid);
3218        $this->task->set_blockname($data->blockname);
3219
3220        // Let's look for anything within configdata neededing processing
3221        // (nulls and uses of legacy file.php)
3222        if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
3223            $configdata = (array)unserialize(base64_decode($data->configdata));
3224            foreach ($configdata as $attribute => $value) {
3225                if (in_array($attribute, $attrstotransform)) {
3226                    $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
3227                }
3228            }
3229            $data->configdata = base64_encode(serialize((object)$configdata));
3230        }
3231
3232        // Create the block instance
3233        $newitemid = $DB->insert_record('block_instances', $data);
3234        // Save the mapping (with restorefiles support)
3235        $this->set_mapping('block_instance', $oldid, $newitemid, true);
3236        // Create the block context
3237        $newcontextid = context_block::instance($newitemid)->id;
3238        // Save the block contexts mapping and sent it to task
3239        $this->set_mapping('context', $oldcontextid, $newcontextid);
3240        $this->task->set_contextid($newcontextid);
3241        $this->task->set_blockid($newitemid);
3242
3243        // Restore block fileareas if declared
3244        $component = 'block_' . $this->task->get_blockname();
3245        foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
3246            $this->add_related_files($component, $filearea, null);
3247        }
3248
3249        // Process block positions, creating them or accumulating for final step
3250        foreach($positions as $position) {
3251            $position = (object)$position;
3252            $position->blockinstanceid = $newitemid; // The instance is always the restored one
3253            // If position is for one already mapped (known) contextid
3254            // process it now, creating the position
3255            if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
3256                $position->contextid = $newpositionctxid;
3257                // Create the block position
3258                $DB->insert_record('block_positions', $position);
3259
3260            // The position belongs to an unknown context, send it to backup_ids
3261            // to process them as part of the final steps of restore. We send the
3262            // whole $position object there, hence use the low level method.
3263            } else {
3264                restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
3265            }
3266        }
3267    }
3268}
3269
3270/**
3271 * Structure step to restore common course_module information
3272 *
3273 * This step will process the module.xml file for one activity, in order to restore
3274 * the corresponding information to the course_modules table, skipping various bits
3275 * of information based on CFG settings (groupings, completion...) in order to fullfill
3276 * all the reqs to be able to create the context to be used by all the rest of steps
3277 * in the activity restore task
3278 */
3279class restore_module_structure_step extends restore_structure_step {
3280
3281    protected function define_structure() {
3282        global $CFG;
3283
3284        $paths = array();
3285
3286        $module = new restore_path_element('module', '/module');
3287        $paths[] = $module;
3288        if ($CFG->enableavailability) {
3289            $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
3290            $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
3291        }
3292
3293        // Apply for 'format' plugins optional paths at module level
3294        $this->add_plugin_structure('format', $module);
3295
3296        // Apply for 'plagiarism' plugins optional paths at module level
3297        $this->add_plugin_structure('plagiarism', $module);
3298
3299        // Apply for 'local' plugins optional paths at module level
3300        $this->add_plugin_structure('local', $module);
3301
3302        return $paths;
3303    }
3304
3305    protected function process_module($data) {
3306        global $CFG, $DB;
3307
3308        $data = (object)$data;
3309        $oldid = $data->id;
3310        $this->task->set_old_moduleversion($data->version);
3311
3312        $data->course = $this->task->get_courseid();
3313        $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
3314        // Map section (first try by course_section mapping match. Useful in course and section restores)
3315        $data->section = $this->get_mappingid('course_section', $data->sectionid);
3316        if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
3317            $params = array(
3318                'course' => $this->get_courseid(),
3319                'section' => $data->sectionnumber);
3320            $data->section = $DB->get_field('course_sections', 'id', $params);
3321        }
3322        if (!$data->section) { // sectionnumber failed, try to get first section in course
3323            $params = array(
3324                'course' => $this->get_courseid());
3325            $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
3326        }
3327        if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
3328            $sectionrec = array(
3329                'course' => $this->get_courseid(),
3330                'section' => 0);
3331            $DB->insert_record('course_sections', $sectionrec); // section 0
3332            $sectionrec = array(
3333                'course' => $this->get_courseid(),
3334                'section' => 1);
3335            $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
3336        }
3337        $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
3338        if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
3339            $data->idnumber = '';
3340        }
3341        if (empty($CFG->enablecompletion)) { // completion
3342            $data->completion = 0;
3343            $data->completiongradeitemnumber = null;
3344            $data->completionview = 0;
3345            $data->completionexpected = 0;
3346        } else {
3347            $data->completionexpected = $this->apply_date_offset($data->completionexpected);
3348        }
3349        if (empty($CFG->enableavailability)) {
3350            $data->availability = null;
3351        }
3352        // Backups that did not include showdescription, set it to default 0
3353        // (this is not totally necessary as it has a db default, but just to
3354        // be explicit).
3355        if (!isset($data->showdescription)) {
3356            $data->showdescription = 0;
3357        }
3358        $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
3359
3360        if (empty($data->availability)) {
3361            // If there are legacy availablility data fields (and no new format data),
3362            // convert the old fields.
3363            $data->availability = \core_availability\info::convert_legacy_fields(
3364                    $data, false);
3365        } else if (!empty($data->groupmembersonly)) {
3366            // There is current availability data, but it still has groupmembersonly
3367            // as well (2.7 backups), convert just that part.
3368            require_once($CFG->dirroot . '/lib/db/upgradelib.php');
3369            $data->availability = upgrade_group_members_only($data->groupingid, $data->availability);
3370        }
3371
3372        // course_module record ready, insert it
3373        $newitemid = $DB->insert_record('course_modules', $data);
3374        // save mapping
3375        $this->set_mapping('course_module', $oldid, $newitemid);
3376        // set the new course_module id in the task
3377        $this->task->set_moduleid($newitemid);
3378        // we can now create the context safely
3379        $ctxid = context_module::instance($newitemid)->id;
3380        // set the new context id in the task
3381        $this->task->set_contextid($ctxid);
3382        // update sequence field in course_section
3383        if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
3384            $sequence .= ',' . $newitemid;
3385        } else {
3386            $sequence = $newitemid;
3387        }
3388        $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
3389
3390        // If there is the legacy showavailability data, store this for later use.
3391        // (This data is not present when restoring 'new' backups.)
3392        if (isset($data->showavailability)) {
3393            // Cache the showavailability flag using the backup_ids data field.
3394            restore_dbops::set_backup_ids_record($this->get_restoreid(),
3395                    'module_showavailability', $newitemid, 0, null,
3396                    (object)array('showavailability' => $data->showavailability));
3397        }
3398    }
3399
3400    /**
3401     * Process the legacy availability table record. This table does not exist
3402     * in Moodle 2.7+ but we still support restore.
3403     *
3404     * @param stdClass $data Record data
3405     */
3406    protected function process_availability($data) {
3407        $data = (object)$data;
3408        // Simply going to store the whole availability record now, we'll process
3409        // all them later in the final task (once all activities have been restored)
3410        // Let's call the low level one to be able to store the whole object
3411        $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
3412        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
3413    }
3414
3415    /**
3416     * Process the legacy availability fields table record. This table does not
3417     * exist in Moodle 2.7+ but we still support restore.
3418     *
3419     * @param stdClass $data Record data
3420     */
3421    protected function process_availability_field($data) {
3422        global $DB;
3423        $data = (object)$data;
3424        // Mark it is as passed by default
3425        $passed = true;
3426        $customfieldid = null;
3427
3428        // If a customfield has been used in order to pass we must be able to match an existing
3429        // customfield by name (data->customfield) and type (data->customfieldtype)
3430        if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
3431            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
3432            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
3433            $passed = false;
3434        } else if (!empty($data->customfield)) {
3435            $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
3436            $customfieldid = $DB->get_field('user_info_field', 'id', $params);
3437            $passed = ($customfieldid !== false);
3438        }
3439
3440        if ($passed) {
3441            // Create the object to insert into the database
3442            $availfield = new stdClass();
3443            $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
3444            $availfield->userfield = $data->userfield;
3445            $availfield->customfieldid = $customfieldid;
3446            $availfield->operator = $data->operator;
3447            $availfield->value = $data->value;
3448
3449            // Get showavailability option.
3450            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
3451                    'module_showavailability', $availfield->coursemoduleid);
3452            if (!$showrec) {
3453                // Should not happen.
3454                throw new coding_exception('No matching showavailability record');
3455            }
3456            $show = $showrec->info->showavailability;
3457
3458            // The $availfieldobject is now in the format used in the old
3459            // system. Interpret this and convert to new system.
3460            $currentvalue = $DB->get_field('course_modules', 'availability',
3461                    array('id' => $availfield->coursemoduleid), MUST_EXIST);
3462            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
3463                    $currentvalue, $availfield, $show);
3464            $DB->set_field('course_modules', 'availability', $newvalue,
3465                    array('id' => $availfield->coursemoduleid));
3466        }
3467    }
3468}
3469
3470/**
3471 * Structure step that will process the user activity completion
3472 * information if all these conditions are met:
3473 *  - Target site has completion enabled ($CFG->enablecompletion)
3474 *  - Activity includes completion info (file_exists)
3475 */
3476class restore_userscompletion_structure_step extends restore_structure_step {
3477    /**
3478     * To conditionally decide if this step must be executed
3479     * Note the "settings" conditions are evaluated in the
3480     * corresponding task. Here we check for other conditions
3481     * not being restore settings (files, site settings...)
3482     */
3483     protected function execute_condition() {
3484         global $CFG;
3485
3486         // Completion disabled in this site, don't execute
3487         if (empty($CFG->enablecompletion)) {
3488             return false;
3489         }
3490
3491         // No user completion info found, don't execute
3492        $fullpath = $this->task->get_taskbasepath();
3493        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3494         if (!file_exists($fullpath)) {
3495             return false;
3496         }
3497
3498         // Arrived here, execute the step
3499         return true;
3500     }
3501
3502     protected function define_structure() {
3503
3504        $paths = array();
3505
3506        $paths[] = new restore_path_element('completion', '/completions/completion');
3507
3508        return $paths;
3509    }
3510
3511    protected function process_completion($data) {
3512        global $DB;
3513
3514        $data = (object)$data;
3515
3516        $data->coursemoduleid = $this->task->get_moduleid();
3517        $data->userid = $this->get_mappingid('user', $data->userid);
3518        $data->timemodified = $this->apply_date_offset($data->timemodified);
3519
3520        // Find the existing record
3521        $existing = $DB->get_record('course_modules_completion', array(
3522                'coursemoduleid' => $data->coursemoduleid,
3523                'userid' => $data->userid), 'id, timemodified');
3524        // Check we didn't already insert one for this cmid and userid
3525        // (there aren't supposed to be duplicates in that field, but
3526        // it was possible until MDL-28021 was fixed).
3527        if ($existing) {
3528            // Update it to these new values, but only if the time is newer
3529            if ($existing->timemodified < $data->timemodified) {
3530                $data->id = $existing->id;
3531                $DB->update_record('course_modules_completion', $data);
3532            }
3533        } else {
3534            // Normal entry where it doesn't exist already
3535            $DB->insert_record('course_modules_completion', $data);
3536        }
3537    }
3538}
3539
3540/**
3541 * Abstract structure step, parent of all the activity structure steps. Used to suuport
3542 * the main <activity ...> tag and process it. Also provides subplugin support for
3543 * activities.
3544 */
3545abstract class restore_activity_structure_step extends restore_structure_step {
3546
3547    protected function add_subplugin_structure($subplugintype, $element) {
3548
3549        global $CFG;
3550
3551        // Check the requested subplugintype is a valid one
3552        $subpluginsfile = $CFG->dirroot . '/mod/' . $this->task->get_modulename() . '/db/subplugins.php';
3553        if (!file_exists($subpluginsfile)) {
3554             throw new restore_step_exception('activity_missing_subplugins_php_file', $this->task->get_modulename());
3555        }
3556        include($subpluginsfile);
3557        if (!array_key_exists($subplugintype, $subplugins)) {
3558             throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
3559        }
3560        // Get all the restore path elements, looking across all the subplugin dirs
3561        $subpluginsdirs = core_component::get_plugin_list($subplugintype);
3562        foreach ($subpluginsdirs as $name => $subpluginsdir) {
3563            $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
3564            $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
3565            if (file_exists($restorefile)) {
3566                require_once($restorefile);
3567                $restoresubplugin = new $classname($subplugintype, $name, $this);
3568                // Add subplugin paths to the step
3569                $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
3570            }
3571        }
3572    }
3573
3574    /**
3575     * As far as activity restore steps are implementing restore_subplugin stuff, they need to
3576     * have the parent task available for wrapping purposes (get course/context....)
3577     * @return restore_task
3578     */
3579    public function get_task() {
3580        return $this->task;
3581    }
3582
3583    /**
3584     * Adds support for the 'activity' path that is common to all the activities
3585     * and will be processed globally here
3586     */
3587    protected function prepare_activity_structure($paths) {
3588
3589        $paths[] = new restore_path_element('activity', '/activity');
3590
3591        return $paths;
3592    }
3593
3594    /**
3595     * Process the activity path, informing the task about various ids, needed later
3596     */
3597    protected function process_activity($data) {
3598        $data = (object)$data;
3599        $this->task->set_old_contextid($data->contextid); // Save old contextid in task
3600        $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
3601        $this->task->set_old_activityid($data->id); // Save old activityid in task
3602    }
3603
3604    /**
3605     * This must be invoked immediately after creating the "module" activity record (forum, choice...)
3606     * and will adjust the new activity id (the instance) in various places
3607     */
3608    protected function apply_activity_instance($newitemid) {
3609        global $DB;
3610
3611        $this->task->set_activityid($newitemid); // Save activity id in task
3612        // Apply the id to course_sections->instanceid
3613        $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
3614        // Do the mapping for modulename, preparing it for files by oldcontext
3615        $modulename = $this->task->get_modulename();
3616        $oldid = $this->task->get_old_activityid();
3617        $this->set_mapping($modulename, $oldid, $newitemid, true);
3618    }
3619}
3620
3621/**
3622 * Structure step in charge of creating/mapping all the qcats and qs
3623 * by parsing the questions.xml file and checking it against the
3624 * results calculated by {@link restore_process_categories_and_questions}
3625 * and stored in backup_ids_temp
3626 */
3627class restore_create_categories_and_questions extends restore_structure_step {
3628
3629    /** @var array $cachecategory store a question category */
3630    protected $cachedcategory = null;
3631
3632    protected function define_structure() {
3633
3634        $category = new restore_path_element('question_category', '/question_categories/question_category');
3635        $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
3636        $hint = new restore_path_element('question_hint',
3637                '/question_categories/question_category/questions/question/question_hints/question_hint');
3638
3639        $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
3640
3641        // Apply for 'qtype' plugins optional paths at question level
3642        $this->add_plugin_structure('qtype', $question);
3643
3644        // Apply for 'local' plugins optional paths at question level
3645        $this->add_plugin_structure('local', $question);
3646
3647        return array($category, $question, $hint, $tag);
3648    }
3649
3650    protected function process_question_category($data) {
3651        global $DB;
3652
3653        $data = (object)$data;
3654        $oldid = $data->id;
3655
3656        // Check we have one mapping for this category
3657        if (!$mapping = $this->get_mapping('question_category', $oldid)) {
3658            return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
3659        }
3660
3661        // Check we have to create the category (newitemid = 0)
3662        if ($mapping->newitemid) {
3663            return; // newitemid != 0, this category is going to be mapped. Nothing to do
3664        }
3665
3666        // Arrived here, newitemid = 0, we need to create the category
3667        // we'll do it at parentitemid context, but for CONTEXT_MODULE
3668        // categories, that will be created at CONTEXT_COURSE and moved
3669        // to module context later when the activity is created
3670        if ($mapping->info->contextlevel == CONTEXT_MODULE) {
3671            $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
3672        }
3673        $data->contextid = $mapping->parentitemid;
3674
3675        // Let's create the question_category and save mapping
3676        $newitemid = $DB->insert_record('question_categories', $data);
3677        $this->set_mapping('question_category', $oldid, $newitemid);
3678        // Also annotate them as question_category_created, we need
3679        // that later when remapping parents
3680        $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
3681    }
3682
3683    protected function process_question($data) {
3684        global $DB;
3685
3686        $data = (object)$data;
3687        $oldid = $data->id;
3688
3689        // Check we have one mapping for this question
3690        if (!$questionmapping = $this->get_mapping('question', $oldid)) {
3691            return; // No mapping = this question doesn't need to be created/mapped
3692        }
3693
3694        // Get the mapped category (cannot use get_new_parentid() because not
3695        // all the categories have been created, so it is not always available
3696        // Instead we get the mapping for the question->parentitemid because
3697        // we have loaded qcatids there for all parsed questions
3698        $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
3699
3700        // In the past, there were some very sloppy values of penalty. Fix them.
3701        if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
3702            $data->penalty = 0.3333333;
3703        }
3704        if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
3705            $data->penalty = 0.6666667;
3706        }
3707        if ($data->penalty >= 1) {
3708            $data->penalty = 1;
3709        }
3710
3711        $userid = $this->get_mappingid('user', $data->createdby);
3712        $data->createdby = $userid ? $userid : $this->task->get_userid();
3713
3714        $userid = $this->get_mappingid('user', $data->modifiedby);
3715        $data->modifiedby = $userid ? $userid : $this->task->get_userid();
3716
3717        // With newitemid = 0, let's create the question
3718        if (!$questionmapping->newitemid) {
3719            $newitemid = $DB->insert_record('question', $data);
3720            $this->set_mapping('question', $oldid, $newitemid);
3721            // Also annotate them as question_created, we need
3722            // that later when remapping parents (keeping the old categoryid as parentid)
3723            $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
3724        } else {
3725            // By performing this set_mapping() we make get_old/new_parentid() to work for all the
3726            // children elements of the 'question' one (so qtype plugins will know the question they belong to)
3727            $this->set_mapping('question', $oldid, $questionmapping->newitemid);
3728        }
3729
3730        // Note, we don't restore any question files yet
3731        // as far as the CONTEXT_MODULE categories still
3732        // haven't their contexts to be restored to
3733        // The {@link restore_create_question_files}, executed in the final step
3734        // step will be in charge of restoring all the question files
3735    }
3736
3737    protected function process_question_hint($data) {
3738        global $DB;
3739
3740        $data = (object)$data;
3741        $oldid = $data->id;
3742
3743        // Detect if the question is created or mapped
3744        $oldquestionid   = $this->get_old_parentid('question');
3745        $newquestionid   = $this->get_new_parentid('question');
3746        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
3747
3748        // If the question has been created by restore, we need to create its question_answers too
3749        if ($questioncreated) {
3750            // Adjust some columns
3751            $data->questionid = $newquestionid;
3752            // Insert record
3753            $newitemid = $DB->insert_record('question_hints', $data);
3754
3755        // The question existed, we need to map the existing question_hints
3756        } else {
3757            // Look in question_hints by hint text matching
3758            $sql = 'SELECT id
3759                      FROM {question_hints}
3760                     WHERE questionid = ?
3761                       AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
3762            $params = array($newquestionid, $data->hint);
3763            $newitemid = $DB->get_field_sql($sql, $params);
3764
3765            // Not able to find the hint, let's try cleaning the hint text
3766            // of all the question's hints in DB as slower fallback. MDL-33863.
3767            if (!$newitemid) {
3768                $potentialhints = $DB->get_records('question_hints',
3769                        array('questionid' => $newquestionid), '', 'id, hint');
3770                foreach ($potentialhints as $potentialhint) {
3771                    // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
3772                    $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
3773                    $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
3774                    if ($cleanhint === $data->hint) {
3775                        $newitemid = $data->id;
3776                    }
3777                }
3778            }
3779
3780            // If we haven't found the newitemid, something has gone really wrong, question in DB
3781            // is missing hints, exception
3782            if (!$newitemid) {
3783                $info = new stdClass();
3784                $info->filequestionid = $oldquestionid;
3785                $info->dbquestionid   = $newquestionid;
3786                $info->hint           = $data->hint;
3787                throw new restore_step_exception('error_question_hint_missing_in_db', $info);
3788            }
3789        }
3790        // Create mapping (I'm not sure if this is really needed?)
3791        $this->set_mapping('question_hint', $oldid, $newitemid);
3792    }
3793
3794    protected function process_tag($data) {
3795        global $CFG, $DB;
3796
3797        $data = (object)$data;
3798        $newquestion = $this->get_new_parentid('question');
3799
3800        if (!empty($CFG->usetags)) { // if enabled in server
3801            // TODO: This is highly inefficient. Each time we add one tag
3802            // we fetch all the existing because tag_set() deletes them
3803            // so everything must be reinserted on each call
3804            $tags = array();
3805            $existingtags = tag_get_tags('question', $newquestion);
3806            // Re-add all the existitng tags
3807            foreach ($existingtags as $existingtag) {
3808                $tags[] = $existingtag->rawname;
3809            }
3810            // Add the one being restored
3811            $tags[] = $data->rawname;
3812            // Get the category, so we can then later get the context.
3813            $categoryid = $this->get_new_parentid('question_category');
3814            if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
3815                $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
3816            }
3817            // Send all the tags back to the question
3818            tag_set('question', $newquestion, $tags, 'core_question', $this->cachedcategory->contextid);
3819        }
3820    }
3821
3822    protected function after_execute() {
3823        global $DB;
3824
3825        // First of all, recode all the created question_categories->parent fields
3826        $qcats = $DB->get_records('backup_ids_temp', array(
3827                     'backupid' => $this->get_restoreid(),
3828                     'itemname' => 'question_category_created'));
3829        foreach ($qcats as $qcat) {
3830            $newparent = 0;
3831            $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
3832            // Get new parent (mapped or created, so we look in quesiton_category mappings)
3833            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
3834                                 'backupid' => $this->get_restoreid(),
3835                                 'itemname' => 'question_category',
3836                                 'itemid'   => $dbcat->parent))) {
3837                // contextids must match always, as far as we always include complete qbanks, just check it
3838                $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
3839                if ($dbcat->contextid == $newparentctxid) {
3840                    $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
3841                } else {
3842                    $newparent = 0; // No ctx match for both cats, no parent relationship
3843                }
3844            }
3845            // Here with $newparent empty, problem with contexts or remapping, set it to top cat
3846            if (!$newparent) {
3847                $DB->set_field('question_categories', 'parent', 0, array('id' => $dbcat->id));
3848            }
3849        }
3850
3851        // Now, recode all the created question->parent fields
3852        $qs = $DB->get_records('backup_ids_temp', array(
3853                  'backupid' => $this->get_restoreid(),
3854                  'itemname' => 'question_created'));
3855        foreach ($qs as $q) {
3856            $newparent = 0;
3857            $dbq = $DB->get_record('question', array('id' => $q->newitemid));
3858            // Get new parent (mapped or created, so we look in question mappings)
3859            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
3860                                 'backupid' => $this->get_restoreid(),
3861                                 'itemname' => 'question',
3862                                 'itemid'   => $dbq->parent))) {
3863                $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
3864            }
3865        }
3866
3867        // Note, we don't restore any question files yet
3868        // as far as the CONTEXT_MODULE categories still
3869        // haven't their contexts to be restored to
3870        // The {@link restore_create_question_files}, executed in the final step
3871        // step will be in charge of restoring all the question files
3872    }
3873}
3874
3875/**
3876 * Execution step that will move all the CONTEXT_MODULE question categories
3877 * created at early stages of restore in course context (because modules weren't
3878 * created yet) to their target module (matching by old-new-contextid mapping)
3879 */
3880class restore_move_module_questions_categories extends restore_execution_step {
3881
3882    protected function define_execution() {
3883        global $DB;
3884
3885        $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
3886        foreach ($contexts as $contextid => $contextlevel) {
3887            // Only if context mapping exists (i.e. the module has been restored)
3888            if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
3889                // Update all the qcats having their parentitemid set to the original contextid
3890                $modulecats = $DB->get_records_sql("SELECT itemid, newitemid
3891                                                      FROM {backup_ids_temp}
3892                                                     WHERE backupid = ?
3893                                                       AND itemname = 'question_category'
3894                                                       AND parentitemid = ?", array($this->get_restoreid(), $contextid));
3895                foreach ($modulecats as $modulecat) {
3896                    $DB->set_field('question_categories', 'contextid', $newcontext->newitemid, array('id' => $modulecat->newitemid));
3897                    // And set new contextid also in question_category mapping (will be
3898                    // used by {@link restore_create_question_files} later
3899                    restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid);
3900                }
3901            }
3902        }
3903    }
3904}
3905
3906/**
3907 * Execution step that will create all the question/answers/qtype-specific files for the restored
3908 * questions. It must be executed after {@link restore_move_module_questions_categories}
3909 * because only then each question is in its final category and only then the
3910 * contexts can be determined.
3911 */
3912class restore_create_question_files extends restore_execution_step {
3913
3914    /** @var array Question-type specific component items cache. */
3915    private $qtypecomponentscache = array();
3916
3917    /**
3918     * Preform the restore_create_question_files step.
3919     */
3920    protected function define_execution() {
3921        global $DB;
3922
3923        // Track progress, as this task can take a long time.
3924        $progress = $this->task->get_progress();
3925        $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
3926
3927        // Parentitemids of question_createds in backup_ids_temp are the category it is in.
3928        // MUST use a recordset, as there is no unique key in the first (or any) column.
3929        $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype
3930                                               FROM {backup_ids_temp} bi
3931                                               JOIN {question} q ON q.id = bi.newitemid
3932                                              WHERE bi.backupid = ?
3933                                                AND bi.itemname = 'question_created'
3934                                           ORDER BY categoryid ASC", array($this->get_restoreid()));
3935
3936        $currentcatid = -1;
3937        foreach ($catqtypes as $categoryid => $row) {
3938            $qtype = $row->qtype;
3939
3940            // Check if we are in a new category.
3941            if ($currentcatid !== $categoryid) {
3942                // Report progress for each category.
3943                $progress->progress();
3944
3945                if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(),
3946                        'question_category', $categoryid)) {
3947                    // Something went really wrong, cannot find the question_category for the question_created records.
3948                    debugging('Error fetching target context for question', DEBUG_DEVELOPER);
3949                    continue;
3950                }
3951
3952                // Calculate source and target contexts.
3953                $oldctxid = $qcatmapping->info->contextid;
3954                $newctxid = $qcatmapping->parentitemid;
3955
3956                $this->send_common_files($oldctxid, $newctxid, $progress);
3957                $currentcatid = $categoryid;
3958            }
3959
3960            $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress);
3961        }
3962        $catqtypes->close();
3963        $progress->end_progress();
3964    }
3965
3966    /**
3967     * Send the common question files to a new context.
3968     *
3969     * @param int             $oldctxid Old context id.
3970     * @param int             $newctxid New context id.
3971     * @param \core\progress  $progress Progress object to use.
3972     */
3973    private function send_common_files($oldctxid, $newctxid, $progress) {
3974        // Add common question files (question and question_answer ones).
3975        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
3976                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
3977        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
3978                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
3979        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
3980                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
3981        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
3982                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
3983        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
3984                $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
3985        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
3986                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
3987        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
3988                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
3989        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
3990                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
3991    }
3992
3993    /**
3994     * Send the question type specific files to a new context.
3995     *
3996     * @param text            $qtype The qtype name to send.
3997     * @param int             $oldctxid Old context id.
3998     * @param int             $newctxid New context id.
3999     * @param \core\progress  $progress Progress object to use.
4000     */
4001    private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) {
4002        if (!isset($this->qtypecomponentscache[$qtype])) {
4003            $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype);
4004        }
4005        $components = $this->qtypecomponentscache[$qtype];
4006        foreach ($components as $component => $fileareas) {
4007            foreach ($fileareas as $filearea => $mapping) {
4008                restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,
4009                        $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress);
4010            }
4011        }
4012    }
4013}
4014
4015/**
4016 * Try to restore aliases and references to external files.
4017 *
4018 * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}.
4019 * We expect that all regular (non-alias) files have already been restored. Make sure
4020 * there is no restore step executed after this one that would call send_files_to_pool() again.
4021 *
4022 * You may notice we have hardcoded support for Server files, Legacy course files
4023 * and user Private files here at the moment. This could be eventually replaced with a set of
4024 * callbacks in the future if needed.
4025 *
4026 * @copyright 2012 David Mudrak <david@moodle.com>
4027 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4028 */
4029class restore_process_file_aliases_queue extends restore_execution_step {
4030
4031    /** @var array internal cache for {@link choose_repository()} */
4032    private $cachereposbyid = array();
4033
4034    /** @var array internal cache for {@link choose_repository()} */
4035    private $cachereposbytype = array();
4036
4037    /**
4038     * What to do when this step is executed.
4039     */
4040    protected function define_execution() {
4041        global $DB;
4042
4043        $this->log('processing file aliases queue', backup::LOG_DEBUG);
4044
4045        $fs = get_file_storage();
4046
4047        // Load the queue.
4048        $rs = $DB->get_recordset('backup_ids_temp',
4049            array('backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'),
4050            '', 'info');
4051
4052        // Iterate over aliases in the queue.
4053        foreach ($rs as $record) {
4054            $info = backup_controller_dbops::decode_backup_temp_info($record->info);
4055
4056            // Try to pick a repository instance that should serve the alias.
4057            $repository = $this->choose_repository($info);
4058
4059            if (is_null($repository)) {
4060                $this->notify_failure($info, 'unable to find a matching repository instance');
4061                continue;
4062            }
4063
4064            if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
4065                // Aliases to Server files and Legacy course files may refer to a file
4066                // contained in the backup file or to some existing file (if we are on the
4067                // same site).
4068                try {
4069                    $reference = file_storage::unpack_reference($info->oldfile->reference);
4070                } catch (Exception $e) {
4071                    $this->notify_failure($info, 'invalid reference field format');
4072                    continue;
4073                }
4074
4075                // Let's see if the referred source file was also included in the backup.
4076                $candidates = $DB->get_recordset('backup_files_temp', array(
4077                        'backupid' => $this->get_restoreid(),
4078                        'contextid' => $reference['contextid'],
4079                        'component' => $reference['component'],
4080                        'filearea' => $reference['filearea'],
4081                        'itemid' => $reference['itemid'],
4082                    ), '', 'info, newcontextid, newitemid');
4083
4084                $source = null;
4085
4086                foreach ($candidates as $candidate) {
4087                    $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
4088                    if ($candidateinfo->filename === $reference['filename']
4089                            and $candidateinfo->filepath === $reference['filepath']
4090                            and !is_null($candidate->newcontextid)
4091                            and !is_null($candidate->newitemid) ) {
4092                        $source = $candidateinfo;
4093                        $source->contextid = $candidate->newcontextid;
4094                        $source->itemid = $candidate->newitemid;
4095                        break;
4096                    }
4097                }
4098                $candidates->close();
4099
4100                if ($source) {
4101                    // We have an alias that refers to another file also included in
4102                    // the backup. Let us change the reference field so that it refers
4103                    // to the restored copy of the original file.
4104                    $reference = file_storage::pack_reference($source);
4105
4106                    // Send the new alias to the filepool.
4107                    $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4108                    $this->notify_success($info);
4109                    continue;
4110
4111                } else {
4112                    // This is a reference to some moodle file that was not contained in the backup
4113                    // file. If we are restoring to the same site, keep the reference untouched
4114                    // and restore the alias as is if the referenced file exists.
4115                    if ($this->task->is_samesite()) {
4116                        if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
4117                                $reference['itemid'], $reference['filepath'], $reference['filename'])) {
4118                            $reference = file_storage::pack_reference($reference);
4119                            $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4120                            $this->notify_success($info);
4121                            continue;
4122                        } else {
4123                            $this->notify_failure($info, 'referenced file not found');
4124                            continue;
4125                        }
4126
4127                    // If we are at other site, we can't restore this alias.
4128                    } else {
4129                        $this->notify_failure($info, 'referenced file not included');
4130                        continue;
4131                    }
4132                }
4133
4134            } else if ($info->oldfile->repositorytype === 'user') {
4135                if ($this->task->is_samesite()) {
4136                    // For aliases to user Private files at the same site, we have a chance to check
4137                    // if the referenced file still exists.
4138                    try {
4139                        $reference = file_storage::unpack_reference($info->oldfile->reference);
4140                    } catch (Exception $e) {
4141                        $this->notify_failure($info, 'invalid reference field format');
4142                        continue;
4143                    }
4144                    if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
4145                            $reference['itemid'], $reference['filepath'], $reference['filename'])) {
4146                        $reference = file_storage::pack_reference($reference);
4147                        $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
4148                        $this->notify_success($info);
4149                        continue;
4150                    } else {
4151                        $this->notify_failure($info, 'referenced file not found');
4152                        continue;
4153                    }
4154
4155                // If we are at other site, we can't restore this alias.
4156                } else {
4157                    $this->notify_failure($info, 'restoring at another site');
4158                    continue;
4159                }
4160
4161            } else {
4162                // This is a reference to some external file such as in boxnet or dropbox.
4163                // If we are restoring to the same site, keep the reference untouched and
4164                // restore the alias as is.
4165                if ($this->task->is_samesite()) {
4166                    $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference);
4167                    $this->notify_success($info);
4168                    continue;
4169
4170                // If we are at other site, we can't restore this alias.
4171                } else {
4172                    $this->notify_failure($info, 'restoring at another site');
4173                    continue;
4174                }
4175            }
4176        }
4177        $rs->close();
4178    }
4179
4180    /**
4181     * Choose the repository instance that should handle the alias.
4182     *
4183     * At the same site, we can rely on repository instance id and we just
4184     * check it still exists. On other site, try to find matching Server files or
4185     * Legacy course files repository instance. Return null if no matching
4186     * repository instance can be found.
4187     *
4188     * @param stdClass $info
4189     * @return repository|null
4190     */
4191    private function choose_repository(stdClass $info) {
4192        global $DB, $CFG;
4193        require_once($CFG->dirroot.'/repository/lib.php');
4194
4195        if ($this->task->is_samesite()) {
4196            // We can rely on repository instance id.
4197
4198            if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) {
4199                return $this->cachereposbyid[$info->oldfile->repositoryid];
4200            }
4201
4202            $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1);
4203
4204            try {
4205                $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID);
4206                return $this->cachereposbyid[$info->oldfile->repositoryid];
4207            } catch (Exception $e) {
4208                $this->cachereposbyid[$info->oldfile->repositoryid] = null;
4209                return null;
4210            }
4211
4212        } else {
4213            // We can rely on repository type only.
4214
4215            if (empty($info->oldfile->repositorytype)) {
4216                return null;
4217            }
4218
4219            if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) {
4220                return $this->cachereposbytype[$info->oldfile->repositorytype];
4221            }
4222
4223            $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1);
4224
4225            // Both Server files and Legacy course files repositories have a single
4226            // instance at the system context to use. Let us try to find it.
4227            if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
4228                $sql = "SELECT ri.id
4229                          FROM {repository} r
4230                          JOIN {repository_instances} ri ON ri.typeid = r.id
4231                         WHERE r.type = ? AND ri.contextid = ?";
4232                $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID));
4233                if (empty($ris)) {
4234                    return null;
4235                }
4236                $repoids = array_keys($ris);
4237                $repoid = reset($repoids);
4238                try {
4239                    $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID);
4240                    return $this->cachereposbytype[$info->oldfile->repositorytype];
4241                } catch (Exception $e) {
4242                    $this->cachereposbytype[$info->oldfile->repositorytype] = null;
4243                    return null;
4244                }
4245            }
4246
4247            $this->cachereposbytype[$info->oldfile->repositorytype] = null;
4248            return null;
4249        }
4250    }
4251
4252    /**
4253     * Let the user know that the given alias was successfully restored
4254     *
4255     * @param stdClass $info
4256     */
4257    private function notify_success(stdClass $info) {
4258        $filedesc = $this->describe_alias($info);
4259        $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1);
4260    }
4261
4262    /**
4263     * Let the user know that the given alias can't be restored
4264     *
4265     * @param stdClass $info
4266     * @param string $reason detailed reason to be logged
4267     */
4268    private function notify_failure(stdClass $info, $reason = '') {
4269        $filedesc = $this->describe_alias($info);
4270        if ($reason) {
4271            $reason = ' ('.$reason.')';
4272        }
4273        $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1);
4274        $this->add_result_item('file_aliases_restore_failures', $filedesc);
4275    }
4276
4277    /**
4278     * Return a human readable description of the alias file
4279     *
4280     * @param stdClass $info
4281     * @return string
4282     */
4283    private function describe_alias(stdClass $info) {
4284
4285        $filedesc = $this->expected_alias_location($info->newfile);
4286
4287        if (!is_null($info->oldfile->source)) {
4288            $filedesc .= ' ('.$info->oldfile->source.')';
4289        }
4290
4291        return $filedesc;
4292    }
4293
4294    /**
4295     * Return the expected location of a file
4296     *
4297     * Please note this may and may not work as a part of URL to pluginfile.php
4298     * (depends on how the given component/filearea deals with the itemid).
4299     *
4300     * @param stdClass $filerecord
4301     * @return string
4302     */
4303    private function expected_alias_location($filerecord) {
4304
4305        $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea;
4306        if (!is_null($filerecord->itemid)) {
4307            $filedesc .= '/'.$filerecord->itemid;
4308        }
4309        $filedesc .= $filerecord->filepath.$filerecord->filename;
4310
4311        return $filedesc;
4312    }
4313
4314    /**
4315     * Append a value to the given resultset
4316     *
4317     * @param string $name name of the result containing a list of values
4318     * @param mixed $value value to add as another item in that result
4319     */
4320    private function add_result_item($name, $value) {
4321
4322        $results = $this->task->get_results();
4323
4324        if (isset($results[$name])) {
4325            if (!is_array($results[$name])) {
4326                throw new coding_exception('Unable to append a result item into a non-array structure.');
4327            }
4328            $current = $results[$name];
4329            $current[] = $value;
4330            $this->task->add_result(array($name => $current));
4331
4332        } else {
4333            $this->task->add_result(array($name => array($value)));
4334        }
4335    }
4336}
4337
4338
4339/**
4340 * Abstract structure step, to be used by all the activities using core questions stuff
4341 * (like the quiz module), to support qtype plugins, states and sessions
4342 */
4343abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
4344    /** @var array question_attempt->id to qtype. */
4345    protected $qtypes = array();
4346    /** @var array question_attempt->id to questionid. */
4347    protected $newquestionids = array();
4348
4349    /**
4350     * Attach below $element (usually attempts) the needed restore_path_elements
4351     * to restore question_usages and all they contain.
4352     *
4353     * If you use the $nameprefix parameter, then you will need to implement some
4354     * extra methods in your class, like
4355     *
4356     * protected function process_{nameprefix}question_attempt($data) {
4357     *     $this->restore_question_usage_worker($data, '{nameprefix}');
4358     * }
4359     * protected function process_{nameprefix}question_attempt($data) {
4360     *     $this->restore_question_attempt_worker($data, '{nameprefix}');
4361     * }
4362     * protected function process_{nameprefix}question_attempt_step($data) {
4363     *     $this->restore_question_attempt_step_worker($data, '{nameprefix}');
4364     * }
4365     *
4366     * @param restore_path_element $element the parent element that the usages are stored inside.
4367     * @param array $paths the paths array that is being built.
4368     * @param string $nameprefix should match the prefix passed to the corresponding
4369     *      backup_questions_activity_structure_step::add_question_usages call.
4370     */
4371    protected function add_question_usages($element, &$paths, $nameprefix = '') {
4372        // Check $element is restore_path_element
4373        if (! $element instanceof restore_path_element) {
4374            throw new restore_step_exception('element_must_be_restore_path_element', $element);
4375        }
4376
4377        // Check $paths is one array
4378        if (!is_array($paths)) {
4379            throw new restore_step_exception('paths_must_be_array', $paths);
4380        }
4381        $paths[] = new restore_path_element($nameprefix . 'question_usage',
4382                $element->get_path() . "/{$nameprefix}question_usage");
4383        $paths[] = new restore_path_element($nameprefix . 'question_attempt',
4384                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
4385        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
4386                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
4387                true);
4388        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
4389                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
4390    }
4391
4392    /**
4393     * Process question_usages
4394     */
4395    protected function process_question_usage($data) {
4396        $this->restore_question_usage_worker($data, '');
4397    }
4398
4399    /**
4400     * Process question_attempts
4401     */
4402    protected function process_question_attempt($data) {
4403        $this->restore_question_attempt_worker($data, '');
4404    }
4405
4406    /**
4407     * Process question_attempt_steps
4408     */
4409    protected function process_question_attempt_step($data) {
4410        $this->restore_question_attempt_step_worker($data, '');
4411    }
4412
4413    /**
4414     * This method does the acutal work for process_question_usage or
4415     * process_{nameprefix}_question_usage.
4416     * @param array $data the data from the XML file.
4417     * @param string $nameprefix the element name prefix.
4418     */
4419    protected function restore_question_usage_worker($data, $nameprefix) {
4420        global $DB;
4421
4422        // Clear our caches.
4423        $this->qtypes = array();
4424        $this->newquestionids = array();
4425
4426        $data = (object)$data;
4427        $oldid = $data->id;
4428
4429        $oldcontextid = $this->get_task()->get_old_contextid();
4430        $data->contextid  = $this->get_mappingid('context', $this->task->get_old_contextid());
4431
4432        // Everything ready, insert (no mapping needed)
4433        $newitemid = $DB->insert_record('question_usages', $data);
4434
4435        $this->inform_new_usage_id($newitemid);
4436
4437        $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
4438    }
4439
4440    /**
4441     * When process_question_usage creates the new usage, it calls this method
4442     * to let the activity link to the new usage. For example, the quiz uses
4443     * this method to set quiz_attempts.uniqueid to the new usage id.
4444     * @param integer $newusageid
4445     */
4446    abstract protected function inform_new_usage_id($newusageid);
4447
4448    /**
4449     * This method does the acutal work for process_question_attempt or
4450     * process_{nameprefix}_question_attempt.
4451     * @param array $data the data from the XML file.
4452     * @param string $nameprefix the element name prefix.
4453     */
4454    protected function restore_question_attempt_worker($data, $nameprefix) {
4455        global $DB;
4456
4457        $data = (object)$data;
4458        $oldid = $data->id;
4459        $question = $this->get_mapping('question', $data->questionid);
4460
4461        $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
4462        $data->questionid      = $question->newitemid;
4463        if (!property_exists($data, 'variant')) {
4464            $data->variant = 1;
4465        }
4466        $data->timemodified    = $this->apply_date_offset($data->timemodified);
4467
4468        if (!property_exists($data, 'maxfraction')) {
4469            $data->maxfraction = 1;
4470        }
4471
4472        $newitemid = $DB->insert_record('question_attempts', $data);
4473
4474        $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
4475        $this->qtypes[$newitemid] = $question->info->qtype;
4476        $this->newquestionids[$newitemid] = $data->questionid;
4477    }
4478
4479    /**
4480     * This method does the acutal work for process_question_attempt_step or
4481     * process_{nameprefix}_question_attempt_step.
4482     * @param array $data the data from the XML file.
4483     * @param string $nameprefix the element name prefix.
4484     */
4485    protected function restore_question_attempt_step_worker($data, $nameprefix) {
4486        global $DB;
4487
4488        $data = (object)$data;
4489        $oldid = $data->id;
4490
4491        // Pull out the response data.
4492        $response = array();
4493        if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
4494            foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
4495                $response[$variable['name']] = $variable['value'];
4496            }
4497        }
4498        unset($data->response);
4499
4500        $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
4501        $data->timecreated = $this->apply_date_offset($data->timecreated);
4502        $data->userid      = $this->get_mappingid('user', $data->userid);
4503
4504        // Everything ready, insert and create mapping (needed by question_sessions)
4505        $newitemid = $DB->insert_record('question_attempt_steps', $data);
4506        $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
4507
4508        // Now process the response data.
4509        $response = $this->questions_recode_response_data(
4510                $this->qtypes[$data->questionattemptid],
4511                $this->newquestionids[$data->questionattemptid],
4512                $data->sequencenumber, $response);
4513
4514        foreach ($response as $name => $value) {
4515            $row = new stdClass();
4516            $row->attemptstepid = $newitemid;
4517            $row->name = $name;
4518            $row->value = $value;
4519            $DB->insert_record('question_attempt_step_data', $row, false);
4520        }
4521    }
4522
4523    /**
4524     * Recode the respones data for a particular step of an attempt at at particular question.
4525     * @param string $qtype the question type.
4526     * @param int $newquestionid the question id.
4527     * @param int $sequencenumber the sequence number.
4528     * @param array $response the response data to recode.
4529     */
4530    public function questions_recode_response_data(
4531            $qtype, $newquestionid, $sequencenumber, array $response) {
4532        $qtyperestorer = $this->get_qtype_restorer($qtype);
4533        if ($qtyperestorer) {
4534            $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response);
4535        }
4536        return $response;
4537    }
4538
4539    /**
4540     * Given a list of question->ids, separated by commas, returns the
4541     * recoded list, with all the restore question mappings applied.
4542     * Note: Used by quiz->questions and quiz_attempts->layout
4543     * Note: 0 = page break (unconverted)
4544     */
4545    protected function questions_recode_layout($layout) {
4546        // Extracts question id from sequence
4547        if ($questionids = explode(',', $layout)) {
4548            foreach ($questionids as $id => $questionid) {
4549                if ($questionid) { // If it is zero then this is a pagebreak, don't translate
4550                    $newquestionid = $this->get_mappingid('question', $questionid);
4551                    $questionids[$id] = $newquestionid;
4552                }
4553            }
4554        }
4555        return implode(',', $questionids);
4556    }
4557
4558    /**
4559     * Get the restore_qtype_plugin subclass for a specific question type.
4560     * @param string $qtype e.g. multichoice.
4561     * @return restore_qtype_plugin instance.
4562     */
4563    protected function get_qtype_restorer($qtype) {
4564        // Build one static cache to store {@link restore_qtype_plugin}
4565        // while we are needing them, just to save zillions of instantiations
4566        // or using static stuff that will break our nice API
4567        static $qtypeplugins = array();
4568
4569        if (!isset($qtypeplugins[$qtype])) {
4570            $classname = 'restore_qtype_' . $qtype . '_plugin';
4571            if (class_exists($classname)) {
4572                $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
4573            } else {
4574                $qtypeplugins[$qtype] = null;
4575            }
4576        }
4577        return $qtypeplugins[$qtype];
4578    }
4579
4580    protected function after_execute() {
4581        parent::after_execute();
4582
4583        // Restore any files belonging to responses.
4584        foreach (question_engine::get_all_response_file_areas() as $filearea) {
4585            $this->add_related_files('question', $filearea, 'question_attempt_step');
4586        }
4587    }
4588
4589    /**
4590     * Attach below $element (usually attempts) the needed restore_path_elements
4591     * to restore question attempt data from Moodle 2.0.
4592     *
4593     * When using this method, the parent element ($element) must be defined with
4594     * $grouped = true. Then, in that elements process method, you must call
4595     * {@link process_legacy_attempt_data()} with the groupded data. See, for
4596     * example, the usage of this method in {@link restore_quiz_activity_structure_step}.
4597     * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
4598     * @param array $paths the paths array that is being built to describe the
4599     *      structure.
4600     */
4601    protected function add_legacy_question_attempt_data($element, &$paths) {
4602        global $CFG;
4603        require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php');
4604
4605        // Check $element is restore_path_element
4606        if (!($element instanceof restore_path_element)) {
4607            throw new restore_step_exception('element_must_be_restore_path_element', $element);
4608        }
4609        // Check $paths is one array
4610        if (!is_array($paths)) {
4611            throw new restore_step_exception('paths_must_be_array', $paths);
4612        }
4613
4614        $paths[] = new restore_path_element('question_state',
4615                $element->get_path() . '/states/state');
4616        $paths[] = new restore_path_element('question_session',
4617                $element->get_path() . '/sessions/session');
4618    }
4619
4620    protected function get_attempt_upgrader() {
4621        if (empty($this->attemptupgrader)) {
4622            $this->attemptupgrader = new question_engine_attempt_upgrader();
4623            $this->attemptupgrader->prepare_to_restore();
4624        }
4625        return $this->attemptupgrader;
4626    }
4627
4628    /**
4629     * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
4630     * @param object $data contains all the grouped attempt data to process.
4631     * @param pbject $quiz data about the activity the attempts belong to. Required
4632     * fields are (basically this only works for the quiz module):
4633     *      oldquestions => list of question ids in this activity - using old ids.
4634     *      preferredbehaviour => the behaviour to use for questionattempts.
4635     */
4636    protected function process_legacy_quiz_attempt_data($data, $quiz) {
4637        global $DB;
4638        $upgrader = $this->get_attempt_upgrader();
4639
4640        $data = (object)$data;
4641
4642        $layout = explode(',', $data->layout);
4643        $newlayout = $layout;
4644
4645        // Convert each old question_session into a question_attempt.
4646        $qas = array();
4647        foreach (explode(',', $quiz->oldquestions) as $questionid) {
4648            if ($questionid == 0) {
4649                continue;
4650            }
4651
4652            $newquestionid = $this->get_mappingid('question', $questionid);
4653            if (!$newquestionid) {
4654                throw new restore_step_exception('questionattemptreferstomissingquestion',
4655                        $questionid, $questionid);
4656            }
4657
4658            $question = $upgrader->load_question($newquestionid, $quiz->id);
4659
4660            foreach ($layout as $key => $qid) {
4661                if ($qid == $questionid) {
4662                    $newlayout[$key] = $newquestionid;
4663                }
4664            }
4665
4666            list($qsession, $qstates) = $this->find_question_session_and_states(
4667                    $data, $questionid);
4668
4669            if (empty($qsession) || empty($qstates)) {
4670                throw new restore_step_exception('questionattemptdatamissing',
4671                        $questionid, $questionid);
4672            }
4673
4674            list($qsession, $qstates) = $this->recode_legacy_response_data(
4675                    $question, $qsession, $qstates);
4676
4677            $data->layout = implode(',', $newlayout);
4678            $qas[$newquestionid] = $upgrader->convert_question_attempt(
4679                    $quiz, $data, $question, $qsession, $qstates);
4680        }
4681
4682        // Now create a new question_usage.
4683        $usage = new stdClass();
4684        $usage->component = 'mod_quiz';
4685        $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid());
4686        $usage->preferredbehaviour = $quiz->preferredbehaviour;
4687        $usage->id = $DB->insert_record('question_usages', $usage);
4688
4689        $this->inform_new_usage_id($usage->id);
4690
4691        $data->uniqueid = $usage->id;
4692        $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
4693                 $this->questions_recode_layout($quiz->oldquestions));
4694    }
4695
4696    protected function find_question_session_and_states($data, $questionid) {
4697        $qsession = null;
4698        foreach ($data->sessions['session'] as $session) {
4699            if ($session['questionid'] == $questionid) {
4700                $qsession = (object) $session;
4701                break;
4702            }
4703        }
4704
4705        $qstates = array();
4706        foreach ($data->states['state'] as $state) {
4707            if ($state['question'] == $questionid) {
4708                // It would be natural to use $state['seq_number'] as the array-key
4709                // here, but it seems that buggy behaviour in 2.0 and early can
4710                // mean that that is not unique, so we use id, which is guaranteed
4711                // to be unique.
4712                $qstates[$state['id']] = (object) $state;
4713            }
4714        }
4715        ksort($qstates);
4716        $qstates = array_values($qstates);
4717
4718        return array($qsession, $qstates);
4719    }
4720
4721    /**
4722     * Recode any ids in the response data
4723     * @param object $question the question data
4724     * @param object $qsession the question sessions.
4725     * @param array $qstates the question states.
4726     */
4727    protected function recode_legacy_response_data($question, $qsession, $qstates) {
4728        $qsession->questionid = $question->id;
4729
4730        foreach ($qstates as &$state) {
4731            $state->question = $question->id;
4732            $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype);
4733        }
4734
4735        return array($qsession, $qstates);
4736    }
4737
4738    /**
4739     * Recode the legacy answer field.
4740     * @param object $state the state to recode the answer of.
4741     * @param string $qtype the question type.
4742     */
4743    public function restore_recode_legacy_answer($state, $qtype) {
4744        $restorer = $this->get_qtype_restorer($qtype);
4745        if ($restorer) {
4746            return $restorer->recode_legacy_state_answer($state);
4747        } else {
4748            return $state->answer;
4749        }
4750    }
4751}
Note: See TracBrowser for help on using the repository browser.