Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
/**
31
 * delete old directories and conditionally create backup_temp_ids table
32
 */
33
class restore_create_and_clean_temp_stuff extends restore_execution_step {
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
 * Drop temp ids table and delete the temp dir used by backup/restore (conditionally).
61
 */
62
class restore_drop_and_clean_temp_stuff extends restore_execution_step {
63
 
64
    protected function define_execution() {
65
        global $CFG;
66
        restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
67
        if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
68
            $progress = $this->task->get_progress();
69
            $progress->start_progress('Deleting backup dir');
70
            backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
71
            $progress->end_progress();
72
        }
73
    }
74
}
75
 
76
/**
77
 * Restore calculated grade items, grade categories etc
78
 */
79
class restore_gradebook_structure_step extends restore_structure_step {
80
 
81
    /**
82
     * To conditionally decide if this step must be executed
83
     * Note the "settings" conditions are evaluated in the
84
     * corresponding task. Here we check for other conditions
85
     * not being restore settings (files, site settings...)
86
     */
87
     protected function execute_condition() {
88
        global $CFG, $DB;
89
 
90
        if ($this->get_courseid() == SITEID) {
91
            return false;
92
        }
93
 
94
        // No gradebook info found, don't execute
95
        $fullpath = $this->task->get_taskbasepath();
96
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
97
        if (!file_exists($fullpath)) {
98
            return false;
99
        }
100
 
101
        // Some module present in backup file isn't available to restore
102
        // in this site, don't execute
103
        if ($this->task->is_missing_modules()) {
104
            return false;
105
        }
106
 
107
        // Some activity has been excluded to be restored, don't execute
108
        if ($this->task->is_excluding_activities()) {
109
            return false;
110
        }
111
 
112
        // There should only be one grade category (the 1 associated with the course itself)
113
        // If other categories already exist we're restoring into an existing course.
114
        // Restoring categories into a course with an existing category structure is unlikely to go well
115
        $category = new stdclass();
116
        $category->courseid  = $this->get_courseid();
117
        $catcount = $DB->count_records('grade_categories', (array)$category);
118
        if ($catcount>1) {
119
            return false;
120
        }
121
 
122
        $restoretask = $this->get_task();
123
 
124
        // On older versions the freeze value has to be converted.
125
        // We do this from here as it is happening right before the file is read.
126
        // This only targets the backup files that can contain the legacy freeze.
127
        if ($restoretask->backup_version_compare(20150618, '>')
128
                && $restoretask->backup_release_compare('3.0', '<')
129
                || $restoretask->backup_version_compare(20160527, '<')) {
130
            $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
131
        }
132
 
133
        // Arrived here, execute the step
134
        return true;
135
     }
136
 
137
    protected function define_structure() {
138
        $paths = array();
139
        $userinfo = $this->task->get_setting_value('users');
140
 
141
        $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
142
        $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
143
 
144
        $gradeitem = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
145
        $paths[] = $gradeitem;
146
        $this->add_plugin_structure('local', $gradeitem);
147
 
148
        if ($userinfo) {
149
            $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
150
        }
151
        $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
152
        $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
153
 
154
        return $paths;
155
    }
156
 
157
    protected function process_attributes($data) {
158
        // For non-merge restore types:
159
        // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
160
        $target = $this->get_task()->get_target();
161
        if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
162
            set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
163
        }
164
        if (!empty($data['calculations_freeze'])) {
165
            if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
166
                    $target == backup::TARGET_EXISTING_DELETING) {
167
                set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
168
            }
169
        }
170
    }
171
 
172
    protected function process_grade_item($data) {
173
        global $DB;
174
 
175
        $data = (object)$data;
176
 
177
        $oldid = $data->id;
178
        $data->course = $this->get_courseid();
179
 
180
        $data->courseid = $this->get_courseid();
181
 
182
        if ($data->itemtype=='manual') {
183
            // manual grade items store category id in categoryid
184
            $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
185
            // if mapping failed put in course's grade category
186
            if (NULL == $data->categoryid) {
187
                $coursecat = grade_category::fetch_course_category($this->get_courseid());
188
                $data->categoryid = $coursecat->id;
189
            }
190
        } else if ($data->itemtype=='course') {
191
            // course grade item stores their category id in iteminstance
192
            $coursecat = grade_category::fetch_course_category($this->get_courseid());
193
            $data->iteminstance = $coursecat->id;
194
        } else if ($data->itemtype=='category') {
195
            // category grade items store their category id in iteminstance
196
            $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
197
        } else {
198
            throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
199
        }
200
 
201
        $data->scaleid   = $this->get_mappingid('scale', $data->scaleid, NULL);
202
        $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
203
 
204
        $data->locktime = $this->apply_date_offset($data->locktime);
205
 
206
        $coursecategory = $newitemid = null;
207
        //course grade item should already exist so updating instead of inserting
208
        if($data->itemtype=='course') {
209
            //get the ID of the already created grade item
210
            $gi = new stdclass();
211
            $gi->courseid  = $this->get_courseid();
212
            $gi->itemtype  = $data->itemtype;
213
 
214
            //need to get the id of the grade_category that was automatically created for the course
215
            $category = new stdclass();
216
            $category->courseid  = $this->get_courseid();
217
            $category->parent  = null;
218
            //course category fullname starts out as ? but may be edited
219
            //$category->fullname  = '?';
220
            $coursecategory = $DB->get_record('grade_categories', (array)$category);
221
            $gi->iteminstance = $coursecategory->id;
222
 
223
            $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
224
            if (!empty($existinggradeitem)) {
225
                $data->id = $newitemid = $existinggradeitem->id;
226
                $DB->update_record('grade_items', $data);
227
            }
228
        } else if ($data->itemtype == 'manual') {
229
            // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
230
            $gi = array(
231
                'itemtype' => $data->itemtype,
232
                'courseid' => $data->courseid,
233
                'itemname' => $data->itemname,
234
                'categoryid' => $data->categoryid,
235
            );
236
            $newitemid = $DB->get_field('grade_items', 'id', $gi);
237
        }
238
 
239
        if (empty($newitemid)) {
240
            //in case we found the course category but still need to insert the course grade item
241
            if ($data->itemtype=='course' && !empty($coursecategory)) {
242
                $data->iteminstance = $coursecategory->id;
243
            }
244
 
245
            $newitemid = $DB->insert_record('grade_items', $data);
246
            $data->id = $newitemid;
247
            $gradeitem = new grade_item($data);
248
            core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger();
249
        }
250
        $this->set_mapping('grade_item', $oldid, $newitemid);
251
    }
252
 
253
    protected function process_grade_grade($data) {
254
        global $DB;
255
 
256
        $data = (object)$data;
257
        $oldid = $data->id;
258
        $olduserid = $data->userid;
259
 
260
        $data->itemid = $this->get_new_parentid('grade_item');
261
 
262
        $data->userid = $this->get_mappingid('user', $data->userid, null);
263
        if (!empty($data->userid)) {
264
            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
265
            $data->locktime     = $this->apply_date_offset($data->locktime);
266
 
267
            $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
268
            if ($gradeexists) {
269
                $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
270
                $this->log($message, backup::LOG_DEBUG);
271
            } else {
272
                $newitemid = $DB->insert_record('grade_grades', $data);
273
                $this->set_mapping('grade_grades', $oldid, $newitemid);
274
            }
275
        } else {
276
            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
277
            $this->log($message, backup::LOG_DEBUG);
278
        }
279
    }
280
 
281
    protected function process_grade_category($data) {
282
        global $DB;
283
 
284
        $data = (object)$data;
285
        $oldid = $data->id;
286
 
287
        $data->course = $this->get_courseid();
288
        $data->courseid = $data->course;
289
 
290
        $newitemid = null;
291
        //no parent means a course level grade category. That may have been created when the course was created
292
        if(empty($data->parent)) {
293
            //parent was being saved as 0 when it should be null
294
            $data->parent = null;
295
 
296
            //get the already created course level grade category
297
            $category = new stdclass();
298
            $category->courseid = $this->get_courseid();
299
            $category->parent = null;
300
 
301
            $coursecategory = $DB->get_record('grade_categories', (array)$category);
302
            if (!empty($coursecategory)) {
303
                $data->id = $newitemid = $coursecategory->id;
304
                $DB->update_record('grade_categories', $data);
305
            }
306
        }
307
 
308
        // Add a warning about a removed setting.
309
        if (!empty($data->aggregatesubcats)) {
310
            set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
311
        }
312
 
313
        //need to insert a course category
314
        if (empty($newitemid)) {
315
            $newitemid = $DB->insert_record('grade_categories', $data);
316
        }
317
        $this->set_mapping('grade_category', $oldid, $newitemid);
318
    }
319
    protected function process_grade_letter($data) {
320
        global $DB;
321
 
322
        $data = (object)$data;
323
        $oldid = $data->id;
324
 
325
        $data->contextid = context_course::instance($this->get_courseid())->id;
326
 
327
        $gradeletter = (array)$data;
328
        unset($gradeletter['id']);
329
        if (!$DB->record_exists('grade_letters', $gradeletter)) {
330
            $newitemid = $DB->insert_record('grade_letters', $data);
331
        } else {
332
            $newitemid = $data->id;
333
        }
334
 
335
        $this->set_mapping('grade_letter', $oldid, $newitemid);
336
    }
337
    protected function process_grade_setting($data) {
338
        global $DB;
339
 
340
        $data = (object)$data;
341
        $oldid = $data->id;
342
 
343
        $data->courseid = $this->get_courseid();
344
 
345
        $target = $this->get_task()->get_target();
346
        if ($data->name == 'minmaxtouse' &&
347
                ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
348
            // We never restore minmaxtouse during merge.
349
            return;
350
        }
351
 
352
        if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
353
            $newitemid = $DB->insert_record('grade_settings', $data);
354
        } else {
355
            $newitemid = $data->id;
356
        }
357
 
358
        if (!empty($oldid)) {
359
            // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
360
            $this->set_mapping('grade_setting', $oldid, $newitemid);
361
        }
362
    }
363
 
364
    /**
365
     * put all activity grade items in the correct grade category and mark all for recalculation
366
     */
367
    protected function after_execute() {
368
        global $DB;
369
 
370
        $conditions = array(
371
            'backupid' => $this->get_restoreid(),
372
            'itemname' => 'grade_item'//,
373
            //'itemid'   => $itemid
374
        );
375
        $rs = $DB->get_recordset('backup_ids_temp', $conditions);
376
 
377
        // We need this for calculation magic later on.
378
        $mappings = array();
379
 
380
        if (!empty($rs)) {
381
            foreach($rs as $grade_item_backup) {
382
 
383
                // Store the oldid with the new id.
384
                $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
385
 
386
                $updateobj = new stdclass();
387
                $updateobj->id = $grade_item_backup->newitemid;
388
 
389
                //if this is an activity grade item that needs to be put back in its correct category
390
                if (!empty($grade_item_backup->parentitemid)) {
391
                    $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
392
                    if (!is_null($oldcategoryid)) {
393
                        $updateobj->categoryid = $oldcategoryid;
394
                        $DB->update_record('grade_items', $updateobj);
395
                    }
396
                } else {
397
                    //mark course and category items as needing to be recalculated
398
                    $updateobj->needsupdate=1;
399
                    $DB->update_record('grade_items', $updateobj);
400
                }
401
            }
402
        }
403
        $rs->close();
404
 
405
        // We need to update the calculations for calculated grade items that may reference old
406
        // grade item ids using ##gi\d+##.
407
        // $mappings can be empty, use 0 if so (won't match ever)
408
        list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
409
        $sql = "SELECT gi.id, gi.calculation
410
                  FROM {grade_items} gi
411
                 WHERE gi.id {$sql} AND
412
                       calculation IS NOT NULL";
413
        $rs = $DB->get_recordset_sql($sql, $params);
414
        foreach ($rs as $gradeitem) {
415
            // Collect all of the used grade item id references
416
            if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
417
                // This calculation doesn't reference any other grade items... EASY!
418
                continue;
419
            }
420
            // For this next bit we are going to do the replacement of id's in two steps:
421
            // 1. We will replace all old id references with a special mapping reference.
422
            // 2. We will replace all mapping references with id's
423
            // Why do we do this?
424
            // Because there potentially there will be an overlap of ids within the query and we
425
            // we substitute the wrong id.. safest way around this is the two step system
426
            $calculationmap = array();
427
            $mapcount = 0;
428
            foreach ($matches[1] as $match) {
429
                // Check that the old id is known to us, if not it was broken to begin with and will
430
                // continue to be broken.
431
                if (!array_key_exists($match, $mappings)) {
432
                    continue;
433
                }
434
                // Our special mapping key
435
                $mapping = '##MAPPING'.$mapcount.'##';
436
                // The old id that exists within the calculation now
437
                $oldid = '##gi'.$match.'##';
438
                // The new id that we want to replace the old one with.
439
                $newid = '##gi'.$mappings[$match].'##';
440
                // Replace in the special mapping key
441
                $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
442
                // And record the mapping
443
                $calculationmap[$mapping] = $newid;
444
                $mapcount++;
445
            }
446
            // Iterate all special mappings for this calculation and replace in the new id's
447
            foreach ($calculationmap as $mapping => $newid) {
448
                $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
449
            }
450
            // Update the calculation now that its being remapped
451
            $DB->update_record('grade_items', $gradeitem);
452
        }
453
        $rs->close();
454
 
455
        // Need to correct the grade category path and parent
456
        $conditions = array(
457
            'courseid' => $this->get_courseid()
458
        );
459
 
460
        $rs = $DB->get_recordset('grade_categories', $conditions);
461
        // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
462
        foreach ($rs as $gc) {
463
            if (!empty($gc->parent)) {
464
                $grade_category = new stdClass();
465
                $grade_category->id = $gc->id;
466
                $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
467
                $DB->update_record('grade_categories', $grade_category);
468
            }
469
        }
470
        $rs->close();
471
 
472
        // Now we can rebuild all the paths
473
        $rs = $DB->get_recordset('grade_categories', $conditions);
474
        foreach ($rs as $gc) {
475
            $grade_category = new stdClass();
476
            $grade_category->id = $gc->id;
477
            $grade_category->path = grade_category::build_path($gc);
478
            $grade_category->depth = substr_count($grade_category->path, '/') - 1;
479
            $DB->update_record('grade_categories', $grade_category);
480
        }
481
        $rs->close();
482
 
483
        // Check what to do with the minmaxtouse setting.
484
        $this->check_minmaxtouse();
485
 
486
        // Freeze gradebook calculations if needed.
487
        $this->gradebook_calculation_freeze();
488
 
489
        // Ensure the module cache is current when recalculating grades.
490
        rebuild_course_cache($this->get_courseid(), true);
491
 
492
        // Restore marks items as needing update. Update everything now.
1441 ariadna 493
        grade_regrade_final_grades($this->get_courseid(), async: true);
1 efrain 494
    }
495
 
496
    /**
497
     * Freeze gradebook calculation if needed.
498
     *
499
     * This is similar to various upgrade scripts that check if the freeze is needed.
500
     */
501
    protected function gradebook_calculation_freeze() {
502
        global $CFG;
503
        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
504
        $restoretask = $this->get_task();
505
 
506
        // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
507
        if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
508
                && $restoretask->backup_version_compare(20150619, '<')) {
509
            require_once($CFG->libdir . '/db/upgradelib.php');
510
            upgrade_extra_credit_weightoverride($this->get_courseid());
511
        }
512
        // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
513
        if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=')
514
                && $restoretask->backup_version_compare(20150627, '<')) {
515
            require_once($CFG->libdir . '/db/upgradelib.php');
516
            upgrade_calculated_grade_items($this->get_courseid());
517
        }
518
        // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
519
        // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
520
        // be checked for this problem.
521
        if (!$gradebookcalculationsfreeze
522
                && ($restoretask->backup_version_compare(20160518, '<') || $restoretask->backup_release_compare('2.9', '<='))) {
523
            require_once($CFG->libdir . '/db/upgradelib.php');
524
            upgrade_course_letter_boundary($this->get_courseid());
525
        }
526
 
527
    }
528
 
529
    /**
530
     * Checks what should happen with the course grade setting minmaxtouse.
531
     *
532
     * This is related to the upgrade step at the time the setting was added.
533
     *
534
     * @see MDL-48618
535
     * @return void
536
     */
537
    protected function check_minmaxtouse() {
538
        global $CFG, $DB;
539
        require_once($CFG->libdir . '/gradelib.php');
540
 
541
        $userinfo = $this->task->get_setting_value('users');
542
        $settingname = 'minmaxtouse';
543
        $courseid = $this->get_courseid();
544
        $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
545
        $version28start = 2014111000.00;
546
        $version28last = 2014111006.05;
547
        $version29start = 2015051100.00;
548
        $version29last = 2015060400.02;
549
 
550
        $target = $this->get_task()->get_target();
551
        if ($minmaxtouse === false &&
552
                ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
553
            // The setting was not found because this setting did not exist at the time the backup was made.
554
            // And we are not restoring as merge, in which case we leave the course as it was.
555
            $version = $this->get_task()->get_info()->moodle_version;
556
 
557
            if ($version < $version28start) {
558
                // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
559
                if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
560
                    grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
561
                }
562
 
563
            } else if (($version >= $version28start && $version < $version28last) ||
564
                    ($version >= $version29start && $version < $version29last)) {
565
                // They should be using grade_grade when the course has inconsistencies.
566
 
567
                $sql = "SELECT gi.id
568
                          FROM {grade_items} gi
569
                          JOIN {grade_grades} gg
570
                            ON gg.itemid = gi.id
571
                         WHERE gi.courseid = ?
572
                           AND (gi.itemtype != ? AND gi.itemtype != ?)
573
                           AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
574
 
575
                // The course can only have inconsistencies when we restore the user info,
576
                // we do not need to act on existing grades that were not restored as part of this backup.
577
                if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
578
 
579
                    // Display the notice as we do during upgrade.
580
                    set_config('show_min_max_grades_changed_' . $courseid, 1);
581
 
582
                    if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
583
                        // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
584
                        // If they are using the site-wide grade_grade setting, we only want to notice them.
585
                        grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
586
                    }
587
                }
588
 
589
            } else {
590
                // This should never happen because from now on minmaxtouse is always saved in backups.
591
            }
592
        }
593
    }
594
 
595
    /**
596
     * Rewrite step definition to handle the legacy freeze attribute.
597
     *
598
     * In previous backups the calculations_freeze property was stored as an attribute of the
599
     * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
600
     * It only processes definitive children, and their parent attributes.
601
     *
602
     * We had:
603
     *
604
     * <gradebook calculations_freeze="20160511">
605
     *   <grade_categories>
606
     *     <grade_category id="10">
607
     *       <depth>1</depth>
608
     *       ...
609
     *     </grade_category>
610
     *   </grade_categories>
611
     *   ...
612
     * </gradebook>
613
     *
614
     * And this method will convert it to:
615
     *
616
     * <gradebook >
617
     *   <attributes>
618
     *     <calculations_freeze>20160511</calculations_freeze>
619
     *   </attributes>
620
     *   <grade_categories>
621
     *     <grade_category id="10">
622
     *       <depth>1</depth>
623
     *       ...
624
     *     </grade_category>
625
     *   </grade_categories>
626
     *   ...
627
     * </gradebook>
628
     *
629
     * Note that we cannot just load the XML file in memory as it could potentially be huge.
630
     * We can also completely ignore if the node <attributes> is already in the backup
631
     * file as it never existed before.
632
     *
633
     * @param string $filepath The absolute path to the XML file.
634
     * @return void
635
     */
636
    protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
637
        $foundnode = false;
638
        $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
639
        $fr = fopen($filepath, 'r');
640
        $fw = fopen($newfile, 'w');
641
        if ($fr && $fw) {
642
            while (($line = fgets($fr, 4096)) !== false) {
643
                if (!$foundnode && strpos($line, '<gradebook ') === 0) {
644
                    $foundnode = true;
645
                    $matches = array();
646
                    $pattern = '@calculations_freeze=.([0-9]+).@';
647
                    if (preg_match($pattern, $line, $matches)) {
648
                        $freeze = $matches[1];
649
                        $line = preg_replace($pattern, '', $line);
650
                        $line .= "  <attributes>\n    <calculations_freeze>$freeze</calculations_freeze>\n  </attributes>\n";
651
                    }
652
                }
653
                fputs($fw, $line);
654
            }
655
            if (!feof($fr)) {
656
                throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
657
            }
658
            fclose($fr);
659
            fclose($fw);
660
            if (!rename($newfile, $filepath)) {
661
                throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
662
            }
663
        } else {
664
            if ($fr) {
665
                fclose($fr);
666
            }
667
            if ($fw) {
668
                fclose($fw);
669
            }
670
        }
671
    }
672
 
673
}
674
 
675
/**
676
 * Step in charge of restoring the grade history of a course.
677
 *
678
 * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
679
 * we do not want to restore the history if the gradebook and its content has not been
680
 * restored. At least for now.
681
 */
682
class restore_grade_history_structure_step extends restore_structure_step {
683
 
684
     protected function execute_condition() {
685
        global $CFG, $DB;
686
 
687
        if ($this->get_courseid() == SITEID) {
688
            return false;
689
        }
690
 
691
        // No gradebook info found, don't execute.
692
        $fullpath = $this->task->get_taskbasepath();
693
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
694
        if (!file_exists($fullpath)) {
695
            return false;
696
        }
697
 
698
        // Some module present in backup file isn't available to restore in this site, don't execute.
699
        if ($this->task->is_missing_modules()) {
700
            return false;
701
        }
702
 
703
        // Some activity has been excluded to be restored, don't execute.
704
        if ($this->task->is_excluding_activities()) {
705
            return false;
706
        }
707
 
708
        // There should only be one grade category (the 1 associated with the course itself).
709
        $category = new stdclass();
710
        $category->courseid  = $this->get_courseid();
711
        $catcount = $DB->count_records('grade_categories', (array)$category);
712
        if ($catcount > 1) {
713
            return false;
714
        }
715
 
716
        // Arrived here, execute the step.
717
        return true;
718
     }
719
 
720
    protected function define_structure() {
721
        $paths = array();
722
 
723
        // Settings to use.
724
        $userinfo = $this->get_setting_value('users');
725
        $history = $this->get_setting_value('grade_histories');
726
 
727
        if ($userinfo && $history) {
728
            $paths[] = new restore_path_element('grade_grade',
729
               '/grade_history/grade_grades/grade_grade');
730
        }
731
 
732
        return $paths;
733
    }
734
 
735
    protected function process_grade_grade($data) {
736
        global $DB;
737
 
738
        $data = (object)($data);
739
        $olduserid = $data->userid;
740
        unset($data->id);
741
 
742
        $data->userid = $this->get_mappingid('user', $data->userid, null);
743
        if (!empty($data->userid)) {
744
            // Do not apply the date offsets as this is history.
745
            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
746
            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
747
            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
748
            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
749
            $DB->insert_record('grade_grades_history', $data);
750
        } else {
751
            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
752
            $this->log($message, backup::LOG_DEBUG);
753
        }
754
    }
755
 
756
}
757
 
758
/**
759
 * decode all the interlinks present in restored content
760
 * relying 100% in the restore_decode_processor that handles
761
 * both the contents to modify and the rules to be applied
762
 */
763
class restore_decode_interlinks extends restore_execution_step {
764
 
765
    protected function define_execution() {
766
        // Get the decoder (from the plan)
767
        /** @var restore_decode_processor $decoder */
768
        $decoder = $this->task->get_decoder();
769
        restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
770
        // And launch it, everything will be processed
771
        $decoder->execute();
772
    }
773
}
774
 
775
/**
776
 * first, ensure that we have no gaps in section numbers
777
 * and then, rebuid the course cache
778
 */
779
class restore_rebuild_course_cache extends restore_execution_step {
780
 
781
    protected function define_execution() {
782
        global $DB;
783
 
784
        // Although there is some sort of auto-recovery of missing sections
785
        // present in course/formats... here we check that all the sections
786
        // from 0 to MAX(section->section) exist, creating them if necessary
787
        $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
788
        // Iterate over all sections
789
        for ($i = 0; $i <= $maxsection; $i++) {
790
            // If the section $i doesn't exist, create it
791
            if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
792
                $sectionrec = array(
793
                    'course' => $this->get_courseid(),
794
                    'section' => $i,
795
                    'timemodified' => time());
796
                $DB->insert_record('course_sections', $sectionrec); // missing section created
797
            }
798
        }
799
 
800
        // Rebuild cache now that all sections are in place
801
        rebuild_course_cache($this->get_courseid());
802
        cache_helper::purge_by_event('changesincourse');
803
        cache_helper::purge_by_event('changesincoursecat');
804
    }
805
}
806
 
807
/**
808
 * Review all the tasks having one after_restore method
809
 * executing it to perform some final adjustments of information
810
 * not available when the task was executed.
811
 */
812
class restore_execute_after_restore extends restore_execution_step {
813
 
814
    protected function define_execution() {
815
 
816
        // Simply call to the execute_after_restore() method of the task
817
        // that always is the restore_final_task
818
        $this->task->launch_execute_after_restore();
819
    }
820
}
821
 
822
 
823
/**
824
 * Review all the (pending) block positions in backup_ids, matching by
825
 * contextid, creating positions as needed. This is executed by the
826
 * final task, once all the contexts have been created
827
 */
828
class restore_review_pending_block_positions extends restore_execution_step {
829
 
830
    protected function define_execution() {
831
        global $DB;
832
 
833
        // Get all the block_position objects pending to match
834
        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
835
        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
836
        // Process block positions, creating them or accumulating for final step
837
        foreach($rs as $posrec) {
838
            // Get the complete position object out of the info field.
839
            $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
840
            // If position is for one already mapped (known) contextid
841
            // process it now, creating the position, else nothing to
842
            // do, position finally discarded
843
            if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
844
                $position->contextid = $newctx->newitemid;
845
                // Create the block position
846
                $DB->insert_record('block_positions', $position);
847
            }
848
        }
849
        $rs->close();
850
    }
851
}
852
 
853
 
854
/**
855
 * Updates the availability data for course modules and sections.
856
 *
857
 * Runs after the restore of all course modules, sections, and grade items has
858
 * completed. This is necessary in order to update IDs that have changed during
859
 * restore.
860
 *
861
 * @package core_backup
862
 * @copyright 2014 The Open University
863
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
864
 */
865
class restore_update_availability extends restore_execution_step {
866
 
867
    protected function define_execution() {
868
        global $CFG, $DB;
869
 
870
        // Note: This code runs even if availability is disabled when restoring.
871
        // That will ensure that if you later turn availability on for the site,
872
        // there will be no incorrect IDs. (It doesn't take long if the restored
873
        // data does not contain any availability information.)
874
 
875
        // Get modinfo with all data after resetting cache.
876
        rebuild_course_cache($this->get_courseid(), true);
877
        $modinfo = get_fast_modinfo($this->get_courseid());
878
 
879
        // Get the date offset for this restore.
880
        $dateoffset = $this->apply_date_offset(1) - 1;
881
 
882
        // Update all sections that were restored.
883
        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
884
        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
885
        $sectionsbyid = null;
886
        foreach ($rs as $rec) {
887
            if (is_null($sectionsbyid)) {
888
                $sectionsbyid = array();
889
                foreach ($modinfo->get_section_info_all() as $section) {
890
                    $sectionsbyid[$section->id] = $section;
891
                }
892
            }
893
            if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
894
                // If the section was not fully restored for some reason
895
                // (e.g. due to an earlier error), skip it.
896
                $this->get_logger()->process('Section not fully restored: id ' .
897
                        $rec->newitemid, backup::LOG_WARNING);
898
                continue;
899
            }
900
            $section = $sectionsbyid[$rec->newitemid];
901
            if (!is_null($section->availability)) {
902
                $info = new \core_availability\info_section($section);
903
                $info->update_after_restore($this->get_restoreid(),
904
                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
905
            }
906
        }
907
        $rs->close();
908
 
909
        // Update all modules that were restored.
910
        $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
911
        $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
912
        foreach ($rs as $rec) {
913
            if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
914
                // If the module was not fully restored for some reason
915
                // (e.g. due to an earlier error), skip it.
916
                $this->get_logger()->process('Module not fully restored: id ' .
917
                        $rec->newitemid, backup::LOG_WARNING);
918
                continue;
919
            }
920
            $cm = $modinfo->get_cm($rec->newitemid);
921
            if (!is_null($cm->availability)) {
922
                $info = new \core_availability\info_module($cm);
923
                $info->update_after_restore($this->get_restoreid(),
924
                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
925
            }
926
        }
927
        $rs->close();
928
    }
929
}
930
 
931
 
932
/**
933
 * Process legacy module availability records in backup_ids.
934
 *
935
 * Matches course modules and grade item id once all them have been already restored.
936
 * Only if all matchings are satisfied the availability condition will be created.
937
 * At the same time, it is required for the site to have that functionality enabled.
938
 *
939
 * This step is included only to handle legacy backups (2.6 and before). It does not
940
 * do anything for newer backups.
941
 *
942
 * @copyright 2014 The Open University
943
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
944
 */
945
class restore_process_course_modules_availability extends restore_execution_step {
946
 
947
    protected function define_execution() {
948
        global $CFG, $DB;
949
 
950
        // Site hasn't availability enabled
951
        if (empty($CFG->enableavailability)) {
952
            return;
953
        }
954
 
955
        // Do both modules and sections.
956
        foreach (array('module', 'section') as $table) {
957
            // Get all the availability objects to process.
958
            $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
959
            $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
960
            // Process availabilities, creating them if everything matches ok.
961
            foreach ($rs as $availrec) {
962
                $allmatchesok = true;
963
                // Get the complete legacy availability object.
964
                $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
965
 
966
                // Note: This code used to update IDs, but that is now handled by the
967
                // current code (after restore) instead of this legacy code.
968
 
969
                // Get showavailability option.
970
                $thingid = ($table === 'module') ? $availability->coursemoduleid :
971
                        $availability->coursesectionid;
972
                $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
973
                        $table . '_showavailability', $thingid);
974
                if (!$showrec) {
975
                    // Should not happen.
976
                    throw new coding_exception('No matching showavailability record');
977
                }
978
                $show = $showrec->info->showavailability;
979
 
980
                // The $availability object is now in the format used in the old
981
                // system. Interpret this and convert to new system.
982
                $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
983
                        array('id' => $thingid), MUST_EXIST);
984
                $newvalue = \core_availability\info::add_legacy_availability_condition(
985
                        $currentvalue, $availability, $show);
986
                $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
987
                        array('id' => $thingid));
988
            }
989
            $rs->close();
990
        }
991
    }
992
}
993
 
994
 
995
/*
996
 * Execution step that, *conditionally* (if there isn't preloaded information)
997
 * will load the inforef files for all the included course/section/activity tasks
998
 * to backup_temp_ids. They will be stored with "xxxxref" as itemname
999
 */
1000
class restore_load_included_inforef_records extends restore_execution_step {
1001
 
1002
    protected function define_execution() {
1003
 
1004
        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1005
            return;
1006
        }
1007
 
1008
        // Get all the included tasks
1009
        $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
1010
        $progress = $this->task->get_progress();
1011
        $progress->start_progress($this->get_name(), count($tasks));
1012
        foreach ($tasks as $task) {
1013
            // Load the inforef.xml file if exists
1014
            $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
1015
            if (file_exists($inforefpath)) {
1016
                // Load each inforef file to temp_ids.
1017
                restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
1018
            }
1019
        }
1020
        $progress->end_progress();
1021
    }
1022
}
1023
 
1024
/*
1025
 * Execution step that will load all the needed files into backup_files_temp
1026
 *   - info: contains the whole original object (times, names...)
1027
 * (all them being original ids as loaded from xml)
1028
 */
1029
class restore_load_included_files extends restore_structure_step {
1030
 
1031
    protected function define_structure() {
1032
 
1033
        $file = new restore_path_element('file', '/files/file');
1034
 
1035
        return array($file);
1036
    }
1037
 
1038
    /**
1039
     * Process one <file> element from files.xml
1040
     *
1041
     * @param array $data the element data
1042
     */
1043
    public function process_file($data) {
1044
 
1045
        $data = (object)$data; // handy
1046
 
1047
        // load it if needed:
1048
        //   - it it is one of the annotated inforef files (course/section/activity/block)
1049
        //   - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
1050
        // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
1051
        //       but then we'll need to change it to load plugins itself (because this is executed too early in restore)
1052
        $isfileref   = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
1053
        $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
1054
                        $data->component == 'grouping' || $data->component == 'grade' ||
1055
                        $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
1056
        if ($isfileref || $iscomponent) {
1057
            restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
1058
        }
1059
    }
1060
}
1061
 
1062
/**
1063
 * Execution step that, *conditionally* (if there isn't preloaded information),
1064
 * will load all the needed roles to backup_temp_ids. They will be stored with
1065
 * "role" itemname. Also it will perform one automatic mapping to roles existing
1066
 * in the target site, based in permissions of the user performing the restore,
1067
 * archetypes and other bits. At the end, each original role will have its associated
1068
 * target role or 0 if it's going to be skipped. Note we wrap everything over one
1069
 * restore_dbops method, as far as the same stuff is going to be also executed
1070
 * by restore prechecks
1071
 */
1072
class restore_load_and_map_roles extends restore_execution_step {
1073
 
1074
    protected function define_execution() {
1075
        if ($this->task->get_preloaded_information()) { // if info is already preloaded
1076
            return;
1077
        }
1078
 
1079
        $file = $this->get_basepath() . '/roles.xml';
1080
        // Load needed toles to temp_ids
1081
        restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
1082
 
1083
        // Process roles, mapping/skipping. Any error throws exception
1084
        // Note we pass controller's info because it can contain role mapping information
1085
        // about manual mappings performed by UI
1086
        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);
1087
    }
1088
}
1089
 
1090
/**
1091
 * Execution step that, *conditionally* (if there isn't preloaded information
1092
 * and users have been selected in settings, will load all the needed users
1093
 * to backup_temp_ids. They will be stored with "user" itemname and with
1094
 * their original contextid as paremitemid
1095
 */
1096
class restore_load_included_users extends restore_execution_step {
1097
 
1098
    protected function define_execution() {
1099
 
1100
        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1101
            return;
1102
        }
1103
        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1104
            return;
1105
        }
1106
        $file = $this->get_basepath() . '/users.xml';
1107
        // Load needed users to temp_ids.
1108
        restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
1109
    }
1110
}
1111
 
1112
/**
1113
 * Execution step that, *conditionally* (if there isn't preloaded information
1114
 * and users have been selected in settings, will process all the needed users
1115
 * in order to decide and perform any action with them (create / map / error)
1116
 * Note: Any error will cause exception, as far as this is the same processing
1117
 * than the one into restore prechecks (that should have stopped process earlier)
1118
 */
1119
class restore_process_included_users extends restore_execution_step {
1120
 
1121
    protected function define_execution() {
1122
 
1123
        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1124
            return;
1125
        }
1126
        if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
1127
            return;
1128
        }
1129
        restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
1130
                $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
1131
    }
1132
}
1133
 
1134
/**
1135
 * Execution step that will create all the needed users as calculated
1136
 * by @restore_process_included_users (those having newiteind = 0)
1137
 */
1138
class restore_create_included_users extends restore_execution_step {
1139
 
1140
    protected function define_execution() {
1141
 
1142
        restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
1143
                $this->task->get_userid(), $this->task->get_progress(), $this->task->get_courseid());
1144
    }
1145
}
1146
 
1147
/**
1148
 * Structure step that will create all the needed groups and groupings
1149
 * by loading them from the groups.xml file performing the required matches.
1150
 * Note group members only will be added if restoring user info
1151
 */
1152
class restore_groups_structure_step extends restore_structure_step {
1153
 
1154
    protected function define_structure() {
1155
 
1156
        $paths = array(); // Add paths here
1157
 
1158
        // Do not include group/groupings information if not requested.
1159
        $groupinfo = $this->get_setting_value('groups');
1160
        if ($groupinfo) {
1161
            $paths[] = new restore_path_element('group', '/groups/group');
1162
            $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
1163
            $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
1441 ariadna 1164
 
1165
            // Custom fields.
1166
            if ($this->get_setting_value('customfield')) {
1167
                $paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield');
1168
                $paths[] = new restore_path_element('groupingcustomfield',
1169
                    '/groups/groupings/groupingcustomfields/groupingcustomfield');
1170
            }
1 efrain 1171
        }
1172
        return $paths;
1173
    }
1174
 
1175
    // Processing functions go here
1176
    public function process_group($data) {
1177
        global $DB;
1178
 
1179
        $data = (object)$data; // handy
1180
        $data->courseid = $this->get_courseid();
1181
 
1182
        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1183
        // another a group in the same course
1184
        $context = context_course::instance($data->courseid);
1185
        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1186
            if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
1187
                unset($data->idnumber);
1188
            }
1189
        } else {
1190
            unset($data->idnumber);
1191
        }
1192
 
1193
        $oldid = $data->id;    // need this saved for later
1194
 
1195
        $restorefiles = false; // Only if we end creating the group
1196
 
1197
        // This is for backwards compatibility with old backups. If the backup data for a group contains a non-empty value of
1198
        // hidepicture, then we'll exclude this group's picture from being restored.
1199
        if (!empty($data->hidepicture)) {
1200
            // Exclude the group picture from being restored if hidepicture is set to 1 in the backup data.
1201
            unset($data->picture);
1202
        }
1203
 
1204
        // Search if the group already exists (by name & description) in the target course
1205
        $description_clause = '';
1206
        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1207
        if (!empty($data->description)) {
1208
            $description_clause = ' AND ' .
1209
                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1210
           $params['description'] = $data->description;
1211
        }
1212
        if (!$groupdb = $DB->get_record_sql("SELECT *
1213
                                               FROM {groups}
1214
                                              WHERE courseid = :courseid
1215
                                                AND name = :grname $description_clause", $params)) {
1216
            // group doesn't exist, create
1217
            $newitemid = $DB->insert_record('groups', $data);
1218
            $restorefiles = true; // We'll restore the files
1219
        } else {
1220
            // group exists, use it
1221
            $newitemid = $groupdb->id;
1222
        }
1223
        // Save the id mapping
1224
        $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
1225
 
1226
        // Add the related group picture file if it's available at this point.
1227
        if (!empty($data->picture)) {
1228
            $this->add_related_files('group', 'icon', 'group', null, $oldid);
1229
        }
1230
 
1231
        // Invalidate the course group data cache just in case.
1232
        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1233
    }
1234
 
1235
    /**
1236
     * Restore group custom field values.
1237
     * @param array $data data for group custom field
1238
     * @return void
1239
     */
1240
    public function process_groupcustomfield($data) {
1241
        $newgroup = $this->get_mapping('group', $data['groupid']);
1441 ariadna 1242
        if ($newgroup && $newgroup->newitemid) {
1243
            $data['groupid'] = $newgroup->newitemid;
1244
            $handler = \core_group\customfield\group_handler::create();
1245
            $handler->restore_instance_data_from_backup($this->task, $data);
1246
        }
1 efrain 1247
    }
1248
 
1249
    public function process_grouping($data) {
1250
        global $DB;
1251
 
1252
        $data = (object)$data; // handy
1253
        $data->courseid = $this->get_courseid();
1254
 
1255
        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
1256
        // another a grouping in the same course
1257
        $context = context_course::instance($data->courseid);
1258
        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
1259
            if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
1260
                unset($data->idnumber);
1261
            }
1262
        } else {
1263
            unset($data->idnumber);
1264
        }
1265
 
1266
        $oldid = $data->id;    // need this saved for later
1267
        $restorefiles = false; // Only if we end creating the grouping
1268
 
1269
        // Search if the grouping already exists (by name & description) in the target course
1270
        $description_clause = '';
1271
        $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
1272
        if (!empty($data->description)) {
1273
            $description_clause = ' AND ' .
1274
                                  $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
1275
           $params['description'] = $data->description;
1276
        }
1277
        if (!$groupingdb = $DB->get_record_sql("SELECT *
1278
                                                  FROM {groupings}
1279
                                                 WHERE courseid = :courseid
1280
                                                   AND name = :grname $description_clause", $params)) {
1281
            // grouping doesn't exist, create
1282
            $newitemid = $DB->insert_record('groupings', $data);
1283
            $restorefiles = true; // We'll restore the files
1284
        } else {
1285
            // grouping exists, use it
1286
            $newitemid = $groupingdb->id;
1287
        }
1288
        // Save the id mapping
1289
        $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
1290
        // Invalidate the course group data cache just in case.
1291
        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
1292
    }
1293
 
1294
    /**
1295
     * Restore grouping custom field values.
1296
     * @param array $data data for grouping custom field
1297
     * @return void
1298
     */
1299
    public function process_groupingcustomfield($data) {
1441 ariadna 1300
        $newgrouping = $this->get_mapping('grouping', $data['groupingid']);
1301
        if ($newgrouping && $newgrouping->newitemid) {
1302
            $data['groupingid'] = $newgrouping->newitemid;
1303
            $handler = \core_group\customfield\grouping_handler::create();
1304
            $handler->restore_instance_data_from_backup($this->task, $data);
1305
        }
1 efrain 1306
    }
1307
 
1308
    public function process_grouping_group($data) {
1309
        global $CFG;
1310
 
1311
        require_once($CFG->dirroot.'/group/lib.php');
1312
 
1313
        $data = (object)$data;
1314
        groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
1315
    }
1316
 
1317
    protected function after_execute() {
1318
        // Add group related files, matching with "group" mappings.
1319
        $this->add_related_files('group', 'description', 'group');
1320
        // Add grouping related files, matching with "grouping" mappings
1321
        $this->add_related_files('grouping', 'description', 'grouping');
1322
        // Invalidate the course group data.
1323
        cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
1324
    }
1325
 
1326
}
1327
 
1328
/**
1329
 * Structure step that will create all the needed group memberships
1330
 * by loading them from the groups.xml file performing the required matches.
1331
 */
1332
class restore_groups_members_structure_step extends restore_structure_step {
1333
 
1334
    protected $plugins = null;
1335
 
1336
    protected function define_structure() {
1337
 
1338
        $paths = array(); // Add paths here
1339
 
1340
        if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
1341
            $paths[] = new restore_path_element('group', '/groups/group');
1342
            $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
1343
        }
1344
 
1345
        return $paths;
1346
    }
1347
 
1348
    public function process_group($data) {
1349
        $data = (object)$data; // handy
1350
 
1351
        // HACK ALERT!
1352
        // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
1353
        // Let's fake internal state to make $this->get_new_parentid('group') work.
1354
 
1355
        $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
1356
    }
1357
 
1358
    public function process_member($data) {
1359
        global $DB, $CFG;
1360
        require_once("$CFG->dirroot/group/lib.php");
1361
 
1362
        // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
1363
 
1364
        $data = (object)$data; // handy
1365
 
1366
        // get parent group->id
1367
        $data->groupid = $this->get_new_parentid('group');
1368
 
1369
        // map user newitemid and insert if not member already
1370
        if ($data->userid = $this->get_mappingid('user', $data->userid)) {
1371
            if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
1372
                // Check the component, if any, exists.
1373
                if (empty($data->component)) {
1374
                    groups_add_member($data->groupid, $data->userid);
1375
 
1376
                } else if ((strpos($data->component, 'enrol_') === 0)) {
1377
                    // Deal with enrolment groups - ignore the component and just find out the instance via new id,
1378
                    // it is possible that enrolment was restored using different plugin type.
1379
                    if (!isset($this->plugins)) {
1380
                        $this->plugins = enrol_get_plugins(true);
1381
                    }
1382
                    if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
1383
                        if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
1384
                            if (isset($this->plugins[$instance->enrol])) {
1385
                                $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
1386
                            }
1387
                        }
1388
                    }
1389
 
1390
                } else {
1391
                    $dir = core_component::get_component_directory($data->component);
1392
                    if ($dir and is_dir($dir)) {
1393
                        if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
1394
                            return;
1395
                        }
1396
                    }
1397
                    // Bad luck, plugin could not restore the data, let's add normal membership.
1398
                    groups_add_member($data->groupid, $data->userid);
1399
                    $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
1400
                    $this->log($message, backup::LOG_WARNING);
1401
                }
1402
            }
1403
        }
1404
    }
1405
}
1406
 
1407
/**
1408
 * Structure step that will create all the needed scales
1409
 * by loading them from the scales.xml
1410
 */
1411
class restore_scales_structure_step extends restore_structure_step {
1412
 
1413
    protected function define_structure() {
1414
 
1415
        $paths = array(); // Add paths here
1416
        $paths[] = new restore_path_element('scale', '/scales_definition/scale');
1417
        return $paths;
1418
    }
1419
 
1420
    protected function process_scale($data) {
1421
        global $DB;
1422
 
1423
        $data = (object)$data;
1424
 
1425
        $restorefiles = false; // Only if we end creating the group
1426
 
1427
        $oldid = $data->id;    // need this saved for later
1428
 
1429
        // Look for scale (by 'scale' both in standard (course=0) and current course
1430
        // with priority to standard scales (ORDER clause)
1431
        // scale is not course unique, use get_record_sql to suppress warning
1432
        // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
1433
        $compare_scale_clause = $DB->sql_compare_text('scale')  . ' = ' . $DB->sql_compare_text(':scaledesc');
1434
        $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
1435
        if (!$scadb = $DB->get_record_sql("SELECT *
1436
                                            FROM {scale}
1437
                                           WHERE courseid IN (0, :courseid)
1438
                                             AND $compare_scale_clause
1439
                                        ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
1440
            // Remap the user if possible, defaut to user performing the restore if not
1441
            $userid = $this->get_mappingid('user', $data->userid);
1442
            $data->userid = $userid ? $userid : $this->task->get_userid();
1443
            // Remap the course if course scale
1444
            $data->courseid = $data->courseid ? $this->get_courseid() : 0;
1445
            // If global scale (course=0), check the user has perms to create it
1446
            // falling to course scale if not
1447
            $systemctx = context_system::instance();
1448
            if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
1449
                $data->courseid = $this->get_courseid();
1450
            }
1451
            // scale doesn't exist, create
1452
            $newitemid = $DB->insert_record('scale', $data);
1453
            $restorefiles = true; // We'll restore the files
1454
        } else {
1455
            // scale exists, use it
1456
            $newitemid = $scadb->id;
1457
        }
1458
        // Save the id mapping (with files support at system context)
1459
        $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1460
    }
1461
 
1462
    protected function after_execute() {
1463
        // Add scales related files, matching with "scale" mappings
1464
        $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
1465
    }
1466
}
1467
 
1468
 
1469
/**
1470
 * Structure step that will create all the needed outocomes
1471
 * by loading them from the outcomes.xml
1472
 */
1473
class restore_outcomes_structure_step extends restore_structure_step {
1474
 
1475
    protected function define_structure() {
1476
 
1477
        $paths = array(); // Add paths here
1478
        $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
1479
        return $paths;
1480
    }
1481
 
1482
    protected function process_outcome($data) {
1483
        global $DB;
1484
 
1485
        $data = (object)$data;
1486
 
1487
        $restorefiles = false; // Only if we end creating the group
1488
 
1489
        $oldid = $data->id;    // need this saved for later
1490
 
1491
        // Look for outcome (by shortname both in standard (courseid=null) and current course
1492
        // with priority to standard outcomes (ORDER clause)
1493
        // outcome is not course unique, use get_record_sql to suppress warning
1494
        $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
1495
        if (!$outdb = $DB->get_record_sql('SELECT *
1496
                                             FROM {grade_outcomes}
1497
                                            WHERE shortname = :shortname
1498
                                              AND (courseid = :courseid OR courseid IS NULL)
1499
                                         ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
1500
            // Remap the user
1501
            $userid = $this->get_mappingid('user', $data->usermodified);
1502
            $data->usermodified = $userid ? $userid : $this->task->get_userid();
1503
            // Remap the scale
1504
            $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
1505
            // Remap the course if course outcome
1506
            $data->courseid = $data->courseid ? $this->get_courseid() : null;
1507
            // If global outcome (course=null), check the user has perms to create it
1508
            // falling to course outcome if not
1509
            $systemctx = context_system::instance();
1510
            if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
1511
                $data->courseid = $this->get_courseid();
1512
            }
1513
            // outcome doesn't exist, create
1514
            $newitemid = $DB->insert_record('grade_outcomes', $data);
1515
            $restorefiles = true; // We'll restore the files
1516
        } else {
1517
            // scale exists, use it
1518
            $newitemid = $outdb->id;
1519
        }
1520
        // Set the corresponding grade_outcomes_courses record
1521
        $outcourserec = new stdclass();
1522
        $outcourserec->courseid  = $this->get_courseid();
1523
        $outcourserec->outcomeid = $newitemid;
1524
        if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
1525
            $DB->insert_record('grade_outcomes_courses', $outcourserec);
1526
        }
1527
        // Save the id mapping (with files support at system context)
1528
        $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
1529
    }
1530
 
1531
    protected function after_execute() {
1532
        // Add outcomes related files, matching with "outcome" mappings
1533
        $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
1534
    }
1535
}
1536
 
1537
/**
1538
 * Execution step that, *conditionally* (if there isn't preloaded information
1539
 * will load all the question categories and questions (header info only)
1540
 * to backup_temp_ids. They will be stored with "question_category" and
1541
 * "question" itemnames and with their original contextid and question category
1542
 * id as paremitemids
1543
 */
1544
class restore_load_categories_and_questions extends restore_execution_step {
1545
 
1546
    protected function define_execution() {
1547
 
1548
        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1549
            return;
1550
        }
1551
        $file = $this->get_basepath() . '/questions.xml';
1552
        restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
1553
    }
1554
}
1555
 
1556
/**
1557
 * Execution step that, *conditionally* (if there isn't preloaded information)
1558
 * will process all the needed categories and questions
1559
 * in order to decide and perform any action with them (create / map / error)
1560
 * Note: Any error will cause exception, as far as this is the same processing
1561
 * than the one into restore prechecks (that should have stopped process earlier)
1562
 */
1563
class restore_process_categories_and_questions extends restore_execution_step {
1564
 
1565
    protected function define_execution() {
1566
 
1567
        if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
1568
            return;
1569
        }
1570
        restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
1571
    }
1572
}
1573
 
1574
/**
1575
 * Structure step that will read the section.xml creating/updating sections
1576
 * as needed, rebuilding course cache and other friends
1577
 */
1578
class restore_section_structure_step extends restore_structure_step {
1579
    /** @var array Cache: Array of id => course format */
1580
    private static $courseformats = array();
1581
 
1582
    /**
1583
     * Resets a static cache of course formats. Required for unit testing.
1584
     */
1585
    public static function reset_caches() {
1586
        self::$courseformats = array();
1587
    }
1588
 
1589
    protected function define_structure() {
1590
        global $CFG;
1591
 
1592
        $paths = array();
1593
 
1594
        $section = new restore_path_element('section', '/section');
1595
        $paths[] = $section;
1596
        if ($CFG->enableavailability) {
1597
            $paths[] = new restore_path_element('availability', '/section/availability');
1598
            $paths[] = new restore_path_element('availability_field', '/section/availability_field');
1599
        }
1600
        $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
1601
 
1602
        // Apply for 'format' plugins optional paths at section level
1603
        $this->add_plugin_structure('format', $section);
1604
 
1605
        // Apply for 'local' plugins optional paths at section level
1606
        $this->add_plugin_structure('local', $section);
1607
 
1608
        return $paths;
1609
    }
1610
 
1611
    public function process_section($data) {
1612
        global $CFG, $DB;
1613
        $data = (object)$data;
1614
        $oldid = $data->id; // We'll need this later
1615
 
1616
        $restorefiles = false;
1617
 
1618
        // Look for the section
1619
        $section = new stdclass();
1620
        $section->course  = $this->get_courseid();
1621
        $section->section = $data->number;
1622
        $section->timemodified = $data->timemodified ?? 0;
1441 ariadna 1623
        $section->component = null;
1624
        $section->itemid = null;
1625
 
1626
        $secrec = $DB->get_record(
1627
            'course_sections',
1628
            ['course' => $this->get_courseid(), 'section' => $data->number, 'component' => null]
1629
        );
1630
        $createsection = empty($secrec);
1631
 
1632
        // Delegated sections are always restored as new sections.
1633
        if (!empty($data->component)) {
1634
            $section->itemid = $this->get_delegated_section_mapping($data->component, $data->itemid);
1635
            // If the delegate component does not set the mapping id, the section must be converted
1636
            // into a regular section. Otherwise, it won't be accessible.
1637
            $createsection = $createsection || $section->itemid !== null;
1638
            $section->component = ($section->itemid !== null) ? $data->component : null;
1639
            // The section number will be always the last of the course, no matter the case.
1640
            $section->section = $this->get_last_section_number($this->get_courseid()) + 1;
1641
 
1642
        }
1 efrain 1643
        // Section doesn't exist, create it with all the info from backup
1441 ariadna 1644
        if ($createsection) {
1 efrain 1645
            $section->name = $data->name;
1646
            $section->summary = $data->summary;
1647
            $section->summaryformat = $data->summaryformat;
1648
            $section->sequence = '';
1649
            $section->visible = $data->visible;
1650
            if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
1651
                $section->availability = null;
1652
            } else {
1653
                $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
1654
                // Include legacy [<2.7] availability data if provided.
1655
                if (is_null($section->availability)) {
1656
                    $section->availability = \core_availability\info::convert_legacy_fields(
1657
                            $data, true);
1658
                }
1659
            }
1441 ariadna 1660
 
1661
            // Delegated sections should be always after the normal sections.
1662
            $this->displace_delegated_sections_after($section->section);
1663
 
1 efrain 1664
            $newitemid = $DB->insert_record('course_sections', $section);
1665
            $section->id = $newitemid;
1666
 
1667
            core\event\course_section_created::create_from_section($section)->trigger();
1668
 
1669
            $restorefiles = true;
1670
 
1671
        // Section exists, update non-empty information
1672
        } else {
1673
            $section->id = $secrec->id;
1674
            if ((string)$secrec->name === '') {
1675
                $section->name = $data->name;
1676
            }
1677
            if (empty($secrec->summary)) {
1678
                $section->summary = $data->summary;
1679
                $section->summaryformat = $data->summaryformat;
1680
                $restorefiles = true;
1681
            }
1682
 
1683
            // Don't update availability (I didn't see a useful way to define
1684
            // whether existing or new one should take precedence).
1685
 
1686
            $DB->update_record('course_sections', $section);
1687
            $newitemid = $secrec->id;
1688
 
1689
            // Trigger an event for course section update.
1690
            $event = \core\event\course_section_updated::create(
1691
                array(
1692
                    'objectid' => $section->id,
1693
                    'courseid' => $section->course,
1694
                    'context' => context_course::instance($section->course),
1695
                    'other' => array('sectionnum' => $section->section)
1696
                )
1697
            );
1698
            $event->trigger();
1699
        }
1700
 
1701
        // Annotate the section mapping, with restorefiles option if needed
1702
        $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
1703
 
1704
        // set the new course_section id in the task
1705
        $this->task->set_sectionid($newitemid);
1706
 
1707
        // If there is the legacy showavailability data, store this for later use.
1708
        // (This data is not present when restoring 'new' backups.)
1709
        if (isset($data->showavailability)) {
1710
            // Cache the showavailability flag using the backup_ids data field.
1711
            restore_dbops::set_backup_ids_record($this->get_restoreid(),
1712
                    'section_showavailability', $newitemid, 0, null,
1713
                    (object)array('showavailability' => $data->showavailability));
1714
        }
1715
 
1716
        // Commented out. We never modify course->numsections as far as that is used
1717
        // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
1718
        // Note: We keep the code here, to know about and because of the possibility of making this
1719
        // optional based on some setting/attribute in the future
1720
        // If needed, adjust course->numsections
1721
        //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
1722
        //    if ($numsections < $section->section) {
1723
        //        $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
1724
        //    }
1725
        //}
1726
    }
1727
 
1728
    /**
1729
     * Process the legacy availability table record. This table does not exist
1730
     * in Moodle 2.7+ but we still support restore.
1731
     *
1732
     * @param stdClass $data Record data
1733
     */
1734
    public function process_availability($data) {
1735
        $data = (object)$data;
1736
        // Simply going to store the whole availability record now, we'll process
1737
        // all them later in the final task (once all activities have been restored)
1738
        // Let's call the low level one to be able to store the whole object.
1739
        $data->coursesectionid = $this->task->get_sectionid();
1740
        restore_dbops::set_backup_ids_record($this->get_restoreid(),
1741
                'section_availability', $data->id, 0, null, $data);
1742
    }
1743
 
1744
    /**
1745
     * Process the legacy availability fields table record. This table does not
1746
     * exist in Moodle 2.7+ but we still support restore.
1747
     *
1748
     * @param stdClass $data Record data
1749
     */
1750
    public function process_availability_field($data) {
1751
        global $DB, $CFG;
1752
        require_once($CFG->dirroot.'/user/profile/lib.php');
1753
 
1754
        $data = (object)$data;
1755
        // Mark it is as passed by default
1756
        $passed = true;
1757
        $customfieldid = null;
1758
 
1759
        // If a customfield has been used in order to pass we must be able to match an existing
1760
        // customfield by name (data->customfield) and type (data->customfieldtype)
1761
        if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
1762
            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
1763
            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
1764
            $passed = false;
1765
        } else if (!is_null($data->customfield)) {
1766
            $field = profile_get_custom_field_data_by_shortname($data->customfield);
1767
            $passed = $field && $field->datatype == $data->customfieldtype;
1768
        }
1769
 
1770
        if ($passed) {
1771
            // Create the object to insert into the database
1772
            $availfield = new stdClass();
1773
            $availfield->coursesectionid = $this->task->get_sectionid();
1774
            $availfield->userfield = $data->userfield;
1775
            $availfield->customfieldid = $customfieldid;
1776
            $availfield->operator = $data->operator;
1777
            $availfield->value = $data->value;
1778
 
1779
            // Get showavailability option.
1780
            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
1781
                    'section_showavailability', $availfield->coursesectionid);
1782
            if (!$showrec) {
1783
                // Should not happen.
1784
                throw new coding_exception('No matching showavailability record');
1785
            }
1786
            $show = $showrec->info->showavailability;
1787
 
1788
            // The $availfield object is now in the format used in the old
1789
            // system. Interpret this and convert to new system.
1790
            $currentvalue = $DB->get_field('course_sections', 'availability',
1791
                    array('id' => $availfield->coursesectionid), MUST_EXIST);
1792
            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
1793
                    $currentvalue, $availfield, $show);
1794
 
1795
            $section = new stdClass();
1796
            $section->id = $availfield->coursesectionid;
1797
            $section->availability = $newvalue;
1798
            $section->timemodified = time();
1799
            $DB->update_record('course_sections', $section);
1800
        }
1801
    }
1802
 
1803
    public function process_course_format_options($data) {
1804
        global $DB;
1805
        $courseid = $this->get_courseid();
1806
        if (!array_key_exists($courseid, self::$courseformats)) {
1807
            // It is safe to have a static cache of course formats because format can not be changed after this point.
1808
            self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
1809
        }
1810
        $data = (array)$data;
1811
        if (self::$courseformats[$courseid] === $data['format']) {
1812
            // Import section format options only if both courses (the one that was backed up
1813
            // and the one we are restoring into) have same formats.
1814
            $params = array(
1815
                'courseid' => $this->get_courseid(),
1816
                'sectionid' => $this->task->get_sectionid(),
1817
                'format' => $data['format'],
1818
                'name' => $data['name']
1819
            );
1820
            if ($record = $DB->get_record('course_format_options', $params, 'id, value')) {
1821
                // Do not overwrite existing information.
1822
                $newid = $record->id;
1823
            } else {
1824
                $params['value'] = $data['value'];
1825
                $newid = $DB->insert_record('course_format_options', $params);
1826
            }
1827
            $this->set_mapping('course_format_options', $data['id'], $newid);
1828
        }
1829
    }
1830
 
1831
    protected function after_execute() {
1832
        // Add section related files, with 'course_section' itemid to match
1833
        $this->add_related_files('course', 'section', 'course_section');
1834
    }
1441 ariadna 1835
 
1836
    /**
1837
     * Create a delegate section mapping.
1838
     *
1839
     * @param string $component the component name (frankenstyle)
1840
     * @param int $oldsectionid The old section id.
1841
     * @return int|null The new section id or null if not found.
1842
     */
1843
    protected function get_delegated_section_mapping($component, $oldsectionid): ?int {
1844
        $result = $this->get_mappingid("course_section::$component", $oldsectionid, null);
1845
        return $result;
1846
    }
1847
 
1848
    /**
1849
     * Displace delegated sections after the given section number.
1850
     *
1851
     * @param int $sectionnum The section number.
1852
     */
1853
    protected function displace_delegated_sections_after(int $sectionnum): void {
1854
        global $DB;
1855
 
1856
        $sectionstomove = $DB->get_records_select(
1857
            'course_sections',
1858
            'course = ? AND component IS NOT NULL',
1859
            [$this->get_courseid()],
1860
            'section DESC', 'id, section'
1861
        );
1862
        // Here we add the new section to the end of the list so we make sure that all delegated sections are really
1863
        // all located after the normal sections. We can have case where delegated sections are located before the
1864
        // normal sections, so we need to move them to the end (mostly in the restore process more than in the duplicate
1865
        // process in which the order sections => delegated section is mostly there).
1866
        $sectionnum = $sectionnum + count($sectionstomove);
1867
        foreach ($sectionstomove as $section) {
1868
            $section->section = $sectionnum--;
1869
            $DB->update_record('course_sections', $section);
1870
        }
1871
    }
1872
 
1873
    /**
1874
     * Get the last section number in the course.
1875
     *
1876
     * @param int $courseid The course id.
1877
     * @param bool $includedelegated If true, include delegated sections in the count.
1878
     * @return int The last section number.
1879
     */
1880
    protected function get_last_section_number(int $courseid, bool $includedelegated = false): int {
1881
        global $DB;
1882
 
1883
        $delegtadefilter = $includedelegated ? '' : ' AND component IS NULL';
1884
 
1885
        return (int) $DB->get_field_sql(
1886
            'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter,
1887
            [$courseid]
1888
        );
1889
    }
1 efrain 1890
}
1891
 
1892
/**
1893
 * Structure step that will read the course.xml file, loading it and performing
1894
 * various actions depending of the site/restore settings. Note that target
1895
 * course always exist before arriving here so this step will be updating
1896
 * the course record (never inserting)
1897
 */
1898
class restore_course_structure_step extends restore_structure_step {
1899
    /**
1900
     * @var bool this gets set to true by {@link process_course()} if we are
1901
     * restoring an old coures that used the legacy 'module security' feature.
1902
     * If so, we have to do more work in {@link after_execute()}.
1903
     */
1904
    protected $legacyrestrictmodules = false;
1905
 
1906
    /**
1907
     * @var array Used when {@link $legacyrestrictmodules} is true. This is an
1908
     * array with array keys the module names ('forum', 'quiz', etc.). These are
1909
     * the modules that are allowed according to the data in the backup file.
1910
     * In {@link after_execute()} we then have to prevent adding of all the other
1911
     * types of activity.
1912
     */
1913
    protected $legacyallowedmodules = array();
1914
 
1915
    protected function define_structure() {
1916
 
1441 ariadna 1917
        $paths = [];
1918
 
1 efrain 1919
        $course = new restore_path_element('course', '/course');
1441 ariadna 1920
        $paths[] = $course;
1921
        $paths[] = new restore_path_element('category', '/course/category');
1922
        $paths[] = new restore_path_element('tag', '/course/tags/tag');
1923
        $paths[] = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption');
1924
        $paths[] = new restore_path_element('allowed_module', '/course/allowed_modules/module');
1 efrain 1925
 
1441 ariadna 1926
        // Custom fields.
1927
        if ($this->get_setting_value('customfield')) {
1928
            $paths[] = new restore_path_element('customfield', '/course/customfields/customfield');
1929
        }
1930
 
1 efrain 1931
        // Apply for 'format' plugins optional paths at course level
1932
        $this->add_plugin_structure('format', $course);
1933
 
1934
        // Apply for 'theme' plugins optional paths at course level
1935
        $this->add_plugin_structure('theme', $course);
1936
 
1937
        // Apply for 'report' plugins optional paths at course level
1938
        $this->add_plugin_structure('report', $course);
1939
 
1940
        // Apply for 'course report' plugins optional paths at course level
1941
        $this->add_plugin_structure('coursereport', $course);
1942
 
1943
        // Apply for plagiarism plugins optional paths at course level
1944
        $this->add_plugin_structure('plagiarism', $course);
1945
 
1946
        // Apply for local plugins optional paths at course level
1947
        $this->add_plugin_structure('local', $course);
1948
 
1949
        // Apply for admin tool plugins optional paths at course level.
1950
        $this->add_plugin_structure('tool', $course);
1951
 
1441 ariadna 1952
        return $paths;
1 efrain 1953
    }
1954
 
1955
    /**
1956
     * Processing functions go here
1957
     *
1958
     * @global moodledatabase $DB
1959
     * @param stdClass $data
1960
     */
1961
    public function process_course($data) {
1962
        global $CFG, $DB;
1963
        $context = context::instance_by_id($this->task->get_contextid());
1964
        $userid = $this->task->get_userid();
1965
        $target = $this->get_task()->get_target();
1966
        $isnewcourse = $target == backup::TARGET_NEW_COURSE;
1967
 
1968
        // When restoring to a new course we can set all the things except for the ID number.
1969
        $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid);
1970
        $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid);
1971
        $canforcelanguage = has_capability('moodle/course:setforcedlanguage', $context, $userid);
1972
 
1973
        $data = (object)$data;
1974
        $data->id = $this->get_courseid();
1975
 
1976
        // Calculate final course names, to avoid dupes.
1977
        $fullname  = $this->get_setting_value('course_fullname');
1978
        $shortname = $this->get_setting_value('course_shortname');
1979
        list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names($this->get_courseid(),
1980
            $fullname === false ? $data->fullname : $fullname,
1981
            $shortname === false ? $data->shortname : $shortname);
1982
        // Do not modify the course names at all when merging and user selected to keep the names (or prohibited by cap).
1983
        if (!$isnewcourse && $fullname === false) {
1984
            unset($data->fullname);
1985
        }
1986
        if (!$isnewcourse && $shortname === false) {
1987
            unset($data->shortname);
1988
        }
1989
 
1990
        // Unset summary if user can't change it.
1991
        if (!$canchangesummary) {
1992
            unset($data->summary);
1993
            unset($data->summaryformat);
1994
        }
1995
 
1996
        // Unset lang if user can't change it.
1997
        if (!$canforcelanguage) {
1998
            unset($data->lang);
1999
        }
2000
 
2001
        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
2002
        // another course on this site.
2003
        if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite()
2004
                && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
2005
            // Do not reset idnumber.
2006
 
2007
        } else if (!$isnewcourse) {
2008
            // Prevent override when restoring as merge.
2009
            unset($data->idnumber);
2010
 
2011
        } else {
2012
            $data->idnumber = '';
2013
        }
2014
 
2015
        // If we restore a course from this site, let's capture the original course id.
2016
        if ($isnewcourse && $this->get_task()->is_samesite()) {
2017
            $data->originalcourseid = $this->get_task()->get_old_courseid();
2018
        }
2019
 
2020
        // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
2021
        // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
2022
        if (empty($data->hiddensections)) {
2023
            $data->hiddensections = 0;
2024
        }
2025
 
2026
        // Set legacyrestrictmodules to true if the course was resticting modules. If so
2027
        // then we will need to process restricted modules after execution.
2028
        $this->legacyrestrictmodules = !empty($data->restrictmodules);
2029
 
2030
        $data->startdate= $this->apply_date_offset($data->startdate);
2031
        if (isset($data->enddate)) {
2032
            $data->enddate = $this->apply_date_offset($data->enddate);
2033
        }
2034
 
2035
        if ($data->defaultgroupingid) {
2036
            $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
2037
        }
2038
 
2039
        $courseconfig = get_config('moodlecourse');
2040
 
2041
        if (empty($CFG->enablecompletion)) {
2042
            // Completion is disabled globally.
2043
            $data->enablecompletion = 0;
2044
            $data->completionstartonenrol = 0;
2045
            $data->completionnotify = 0;
2046
            $data->showcompletionconditions = null;
2047
        } else {
2048
            $showcompletionconditionsdefault = ($courseconfig->showcompletionconditions ?? null);
2049
            $data->showcompletionconditions = $data->showcompletionconditions ?? $showcompletionconditionsdefault;
2050
        }
2051
 
2052
        $showactivitydatesdefault = ($courseconfig->showactivitydates ?? null);
2053
        $data->showactivitydates = $data->showactivitydates ?? $showactivitydatesdefault;
2054
 
2055
        $pdffontdefault = ($courseconfig->pdfexportfont ?? null);
2056
        $data->pdfexportfont = $data->pdfexportfont ?? $pdffontdefault;
2057
 
2058
        $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
2059
        if (isset($data->lang) && !array_key_exists($data->lang, $languages)) {
2060
            $data->lang = '';
2061
        }
2062
 
2063
        $themes = get_list_of_themes(); // Get themes for quick search later
2064
        if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
2065
            $data->theme = '';
2066
        }
2067
 
2068
        // Check if this is an old SCORM course format.
2069
        if ($data->format == 'scorm') {
2070
            $data->format = 'singleactivity';
2071
            $data->activitytype = 'scorm';
2072
        }
2073
 
2074
        // Course record ready, update it
2075
        $DB->update_record('course', $data);
2076
 
2077
        // Apply any course format options that may be saved against the course
2078
        // entity in earlier-version backups.
2079
        course_get_format($data)->update_course_format_options($data);
2080
 
2081
        // Role name aliases
2082
        restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
2083
    }
2084
 
2085
    public function process_category($data) {
2086
        // Nothing to do with the category. UI sets it before restore starts
2087
    }
2088
 
2089
    public function process_tag($data) {
2090
        global $CFG, $DB;
2091
 
2092
        $data = (object)$data;
2093
 
2094
        core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(),
2095
                context_course::instance($this->get_courseid()), $data->rawname);
2096
    }
2097
 
2098
    /**
2099
     * Process custom fields
2100
     *
2101
     * @param array $data
2102
     */
2103
    public function process_customfield($data) {
2104
        $handler = core_course\customfield\course_handler::create();
2105
        $newid = $handler->restore_instance_data_from_backup($this->task, $data);
2106
 
2107
        if ($newid) {
2108
            $handler->restore_define_structure($this, $newid, $data['id']);
2109
        }
2110
    }
2111
 
2112
    /**
2113
     * Processes a course format option.
2114
     *
2115
     * @param array $data The record being restored.
2116
     * @throws base_step_exception
2117
     * @throws dml_exception
2118
     */
2119
    public function process_course_format_option(array $data): void {
2120
        global $DB;
2121
 
2122
        if ($data['sectionid']) {
2123
            // Ignore section-level format options saved course-level in earlier-version backups.
2124
            return;
2125
        }
2126
 
2127
        $courseid = $this->get_courseid();
2128
        $record = $DB->get_record('course_format_options', [ 'courseid' => $courseid, 'name' => $data['name'],
2129
                'format' => $data['format'], 'sectionid' => 0 ], 'id');
2130
        if ($record !== false) {
2131
            $DB->update_record('course_format_options', (object) [ 'id' => $record->id, 'value' => $data['value'] ]);
2132
        } else {
2133
            $data['courseid'] = $courseid;
2134
            $DB->insert_record('course_format_options', (object) $data);
2135
        }
2136
    }
2137
 
2138
    public function process_allowed_module($data) {
2139
        $data = (object)$data;
2140
 
2141
        // Backwards compatiblity support for the data that used to be in the
2142
        // course_allowed_modules table.
2143
        if ($this->legacyrestrictmodules) {
2144
            $this->legacyallowedmodules[$data->modulename] = 1;
2145
        }
2146
    }
2147
 
2148
    protected function after_execute() {
2149
        global $DB;
2150
 
2151
        // Add course related files, without itemid to match
2152
        $this->add_related_files('course', 'summary', null);
2153
        $this->add_related_files('course', 'overviewfiles', null);
2154
 
2155
        // Deal with legacy allowed modules.
2156
        if ($this->legacyrestrictmodules) {
2157
            $context = context_course::instance($this->get_courseid());
2158
 
2159
            list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
2160
            list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
2161
            foreach ($managerroleids as $roleid) {
2162
                unset($roleids[$roleid]);
2163
            }
2164
 
2165
            foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
2166
                if (isset($this->legacyallowedmodules[$modname])) {
2167
                    // Module is allowed, no worries.
2168
                    continue;
2169
                }
2170
 
2171
                $capability = 'mod/' . $modname . ':addinstance';
2172
 
2173
                if (!get_capability_info($capability)) {
2174
                    $this->log("Capability '{$capability}' was not found!", backup::LOG_WARNING);
2175
                    continue;
2176
                }
2177
 
2178
                foreach ($roleids as $roleid) {
2179
                    assign_capability($capability, CAP_PREVENT, $roleid, $context);
2180
                }
2181
            }
2182
        }
2183
    }
2184
}
2185
 
2186
/**
2187
 * Execution step that will migrate legacy files if present.
2188
 */
2189
class restore_course_legacy_files_step extends restore_execution_step {
2190
    public function define_execution() {
2191
        global $DB;
2192
 
2193
        // Do a check for legacy files and skip if there are none.
2194
        $sql = 'SELECT count(*)
2195
                  FROM {backup_files_temp}
2196
                 WHERE backupid = ?
2197
                   AND contextid = ?
2198
                   AND component = ?
2199
                   AND filearea  = ?';
2200
        $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
2201
 
2202
        if ($DB->count_records_sql($sql, $params)) {
2203
            $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
2204
            restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
2205
                'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
2206
        }
2207
    }
2208
}
2209
 
2210
/*
2211
 * Structure step that will read the roles.xml file (at course/activity/block levels)
2212
 * containing all the role_assignments and overrides for that context. If corresponding to
2213
 * one mapped role, they will be applied to target context. Will observe the role_assignments
2214
 * setting to decide if ras are restored.
2215
 *
2216
 * Note: this needs to be executed after all users are enrolled.
2217
 */
2218
class restore_ras_and_caps_structure_step extends restore_structure_step {
2219
    protected $plugins = null;
2220
 
2221
    protected function define_structure() {
2222
 
2223
        $paths = array();
2224
 
2225
        // Observe the role_assignments setting
2226
        if ($this->get_setting_value('role_assignments')) {
2227
            $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
2228
        }
2229
        if ($this->get_setting_value('permissions')) {
2230
            $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
2231
        }
2232
 
2233
        return $paths;
2234
    }
2235
 
2236
    /**
2237
     * Assign roles
2238
     *
2239
     * This has to be called after enrolments processing.
2240
     *
2241
     * @param mixed $data
2242
     * @return void
2243
     */
2244
    public function process_assignment($data) {
2245
        global $DB;
2246
 
2247
        $data = (object)$data;
2248
 
2249
        // Check roleid, userid are one of the mapped ones
2250
        if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
2251
            return;
2252
        }
2253
        if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
2254
            return;
2255
        }
2256
        if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
2257
            // Only assign roles to not deleted users
2258
            return;
2259
        }
2260
        if (!$contextid = $this->task->get_contextid()) {
2261
            return;
2262
        }
2263
 
2264
        if (empty($data->component)) {
2265
            // assign standard manual roles
2266
            // TODO: role_assign() needs one userid param to be able to specify our restore userid
2267
            role_assign($newroleid, $newuserid, $contextid);
2268
 
2269
        } else if ((strpos($data->component, 'enrol_') === 0)) {
2270
            // Deal with enrolment roles - ignore the component and just find out the instance via new id,
2271
            // it is possible that enrolment was restored using different plugin type.
2272
            if (!isset($this->plugins)) {
2273
                $this->plugins = enrol_get_plugins(true);
2274
            }
2275
            if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
2276
                if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2277
                    if (isset($this->plugins[$instance->enrol])) {
2278
                        $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
2279
                    }
2280
                }
2281
            }
2282
 
2283
        } else {
2284
            $data->roleid    = $newroleid;
2285
            $data->userid    = $newuserid;
2286
            $data->contextid = $contextid;
2287
            $dir = core_component::get_component_directory($data->component);
2288
            if ($dir and is_dir($dir)) {
2289
                if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
2290
                    return;
2291
                }
2292
            }
2293
            // Bad luck, plugin could not restore the data, let's add normal membership.
2294
            role_assign($data->roleid, $data->userid, $data->contextid);
2295
            $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
2296
            $this->log($message, backup::LOG_WARNING);
2297
        }
2298
    }
2299
 
2300
    public function process_override($data) {
2301
        $data = (object)$data;
2302
        // Check roleid is one of the mapped ones
2303
        $newrole = $this->get_mapping('role', $data->roleid);
2304
        $newroleid = $newrole->newitemid ?? false;
2305
        $userid = $this->task->get_userid();
2306
 
2307
        // If newroleid and context are valid assign it via API (it handles dupes and so on)
2308
        if ($newroleid && $this->task->get_contextid()) {
2309
            if (!$capability = get_capability_info($data->capability)) {
2310
                $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING);
2311
            } else {
2312
                $context = context::instance_by_id($this->task->get_contextid());
2313
                $overrideableroles = get_overridable_roles($context, ROLENAME_SHORT);
2314
                $safecapability = is_safe_capability($capability);
2315
 
2316
                // Check if the new role is an overrideable role AND if the user performing the restore has the
2317
                // capability to assign the capability.
2318
                if (in_array($newrole->info['shortname'], $overrideableroles) &&
2319
                    (has_capability('moodle/role:override', $context, $userid) ||
2320
                            ($safecapability && has_capability('moodle/role:safeoverride', $context, $userid)))
2321
                ) {
2322
                    assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
2323
                } else {
2324
                    $this->log("Insufficient capability to assign capability '{$data->capability}' to role!", backup::LOG_WARNING);
2325
                }
2326
            }
2327
        }
2328
    }
2329
}
2330
 
2331
/**
2332
 * If no instances yet add default enrol methods the same way as when creating new course in UI.
2333
 */
2334
class restore_default_enrolments_step extends restore_execution_step {
2335
 
2336
    public function define_execution() {
2337
        global $DB;
2338
 
2339
        // No enrolments in front page.
2340
        if ($this->get_courseid() == SITEID) {
2341
            return;
2342
        }
2343
 
2344
        $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
2345
        // Return any existing course enrolment instances.
2346
        $enrolinstances = enrol_get_instances($course->id, false);
2347
 
2348
        if ($enrolinstances) {
2349
            // Something already added instances.
2350
            // Get the existing enrolment methods in the course.
2351
            $enrolmethods = array_map(function($enrolinstance) {
2352
                return $enrolinstance->enrol;
2353
            }, $enrolinstances);
2354
 
2355
            $plugins = enrol_get_plugins(true);
2356
            foreach ($plugins as $pluginname => $plugin) {
2357
                // Make sure all default enrolment methods exist in the course.
2358
                if (!in_array($pluginname, $enrolmethods)) {
2359
                    $plugin->course_updated(true, $course, null);
2360
                }
2361
                $plugin->restore_sync_course($course);
2362
            }
2363
 
2364
        } else {
2365
            // Looks like a newly created course.
2366
            enrol_course_updated(true, $course, null);
2367
        }
2368
    }
2369
}
2370
 
2371
/**
2372
 * This structure steps restores the enrol plugins and their underlying
2373
 * enrolments, performing all the mappings and/or movements required
2374
 */
2375
class restore_enrolments_structure_step extends restore_structure_step {
2376
    protected $enrolsynced = false;
2377
    protected $plugins = null;
2378
    protected $originalstatus = array();
2379
 
2380
    /**
2381
     * Conditionally decide if this step should be executed.
2382
     *
2383
     * This function checks the following parameter:
2384
     *
2385
     *   1. the course/enrolments.xml file exists
2386
     *
2387
     * @return bool true is safe to execute, false otherwise
2388
     */
2389
    protected function execute_condition() {
2390
 
2391
        if ($this->get_courseid() == SITEID) {
2392
            return false;
2393
        }
2394
 
2395
        // Check it is included in the backup
2396
        $fullpath = $this->task->get_taskbasepath();
2397
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2398
        if (!file_exists($fullpath)) {
2399
            // Not found, can't restore enrolments info
2400
            return false;
2401
        }
2402
 
2403
        return true;
2404
    }
2405
 
2406
    protected function define_structure() {
2407
 
2408
        $userinfo = $this->get_setting_value('users');
2409
 
2410
        $paths = [];
2411
        $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
2412
        if ($userinfo) {
2413
            $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
2414
        }
2415
        // Attach local plugin stucture to enrol element.
2416
        $this->add_plugin_structure('enrol', $enrol);
2417
 
2418
        return $paths;
2419
    }
2420
 
2421
    /**
2422
     * Create enrolment instances.
2423
     *
2424
     * This has to be called after creation of roles
2425
     * and before adding of role assignments.
2426
     *
2427
     * @param mixed $data
2428
     * @return void
2429
     */
2430
    public function process_enrol($data) {
2431
        global $DB;
2432
 
2433
        $data = (object)$data;
2434
        $oldid = $data->id; // We'll need this later.
2435
        unset($data->id);
2436
 
2437
        $this->originalstatus[$oldid] = $data->status;
2438
 
2439
        if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
2440
            $this->set_mapping('enrol', $oldid, 0);
2441
            return;
2442
        }
2443
 
2444
        if (!isset($this->plugins)) {
2445
            $this->plugins = enrol_get_plugins(true);
2446
        }
2447
 
2448
        if (!$this->enrolsynced) {
2449
            // Make sure that all plugin may create instances and enrolments automatically
2450
            // before the first instance restore - this is suitable especially for plugins
2451
            // that synchronise data automatically using course->idnumber or by course categories.
2452
            foreach ($this->plugins as $plugin) {
2453
                $plugin->restore_sync_course($courserec);
2454
            }
2455
            $this->enrolsynced = true;
2456
        }
2457
 
2458
        // Map standard fields - plugin has to process custom fields manually.
2459
        $data->roleid   = $this->get_mappingid('role', $data->roleid);
2460
        $data->courseid = $courserec->id;
2461
 
2462
        if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) {
2463
            $converttomanual = true;
2464
        } else {
2465
            $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER);
2466
        }
2467
 
2468
        if ($converttomanual) {
2469
            // Restore enrolments as manual enrolments.
2470
            unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
2471
            if (!enrol_is_enabled('manual')) {
2472
                $this->set_mapping('enrol', $oldid, 0);
2473
                return;
2474
            }
2475
            if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
2476
                $instance = reset($instances);
2477
                $this->set_mapping('enrol', $oldid, $instance->id);
2478
            } else {
2479
                if ($data->enrol === 'manual') {
2480
                    $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
2481
                } else {
2482
                    $instanceid = $this->plugins['manual']->add_default_instance($courserec);
2483
                }
2484
                $this->set_mapping('enrol', $oldid, $instanceid);
2485
            }
2486
 
2487
        } else {
2488
            if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
2489
                $this->set_mapping('enrol', $oldid, 0);
2490
                $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods";
2491
                $this->log($message, backup::LOG_WARNING);
2492
                return;
2493
            }
2494
            if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
2495
                // Let's keep the sortorder in old backups.
2496
            } else {
2497
                // Prevent problems with colliding sortorders in old backups,
2498
                // new 2.4 backups do not need sortorder because xml elements are ordered properly.
2499
                unset($data->sortorder);
2500
            }
2501
            // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
2502
            $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
2503
        }
2504
    }
2505
 
2506
    /**
2507
     * Create user enrolments.
2508
     *
2509
     * This has to be called after creation of enrolment instances
2510
     * and before adding of role assignments.
2511
     *
2512
     * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
2513
     *
2514
     * @param mixed $data
2515
     * @return void
2516
     */
2517
    public function process_enrolment($data) {
2518
        global $DB;
2519
 
2520
        if (!isset($this->plugins)) {
2521
            $this->plugins = enrol_get_plugins(true);
2522
        }
2523
 
2524
        $data = (object)$data;
2525
 
2526
        // Process only if parent instance have been mapped.
2527
        if ($enrolid = $this->get_new_parentid('enrol')) {
2528
            $oldinstancestatus = ENROL_INSTANCE_ENABLED;
2529
            $oldenrolid = $this->get_old_parentid('enrol');
2530
            if (isset($this->originalstatus[$oldenrolid])) {
2531
                $oldinstancestatus = $this->originalstatus[$oldenrolid];
2532
            }
2533
            if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
2534
                // And only if user is a mapped one.
2535
                if ($userid = $this->get_mappingid('user', $data->userid)) {
2536
                    if (isset($this->plugins[$instance->enrol])) {
2537
                        $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
2538
                    }
2539
                }
2540
            }
2541
        }
2542
    }
2543
}
2544
 
2545
 
2546
/**
2547
 * Make sure the user restoring the course can actually access it.
2548
 */
2549
class restore_fix_restorer_access_step extends restore_execution_step {
2550
    protected function define_execution() {
2551
        global $CFG, $DB;
2552
 
2553
        if (!$userid = $this->task->get_userid()) {
2554
            return;
2555
        }
2556
 
2557
        if (empty($CFG->restorernewroleid)) {
2558
            // Bad luck, no fallback role for restorers specified
2559
            return;
2560
        }
2561
 
2562
        $courseid = $this->get_courseid();
2563
        $context = context_course::instance($courseid);
2564
 
2565
        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2566
            // Current user may access the course (admin, category manager or restored teacher enrolment usually)
2567
            return;
2568
        }
2569
 
2570
        // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
2571
        role_assign($CFG->restorernewroleid, $userid, $context);
2572
 
2573
        if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
2574
            // Extra role is enough, yay!
2575
            return;
2576
        }
2577
 
2578
        // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
2579
        // hopefully admin selected suitable $CFG->restorernewroleid ...
2580
        if (!enrol_is_enabled('manual')) {
2581
            return;
2582
        }
2583
        if (!$enrol = enrol_get_plugin('manual')) {
2584
            return;
2585
        }
2586
        if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
2587
            $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
2588
            $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
2589
            $enrol->add_instance($course, $fields);
2590
        }
2591
 
2592
        enrol_try_internal_enrol($courseid, $userid);
2593
    }
2594
}
2595
 
2596
 
2597
/**
2598
 * This structure steps restores the filters and their configs
2599
 */
2600
class restore_filters_structure_step extends restore_structure_step {
2601
 
2602
    protected function define_structure() {
2603
 
2604
        $paths = array();
2605
 
2606
        $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
2607
        $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
2608
 
2609
        return $paths;
2610
    }
2611
 
2612
    public function process_active($data) {
2613
 
2614
        $data = (object)$data;
2615
 
2616
        if (strpos($data->filter, 'filter/') === 0) {
2617
            $data->filter = substr($data->filter, 7);
2618
 
2619
        } else if (strpos($data->filter, '/') !== false) {
2620
            // Unsupported old filter.
2621
            return;
2622
        }
2623
 
2624
        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2625
            return;
2626
        }
2627
        filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
2628
    }
2629
 
2630
    public function process_config($data) {
2631
 
2632
        $data = (object)$data;
2633
 
2634
        if (strpos($data->filter, 'filter/') === 0) {
2635
            $data->filter = substr($data->filter, 7);
2636
 
2637
        } else if (strpos($data->filter, '/') !== false) {
2638
            // Unsupported old filter.
2639
            return;
2640
        }
2641
 
2642
        if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
2643
            return;
2644
        }
2645
        filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
2646
    }
2647
}
2648
 
2649
 
2650
/**
2651
 * This structure steps restores the comments
2652
 * Note: Cannot use the comments API because defaults to USER->id.
2653
 * That should change allowing to pass $userid
2654
 */
2655
class restore_comments_structure_step extends restore_structure_step {
2656
 
2657
    protected function define_structure() {
2658
 
2659
        $paths = array();
2660
 
2661
        $paths[] = new restore_path_element('comment', '/comments/comment');
2662
 
2663
        return $paths;
2664
    }
2665
 
2666
    public function process_comment($data) {
2667
        global $DB;
2668
 
2669
        $data = (object)$data;
2670
 
2671
        // First of all, if the comment has some itemid, ask to the task what to map
2672
        $mapping = false;
2673
        if ($data->itemid) {
2674
            $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
2675
            $data->itemid = $this->get_mappingid($mapping, $data->itemid);
2676
        }
2677
        // Only restore the comment if has no mapping OR we have found the matching mapping
2678
        if (!$mapping || $data->itemid) {
2679
            // Only if user mapping and context
2680
            $data->userid = $this->get_mappingid('user', $data->userid);
2681
            if ($data->userid && $this->task->get_contextid()) {
2682
                $data->contextid = $this->task->get_contextid();
2683
                // Only if there is another comment with same context/user/timecreated
2684
                $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
2685
                if (!$DB->record_exists('comments', $params)) {
2686
                    $DB->insert_record('comments', $data);
2687
                }
2688
            }
2689
        }
2690
    }
2691
}
2692
 
2693
/**
2694
 * This structure steps restores the badges and their configs
2695
 */
2696
class restore_badges_structure_step extends restore_structure_step {
2697
 
2698
    /**
2699
     * Conditionally decide if this step should be executed.
2700
     *
2701
     * This function checks the following parameters:
2702
     *
2703
     *   1. Badges and course badges are enabled on the site.
2704
     *   2. The course/badges.xml file exists.
2705
     *   3. All modules are restorable.
2706
     *   4. All modules are marked for restore.
2707
     *
2708
     * @return bool True is safe to execute, false otherwise
2709
     */
2710
    protected function execute_condition() {
2711
        global $CFG;
2712
 
2713
        // First check is badges and course level badges are enabled on this site.
2714
        if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
2715
            // Disabled, don't restore course badges.
2716
            return false;
2717
        }
2718
 
2719
        // Check if badges.xml is included in the backup.
2720
        $fullpath = $this->task->get_taskbasepath();
2721
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
2722
        if (!file_exists($fullpath)) {
2723
            // Not found, can't restore course badges.
2724
            return false;
2725
        }
2726
 
2727
        // Check we are able to restore all backed up modules.
2728
        if ($this->task->is_missing_modules()) {
2729
            return false;
2730
        }
2731
 
2732
        // Finally check all modules within the backup are being restored.
2733
        if ($this->task->is_excluding_activities()) {
2734
            return false;
2735
        }
2736
 
2737
        return true;
2738
    }
2739
 
2740
    protected function define_structure() {
2741
        $paths = array();
2742
        $paths[] = new restore_path_element('badge', '/badges/badge');
2743
        $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
2744
        $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
2745
        $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
2746
        $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
2747
        $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
2748
        $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
2749
        $paths[] = new restore_path_element('tag', '/badges/badge/tags/tag');
2750
 
2751
        return $paths;
2752
    }
2753
 
2754
    public function process_badge($data) {
2755
        global $DB, $CFG;
2756
 
2757
        require_once($CFG->libdir . '/badgeslib.php');
2758
 
2759
        $data = (object)$data;
2760
        $data->usercreated = $this->get_mappingid('user', $data->usercreated);
2761
        if (empty($data->usercreated)) {
2762
            $data->usercreated = $this->task->get_userid();
2763
        }
2764
        $data->usermodified = $this->get_mappingid('user', $data->usermodified);
2765
        if (empty($data->usermodified)) {
2766
            $data->usermodified = $this->task->get_userid();
2767
        }
2768
 
2769
        // We'll restore the badge image.
2770
        $restorefiles = true;
2771
 
2772
        $courseid = $this->get_courseid();
2773
 
2774
        $params = array(
2775
                'name'           => $data->name,
2776
                'description'    => $data->description,
2777
                'timecreated'    => $data->timecreated,
2778
                'timemodified'   => $data->timemodified,
2779
                'usercreated'    => $data->usercreated,
2780
                'usermodified'   => $data->usermodified,
2781
                'issuername'     => $data->issuername,
2782
                'issuerurl'      => $data->issuerurl,
2783
                'issuercontact'  => $data->issuercontact,
2784
                'expiredate'     => $this->apply_date_offset($data->expiredate),
2785
                'expireperiod'   => $data->expireperiod,
2786
                'type'           => BADGE_TYPE_COURSE,
2787
                'courseid'       => $courseid,
2788
                'message'        => $data->message,
2789
                'messagesubject' => $data->messagesubject,
2790
                'attachment'     => $data->attachment,
2791
                'notification'   => $data->notification,
2792
                'status'         => BADGE_STATUS_INACTIVE,
2793
                'nextcron'       => $data->nextcron,
2794
                'version'        => $data->version,
2795
                'language'       => $data->language,
2796
                'imagecaption'   => $data->imagecaption
2797
        );
2798
 
2799
        $newid = $DB->insert_record('badge', $params);
2800
        $this->set_mapping('badge', $data->id, $newid, $restorefiles);
2801
    }
2802
 
2803
    /**
2804
     * Create an endorsement for a badge.
2805
     *
2806
     * @param mixed $data
2807
     * @return void
2808
     */
2809
    public function process_endorsement($data) {
2810
        global $DB;
2811
 
2812
        $data = (object)$data;
2813
 
2814
        $params = [
2815
            'badgeid' => $this->get_new_parentid('badge'),
2816
            'issuername' => $data->issuername,
2817
            'issuerurl' => $data->issuerurl,
2818
            'issueremail' => $data->issueremail,
2819
            'claimid' => $data->claimid,
2820
            'claimcomment' => $data->claimcomment,
2821
            'dateissued' => $this->apply_date_offset($data->dateissued)
2822
        ];
2823
        $newid = $DB->insert_record('badge_endorsement', $params);
2824
        $this->set_mapping('endorsement', $data->id, $newid);
2825
    }
2826
 
2827
    /**
2828
     * Link to related badges for a badge. This relies on post processing in after_execute().
2829
     *
2830
     * @param mixed $data
2831
     * @return void
2832
     */
2833
    public function process_relatedbadge($data) {
2834
        global $DB;
2835
 
2836
        $data = (object)$data;
2837
        $relatedbadgeid = $data->relatedbadgeid;
2838
 
2839
        if ($relatedbadgeid) {
2840
            // Only backup and restore related badges if they are contained in the backup file.
2841
            $params = array(
2842
                    'badgeid'           => $this->get_new_parentid('badge'),
2843
                    'relatedbadgeid'    => $relatedbadgeid
2844
            );
2845
            $newid = $DB->insert_record('badge_related', $params);
2846
        }
2847
    }
2848
 
2849
    /**
2850
     * Link to an alignment for a badge.
2851
     *
2852
     * @param mixed $data
2853
     * @return void
2854
     */
2855
    public function process_alignment($data) {
2856
        global $DB;
2857
 
2858
        $data = (object)$data;
2859
        $params = array(
2860
                'badgeid'           => $this->get_new_parentid('badge'),
2861
                'targetname'        => $data->targetname,
2862
                'targeturl'         => $data->targeturl,
2863
                'targetdescription' => $data->targetdescription,
2864
                'targetframework'   => $data->targetframework,
2865
                'targetcode'        => $data->targetcode
2866
        );
2867
        $newid = $DB->insert_record('badge_alignment', $params);
2868
        $this->set_mapping('alignment', $data->id, $newid);
2869
    }
2870
 
2871
    public function process_criterion($data) {
2872
        global $DB;
2873
 
2874
        $data = (object)$data;
2875
 
2876
        $params = array(
2877
                'badgeid'           => $this->get_new_parentid('badge'),
2878
                'criteriatype'      => $data->criteriatype,
2879
                'method'            => $data->method,
2880
                'description'       => isset($data->description) ? $data->description : '',
2881
                'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
2882
        );
2883
 
2884
        $newid = $DB->insert_record('badge_criteria', $params);
2885
        $this->set_mapping('criterion', $data->id, $newid);
2886
    }
2887
 
2888
    public function process_parameter($data) {
2889
        global $DB, $CFG;
2890
 
2891
        require_once($CFG->libdir . '/badgeslib.php');
2892
 
2893
        $data = (object)$data;
2894
        $criteriaid = $this->get_new_parentid('criterion');
2895
 
2896
        // Parameter array that will go to database.
2897
        $params = array();
2898
        $params['critid'] = $criteriaid;
2899
 
2900
        $oldparam = explode('_', $data->name);
2901
 
2902
        if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
2903
            $module = $this->get_mappingid('course_module', $oldparam[1]);
2904
            $params['name'] = $oldparam[0] . '_' . $module;
2905
            $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
2906
        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
2907
            $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
2908
            $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
2909
        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
2910
            $role = $this->get_mappingid('role', $data->value);
2911
            if (!empty($role)) {
2912
                $params['name'] = 'role_' . $role;
2913
                $params['value'] = $role;
2914
            } else {
2915
                return;
2916
            }
2917
        } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
2918
            $competencyid = $this->get_mappingid('competency', $data->value);
2919
            if (!empty($competencyid)) {
2920
                $params['name'] = 'competency_' . $competencyid;
2921
                $params['value'] = $competencyid;
2922
            } else {
2923
                return;
2924
            }
2925
        }
2926
 
2927
        if (!$DB->record_exists('badge_criteria_param', $params)) {
2928
            $DB->insert_record('badge_criteria_param', $params);
2929
        }
2930
    }
2931
 
2932
    public function process_manual_award($data) {
2933
        global $DB;
2934
 
2935
        $data = (object)$data;
2936
        $role = $this->get_mappingid('role', $data->issuerrole);
2937
 
2938
        if (!empty($role)) {
2939
            $award = array(
2940
                'badgeid'     => $this->get_new_parentid('badge'),
2941
                'recipientid' => $this->get_mappingid('user', $data->recipientid),
2942
                'issuerid'    => $this->get_mappingid('user', $data->issuerid),
2943
                'issuerrole'  => $role,
2944
                'datemet'     => $this->apply_date_offset($data->datemet)
2945
            );
2946
 
2947
            // Skip the manual award if recipient or issuer can not be mapped to.
2948
            if (empty($award['recipientid']) || empty($award['issuerid'])) {
2949
                return;
2950
            }
2951
 
2952
            $DB->insert_record('badge_manual_award', $award);
2953
        }
2954
    }
2955
 
2956
    /**
2957
     * Process tag.
2958
     *
2959
     * @param array $data The data.
2960
     * @throws base_step_exception
2961
     */
2962
    public function process_tag(array $data): void {
2963
        $data = (object)$data;
2964
        $badgeid = $this->get_new_parentid('badge');
2965
 
2966
        if (!empty($data->rawname)) {
2967
            core_tag_tag::add_item_tag('core_badges', 'badge', $badgeid,
2968
                context_course::instance($this->get_courseid()), $data->rawname);
2969
        }
2970
    }
2971
 
2972
    protected function after_execute() {
2973
        global $DB;
2974
        // Add related files.
2975
        $this->add_related_files('badges', 'badgeimage', 'badge');
2976
 
2977
        $badgeid = $this->get_new_parentid('badge');
2978
        // Remap any related badges.
2979
        // We do this in the DB directly because this is backup/restore it is not valid to call into
2980
        // the component API.
2981
        $params = array('badgeid' => $badgeid);
2982
        $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
2983
                    FROM {badge_related} br
2984
                   WHERE (br.badgeid = :badgeid)";
2985
        $relatedbadges = $DB->get_records_sql($query, $params);
2986
        $newrelatedids = [];
2987
        foreach ($relatedbadges as $relatedbadge) {
2988
            $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
2989
            $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
2990
            $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
2991
            if ($relatedid) {
2992
                $newrelatedids[] = $relatedid;
2993
            }
2994
        }
2995
        if (!empty($newrelatedids)) {
2996
            $relatedbadges = [];
2997
            foreach ($newrelatedids as $relatedid) {
2998
                $relatedbadge = new stdClass();
2999
                $relatedbadge->badgeid = $badgeid;
3000
                $relatedbadge->relatedbadgeid = $relatedid;
3001
                $relatedbadges[] = $relatedbadge;
3002
            }
3003
            $DB->insert_records('badge_related', $relatedbadges);
3004
        }
3005
    }
3006
}
3007
 
3008
/**
3009
 * This structure steps restores the calendar events
3010
 */
3011
class restore_calendarevents_structure_step extends restore_structure_step {
3012
 
3013
    protected function define_structure() {
3014
 
3015
        $paths = array();
3016
 
3017
        $paths[] = new restore_path_element('calendarevents', '/events/event');
3018
 
3019
        return $paths;
3020
    }
3021
 
3022
    public function process_calendarevents($data) {
3023
        global $DB, $SITE, $USER;
3024
 
3025
        $data = (object)$data;
3026
        $oldid = $data->id;
3027
        $restorefiles = true; // We'll restore the files
3028
 
3029
        // If this is a new action event, it will automatically be populated by the adhoc task.
3030
        // Nothing to do here.
3031
        if (isset($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION) {
3032
            return;
3033
        }
3034
 
3035
        // User overrides for activities are identified by having a courseid of zero with
3036
        // both a modulename and instance value set.
3037
        $isuseroverride = !$data->courseid && $data->modulename && $data->instance;
3038
 
3039
        // If we don't want to include user data and this record is a user override event
3040
        // for an activity then we should not create it. (Only activity events can be user override events - which must have this
3041
        // setting).
3042
        if ($isuseroverride && $this->task->setting_exists('userinfo') && !$this->task->get_setting_value('userinfo')) {
3043
            return;
3044
        }
3045
 
3046
        // Find the userid and the groupid associated with the event.
3047
        $data->userid = $this->get_mappingid('user', $data->userid);
3048
        if ($data->userid === false) {
3049
            // Blank user ID means that we are dealing with module generated events such as quiz starting times.
3050
            // Use the current user ID for these events.
3051
            $data->userid = $USER->id;
3052
        }
3053
        if (!empty($data->groupid)) {
3054
            $data->groupid = $this->get_mappingid('group', $data->groupid);
3055
            if ($data->groupid === false) {
3056
                return;
3057
            }
3058
        }
3059
        // Handle events with empty eventtype //MDL-32827
3060
        if(empty($data->eventtype)) {
3061
            if ($data->courseid == $SITE->id) {                                // Site event
3062
                $data->eventtype = "site";
3063
            } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
3064
                // Course assingment event
3065
                $data->eventtype = "due";
3066
            } else if ($data->courseid != 0 && $data->groupid == 0) {      // Course event
3067
                $data->eventtype = "course";
3068
            } else if ($data->groupid) {                                      // Group event
3069
                $data->eventtype = "group";
3070
            } else if ($data->userid) {                                       // User event
3071
                $data->eventtype = "user";
3072
            } else {
3073
                return;
3074
            }
3075
        }
3076
 
3077
        $params = array(
3078
                'name'           => $data->name,
3079
                'description'    => $data->description,
3080
                'format'         => $data->format,
3081
                // User overrides in activities use a course id of zero. All other event types
3082
                // must use the mapped course id.
3083
                'courseid'       => $data->courseid ? $this->get_courseid() : 0,
3084
                'groupid'        => $data->groupid,
3085
                'userid'         => $data->userid,
3086
                'repeatid'       => $this->get_mappingid('event', $data->repeatid),
3087
                'modulename'     => $data->modulename,
3088
                'type'           => isset($data->type) ? $data->type : 0,
3089
                'eventtype'      => $data->eventtype,
3090
                'timestart'      => $this->apply_date_offset($data->timestart),
3091
                'timeduration'   => $data->timeduration,
3092
                'timesort'       => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null,
3093
                'visible'        => $data->visible,
3094
                'uuid'           => $data->uuid,
3095
                'sequence'       => $data->sequence,
3096
                'timemodified'   => $data->timemodified,
3097
                'priority'       => isset($data->priority) ? $data->priority : null,
3098
                'location'       => isset($data->location) ? $data->location : null);
3099
        if ($this->name == 'activity_calendar') {
3100
            $params['instance'] = $this->task->get_activityid();
3101
        } else {
3102
            $params['instance'] = 0;
3103
        }
3104
        $sql = "SELECT id
3105
                  FROM {event}
3106
                 WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
3107
                   AND courseid = ?
3108
                   AND modulename = ?
3109
                   AND instance = ?
3110
                   AND timestart = ?
3111
                   AND timeduration = ?
3112
                   AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
3113
        $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['instance'], $params['timestart'], $params['timeduration'], $params['description']);
3114
        $result = $DB->record_exists_sql($sql, $arg);
3115
        if (empty($result)) {
3116
            $newitemid = $DB->insert_record('event', $params);
3117
            $this->set_mapping('event', $oldid, $newitemid);
3118
            $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
3119
        }
3120
        // With repeating events, each event has the repeatid pointed at the first occurrence.
3121
        // Since the repeatid will be empty when the first occurrence is restored,
3122
        // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence.
3123
        // Then keep a list of repeatids so we only perform this update once.
3124
        static $repeatids = array();
3125
        if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) {
3126
            // This entry is repeated so the repeatid field must be set.
3127
            $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid']));
3128
            $repeatids[] = $params['repeatid'];
3129
        }
3130
 
3131
    }
3132
    protected function after_execute() {
3133
        // Add related files
3134
        $this->add_related_files('calendar', 'event_description', 'event_description');
3135
    }
3136
}
3137
 
3138
class restore_course_completion_structure_step extends restore_structure_step {
3139
 
3140
    /**
3141
     * Conditionally decide if this step should be executed.
3142
     *
3143
     * This function checks parameters that are not immediate settings to ensure
3144
     * that the enviroment is suitable for the restore of course completion info.
3145
     *
3146
     * This function checks the following four parameters:
3147
     *
3148
     *   1. Course completion is enabled on the site
3149
     *   2. The backup includes course completion information
3150
     *   3. All modules are restorable
3151
     *   4. All modules are marked for restore.
3152
     *   5. No completion criteria already exist for the course.
3153
     *
3154
     * @return bool True is safe to execute, false otherwise
3155
     */
3156
    protected function execute_condition() {
3157
        global $CFG, $DB;
3158
 
3159
        // First check course completion is enabled on this site
3160
        if (empty($CFG->enablecompletion)) {
3161
            // Disabled, don't restore course completion
3162
            return false;
3163
        }
3164
 
3165
        // No course completion on the front page.
3166
        if ($this->get_courseid() == SITEID) {
3167
            return false;
3168
        }
3169
 
3170
        // Check it is included in the backup
3171
        $fullpath = $this->task->get_taskbasepath();
3172
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3173
        if (!file_exists($fullpath)) {
3174
            // Not found, can't restore course completion
3175
            return false;
3176
        }
3177
 
3178
        // Check we are able to restore all backed up modules
3179
        if ($this->task->is_missing_modules()) {
3180
            return false;
3181
        }
3182
 
3183
        // Check all modules within the backup are being restored.
3184
        if ($this->task->is_excluding_activities()) {
3185
            return false;
3186
        }
3187
 
3188
        // Check that no completion criteria is already set for the course.
3189
        if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
3190
            return false;
3191
        }
3192
 
3193
        return true;
3194
    }
3195
 
3196
    /**
3197
     * Define the course completion structure
3198
     *
3199
     * @return array Array of restore_path_element
3200
     */
3201
    protected function define_structure() {
3202
 
3203
        // To know if we are including user completion info
3204
        $userinfo = $this->get_setting_value('userscompletion');
3205
 
3206
        $paths = array();
3207
        $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
3208
        $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
3209
 
3210
        if ($userinfo) {
3211
            $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
3212
            $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
3213
        }
3214
 
3215
        return $paths;
3216
 
3217
    }
3218
 
3219
    /**
3220
     * Process course completion criteria
3221
     *
3222
     * @global moodle_database $DB
3223
     * @param stdClass $data
3224
     */
3225
    public function process_course_completion_criteria($data) {
3226
        global $DB;
3227
 
3228
        $data = (object)$data;
3229
        $data->course = $this->get_courseid();
3230
 
3231
        // Apply the date offset to the time end field
3232
        $data->timeend = $this->apply_date_offset($data->timeend);
3233
 
3234
        // Map the role from the criteria
3235
        if (isset($data->role) && $data->role != '') {
3236
            // Newer backups should include roleshortname, which makes this much easier.
3237
            if (!empty($data->roleshortname)) {
3238
                $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname));
3239
                if (!$roleinstanceid) {
3240
                    $this->log(
3241
                        'Could not match the role shortname in course_completion_criteria, so skipping',
3242
                        backup::LOG_DEBUG
3243
                    );
3244
                    return;
3245
                }
3246
                $data->role = $roleinstanceid;
3247
            } else {
3248
                $data->role = $this->get_mappingid('role', $data->role);
3249
            }
3250
 
3251
            // Check we have an id, otherwise it causes all sorts of bugs.
3252
            if (!$data->role) {
3253
                $this->log(
3254
                    'Could not match role in course_completion_criteria, so skipping',
3255
                    backup::LOG_DEBUG
3256
                );
3257
                return;
3258
            }
3259
        }
3260
 
3261
        // If the completion criteria is for a module we need to map the module instance
3262
        // to the new module id.
3263
        if (!empty($data->moduleinstance) && !empty($data->module)) {
3264
            $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
3265
            if (empty($data->moduleinstance)) {
3266
                $this->log(
3267
                    'Could not match the module instance in course_completion_criteria, so skipping',
3268
                    backup::LOG_DEBUG
3269
                );
3270
                return;
3271
            }
3272
        } else {
3273
            $data->module = null;
3274
            $data->moduleinstance = null;
3275
        }
3276
 
3277
        // We backup the course shortname rather than the ID so that we can match back to the course
3278
        if (!empty($data->courseinstanceshortname)) {
3279
            $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
3280
            if (!$courseinstanceid) {
3281
                $this->log(
3282
                    'Could not match the course instance in course_completion_criteria, so skipping',
3283
                    backup::LOG_DEBUG
3284
                );
3285
                return;
3286
            }
3287
        } else {
3288
            $courseinstanceid = null;
3289
        }
3290
        $data->courseinstance = $courseinstanceid;
3291
 
3292
        $params = array(
3293
            'course'         => $data->course,
3294
            'criteriatype'   => $data->criteriatype,
3295
            'enrolperiod'    => $data->enrolperiod,
3296
            'courseinstance' => $data->courseinstance,
3297
            'module'         => $data->module,
3298
            'moduleinstance' => $data->moduleinstance,
3299
            'timeend'        => $data->timeend,
3300
            'gradepass'      => $data->gradepass,
3301
            'role'           => $data->role
3302
        );
3303
        $newid = $DB->insert_record('course_completion_criteria', $params);
3304
        $this->set_mapping('course_completion_criteria', $data->id, $newid);
3305
    }
3306
 
3307
    /**
3308
     * Processes course compltion criteria complete records
3309
     *
3310
     * @global moodle_database $DB
3311
     * @param stdClass $data
3312
     */
3313
    public function process_course_completion_crit_compl($data) {
3314
        global $DB;
3315
 
3316
        $data = (object)$data;
3317
 
3318
        // This may be empty if criteria could not be restored
3319
        $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
3320
 
3321
        $data->course = $this->get_courseid();
3322
        $data->userid = $this->get_mappingid('user', $data->userid);
3323
 
3324
        if (!empty($data->criteriaid) && !empty($data->userid)) {
3325
            $params = array(
3326
                'userid' => $data->userid,
3327
                'course' => $data->course,
3328
                'criteriaid' => $data->criteriaid,
3329
                'timecompleted' => $data->timecompleted
3330
            );
3331
            if (isset($data->gradefinal)) {
3332
                $params['gradefinal'] = $data->gradefinal;
3333
            }
3334
            if (isset($data->unenroled)) {
3335
                $params['unenroled'] = $data->unenroled;
3336
            }
3337
            $DB->insert_record('course_completion_crit_compl', $params);
3338
        }
3339
    }
3340
 
3341
    /**
3342
     * Process course completions
3343
     *
3344
     * @global moodle_database $DB
3345
     * @param stdClass $data
3346
     */
3347
    public function process_course_completions($data) {
3348
        global $DB;
3349
 
3350
        $data = (object)$data;
3351
 
3352
        $data->course = $this->get_courseid();
3353
        $data->userid = $this->get_mappingid('user', $data->userid);
3354
 
3355
        if (!empty($data->userid)) {
3356
            $params = array(
3357
                'userid' => $data->userid,
3358
                'course' => $data->course,
3359
                'timeenrolled' => $data->timeenrolled,
3360
                'timestarted' => $data->timestarted,
3361
                'timecompleted' => $data->timecompleted,
3362
                'reaggregate' => $data->reaggregate
3363
            );
3364
 
3365
            $existing = $DB->get_record('course_completions', array(
3366
                'userid' => $data->userid,
3367
                'course' => $data->course
3368
            ));
3369
 
3370
            // MDL-46651 - If cron writes out a new record before we get to it
3371
            // then we should replace it with the Truth data from the backup.
3372
            // This may be obsolete after MDL-48518 is resolved
3373
            if ($existing) {
3374
                $params['id'] = $existing->id;
3375
                $DB->update_record('course_completions', $params);
3376
            } else {
3377
                $DB->insert_record('course_completions', $params);
3378
            }
3379
        }
3380
    }
3381
 
3382
    /**
3383
     * Process course completion aggregate methods
3384
     *
3385
     * @global moodle_database $DB
3386
     * @param stdClass $data
3387
     */
3388
    public function process_course_completion_aggr_methd($data) {
3389
        global $DB;
3390
 
3391
        $data = (object)$data;
3392
 
3393
        $data->course = $this->get_courseid();
3394
 
3395
        // Only create the course_completion_aggr_methd records if
3396
        // the target course has not them defined. MDL-28180
3397
        if (!$DB->record_exists('course_completion_aggr_methd', array(
3398
                    'course' => $data->course,
3399
                    'criteriatype' => $data->criteriatype))) {
3400
            $params = array(
3401
                'course' => $data->course,
3402
                'criteriatype' => $data->criteriatype,
3403
                'method' => $data->method,
3404
                'value' => $data->value,
3405
            );
3406
            $DB->insert_record('course_completion_aggr_methd', $params);
3407
        }
3408
    }
3409
}
3410
 
3411
 
3412
/**
3413
 * This structure step restores course logs (cmid = 0), delegating
3414
 * the hard work to the corresponding {@link restore_logs_processor} passing the
3415
 * collection of {@link restore_log_rule} rules to be observed as they are defined
3416
 * by the task. Note this is only executed based in the 'logs' setting.
3417
 *
3418
 * NOTE: This is executed by final task, to have all the activities already restored
3419
 *
3420
 * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
3421
 * records are. There are others like 'calendar' and 'upload' that will be handled
3422
 * later.
3423
 *
3424
 * NOTE: All the missing actions (not able to be restored) are sent to logs for
3425
 * debugging purposes
3426
 */
3427
class restore_course_logs_structure_step extends restore_structure_step {
3428
 
3429
    /**
3430
     * Conditionally decide if this step should be executed.
3431
     *
3432
     * This function checks the following parameter:
3433
     *
3434
     *   1. the course/logs.xml file exists
3435
     *
3436
     * @return bool true is safe to execute, false otherwise
3437
     */
3438
    protected function execute_condition() {
3439
 
3440
        // Check it is included in the backup
3441
        $fullpath = $this->task->get_taskbasepath();
3442
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3443
        if (!file_exists($fullpath)) {
3444
            // Not found, can't restore course logs
3445
            return false;
3446
        }
3447
 
3448
        return true;
3449
    }
3450
 
3451
    protected function define_structure() {
3452
 
3453
        $paths = array();
3454
 
3455
        // Simple, one plain level of information contains them
3456
        $paths[] = new restore_path_element('log', '/logs/log');
3457
 
3458
        return $paths;
3459
    }
3460
 
3461
    protected function process_log($data) {
3462
        global $DB;
3463
 
3464
        $data = (object)($data);
3465
 
3466
        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
3467
 
3468
        $data->userid = $this->get_mappingid('user', $data->userid);
3469
        $data->course = $this->get_courseid();
3470
        $data->cmid = 0;
3471
 
3472
        // For any reason user wasn't remapped ok, stop processing this
3473
        if (empty($data->userid)) {
3474
            return;
3475
        }
3476
 
3477
        // Everything ready, let's delegate to the restore_logs_processor
3478
 
3479
        // Set some fixed values that will save tons of DB requests
3480
        $values = array(
3481
            'course' => $this->get_courseid());
3482
        // Get instance and process log record
3483
        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
3484
 
3485
        // If we have data, insert it, else something went wrong in the restore_logs_processor
3486
        if ($data) {
3487
            if (empty($data->url)) {
3488
                $data->url = '';
3489
            }
3490
            if (empty($data->info)) {
3491
                $data->info = '';
3492
            }
3493
            // Store the data in the legacy log table if we are still using it.
3494
            $manager = get_log_manager();
3495
            if (method_exists($manager, 'legacy_add_to_log')) {
3496
                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
3497
                    $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
3498
            }
3499
        }
3500
    }
3501
}
3502
 
3503
/**
3504
 * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
3505
 * sharing its same structure but modifying the way records are handled
3506
 */
3507
class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
3508
 
3509
    protected function process_log($data) {
3510
        global $DB;
3511
 
3512
        $data = (object)($data);
3513
 
3514
        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
3515
 
3516
        $data->userid = $this->get_mappingid('user', $data->userid);
3517
        $data->course = $this->get_courseid();
3518
        $data->cmid = $this->task->get_moduleid();
3519
 
3520
        // For any reason user wasn't remapped ok, stop processing this
3521
        if (empty($data->userid)) {
3522
            return;
3523
        }
3524
 
3525
        // Everything ready, let's delegate to the restore_logs_processor
3526
 
3527
        // Set some fixed values that will save tons of DB requests
3528
        $values = array(
3529
            'course' => $this->get_courseid(),
3530
            'course_module' => $this->task->get_moduleid(),
3531
            $this->task->get_modulename() => $this->task->get_activityid());
3532
        // Get instance and process log record
3533
        $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
3534
 
3535
        // If we have data, insert it, else something went wrong in the restore_logs_processor
3536
        if ($data) {
3537
            if (empty($data->url)) {
3538
                $data->url = '';
3539
            }
3540
            if (empty($data->info)) {
3541
                $data->info = '';
3542
            }
3543
            // Store the data in the legacy log table if we are still using it.
3544
            $manager = get_log_manager();
3545
            if (method_exists($manager, 'legacy_add_to_log')) {
3546
                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
3547
                    $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
3548
            }
3549
        }
3550
    }
3551
}
3552
 
3553
/**
3554
 * Structure step in charge of restoring the logstores.xml file for the course logs.
3555
 *
3556
 * This restore step will rebuild the logs for all the enabled logstore subplugins supporting
3557
 * it, for logs belonging to the course level.
3558
 */
3559
class restore_course_logstores_structure_step extends restore_structure_step {
3560
 
3561
    /**
3562
     * Conditionally decide if this step should be executed.
3563
     *
3564
     * This function checks the following parameter:
3565
     *
3566
     *   1. the logstores.xml file exists
3567
     *
3568
     * @return bool true is safe to execute, false otherwise
3569
     */
3570
    protected function execute_condition() {
3571
 
3572
        // Check it is included in the backup.
3573
        $fullpath = $this->task->get_taskbasepath();
3574
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3575
        if (!file_exists($fullpath)) {
3576
            // Not found, can't restore logstores.xml information.
3577
            return false;
3578
        }
3579
 
3580
        return true;
3581
    }
3582
 
3583
    /**
3584
     * Return the elements to be processed on restore of logstores.
3585
     *
3586
     * @return restore_path_element[] array of elements to be processed on restore.
3587
     */
3588
    protected function define_structure() {
3589
 
3590
        $paths = array();
3591
 
3592
        $logstore = new restore_path_element('logstore', '/logstores/logstore');
3593
        $paths[] = $logstore;
3594
 
3595
        // Add logstore subplugin support to the 'logstore' element.
3596
        $this->add_subplugin_structure('logstore', $logstore, 'tool', 'log');
3597
 
3598
        return array($logstore);
3599
    }
3600
 
3601
    /**
3602
     * Process the 'logstore' element,
3603
     *
3604
     * Note: This is empty by definition in backup, because stores do not share any
3605
     * data between them, so there is nothing to process here.
3606
     *
3607
     * @param array $data element data
3608
     */
3609
    protected function process_logstore($data) {
3610
        return;
3611
    }
3612
}
3613
 
3614
/**
3615
 * Structure step in charge of restoring the loglastaccess.xml file for the course logs.
3616
 *
3617
 * This restore step will rebuild the table for user_lastaccess table.
3618
 */
3619
class restore_course_loglastaccess_structure_step extends restore_structure_step {
3620
 
3621
    /**
3622
     * Conditionally decide if this step should be executed.
3623
     *
3624
     * This function checks the following parameter:
3625
     *
3626
     *   1. the loglastaccess.xml file exists
3627
     *
3628
     * @return bool true is safe to execute, false otherwise
3629
     */
3630
    protected function execute_condition() {
3631
        // Check it is included in the backup.
3632
        $fullpath = $this->task->get_taskbasepath();
3633
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3634
        if (!file_exists($fullpath)) {
3635
            // Not found, can't restore loglastaccess.xml information.
3636
            return false;
3637
        }
3638
 
3639
        return true;
3640
    }
3641
 
3642
    /**
3643
     * Return the elements to be processed on restore of loglastaccess.
3644
     *
3645
     * @return restore_path_element[] array of elements to be processed on restore.
3646
     */
3647
    protected function define_structure() {
3648
 
3649
        $paths = array();
3650
        // To know if we are including userinfo.
3651
        $userinfo = $this->get_setting_value('users');
3652
 
3653
        if ($userinfo) {
3654
            $paths[] = new restore_path_element('lastaccess', '/lastaccesses/lastaccess');
3655
        }
3656
        // Return the paths wrapped.
3657
        return $paths;
3658
    }
3659
 
3660
    /**
3661
     * Process the 'lastaccess' elements.
3662
     *
3663
     * @param array $data element data
3664
     */
3665
    protected function process_lastaccess($data) {
3666
        global $DB;
3667
 
3668
        $data = (object)$data;
3669
 
3670
        $data->courseid = $this->get_courseid();
3671
        if (!$data->userid = $this->get_mappingid('user', $data->userid)) {
3672
            return; // Nothing to do, not able to find the user to set the lastaccess time.
3673
        }
3674
 
3675
        // Check if record does exist.
3676
        $exists = $DB->get_record('user_lastaccess', array('courseid' => $data->courseid, 'userid' => $data->userid));
3677
        if ($exists) {
3678
            // If the time of last access of the restore is newer, then replace and update.
3679
            if ($exists->timeaccess < $data->timeaccess) {
3680
                $exists->timeaccess = $data->timeaccess;
3681
                $DB->update_record('user_lastaccess', $exists);
3682
            }
3683
        } else {
3684
            $DB->insert_record('user_lastaccess', $data);
3685
        }
3686
    }
3687
}
3688
 
3689
/**
3690
 * Structure step in charge of restoring the logstores.xml file for the activity logs.
3691
 *
3692
 * Note: Activity structure is completely equivalent to the course one, so just extend it.
3693
 */
3694
class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step {
3695
}
3696
 
3697
/**
3698
 * Restore course competencies structure step.
3699
 */
3700
class restore_course_competencies_structure_step extends restore_structure_step {
3701
 
3702
    /**
3703
     * Returns the structure.
3704
     *
3705
     * @return array
3706
     */
3707
    protected function define_structure() {
3708
        $userinfo = $this->get_setting_value('users');
3709
        $paths = array(
3710
            new restore_path_element('course_competency', '/course_competencies/competencies/competency'),
3711
            new restore_path_element('course_competency_settings', '/course_competencies/settings'),
3712
        );
3713
        if ($userinfo) {
3714
            $paths[] = new restore_path_element('user_competency_course',
3715
                '/course_competencies/user_competencies/user_competency');
3716
        }
3717
        return $paths;
3718
    }
3719
 
3720
    /**
3721
     * Process a course competency settings.
3722
     *
3723
     * @param array $data The data.
3724
     */
3725
    public function process_course_competency_settings($data) {
3726
        global $DB;
3727
        $data = (object) $data;
3728
 
3729
        // We do not restore the course settings during merge.
3730
        $target = $this->get_task()->get_target();
3731
        if ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) {
3732
            return;
3733
        }
3734
 
3735
        $courseid = $this->task->get_courseid();
3736
        $exists = \core_competency\course_competency_settings::record_exists_select('courseid = :courseid',
3737
            array('courseid' => $courseid));
3738
 
3739
        // Strangely the course settings already exist, let's just leave them as is then.
3740
        if ($exists) {
3741
            $this->log('Course competency settings not restored, existing settings have been found.', backup::LOG_WARNING);
3742
            return;
3743
        }
3744
 
3745
        $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans);
3746
        $settings = new \core_competency\course_competency_settings(0, $data);
3747
        $settings->create();
3748
    }
3749
 
3750
    /**
3751
     * Process a course competency.
3752
     *
3753
     * @param array $data The data.
3754
     */
3755
    public function process_course_competency($data) {
3756
        $data = (object) $data;
3757
 
3758
        // Mapping the competency by ID numbers.
3759
        $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
3760
        if (!$framework) {
3761
            return;
3762
        }
3763
        $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
3764
            'competencyframeworkid' => $framework->get('id')));
3765
        if (!$competency) {
3766
            return;
3767
        }
3768
        $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get('id'));
3769
 
3770
        $params = array(
3771
            'competencyid' => $competency->get('id'),
3772
            'courseid' => $this->task->get_courseid()
3773
        );
3774
        $query = 'competencyid = :competencyid AND courseid = :courseid';
3775
        $existing = \core_competency\course_competency::record_exists_select($query, $params);
3776
 
3777
        if (!$existing) {
3778
            // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
3779
            $record = (object) $params;
3780
            $record->ruleoutcome = $data->ruleoutcome;
3781
            $coursecompetency = new \core_competency\course_competency(0, $record);
3782
            $coursecompetency->create();
3783
        }
3784
    }
3785
 
3786
    /**
3787
     * Process the user competency course.
3788
     *
3789
     * @param array $data The data.
3790
     */
3791
    public function process_user_competency_course($data) {
3792
        global $USER, $DB;
3793
        $data = (object) $data;
3794
 
3795
        $data->competencyid = $this->get_mappingid(\core_competency\competency::TABLE, $data->competencyid);
3796
        if (!$data->competencyid) {
3797
            // This is strange, the competency does not belong to the course.
3798
            return;
3799
        } else if ($data->grade === null) {
3800
            // We do not need to do anything when there is no grade.
3801
            return;
3802
        }
3803
 
3804
        $data->userid = $this->get_mappingid('user', $data->userid);
3805
        $shortname = $DB->get_field('course', 'shortname', array('id' => $this->task->get_courseid()), MUST_EXIST);
3806
 
3807
        // The method add_evidence also sets the course rating.
3808
        \core_competency\api::add_evidence($data->userid,
3809
                                           $data->competencyid,
3810
                                           $this->task->get_contextid(),
3811
                                           \core_competency\evidence::ACTION_OVERRIDE,
3812
                                           'evidence_courserestored',
3813
                                           'core_competency',
3814
                                           $shortname,
3815
                                           false,
3816
                                           null,
3817
                                           $data->grade,
3818
                                           $USER->id);
3819
    }
3820
 
3821
    /**
3822
     * Execute conditions.
3823
     *
3824
     * @return bool
3825
     */
3826
    protected function execute_condition() {
3827
 
3828
        // Do not execute if competencies are not included.
3829
        if (!$this->get_setting_value('competencies')) {
3830
            return false;
3831
        }
3832
 
3833
        // Do not execute if the competencies XML file is not found.
3834
        $fullpath = $this->task->get_taskbasepath();
3835
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3836
        if (!file_exists($fullpath)) {
3837
            return false;
3838
        }
3839
 
3840
        return true;
3841
    }
3842
}
3843
 
3844
/**
3845
 * Restore activity competencies structure step.
3846
 */
3847
class restore_activity_competencies_structure_step extends restore_structure_step {
3848
 
3849
    /**
3850
     * Defines the structure.
3851
     *
3852
     * @return array
3853
     */
3854
    protected function define_structure() {
3855
        $paths = array(
3856
            new restore_path_element('course_module_competency', '/course_module_competencies/competencies/competency')
3857
        );
3858
        return $paths;
3859
    }
3860
 
3861
    /**
3862
     * Process a course module competency.
3863
     *
3864
     * @param array $data The data.
3865
     */
3866
    public function process_course_module_competency($data) {
3867
        $data = (object) $data;
3868
 
3869
        // Mapping the competency by ID numbers.
3870
        $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
3871
        if (!$framework) {
3872
            return;
3873
        }
3874
        $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
3875
            'competencyframeworkid' => $framework->get('id')));
3876
        if (!$competency) {
3877
            return;
3878
        }
3879
 
3880
        $params = array(
3881
            'competencyid' => $competency->get('id'),
3882
            'cmid' => $this->task->get_moduleid()
3883
        );
3884
        $query = 'competencyid = :competencyid AND cmid = :cmid';
3885
        $existing = \core_competency\course_module_competency::record_exists_select($query, $params);
3886
 
3887
        if (!$existing) {
3888
            // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
3889
            $record = (object) $params;
3890
            $record->ruleoutcome = $data->ruleoutcome;
3891
            $record->overridegrade = $data->overridegrade ?? 0;
3892
            $coursemodulecompetency = new \core_competency\course_module_competency(0, $record);
3893
            $coursemodulecompetency->create();
3894
        }
3895
    }
3896
 
3897
    /**
3898
     * Execute conditions.
3899
     *
3900
     * @return bool
3901
     */
3902
    protected function execute_condition() {
3903
 
3904
        // Do not execute if competencies are not included.
3905
        if (!$this->get_setting_value('competencies')) {
3906
            return false;
3907
        }
3908
 
3909
        // Do not execute if the competencies XML file is not found.
3910
        $fullpath = $this->task->get_taskbasepath();
3911
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3912
        if (!file_exists($fullpath)) {
3913
            return false;
3914
        }
3915
 
3916
        return true;
3917
    }
3918
}
3919
 
3920
/**
3921
 * Defines the restore step for advanced grading methods attached to the activity module
3922
 */
3923
class restore_activity_grading_structure_step extends restore_structure_step {
3924
 
3925
    /**
3926
     * This step is executed only if the grading file is present
3927
     */
3928
     protected function execute_condition() {
3929
 
3930
        if ($this->get_courseid() == SITEID) {
3931
            return false;
3932
        }
3933
 
3934
        $fullpath = $this->task->get_taskbasepath();
3935
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
3936
        if (!file_exists($fullpath)) {
3937
            return false;
3938
        }
3939
 
3940
        return true;
3941
    }
3942
 
3943
 
3944
    /**
3945
     * Declares paths in the grading.xml file we are interested in
3946
     */
3947
    protected function define_structure() {
3948
 
3949
        $paths = array();
3950
        $userinfo = $this->get_setting_value('userinfo');
3951
 
3952
        $area = new restore_path_element('grading_area', '/areas/area');
3953
        $paths[] = $area;
3954
        // attach local plugin stucture to $area element
3955
        $this->add_plugin_structure('local', $area);
3956
 
3957
        $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
3958
        $paths[] = $definition;
3959
        $this->add_plugin_structure('gradingform', $definition);
3960
        // attach local plugin stucture to $definition element
3961
        $this->add_plugin_structure('local', $definition);
3962
 
3963
 
3964
        if ($userinfo) {
3965
            $instance = new restore_path_element('grading_instance',
3966
                '/areas/area/definitions/definition/instances/instance');
3967
            $paths[] = $instance;
3968
            $this->add_plugin_structure('gradingform', $instance);
3969
            // attach local plugin stucture to $intance element
3970
            $this->add_plugin_structure('local', $instance);
3971
        }
3972
 
3973
        return $paths;
3974
    }
3975
 
3976
    /**
3977
     * Processes one grading area element
3978
     *
3979
     * @param array $data element data
3980
     */
3981
    protected function process_grading_area($data) {
3982
        global $DB;
3983
 
3984
        $task = $this->get_task();
3985
        $data = (object)$data;
3986
        $oldid = $data->id;
3987
        $data->component = 'mod_'.$task->get_modulename();
3988
        $data->contextid = $task->get_contextid();
3989
 
3990
        $newid = $DB->insert_record('grading_areas', $data);
3991
        $this->set_mapping('grading_area', $oldid, $newid);
3992
    }
3993
 
3994
    /**
3995
     * Processes one grading definition element
3996
     *
3997
     * @param array $data element data
3998
     */
3999
    protected function process_grading_definition($data) {
4000
        global $DB;
4001
 
4002
        $task = $this->get_task();
4003
        $data = (object)$data;
4004
        $oldid = $data->id;
4005
        $data->areaid = $this->get_new_parentid('grading_area');
4006
        $data->copiedfromid = null;
4007
        $data->timecreated = time();
4008
        $data->usercreated = $task->get_userid();
4009
        $data->timemodified = $data->timecreated;
4010
        $data->usermodified = $data->usercreated;
4011
 
4012
        $newid = $DB->insert_record('grading_definitions', $data);
4013
        $this->set_mapping('grading_definition', $oldid, $newid, true);
4014
    }
4015
 
4016
    /**
4017
     * Processes one grading form instance element
4018
     *
4019
     * @param array $data element data
4020
     */
4021
    protected function process_grading_instance($data) {
4022
        global $DB;
4023
 
4024
        $data = (object)$data;
4025
 
4026
        // new form definition id
4027
        $newformid = $this->get_new_parentid('grading_definition');
4028
 
4029
        // get the name of the area we are restoring to
4030
        $sql = "SELECT ga.areaname
4031
                  FROM {grading_definitions} gd
4032
                  JOIN {grading_areas} ga ON gd.areaid = ga.id
4033
                 WHERE gd.id = ?";
4034
        $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
4035
 
4036
        // get the mapped itemid - the activity module is expected to define the mappings
4037
        // for each gradable area
4038
        $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
4039
 
4040
        $oldid = $data->id;
4041
        $data->definitionid = $newformid;
4042
        $data->raterid = $this->get_mappingid('user', $data->raterid);
4043
        $data->itemid = $newitemid;
4044
 
4045
        $newid = $DB->insert_record('grading_instances', $data);
4046
        $this->set_mapping('grading_instance', $oldid, $newid);
4047
    }
4048
 
4049
    /**
4050
     * Final operations when the database records are inserted
4051
     */
4052
    protected function after_execute() {
4053
        // Add files embedded into the definition description
4054
        $this->add_related_files('grading', 'description', 'grading_definition');
4055
    }
4056
}
4057
 
4058
 
4059
/**
4060
 * This structure step restores the grade items associated with one activity
4061
 * All the grade items are made child of the "course" grade item but the original
4062
 * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
4063
 * the complete gradebook (categories and calculations), that information is
4064
 * available there
4065
 */
4066
class restore_activity_grades_structure_step extends restore_structure_step {
4067
 
4068
    /**
4069
     * No grades in front page.
4070
     * @return bool
4071
     */
4072
    protected function execute_condition() {
4073
        return ($this->get_courseid() != SITEID);
4074
    }
4075
 
4076
    protected function define_structure() {
4077
 
4078
        $paths = array();
4079
        $userinfo = $this->get_setting_value('userinfo');
4080
 
4081
        $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
4082
        $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
4083
        if ($userinfo) {
4084
            $paths[] = new restore_path_element('grade_grade',
4085
                           '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
4086
        }
4087
        return $paths;
4088
    }
4089
 
4090
    protected function process_grade_item($data) {
4091
        global $DB;
4092
 
4093
        $data = (object)($data);
4094
        $oldid       = $data->id;        // We'll need these later
4095
        $oldparentid = $data->categoryid;
4096
        $courseid = $this->get_courseid();
4097
 
4098
        $idnumber = null;
4099
        if (!empty($data->idnumber)) {
4100
            // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
4101
            // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
4102
            // so the best is to keep the ones already in the gradebook
4103
            // Potential problem: duplicates if same items are restored more than once. :-(
4104
            // This needs to be fixed in some way (outcomes & activities with multiple items)
4105
            // $data->idnumber     = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
4106
            // In any case, verify always for uniqueness
4107
            $sql = "SELECT cm.id
4108
                      FROM {course_modules} cm
4109
                     WHERE cm.course = :courseid AND
4110
                           cm.idnumber = :idnumber AND
4111
                           cm.id <> :cmid";
4112
            $params = array(
4113
                'courseid' => $courseid,
4114
                'idnumber' => $data->idnumber,
4115
                'cmid' => $this->task->get_moduleid()
4116
            );
4117
            if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
4118
                $idnumber = $data->idnumber;
4119
            }
4120
        }
4121
 
4122
        if (!empty($data->categoryid)) {
4123
            // If the grade category id of the grade item being restored belongs to this course
4124
            // then it is a fair assumption that this is the correct grade category for the activity
4125
            // and we should leave it in place, if not then unset it.
4126
            // TODO MDL-34790 Gradebook does not import if target course has gradebook categories.
4127
            $conditions = array('id' => $data->categoryid, 'courseid' => $courseid);
4128
            if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) {
4129
                unset($data->categoryid);
4130
            }
4131
        }
4132
 
4133
        unset($data->id);
4134
        $data->courseid     = $this->get_courseid();
4135
        $data->iteminstance = $this->task->get_activityid();
4136
        $data->idnumber     = $idnumber;
4137
        $data->scaleid      = $this->get_mappingid('scale', $data->scaleid);
4138
        $data->outcomeid    = $this->get_mappingid('outcome', $data->outcomeid);
4139
 
4140
        $gradeitem = new grade_item($data, false);
4141
        $gradeitem->insert('restore');
4142
 
4143
        //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
4144
        $gradeitem->sortorder = $data->sortorder;
4145
        $gradeitem->update('restore');
4146
 
4147
        // Set mapping, saving the original category id into parentitemid
4148
        // gradebook restore (final task) will need it to reorganise items
4149
        $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
4150
    }
4151
 
4152
    protected function process_grade_grade($data) {
4153
        global $CFG;
4154
 
4155
        require_once($CFG->libdir . '/grade/constants.php');
4156
 
4157
        $data = (object)($data);
4158
        $olduserid = $data->userid;
4159
        $oldid = $data->id;
4160
        unset($data->id);
4161
 
4162
        $data->itemid = $this->get_new_parentid('grade_item');
4163
 
4164
        $data->userid = $this->get_mappingid('user', $data->userid, null);
4165
        if (!empty($data->userid)) {
4166
            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
4167
            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
4168
 
4169
            $grade = new grade_grade($data, false);
4170
            $grade->insert('restore');
4171
 
4172
            $this->set_mapping('grade_grades', $oldid, $grade->id, true);
4173
 
4174
            $this->add_related_files(
4175
                GRADE_FILE_COMPONENT,
4176
                GRADE_FEEDBACK_FILEAREA,
4177
                'grade_grades',
4178
                null,
4179
                $oldid
4180
            );
4181
        } else {
4182
            debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
4183
        }
4184
    }
4185
 
4186
    /**
4187
     * process activity grade_letters. Note that, while these are possible,
4188
     * because grade_letters are contextid based, in practice, only course
4189
     * context letters can be defined. So we keep here this method knowing
4190
     * it won't be executed ever. gradebook restore will restore course letters.
4191
     */
4192
    protected function process_grade_letter($data) {
4193
        global $DB;
4194
 
4195
        $data['contextid'] = $this->task->get_contextid();
4196
        $gradeletter = (object)$data;
4197
 
4198
        // Check if it exists before adding it
4199
        unset($data['id']);
4200
        if (!$DB->record_exists('grade_letters', $data)) {
4201
            $newitemid = $DB->insert_record('grade_letters', $gradeletter);
4202
        }
4203
        // no need to save any grade_letter mapping
4204
    }
4205
 
4206
    public function after_restore() {
4207
        // Fix grade item's sortorder after restore, as it might have duplicates.
4208
        $courseid = $this->get_task()->get_courseid();
4209
        grade_item::fix_duplicate_sortorder($courseid);
4210
    }
4211
}
4212
 
4213
/**
4214
 * Step in charge of restoring the grade history of an activity.
4215
 *
4216
 * This step is added to the task regardless of the setting 'grade_histories'.
4217
 * The reason is to allow for a more flexible step in case the logic needs to be
4218
 * split accross different settings to control the history of items and/or grades.
4219
 */
4220
class restore_activity_grade_history_structure_step extends restore_structure_step {
4221
 
4222
    /**
4223
     * This step is executed only if the grade history file is present.
4224
     */
4225
     protected function execute_condition() {
4226
 
4227
        if ($this->get_courseid() == SITEID) {
4228
            return false;
4229
        }
4230
 
4231
        $fullpath = $this->task->get_taskbasepath();
4232
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
4233
        if (!file_exists($fullpath)) {
4234
            return false;
4235
        }
4236
        return true;
4237
    }
4238
 
4239
    protected function define_structure() {
4240
        $paths = array();
4241
 
4242
        // Settings to use.
4243
        $userinfo = $this->get_setting_value('userinfo');
4244
        $history = $this->get_setting_value('grade_histories');
4245
 
4246
        if ($userinfo && $history) {
4247
            $paths[] = new restore_path_element('grade_grade',
4248
               '/grade_history/grade_grades/grade_grade');
4249
        }
4250
 
4251
        return $paths;
4252
    }
4253
 
4254
    protected function process_grade_grade($data) {
4255
        global $CFG, $DB;
4256
 
4257
        require_once($CFG->libdir . '/grade/constants.php');
4258
 
4259
        $data = (object) $data;
4260
        $oldhistoryid = $data->id;
4261
        $olduserid = $data->userid;
4262
        unset($data->id);
4263
 
4264
        $data->userid = $this->get_mappingid('user', $data->userid, null);
4265
        if (!empty($data->userid)) {
4266
            // Do not apply the date offsets as this is history.
4267
            $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
4268
            $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
4269
            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
4270
            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
4271
 
4272
            $newhistoryid = $DB->insert_record('grade_grades_history', $data);
4273
 
4274
            $this->set_mapping('grade_grades_history', $oldhistoryid, $newhistoryid, true);
4275
 
4276
            $this->add_related_files(
4277
                GRADE_FILE_COMPONENT,
4278
                GRADE_HISTORY_FEEDBACK_FILEAREA,
4279
                'grade_grades_history',
4280
                null,
4281
                $oldhistoryid
4282
            );
4283
        } else {
4284
            $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
4285
            $this->log($message, backup::LOG_DEBUG);
4286
        }
4287
    }
4288
}
4289
 
4290
/**
4291
 * This structure steps restores the content bank content
4292
 */
4293
class restore_contentbankcontent_structure_step extends restore_structure_step {
4294
 
4295
    /**
4296
     * Define structure for content bank step
4297
     */
4298
    protected function define_structure() {
4299
 
4300
        $paths = [];
4301
        $paths[] = new restore_path_element('contentbankcontent', '/contents/content');
4302
 
4303
        return $paths;
4304
    }
4305
 
4306
    /**
4307
     * Define data processed for content bank
4308
     *
4309
     * @param mixed  $data
4310
     */
4311
    public function process_contentbankcontent($data) {
4312
        global $DB;
4313
 
4314
        $data = (object)$data;
4315
        $oldid = $data->id;
4316
 
4317
        $params = [
4318
            'name'           => $data->name,
4319
            'contextid'      => $this->task->get_contextid(),
4320
            'contenttype'    => $data->contenttype,
4321
            'instanceid'     => $data->instanceid,
4322
            'timecreated'    => $data->timecreated,
4323
        ];
4324
        $exists = $DB->record_exists('contentbank_content', $params);
4325
        if (!$exists) {
4326
            $params['configdata'] = $data->configdata;
4327
            $params['timemodified'] = time();
4328
 
4329
            // Trying to map users. Users cannot always be mapped, e.g. when copying.
4330
            $params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
4331
            if (!$params['usercreated']) {
4332
                // Leave the content creator unchanged when we are restoring the same site.
4333
                // Otherwise use current user id.
4334
                if ($this->task->is_samesite()) {
4335
                    $params['usercreated'] = $data->usercreated;
4336
                } else {
4337
                    $params['usercreated'] = $this->task->get_userid();
4338
                }
4339
            }
4340
            $params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
4341
            if (!$params['usermodified']) {
4342
                // Leave the content modifier unchanged when we are restoring the same site.
4343
                // Otherwise use current user id.
4344
                if ($this->task->is_samesite()) {
4345
                    $params['usermodified'] = $data->usermodified;
4346
                } else {
4347
                    $params['usermodified'] = $this->task->get_userid();
4348
                }
4349
            }
4350
 
4351
            $newitemid = $DB->insert_record('contentbank_content', $params);
4352
            $this->set_mapping('contentbank_content', $oldid, $newitemid, true);
4353
        }
4354
    }
4355
 
4356
    /**
4357
     * Define data processed after execute for content bank
4358
     */
4359
    protected function after_execute() {
4360
        // Add related files.
4361
        $this->add_related_files('contentbank', 'public', 'contentbank_content');
4362
    }
4363
}
4364
 
4365
/**
4366
 * This structure steps restores the xAPI states.
4367
 */
4368
class restore_xapistate_structure_step extends restore_structure_step {
4369
 
4370
    /**
4371
     * Define structure for xAPI state step
4372
     */
4373
    protected function define_structure() {
4374
        return [new restore_path_element('xapistate', '/states/state')];
4375
    }
4376
 
4377
    /**
4378
     * Define data processed for xAPI state.
4379
     *
4380
     * @param array|stdClass $data
4381
     */
4382
    public function process_xapistate($data) {
4383
        global $DB;
4384
 
4385
        $data = (object)$data;
4386
        $oldid = $data->id;
4387
        $exists = false;
4388
 
4389
        $params = [
4390
            'component' => $data->component,
4391
            'itemid' => $this->task->get_contextid(),
4392
            // Set stateid to 'restored', to let plugins identify the origin of this state is a backup.
4393
            'stateid' => 'restored',
4394
            'statedata' => $data->statedata,
4395
            'registration' => $data->registration,
4396
            'timecreated' => $data->timecreated,
4397
            'timemodified' => time(),
4398
        ];
4399
 
4400
        // Trying to map users. Users cannot always be mapped, for instance, when copying.
4401
        $params['userid'] = $this->get_mappingid('user', $data->userid);
4402
        if (!$params['userid']) {
4403
            // Leave the userid unchanged when we are restoring the same site.
4404
            if ($this->task->is_samesite()) {
4405
                $params['userid'] = $data->userid;
4406
            }
4407
            $filter = $params;
4408
            unset($filter['statedata']);
4409
            $exists = $DB->record_exists('xapi_states', $filter);
4410
        }
4411
 
4412
        if (!$exists && $params['userid']) {
4413
            // Only insert the record if the user exists or can be mapped.
4414
            $newitemid = $DB->insert_record('xapi_states', $params);
4415
            $this->set_mapping('xapi_states', $oldid, $newitemid, true);
4416
        }
4417
    }
4418
}
4419
 
4420
/**
4421
 * This structure steps restores one instance + positions of one block
4422
 * Note: Positions corresponding to one existing context are restored
4423
 * here, but all the ones having unknown contexts are sent to backup_ids
4424
 * for a later chance to be restored at the end (final task)
4425
 */
4426
class restore_block_instance_structure_step extends restore_structure_step {
4427
 
4428
    protected function define_structure() {
4429
 
4430
        $paths = array();
4431
 
4432
        $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
4433
        $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
4434
 
4435
        return $paths;
4436
    }
4437
 
4438
    public function process_block($data) {
4439
        global $DB, $CFG;
4440
 
4441
        $data = (object)$data; // Handy
4442
        $oldcontextid = $data->contextid;
4443
        $oldid        = $data->id;
4444
        $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
4445
 
4446
        // Look for the parent contextid
4447
        if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
4448
            // Parent contextid does not exist, ignore this block.
4449
            return false;
4450
        }
4451
 
4452
        // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
4453
        // If there is already one block of that type in the parent context
4454
        // and the block is not multiple, stop processing
4455
        // Use blockslib loader / method executor
4456
        if (!$bi = block_instance($data->blockname)) {
4457
            return false;
4458
        }
4459
 
4460
        if (!$bi->instance_allow_multiple()) {
4461
            // The block cannot be added twice, so we will check if the same block is already being
4462
            // displayed on the same page. For this, rather than mocking a page and using the block_manager
4463
            // we use a similar query to the one in block_manager::load_blocks(), this will give us
4464
            // a very good idea of the blocks already displayed in the context.
4465
            $params =  array(
4466
                'blockname' => $data->blockname
4467
            );
4468
 
4469
            // Context matching test.
4470
            $context = context::instance_by_id($data->parentcontextid);
4471
            $contextsql = 'bi.parentcontextid = :contextid';
4472
            $params['contextid'] = $context->id;
4473
 
4474
            $parentcontextids = $context->get_parent_context_ids();
4475
            if ($parentcontextids) {
4476
                list($parentcontextsql, $parentcontextparams) =
4477
                        $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
4478
                $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
4479
                $params = array_merge($params, $parentcontextparams);
4480
            }
4481
 
4482
            // Page type pattern test.
4483
            $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
4484
            list($pagetypepatternsql, $pagetypepatternparams) =
4485
                $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
4486
            $params = array_merge($params, $pagetypepatternparams);
4487
 
4488
            // Sub page pattern test.
4489
            $subpagepatternsql = 'bi.subpagepattern IS NULL';
4490
            if ($data->subpagepattern !== null) {
4491
                $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
4492
                $params['subpagepattern'] = $data->subpagepattern;
4493
            }
4494
 
4495
            $existingblock = $DB->get_records_sql("SELECT bi.id
4496
                                                FROM {block_instances} bi
4497
                                                JOIN {block} b ON b.name = bi.blockname
4498
                                               WHERE bi.blockname = :blockname
4499
                                                 AND $contextsql
4500
                                                 AND bi.pagetypepattern $pagetypepatternsql
4501
                                                 AND $subpagepatternsql", $params);
4502
            if (!empty($existingblock)) {
4503
                // Save the context mapping in case something else is linking to this block's context.
4504
                $newcontext = context_block::instance(reset($existingblock)->id);
4505
                $this->set_mapping('context', $oldcontextid, $newcontext->id);
4506
                // There is at least one very similar block visible on the page where we
4507
                // are trying to restore the block. In these circumstances the block API
4508
                // would not allow the user to add another instance of the block, so we
4509
                // apply the same rule here.
4510
                return false;
4511
            }
4512
        }
4513
 
4514
        // If there is already one block of that type in the parent context
4515
        // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
4516
        // stop processing
4517
        $params = array(
4518
            'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
4519
            'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
4520
            'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
4521
        if ($birecs = $DB->get_records('block_instances', $params)) {
4522
            foreach($birecs as $birec) {
4523
                if ($birec->configdata == $data->configdata) {
4524
                    // Save the context mapping in case something else is linking to this block's context.
4525
                    $newcontext = context_block::instance($birec->id);
4526
                    $this->set_mapping('context', $oldcontextid, $newcontext->id);
4527
                    return false;
4528
                }
4529
            }
4530
        }
4531
 
4532
        // Set task old contextid, blockid and blockname once we know them
4533
        $this->task->set_old_contextid($oldcontextid);
4534
        $this->task->set_old_blockid($oldid);
4535
        $this->task->set_blockname($data->blockname);
4536
 
4537
        // Let's look for anything within configdata neededing processing
4538
        // (nulls and uses of legacy file.php)
4539
        if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
4540
            $configdata = array_filter(
4541
                (array) unserialize_object(base64_decode($data->configdata)),
4542
                static function($value): bool {
4543
                    return !($value instanceof __PHP_Incomplete_Class);
4544
                }
4545
            );
4546
 
4547
            foreach ($configdata as $attribute => $value) {
4548
                if (in_array($attribute, $attrstotransform)) {
4549
                    $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
4550
                }
4551
            }
4552
            $data->configdata = base64_encode(serialize((object)$configdata));
4553
        }
4554
 
4555
        // Set timecreated, timemodified if not included (older backup).
4556
        if (empty($data->timecreated)) {
4557
            $data->timecreated = time();
4558
        }
4559
        if (empty($data->timemodified)) {
4560
            $data->timemodified = $data->timecreated;
4561
        }
4562
 
4563
        // Create the block instance
4564
        $newitemid = $DB->insert_record('block_instances', $data);
4565
        // Save the mapping (with restorefiles support)
4566
        $this->set_mapping('block_instance', $oldid, $newitemid, true);
4567
        // Create the block context
4568
        $newcontextid = context_block::instance($newitemid)->id;
4569
        // Save the block contexts mapping and sent it to task
4570
        $this->set_mapping('context', $oldcontextid, $newcontextid);
4571
        $this->task->set_contextid($newcontextid);
4572
        $this->task->set_blockid($newitemid);
4573
 
4574
        // Restore block fileareas if declared
4575
        $component = 'block_' . $this->task->get_blockname();
4576
        foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
4577
            $this->add_related_files($component, $filearea, null);
4578
        }
4579
 
4580
        // Process block positions, creating them or accumulating for final step
4581
        foreach($positions as $position) {
4582
            $position = (object)$position;
4583
            $position->blockinstanceid = $newitemid; // The instance is always the restored one
4584
            // If position is for one already mapped (known) contextid
4585
            // process it now, creating the position
4586
            if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
4587
                $position->contextid = $newpositionctxid;
4588
                // Create the block position
4589
                $DB->insert_record('block_positions', $position);
4590
 
4591
            // The position belongs to an unknown context, send it to backup_ids
4592
            // to process them as part of the final steps of restore. We send the
4593
            // whole $position object there, hence use the low level method.
4594
            } else {
4595
                restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
4596
            }
4597
        }
4598
    }
4599
}
4600
 
4601
/**
4602
 * Structure step to restore common course_module information
4603
 *
4604
 * This step will process the module.xml file for one activity, in order to restore
4605
 * the corresponding information to the course_modules table, skipping various bits
4606
 * of information based on CFG settings (groupings, completion...) in order to fullfill
4607
 * all the reqs to be able to create the context to be used by all the rest of steps
4608
 * in the activity restore task
4609
 */
4610
class restore_module_structure_step extends restore_structure_step {
4611
 
4612
    protected function define_structure() {
4613
        global $CFG;
4614
 
4615
        $paths = array();
4616
 
4617
        $module = new restore_path_element('module', '/module');
4618
        $paths[] = $module;
4619
        if ($CFG->enableavailability) {
4620
            $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
4621
            $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
4622
        }
4623
 
4624
        $paths[] = new restore_path_element('tag', '/module/tags/tag');
4625
 
4626
        // Apply for 'format' plugins optional paths at module level
4627
        $this->add_plugin_structure('format', $module);
4628
 
4629
        // Apply for 'report' plugins optional paths at module level.
4630
        $this->add_plugin_structure('report', $module);
4631
 
4632
        // Apply for 'plagiarism' plugins optional paths at module level
4633
        $this->add_plugin_structure('plagiarism', $module);
4634
 
4635
        // Apply for 'local' plugins optional paths at module level
4636
        $this->add_plugin_structure('local', $module);
4637
 
4638
        // Apply for 'admin tool' plugins optional paths at module level.
4639
        $this->add_plugin_structure('tool', $module);
4640
 
4641
        return $paths;
4642
    }
4643
 
4644
    protected function process_module($data) {
4645
        global $CFG, $DB;
4646
 
4647
        $data = (object)$data;
4648
        $oldid = $data->id;
4649
        $this->task->set_old_moduleversion($data->version);
4650
 
4651
        $data->course = $this->task->get_courseid();
4652
        $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
4653
        // Map section (first try by course_section mapping match. Useful in course and section restores)
4654
        $data->section = $this->get_mappingid('course_section', $data->sectionid);
4655
        if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
4656
            $params = array(
4657
                'course' => $this->get_courseid(),
4658
                'section' => $data->sectionnumber);
4659
            $data->section = $DB->get_field('course_sections', 'id', $params);
4660
        }
4661
        if (!$data->section) { // sectionnumber failed, try to get first section in course
4662
            $params = array(
4663
                'course' => $this->get_courseid());
4664
            $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
4665
        }
4666
        if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
4667
            $sectionrec = array(
4668
                'course' => $this->get_courseid(),
4669
                'section' => 0,
4670
                'timemodified' => time());
4671
            $DB->insert_record('course_sections', $sectionrec); // section 0
4672
            $sectionrec = array(
4673
                'course' => $this->get_courseid(),
4674
                'section' => 1,
4675
                'timemodified' => time());
4676
            $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
4677
        }
4678
        $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
4679
        if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) {        // idnumber uniqueness
4680
            $data->idnumber = '';
4681
        }
4682
        if (empty($CFG->enablecompletion)) { // completion
4683
            $data->completion = 0;
4684
            $data->completiongradeitemnumber = null;
4685
            $data->completionview = 0;
4686
            $data->completionexpected = 0;
4687
        } else {
4688
            $data->completionexpected = $this->apply_date_offset($data->completionexpected);
4689
        }
4690
        if (empty($CFG->enableavailability)) {
4691
            $data->availability = null;
4692
        }
4693
        // Backups that did not include showdescription, set it to default 0
4694
        // (this is not totally necessary as it has a db default, but just to
4695
        // be explicit).
4696
        if (!isset($data->showdescription)) {
4697
            $data->showdescription = 0;
4698
        }
4699
        $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
4700
 
4701
        if (empty($data->availability)) {
4702
            // If there are legacy availablility data fields (and no new format data),
4703
            // convert the old fields.
4704
            $data->availability = \core_availability\info::convert_legacy_fields(
4705
                    $data, false);
4706
        } else if (!empty($data->groupmembersonly)) {
4707
            // There is current availability data, but it still has groupmembersonly
4708
            // as well (2.7 backups), convert just that part.
4709
            require_once($CFG->dirroot . '/lib/db/upgradelib.php');
4710
            $data->availability = upgrade_group_members_only($data->groupingid, $data->availability);
4711
        }
4712
 
4713
        if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($data->course))) {
4714
            unset($data->lang);
4715
        }
4716
 
4717
        // course_module record ready, insert it
4718
        $newitemid = $DB->insert_record('course_modules', $data);
4719
        // save mapping
4720
        $this->set_mapping('course_module', $oldid, $newitemid);
4721
        // set the new course_module id in the task
4722
        $this->task->set_moduleid($newitemid);
4723
        // we can now create the context safely
4724
        $ctxid = context_module::instance($newitemid)->id;
4725
        // set the new context id in the task
4726
        $this->task->set_contextid($ctxid);
4727
        // update sequence field in course_section
4728
        if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
4729
            $sequence .= ',' . $newitemid;
4730
        } else {
4731
            $sequence = $newitemid;
4732
        }
4733
 
4734
        $updatesection = new \stdClass();
4735
        $updatesection->id = $data->section;
4736
        $updatesection->sequence = $sequence;
4737
        $updatesection->timemodified = time();
4738
        $DB->update_record('course_sections', $updatesection);
4739
 
4740
        // If there is the legacy showavailability data, store this for later use.
4741
        // (This data is not present when restoring 'new' backups.)
4742
        if (isset($data->showavailability)) {
4743
            // Cache the showavailability flag using the backup_ids data field.
4744
            restore_dbops::set_backup_ids_record($this->get_restoreid(),
4745
                    'module_showavailability', $newitemid, 0, null,
4746
                    (object)array('showavailability' => $data->showavailability));
4747
        }
4748
    }
4749
 
4750
    /**
4751
     * Fetch all the existing because tag_set() deletes them
4752
     * so everything must be reinserted on each call.
4753
     *
4754
     * @param stdClass $data Record data
4755
     */
4756
    protected function process_tag($data) {
4757
        global $CFG;
4758
 
4759
        $data = (object)$data;
4760
 
4761
        if (core_tag_tag::is_enabled('core', 'course_modules')) {
4762
            $modcontext = context::instance_by_id($this->task->get_contextid());
4763
            $instanceid = $this->task->get_moduleid();
4764
 
4765
            core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
4766
        }
4767
    }
4768
 
4769
    /**
4770
     * Process the legacy availability table record. This table does not exist
4771
     * in Moodle 2.7+ but we still support restore.
4772
     *
4773
     * @param stdClass $data Record data
4774
     */
4775
    protected function process_availability($data) {
4776
        $data = (object)$data;
4777
        // Simply going to store the whole availability record now, we'll process
4778
        // all them later in the final task (once all activities have been restored)
4779
        // Let's call the low level one to be able to store the whole object
4780
        $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
4781
        restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
4782
    }
4783
 
4784
    /**
4785
     * Process the legacy availability fields table record. This table does not
4786
     * exist in Moodle 2.7+ but we still support restore.
4787
     *
4788
     * @param stdClass $data Record data
4789
     */
4790
    protected function process_availability_field($data) {
4791
        global $DB, $CFG;
4792
        require_once($CFG->dirroot.'/user/profile/lib.php');
4793
 
4794
        $data = (object)$data;
4795
        // Mark it is as passed by default
4796
        $passed = true;
4797
        $customfieldid = null;
4798
 
4799
        // If a customfield has been used in order to pass we must be able to match an existing
4800
        // customfield by name (data->customfield) and type (data->customfieldtype)
4801
        if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
4802
            // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
4803
            // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
4804
            $passed = false;
4805
        } else if (!empty($data->customfield)) {
4806
            $field = profile_get_custom_field_data_by_shortname($data->customfield);
4807
            $passed = $field && $field->datatype == $data->customfieldtype;
4808
        }
4809
 
4810
        if ($passed) {
4811
            // Create the object to insert into the database
4812
            $availfield = new stdClass();
4813
            $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
4814
            $availfield->userfield = $data->userfield;
4815
            $availfield->customfieldid = $customfieldid;
4816
            $availfield->operator = $data->operator;
4817
            $availfield->value = $data->value;
4818
 
4819
            // Get showavailability option.
4820
            $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
4821
                    'module_showavailability', $availfield->coursemoduleid);
4822
            if (!$showrec) {
4823
                // Should not happen.
4824
                throw new coding_exception('No matching showavailability record');
4825
            }
4826
            $show = $showrec->info->showavailability;
4827
 
4828
            // The $availfieldobject is now in the format used in the old
4829
            // system. Interpret this and convert to new system.
4830
            $currentvalue = $DB->get_field('course_modules', 'availability',
4831
                    array('id' => $availfield->coursemoduleid), MUST_EXIST);
4832
            $newvalue = \core_availability\info::add_legacy_availability_field_condition(
4833
                    $currentvalue, $availfield, $show);
4834
            $DB->set_field('course_modules', 'availability', $newvalue,
4835
                    array('id' => $availfield->coursemoduleid));
4836
        }
4837
    }
4838
    /**
4839
     * This method will be executed after the rest of the restore has been processed.
4840
     *
4841
     * Update old tag instance itemid(s).
4842
     */
4843
    protected function after_restore() {
4844
        global $DB;
4845
 
4846
        $contextid = $this->task->get_contextid();
4847
        $instanceid = $this->task->get_activityid();
4848
        $olditemid = $this->task->get_old_activityid();
4849
 
4850
        $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
4851
    }
4852
}
4853
 
4854
/**
4855
 * Structure step that will process the user activity completion
4856
 * information if all these conditions are met:
4857
 *  - Target site has completion enabled ($CFG->enablecompletion)
4858
 *  - Activity includes completion info (file_exists)
4859
 */
4860
class restore_userscompletion_structure_step extends restore_structure_step {
4861
    /**
4862
     * To conditionally decide if this step must be executed
4863
     * Note the "settings" conditions are evaluated in the
4864
     * corresponding task. Here we check for other conditions
4865
     * not being restore settings (files, site settings...)
4866
     */
4867
     protected function execute_condition() {
4868
         global $CFG;
4869
 
4870
         // Completion disabled in this site, don't execute
4871
         if (empty($CFG->enablecompletion)) {
4872
             return false;
4873
         }
4874
 
4875
        // No completion on the front page.
4876
        if ($this->get_courseid() == SITEID) {
4877
            return false;
4878
        }
4879
 
4880
         // No user completion info found, don't execute
4881
        $fullpath = $this->task->get_taskbasepath();
4882
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
4883
         if (!file_exists($fullpath)) {
4884
             return false;
4885
         }
4886
 
4887
         // Arrived here, execute the step
4888
         return true;
4889
     }
4890
 
4891
     protected function define_structure() {
4892
 
4893
        $paths = array();
4894
 
4895
        // Restore completion.
4896
        $paths[] = new restore_path_element('completion', '/completions/completion');
4897
 
4898
        // Restore completion view.
4899
        $paths[] = new restore_path_element('completionview', '/completions/completionviews/completionview');
4900
 
4901
        return $paths;
4902
    }
4903
 
4904
    protected function process_completion($data) {
4905
        global $DB;
4906
 
4907
        $data = (object)$data;
4908
 
4909
        $data->coursemoduleid = $this->task->get_moduleid();
4910
        $data->userid = $this->get_mappingid('user', $data->userid);
4911
 
4912
        // Find the existing record
4913
        $existing = $DB->get_record('course_modules_completion', array(
4914
                'coursemoduleid' => $data->coursemoduleid,
4915
                'userid' => $data->userid), 'id, timemodified');
4916
        // Check we didn't already insert one for this cmid and userid
4917
        // (there aren't supposed to be duplicates in that field, but
4918
        // it was possible until MDL-28021 was fixed).
4919
        if ($existing) {
4920
            // Update it to these new values, but only if the time is newer
4921
            if ($existing->timemodified < $data->timemodified) {
4922
                $data->id = $existing->id;
4923
                $DB->update_record('course_modules_completion', $data);
4924
            }
4925
        } else {
4926
            // Normal entry where it doesn't exist already
4927
            $DB->insert_record('course_modules_completion', $data);
4928
        }
4929
 
4930
        // Add viewed to course_modules_viewed.
4931
        if (isset($data->viewed) && $data->viewed) {
4932
            $dataview = clone($data);
4933
            unset($dataview->id);
4934
            unset($dataview->viewed);
4935
            $dataview->timecreated = $data->timemodified;
4936
            $DB->insert_record('course_modules_viewed', $dataview);
4937
        }
4938
    }
4939
 
4940
    /**
4941
     * Process the completioinview data.
4942
     * @param array $data The data from the XML file.
4943
     */
4944
    protected function process_completionview(array $data) {
4945
        global $DB;
4946
 
4947
        $data = (object)$data;
4948
        $data->coursemoduleid = $this->task->get_moduleid();
4949
        $data->userid = $this->get_mappingid('user', $data->userid);
4950
 
4951
        $DB->insert_record('course_modules_viewed', $data);
4952
    }
4953
}
4954
 
4955
/**
4956
 * Abstract structure step, parent of all the activity structure steps. Used to support
4957
 * the main <activity ...> tag and process it.
4958
 */
4959
abstract class restore_activity_structure_step extends restore_structure_step {
4960
 
4961
    /**
4962
     * Adds support for the 'activity' path that is common to all the activities
4963
     * and will be processed globally here
4964
     */
4965
    protected function prepare_activity_structure($paths) {
4966
 
4967
        $paths[] = new restore_path_element('activity', '/activity');
4968
 
4969
        return $paths;
4970
    }
4971
 
4972
    /**
4973
     * Process the activity path, informing the task about various ids, needed later
4974
     */
4975
    protected function process_activity($data) {
4976
        $data = (object)$data;
4977
        $this->task->set_old_contextid($data->contextid); // Save old contextid in task
4978
        $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
4979
        $this->task->set_old_activityid($data->id); // Save old activityid in task
4980
    }
4981
 
4982
    /**
4983
     * This must be invoked immediately after creating the "module" activity record (forum, choice...)
4984
     * and will adjust the new activity id (the instance) in various places
4985
     */
4986
    protected function apply_activity_instance($newitemid) {
4987
        global $DB;
4988
 
4989
        $this->task->set_activityid($newitemid); // Save activity id in task
4990
        // Apply the id to course_sections->instanceid
4991
        $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
4992
        // Do the mapping for modulename, preparing it for files by oldcontext
4993
        $modulename = $this->task->get_modulename();
4994
        $oldid = $this->task->get_old_activityid();
4995
        $this->set_mapping($modulename, $oldid, $newitemid, true);
4996
    }
1441 ariadna 4997
 
4998
    /**
4999
     * Create a delegate section mapping.
5000
     *
5001
     * @param string $component The component name (frankenstyle)
5002
     * @param int $olditemid The old section id.
5003
     * @param int $newitemid The new section id.
5004
     */
5005
    protected function set_delegated_section_mapping($component, $olditemid, $newitemid) {
5006
        $this->set_mapping("course_section::$component", $olditemid, $newitemid);
5007
    }
1 efrain 5008
}
5009
 
5010
/**
5011
 * Structure step in charge of creating/mapping all the qcats and qs
5012
 * by parsing the questions.xml file and checking it against the
5013
 * results calculated by {@link restore_process_categories_and_questions}
5014
 * and stored in backup_ids_temp.
5015
 */
5016
class restore_create_categories_and_questions extends restore_structure_step {
5017
 
5018
    /** @var array $cachedcategory store a question category */
5019
    protected $cachedcategory = null;
5020
 
1441 ariadna 5021
    /** @var stdClass the last question_bank_entry seen during the restore. Processed when we get to a question. */
5022
    protected $latestqbe;
5023
 
5024
    /** @var stdClass the last question_version seen during the restore. Processed when we get to a question. */
5025
    protected $latestversion;
5026
 
1 efrain 5027
    protected function define_structure() {
5028
 
5029
        // Check if the backup is a pre 4.0 one.
5030
        $restoretask = $this->get_task();
5031
        $before40 = $restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<');
5032
        // Start creating the path, category should be the first one.
5033
        $paths = [];
5034
        $paths [] = new restore_path_element('question_category', '/question_categories/question_category');
5035
        // For the backups done before 4.0.
5036
        if ($before40) {
5037
            // This path is to recreate the bank entry and version for the legacy question objets.
5038
            $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
5039
 
5040
            // Apply for 'qtype' plugins optional paths at question level.
5041
            $this->add_plugin_structure('qtype', $question);
5042
 
5043
            // Apply for 'local' plugins optional paths at question level.
5044
            $this->add_plugin_structure('local', $question);
5045
 
5046
            $paths [] = $question;
5047
            $paths [] = new restore_path_element('question_hint',
5048
                '/question_categories/question_category/questions/question/question_hints/question_hint');
5049
            $paths [] = new restore_path_element('tag', '/question_categories/question_category/questions/question/tags/tag');
5050
        } else {
5051
            // For all the new backups.
5052
            $paths [] = new restore_path_element('question_bank_entry',
5053
                '/question_categories/question_category/question_bank_entries/question_bank_entry');
5054
            $paths [] = new restore_path_element('question_versions', '/question_categories/question_category/'.
5055
                'question_bank_entries/question_bank_entry/question_version/question_versions');
5056
            $question = new restore_path_element('question', '/question_categories/question_category/'.
5057
                'question_bank_entries/question_bank_entry/question_version/question_versions/questions/question');
5058
 
5059
            // Apply for 'qtype' plugins optional paths at question level.
5060
            $this->add_plugin_structure('qtype', $question);
5061
 
5062
            // Apply for 'qbank' plugins optional paths at question level.
5063
            $this->add_plugin_structure('qbank', $question);
5064
 
5065
            // Apply for 'local' plugins optional paths at question level.
5066
            $this->add_plugin_structure('local', $question);
5067
 
5068
            $paths [] = $question;
5069
            $paths [] = new restore_path_element('question_hint', '/question_categories/question_category/question_bank_entries/'.
5070
                'question_bank_entry/question_version/question_versions/questions/question/question_hints/question_hint');
5071
            $paths [] = new restore_path_element('tag', '/question_categories/question_category/question_bank_entries/'.
5072
                'question_bank_entry/question_version/question_versions/questions/question/tags/tag');
5073
        }
5074
 
5075
        return $paths;
5076
    }
5077
 
5078
    /**
5079
     * Process question category restore.
5080
     *
5081
     * @param array $data the data from the XML file.
5082
     */
5083
    protected function process_question_category($data) {
5084
        global $DB;
5085
 
5086
        $data = (object)$data;
5087
        $oldid = $data->id;
5088
 
5089
        // Check we have one mapping for this category.
5090
        if (!$mapping = $this->get_mapping('question_category', $oldid)) {
5091
            return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
5092
        }
5093
 
5094
        // Check we have to create the category (newitemid = 0).
5095
        if ($mapping->newitemid) {
5096
            // By performing this set_mapping() we make get_old/new_parentid() to work for all the
5097
            // children elements of the 'question_category' one.
5098
            $this->set_mapping('question_category', $oldid, $mapping->newitemid);
5099
            return; // newitemid != 0, this category is going to be mapped. Nothing to do
5100
        }
5101
 
5102
        // Arrived here, newitemid = 0, we need to create the category
5103
        // we'll do it at parentitemid context, but for CONTEXT_MODULE
5104
        // categories, that will be created at CONTEXT_COURSE and moved
5105
        // to module context later when the activity is created.
5106
        if ($mapping->info->contextlevel == CONTEXT_MODULE) {
5107
            $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
5108
        }
5109
        $data->contextid = $mapping->parentitemid;
5110
 
1441 ariadna 5111
        $context = \context::instance_by_id($data->contextid);
5112
 
1 efrain 5113
        // Before 3.5, question categories could be created at top level.
5114
        // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
5115
        $restoretask = $this->get_task();
5116
        $before35 = $restoretask->backup_release_compare('3.5', '<') || $restoretask->backup_version_compare(20180205, '<');
1441 ariadna 5117
 
5118
        // We need a 'Top' question category for an activity module and activity modules are mapped to CONTEXT_COURSE and moved
5119
        // to the correct module context in restore_move_module_questions_categories.
5120
        // As we can't create a 'Top' category in CONTEXT_COURSE we'll make a default
5121
        // qbank module and map it to that until they are created later.
1 efrain 5122
        if (empty($mapping->info->parent) && $before35) {
1441 ariadna 5123
            if ($context->contextlevel === CONTEXT_COURSE) {
5124
                $course = get_course($context->instanceid);
5125
                $defaultbank = \core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
5126
                $bankcontextid = $defaultbank->context->id;
5127
            } else {
5128
                $bankcontextid = $data->contextid;
5129
            }
5130
            $top = question_get_top_category($bankcontextid, true);
1 efrain 5131
            $data->parent = $top->id;
5132
        }
5133
 
1441 ariadna 5134
        if (!empty($data->parent)) {
1 efrain 5135
            // Before 3.1, the 'stamp' field could be erroneously duplicated.
5136
            // From 3.1 onwards, there's a unique index of (contextid, stamp).
5137
            // If we encounter a duplicate in an old restore file, just generate a new stamp.
5138
            // This is the same as what happens during an upgrade to 3.1+ anyway.
5139
            if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) {
5140
                $data->stamp = make_unique_id_code();
5141
            }
5142
 
5143
            // The idnumber if it exists also needs to be unique within a context or reset it to null.
5144
            if (!empty($data->idnumber) && $DB->record_exists('question_categories',
5145
                    ['idnumber' => $data->idnumber, 'contextid' => $data->contextid])) {
5146
                unset($data->idnumber);
5147
            }
5148
 
5149
            // Let's create the question_category and save mapping.
5150
            $newitemid = $DB->insert_record('question_categories', $data);
5151
            $this->set_mapping('question_category', $oldid, $newitemid);
5152
            // Also annotate them as question_category_created, we need
5153
            // that later when remapping parents.
5154
            $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
5155
        }
5156
    }
5157
 
5158
    /**
1441 ariadna 5159
     * Set up date to allow restore of questions from pre-4.0 backups.
1 efrain 5160
     *
1441 ariadna 5161
     * @param stdClass $data the data from the XML file.
1 efrain 5162
     */
5163
    protected function process_question_legacy_data($data) {
1441 ariadna 5164
        $this->latestqbe = (object) [
5165
            'id' => $data->id,
5166
            'questioncategoryid' => $data->category,
5167
            'ownerid' => $data->createdby,
5168
            'idnumber' => $data->idnumber ?? null,
5169
        ];
1 efrain 5170
 
1441 ariadna 5171
        $this->latestversion = (object) [
5172
            'id' => $data->id,
5173
            'version' => 1,
5174
            'status' => $data->hidden ?
5175
                \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN :
5176
                \core_question\local\bank\question_version_status::QUESTION_STATUS_READY,
5177
        ];
1 efrain 5178
    }
5179
 
5180
    /**
5181
     * Process question bank entry data.
5182
     *
5183
     * @param array $data the data from the XML file.
5184
     */
5185
    protected function process_question_bank_entry($data) {
1441 ariadna 5186
        // We can only determine the right way to process this once we get to
5187
        // process_question and have more information, so for now just store.
5188
        $this->latestqbe = (object) $data;
1 efrain 5189
    }
5190
 
5191
    /**
5192
     * Process question versions.
5193
     *
5194
     * @param array $data the data from the XML file.
5195
     */
5196
    protected function process_question_versions($data) {
1441 ariadna 5197
        // We can only determine the right way to process this once we get to
5198
        // process_question and have more information, so for now just store.
5199
        $this->latestversion = (object) $data;
1 efrain 5200
    }
5201
 
5202
    /**
5203
     * Process the actual question.
5204
     *
5205
     * @param array $data the data from the XML file.
5206
     */
5207
    protected function process_question($data) {
5208
        global $DB;
5209
 
1441 ariadna 5210
        $data = (object) $data;
1 efrain 5211
        $oldid = $data->id;
5212
 
1441 ariadna 5213
        // Check we have one mapping for this question.
5214
        if (!$questionmapping = $this->get_mapping('question', $oldid)) {
5215
            // No mapping = this question doesn't need to be created/mapped.
5216
            return;
5217
        }
5218
 
5219
        // Check if this is a pre 4.0 backup, then there will not be a question bank entry
5220
        // or question version in the file. So, we need to set up that data ready to be used below.
1 efrain 5221
        $restoretask = $this->get_task();
5222
        if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) {
5223
            // Get the mapped category (cannot use get_new_parentid() because not
5224
            // all the categories have been created, so it is not always available
5225
            // Instead we get the mapping for the question->parentitemid because
5226
            // we have loaded qcatids there for all parsed questions.
5227
            $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
5228
            $this->process_question_legacy_data($data);
5229
        }
5230
 
5231
        // In the past, there were some very sloppy values of penalty. Fix them.
5232
        if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
5233
            $data->penalty = 0.3333333;
5234
        }
5235
        if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
5236
            $data->penalty = 0.6666667;
5237
        }
5238
        if ($data->penalty >= 1) {
5239
            $data->penalty = 1;
5240
        }
5241
 
5242
        $userid = $this->get_mappingid('user', $data->createdby);
5243
        if ($userid) {
5244
            // The question creator is included in the backup, so we can use their mapping id.
5245
            $data->createdby = $userid;
5246
        } else {
5247
            // Leave the question creator unchanged when we are restoring the same site.
5248
            // Otherwise use current user id.
5249
            if (!$this->task->is_samesite()) {
5250
                $data->createdby = $this->task->get_userid();
5251
            }
5252
        }
5253
 
5254
        $userid = $this->get_mappingid('user', $data->modifiedby);
5255
        if ($userid) {
5256
            // The question modifier is included in the backup, so we can use their mapping id.
5257
            $data->modifiedby = $userid;
5258
        } else {
5259
            // Leave the question modifier unchanged when we are restoring the same site.
5260
            // Otherwise use current user id.
5261
            if (!$this->task->is_samesite()) {
5262
                $data->modifiedby = $this->task->get_userid();
5263
            }
5264
        }
5265
 
1441 ariadna 5266
        // With newitemid = 0, let's create the question.
5267
        if (!$questionmapping->newitemid) {
5268
            // Now we know we are inserting a question, we may need to insert the questionbankentry.
5269
            if (empty($this->latestqbe->newid)) {
5270
                $this->latestqbe->oldid = $this->latestqbe->id;
5271
 
5272
                $this->latestqbe->questioncategoryid = $this->get_new_parentid('question_category');
5273
                $userid = $this->get_mappingid('user', $this->latestqbe->ownerid);
5274
                if ($userid) {
5275
                    $this->latestqbe->ownerid = $userid;
5276
                } else {
5277
                    if (!$this->task->is_samesite()) {
5278
                        $this->latestqbe->ownerid = $this->task->get_userid();
5279
                    }
5280
                }
5281
 
5282
                // The idnumber if it exists also needs to be unique within a category or reset it to null.
5283
                if (!empty($this->latestqbe->idnumber) && $DB->record_exists('question_bank_entries',
5284
                        ['idnumber' => $this->latestqbe->idnumber, 'questioncategoryid' => $this->latestqbe->questioncategoryid])) {
5285
                    unset($this->latestqbe->idnumber);
5286
                }
5287
 
5288
                $this->latestqbe->newid = $DB->insert_record('question_bank_entries', $this->latestqbe);
5289
                $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
5290
            }
5291
 
5292
            // Now store the question.
5293
            $newitemid = $DB->insert_record('question', $data);
5294
            $this->set_mapping('question', $oldid, $newitemid);
5295
            // Also annotate them as question_created, we need
5296
            // that later when remapping parents (keeping the old categoryid as parentid).
5297
            $parentcatid = $this->get_old_parentid('question_category');
5298
            $this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid);
5299
 
5300
            // Also insert this question_version.
5301
            $oldqvid = $this->latestversion->id;
5302
            $this->latestversion->questionbankentryid = $this->latestqbe->newid;
5303
            $this->latestversion->questionid = $newitemid;
5304
            $newqvid = $DB->insert_record('question_versions', $this->latestversion);
5305
            $this->set_mapping('question_versions', $oldqvid, $newqvid);
5306
 
1 efrain 5307
        } else {
1441 ariadna 5308
            // By performing this set_mapping() we make get_old/new_parentid() to work for all the
5309
            // children elements of the 'question' one (so qtype plugins will know the question they belong to).
5310
            $this->set_mapping('question', $oldid, $questionmapping->newitemid);
5311
 
5312
            // Also create the question_bank_entry and version mappings, if required.
5313
            $newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]);
5314
            $this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id);
5315
            if (empty($this->latestqbe->newid)) {
5316
                $this->latestqbe->oldid = $this->latestqbe->id;
5317
                $this->latestqbe->newid = $newquestionversion->questionbankentryid;
5318
                $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid);
5319
            }
1 efrain 5320
        }
5321
 
5322
        // Note, we don't restore any question files yet
5323
        // as far as the CONTEXT_MODULE categories still
5324
        // haven't their contexts to be restored to
1441 ariadna 5325
        // The {@see restore_create_question_files}, executed in the final
1 efrain 5326
        // step will be in charge of restoring all the question files.
5327
    }
5328
 
5329
    protected function process_question_hint($data) {
5330
        global $DB;
5331
 
5332
        $data = (object)$data;
5333
        $oldid = $data->id;
5334
 
5335
        // Detect if the question is created or mapped
5336
        $oldquestionid   = $this->get_old_parentid('question');
5337
        $newquestionid   = $this->get_new_parentid('question');
5338
        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
5339
 
5340
        // If the question has been created by restore, we need to create its question_answers too
5341
        if ($questioncreated) {
5342
            // Adjust some columns
5343
            $data->questionid = $newquestionid;
5344
            // Insert record
5345
            $newitemid = $DB->insert_record('question_hints', $data);
5346
 
5347
        // The question existed, we need to map the existing question_hints
5348
        } else {
5349
            // Look in question_hints by hint text matching
5350
            $sql = 'SELECT id
5351
                      FROM {question_hints}
5352
                     WHERE questionid = ?
5353
                       AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
5354
            $params = array($newquestionid, $data->hint);
5355
            $newitemid = $DB->get_field_sql($sql, $params);
5356
 
5357
            // Not able to find the hint, let's try cleaning the hint text
5358
            // of all the question's hints in DB as slower fallback. MDL-33863.
5359
            if (!$newitemid) {
5360
                $potentialhints = $DB->get_records('question_hints',
5361
                        array('questionid' => $newquestionid), '', 'id, hint');
5362
                foreach ($potentialhints as $potentialhint) {
1441 ariadna 5363
                    $cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars.
1 efrain 5364
                    $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
5365
                    if ($cleanhint === $data->hint) {
5366
                        $newitemid = $data->id;
5367
                    }
5368
                }
5369
            }
5370
 
5371
            // If we haven't found the newitemid, something has gone really wrong, question in DB
5372
            // is missing hints, exception
5373
            if (!$newitemid) {
5374
                $info = new stdClass();
5375
                $info->filequestionid = $oldquestionid;
5376
                $info->dbquestionid   = $newquestionid;
5377
                $info->hint           = $data->hint;
5378
                throw new restore_step_exception('error_question_hint_missing_in_db', $info);
5379
            }
5380
        }
5381
        // Create mapping (I'm not sure if this is really needed?)
5382
        $this->set_mapping('question_hint', $oldid, $newitemid);
5383
    }
5384
 
5385
    protected function process_tag($data) {
5386
        global $DB;
5387
 
5388
        $data = (object)$data;
5389
        $newquestion = $this->get_new_parentid('question');
5390
        $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));
5391
        if (!$questioncreated) {
5392
            // This question already exists in the question bank. Nothing for us to do.
5393
            return;
5394
        }
5395
 
5396
        if (core_tag_tag::is_enabled('core_question', 'question')) {
5397
            $tagname = $data->rawname;
1441 ariadna 5398
            // Get the category, so we can then later get the context.
5399
            $categoryid = $this->get_new_parentid('question_category');
5400
            if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
5401
                $this->cachedcategory = $DB->get_record('question_categories', ['id' => $categoryid]);
1 efrain 5402
            }
1441 ariadna 5403
            $tagcontextid = $this->cachedcategory->contextid;
1 efrain 5404
            // Add the tag to the question.
1441 ariadna 5405
            core_tag_tag::add_item_tag('core_question',
5406
                'question',
5407
                $newquestion,
5408
                context::instance_by_id($tagcontextid),
5409
                $tagname
5410
            );
1 efrain 5411
        }
5412
    }
5413
 
5414
    protected function after_execute() {
5415
        global $DB;
5416
 
5417
        // First of all, recode all the created question_categories->parent fields
5418
        $qcats = $DB->get_records('backup_ids_temp', array(
5419
                     'backupid' => $this->get_restoreid(),
5420
                     'itemname' => 'question_category_created'));
5421
        foreach ($qcats as $qcat) {
5422
            $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
5423
            // Get new parent (mapped or created, so we look in quesiton_category mappings)
5424
            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
5425
                                 'backupid' => $this->get_restoreid(),
5426
                                 'itemname' => 'question_category',
5427
                                 'itemid'   => $dbcat->parent))) {
5428
                // contextids must match always, as far as we always include complete qbanks, just check it
5429
                $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
5430
                if ($dbcat->contextid == $newparentctxid) {
5431
                    $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
5432
                } else {
5433
                    $newparent = 0; // No ctx match for both cats, no parent relationship
5434
                }
5435
            }
1441 ariadna 5436
            $context = \core\context::instance_by_id($dbcat->contextid);
1 efrain 5437
            // Here with $newparent empty, problem with contexts or remapping, set it to top cat
1441 ariadna 5438
            if (!$newparent && $dbcat->parent && $context->contextlevel === CONTEXT_MODULE) {
1 efrain 5439
                $topcat = question_get_top_category($dbcat->contextid, true);
5440
                if ($dbcat->parent != $topcat->id) {
5441
                    $DB->set_field('question_categories', 'parent', $topcat->id, array('id' => $dbcat->id));
5442
                }
5443
            }
5444
        }
5445
 
5446
        // Now, recode all the created question->parent fields
5447
        $qs = $DB->get_records('backup_ids_temp', array(
5448
                  'backupid' => $this->get_restoreid(),
5449
                  'itemname' => 'question_created'));
5450
        foreach ($qs as $q) {
5451
            $dbq = $DB->get_record('question', array('id' => $q->newitemid));
5452
            // Get new parent (mapped or created, so we look in question mappings)
5453
            if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
5454
                                 'backupid' => $this->get_restoreid(),
5455
                                 'itemname' => 'question',
5456
                                 'itemid'   => $dbq->parent))) {
5457
                $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
5458
            }
5459
        }
5460
 
5461
        // Note, we don't restore any question files yet
5462
        // as far as the CONTEXT_MODULE categories still
5463
        // haven't their contexts to be restored to
5464
        // The {@link restore_create_question_files}, executed in the final step
5465
        // step will be in charge of restoring all the question files
5466
    }
5467
}
5468
 
5469
/**
5470
 * Execution step that will move all the CONTEXT_MODULE question categories
5471
 * created at early stages of restore in course context (because modules weren't
5472
 * created yet) to their target module (matching by old-new-contextid mapping)
5473
 */
5474
class restore_move_module_questions_categories extends restore_execution_step {
5475
 
5476
    protected function define_execution() {
5477
        global $DB;
5478
 
5479
        $after35 = $this->task->backup_release_compare('3.5', '>=') && $this->task->backup_version_compare(20180205, '>');
5480
 
5481
        $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
5482
        foreach ($contexts as $contextid => $contextlevel) {
1441 ariadna 5483
            if (!$newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
5484
                // The bank for the question categories required by this module was not included in the backup,
5485
                // but if that context still exists on the site and the user has access then point question references
5486
                // to the originals.
5487
                $originalcontext = context::instance_by_id($contextid, IGNORE_MISSING);
5488
                if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) {
5489
                    $originalquestions = get_questions_category(question_get_top_category($contextid), false);
5490
                    $targetcoursecontext = context_course::instance($this->get_courseid());
5491
                    foreach ($originalquestions as $originalquestion) {
5492
                        $backupids = restore_dbops::get_backup_ids_record(
5493
                            $this->get_restoreid(),
5494
                            'question',
5495
                            $originalquestion->id,
5496
                        );
5497
                        if (!$backupids) {
5498
                            continue; // This question was not included in the backup.
1 efrain 5499
                        }
1441 ariadna 5500
                        // Restored question references will point to the restored copy of the question. Select question references
5501
                        // that point to that restored copy, only if they are within the target course's context, so we can update
5502
                        // them to point to the original question.
5503
                        $conpathlike = $DB->sql_like('con.path', '?');
5504
                        $references = $DB->get_records_sql(
5505
                            "SELECT qr.id, qr.questionbankentryid
5506
                               FROM {question_references} qr
5507
                                    JOIN {context} con ON qr.usingcontextid = con.id
5508
                                    JOIN {question_versions} qv ON qv.questionbankentryid = qr.questionbankentryid
5509
                              WHERE qv.questionid = ?
5510
                                    AND {$conpathlike}",
5511
                            [
5512
                                $backupids->newitemid,
5513
                                $targetcoursecontext->path . '/%',
5514
                            ],
5515
                        );
5516
                        if (empty($references)) {
5517
                            continue;
5518
                        }
5519
                        [$refin, $refparams] = $DB->get_in_or_equal(array_keys($references));
5520
                        $DB->set_field_select(
5521
                            'question_references',
5522
                            'questionbankentryid',
5523
                            $DB->get_field('question_versions', 'questionbankentryid', ['questionid' => $backupids->itemid]),
5524
                            'id ' . $refin,
5525
                            $refparams,
5526
                        );
1 efrain 5527
                    }
1441 ariadna 5528
                    continue;
1 efrain 5529
                }
1441 ariadna 5530
                // We have no target question bank so create a default bank for categories without a module to attach to.
5531
                // This can occur when a quiz backup contains references to a question bank module,
5532
                // that was not included in the backup and does not exist in the site being restored to.
5533
                $course = get_course($this->get_courseid());
5534
                $defaultqbank = core_question\local\bank\question_bank_helper::get_default_open_instance_system_type($course, true);
5535
                $context = context_module::instance($defaultqbank->id);
5536
                $newcontext = new stdClass();
5537
                $newcontext->newitemid = $context->id;
5538
            }
5539
            // Only if context mapping exists (i.e. the module has been restored)
5540
            // Update all the qcats having their parentitemid set to the original contextid.
5541
            $modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info
5542
                                                  FROM {backup_ids_temp}
5543
                                                 WHERE backupid = ?
5544
                                                   AND itemname = 'question_category'
5545
                                                   AND parentitemid = ?",
5546
                [$this->get_restoreid(), $contextid]
5547
            );
5548
            $top = question_get_top_category($newcontext->newitemid, true);
5549
            $oldtopid = 0;
5550
            $categoryids = [];
5551
            foreach ($modulecats as $modulecat) {
5552
                // Before 3.5, question categories could be created at top level.
5553
                // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
5554
                $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
5555
                if ($after35 && empty($info->parent)) {
5556
                    $oldtopid = $modulecat->newitemid;
5557
                    $modulecat->newitemid = $top->id;
5558
                } else {
5559
                    $cat = new stdClass();
5560
                    $cat->id = $modulecat->newitemid;
5561
                    $cat->contextid = $newcontext->newitemid;
5562
                    if (empty($info->parent)) {
5563
                        $cat->parent = $top->id;
5564
                    }
5565
                    $DB->update_record('question_categories', $cat);
5566
                    $categoryids[] = (int) $cat->id;
5567
                }
1 efrain 5568
 
1441 ariadna 5569
                // And set new contextid (and maybe update newitemid) also in question_category mapping (will be
5570
                // used by {@see restore_create_question_files} later.
5571
                restore_dbops::set_backup_ids_record($this->get_restoreid(),
5572
                    'question_category',
5573
                    $modulecat->itemid,
5574
                    $modulecat->newitemid,
5575
                    $newcontext->newitemid
5576
                );
5577
            }
5578
 
5579
            // Update the context id of any tags applied to any questions in these categories.
5580
            if ($categoryids) {
5581
                [$categorysql, $categoryidparams] = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED);
5582
                $sqlupdate = "UPDATE {tag_instance}
5583
                                 SET contextid = :newcontext
5584
                               WHERE component = :component
5585
                                 AND itemtype = :itemtype
5586
                                 AND itemid IN (SELECT DISTINCT bi.newitemid as questionid
5587
                                FROM {backup_ids_temp} bi
5588
                                JOIN {question} q ON q.id = bi.newitemid
5589
                                JOIN {question_versions} qv ON qv.questionid = q.id
5590
                                JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
5591
                               WHERE bi.backupid = :backupid AND bi.itemname = 'question_created'
5592
                                 AND qbe.questioncategoryid {$categorysql}) ";
5593
                $params = [
1 efrain 5594
                        'newcontext' => $newcontext->newitemid,
5595
                        'component' => 'core_question',
5596
                        'itemtype' => 'question',
5597
                        'backupid' => $this->get_restoreid(),
1441 ariadna 5598
                ];
5599
                $params += $categoryidparams;
5600
                $DB->execute($sqlupdate, $params);
1 efrain 5601
 
1441 ariadna 5602
                // As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()}
5603
                // question_set_references relating to random questions restored from old backups,
5604
                // which pick from context_module question_categores, will have been restored with the wrong questioncontextid.
5605
                // So, now, we need to find those, and updated the questioncontextid.
5606
                // We can only find them by picking apart the filter conditions, and seeign which categories they refer to.
1 efrain 5607
 
1441 ariadna 5608
                // We need to check all the question_set_references belonging to this context_module.
5609
                $references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
5610
                foreach ($references as $reference) {
5611
                    $filtercondition = json_decode($reference->filtercondition);
5612
                    if (!empty($filtercondition->questioncategoryid) &&
5613
                            in_array($filtercondition->questioncategoryid, $categoryids)) {
5614
                        // This is one of ours, update the questionscontextid.
5615
                        $DB->set_field('question_set_references',
5616
                            'questionscontextid', $newcontext->newitemid,
5617
                            ['id' => $reference->id]);
1 efrain 5618
                    }
5619
                }
1441 ariadna 5620
            }
1 efrain 5621
 
1441 ariadna 5622
            // Now set the parent id for the question categories that were in the top category in the course context
5623
            // and have been moved now.
5624
            if ($oldtopid) {
5625
                $DB->set_field('question_categories',
5626
                    'parent',
5627
                    $top->id,
5628
                    ['contextid' => $newcontext->newitemid, 'parent' => $oldtopid]
5629
                );
1 efrain 5630
            }
5631
        }
1441 ariadna 5632
        // Remove any remaining course-level question categories from the restored course.
5633
        $coursecatsql = "
5634
            SELECT qc.id AS categoryid
5635
              FROM {question_categories} qc
5636
              JOIN {context} c ON c.id = qc.contextid
5637
             WHERE c.contextlevel = :courselevel AND c.instanceid = :courseid
5638
        ";
5639
        $DB->delete_records_subquery(
5640
            'question_categories',
5641
            'id',
5642
            'categoryid',
5643
            $coursecatsql,
5644
            ['courselevel' => context_course::LEVEL, 'courseid' => $this->task->get_courseid()]
5645
        );
1 efrain 5646
    }
5647
}
5648
 
5649
/**
5650
 * Execution step that will create all the question/answers/qtype-specific files for the restored
5651
 * questions. It must be executed after {@link restore_move_module_questions_categories}
5652
 * because only then each question is in its final category and only then the
5653
 * contexts can be determined.
5654
 */
5655
class restore_create_question_files extends restore_execution_step {
5656
 
5657
    /** @var array Question-type specific component items cache. */
5658
    private $qtypecomponentscache = array();
5659
 
5660
    /**
5661
     * Preform the restore_create_question_files step.
5662
     */
5663
    protected function define_execution() {
5664
        global $DB;
5665
 
5666
        // Track progress, as this task can take a long time.
5667
        $progress = $this->task->get_progress();
5668
        $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
5669
 
5670
        // Parentitemids of question_createds in backup_ids_temp are the category it is in.
5671
        // MUST use a recordset, as there is no unique key in the first (or any) column.
5672
        $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype
5673
                                                   FROM {backup_ids_temp} bi
5674
                                                   JOIN {question} q ON q.id = bi.newitemid
5675
                                                  WHERE bi.backupid = ?
5676
                                                        AND bi.itemname = 'question_created'
5677
                                               ORDER BY categoryid ASC", array($this->get_restoreid()));
5678
 
5679
        $currentcatid = -1;
5680
        foreach ($catqtypes as $categoryid => $row) {
5681
            $qtype = $row->qtype;
5682
 
5683
            // Check if we are in a new category.
5684
            if ($currentcatid !== $categoryid) {
5685
                // Report progress for each category.
5686
                $progress->progress();
5687
 
5688
                if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(),
5689
                        'question_category', $categoryid)) {
5690
                    // Something went really wrong, cannot find the question_category for the question_created records.
5691
                    debugging('Error fetching target context for question', DEBUG_DEVELOPER);
5692
                    continue;
5693
                }
5694
 
5695
                // Calculate source and target contexts.
5696
                $oldctxid = $qcatmapping->info->contextid;
5697
                $newctxid = $qcatmapping->parentitemid;
5698
 
5699
                $this->send_common_files($oldctxid, $newctxid, $progress);
5700
                $currentcatid = $categoryid;
5701
            }
5702
 
5703
            $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress);
5704
        }
5705
        $catqtypes->close();
5706
        $progress->end_progress();
5707
    }
5708
 
5709
    /**
5710
     * Send the common question files to a new context.
5711
     *
5712
     * @param int             $oldctxid Old context id.
5713
     * @param int             $newctxid New context id.
5714
     * @param \core\progress\base  $progress Progress object to use.
5715
     */
5716
    private function send_common_files($oldctxid, $newctxid, $progress) {
5717
        // Add common question files (question and question_answer ones).
5718
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
5719
                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5720
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
5721
                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5722
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
5723
                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
5724
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
5725
                $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
5726
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
5727
                $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
5728
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
5729
                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5730
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
5731
                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5732
        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
5733
                $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
5734
    }
5735
 
5736
    /**
5737
     * Send the question type specific files to a new context.
5738
     *
5739
     * @param text            $qtype The qtype name to send.
5740
     * @param int             $oldctxid Old context id.
5741
     * @param int             $newctxid New context id.
5742
     * @param \core\progress\base  $progress Progress object to use.
5743
     */
5744
    private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) {
5745
        if (!isset($this->qtypecomponentscache[$qtype])) {
5746
            $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype);
5747
        }
5748
        $components = $this->qtypecomponentscache[$qtype];
5749
        foreach ($components as $component => $fileareas) {
5750
            foreach ($fileareas as $filearea => $mapping) {
5751
                restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,
5752
                        $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress);
5753
            }
5754
        }
5755
    }
5756
}
5757
 
5758
/**
5759
 * Try to restore aliases and references to external files.
5760
 *
5761
 * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}.
5762
 * We expect that all regular (non-alias) files have already been restored. Make sure
5763
 * there is no restore step executed after this one that would call send_files_to_pool() again.
5764
 *
5765
 * You may notice we have hardcoded support for Server files, Legacy course files
5766
 * and user Private files here at the moment. This could be eventually replaced with a set of
5767
 * callbacks in the future if needed.
5768
 *
5769
 * @copyright 2012 David Mudrak <david@moodle.com>
5770
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5771
 */
5772
class restore_process_file_aliases_queue extends restore_execution_step {
5773
 
5774
    /** @var array internal cache for {@link choose_repository()} */
5775
    private $cachereposbyid = array();
5776
 
5777
    /** @var array internal cache for {@link choose_repository()} */
5778
    private $cachereposbytype = array();
5779
 
5780
    /**
5781
     * What to do when this step is executed.
5782
     */
5783
    protected function define_execution() {
5784
        global $DB;
5785
 
5786
        $fs = get_file_storage();
5787
 
5788
        // Load the queue.
5789
        $aliascount = $DB->count_records('backup_ids_temp',
5790
            ['backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue']);
5791
        $rs = $DB->get_recordset('backup_ids_temp',
5792
            ['backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'],
5793
            '', 'info');
5794
 
5795
        $this->log('processing file aliases queue. ' . $aliascount . ' entries.', backup::LOG_DEBUG);
5796
        $progress = $this->task->get_progress();
5797
        $progress->start_progress('Processing file aliases queue', $aliascount);
5798
 
5799
        // Iterate over aliases in the queue.
5800
        foreach ($rs as $record) {
5801
            $progress->increment_progress();
5802
            $info = backup_controller_dbops::decode_backup_temp_info($record->info);
5803
 
5804
            // Try to pick a repository instance that should serve the alias.
5805
            $repository = $this->choose_repository($info);
5806
 
5807
            if (is_null($repository)) {
5808
                $this->notify_failure($info, 'unable to find a matching repository instance');
5809
                continue;
5810
            }
5811
 
5812
            if ($info->oldfile->repositorytype === 'local' || $info->oldfile->repositorytype === 'coursefiles'
5813
                    || $info->oldfile->repositorytype === 'contentbank') {
5814
                // Aliases to Server files and Legacy course files may refer to a file
5815
                // contained in the backup file or to some existing file (if we are on the
5816
                // same site).
5817
                try {
5818
                    $reference = file_storage::unpack_reference($info->oldfile->reference);
5819
                } catch (Exception $e) {
5820
                    $this->notify_failure($info, 'invalid reference field format');
5821
                    continue;
5822
                }
5823
 
5824
                // Let's see if the referred source file was also included in the backup.
5825
                $candidates = $DB->get_recordset('backup_files_temp', array(
5826
                        'backupid' => $this->get_restoreid(),
5827
                        'contextid' => $reference['contextid'],
5828
                        'component' => $reference['component'],
5829
                        'filearea' => $reference['filearea'],
5830
                        'itemid' => $reference['itemid'],
5831
                    ), '', 'info, newcontextid, newitemid');
5832
 
5833
                $source = null;
5834
 
5835
                foreach ($candidates as $candidate) {
5836
                    $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
5837
                    if ($candidateinfo->filename === $reference['filename']
5838
                            and $candidateinfo->filepath === $reference['filepath']
5839
                            and !is_null($candidate->newcontextid)
5840
                            and !is_null($candidate->newitemid) ) {
5841
                        $source = $candidateinfo;
5842
                        $source->contextid = $candidate->newcontextid;
5843
                        $source->itemid = $candidate->newitemid;
5844
                        break;
5845
                    }
5846
                }
5847
                $candidates->close();
5848
 
5849
                if ($source) {
5850
                    // We have an alias that refers to another file also included in
5851
                    // the backup. Let us change the reference field so that it refers
5852
                    // to the restored copy of the original file.
5853
                    $reference = file_storage::pack_reference($source);
5854
 
5855
                    // Send the new alias to the filepool.
5856
                    $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5857
                    $this->notify_success($info);
5858
                    continue;
5859
 
5860
                } else {
5861
                    // This is a reference to some moodle file that was not contained in the backup
5862
                    // file. If we are restoring to the same site, keep the reference untouched
5863
                    // and restore the alias as is if the referenced file exists.
5864
                    if ($this->task->is_samesite()) {
5865
                        if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
5866
                                $reference['itemid'], $reference['filepath'], $reference['filename'])) {
5867
                            $reference = file_storage::pack_reference($reference);
5868
                            $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5869
                            $this->notify_success($info);
5870
                            continue;
5871
                        } else {
5872
                            $this->notify_failure($info, 'referenced file not found');
5873
                            continue;
5874
                        }
5875
 
5876
                    // If we are at other site, we can't restore this alias.
5877
                    } else {
5878
                        $this->notify_failure($info, 'referenced file not included');
5879
                        continue;
5880
                    }
5881
                }
5882
 
5883
            } else if ($info->oldfile->repositorytype === 'user') {
5884
                if ($this->task->is_samesite()) {
5885
                    // For aliases to user Private files at the same site, we have a chance to check
5886
                    // if the referenced file still exists.
5887
                    try {
5888
                        $reference = file_storage::unpack_reference($info->oldfile->reference);
5889
                    } catch (Exception $e) {
5890
                        $this->notify_failure($info, 'invalid reference field format');
5891
                        continue;
5892
                    }
5893
                    if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
5894
                            $reference['itemid'], $reference['filepath'], $reference['filename'])) {
5895
                        $reference = file_storage::pack_reference($reference);
5896
                        $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
5897
                        $this->notify_success($info);
5898
                        continue;
5899
                    } else {
5900
                        $this->notify_failure($info, 'referenced file not found');
5901
                        continue;
5902
                    }
5903
 
5904
                // If we are at other site, we can't restore this alias.
5905
                } else {
5906
                    $this->notify_failure($info, 'restoring at another site');
5907
                    continue;
5908
                }
5909
 
5910
            } else {
5911
                // This is a reference to some external file such as dropbox.
5912
                // If we are restoring to the same site, keep the reference untouched and
5913
                // restore the alias as is.
5914
                if ($this->task->is_samesite()) {
5915
                    $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference);
5916
                    $this->notify_success($info);
5917
                    continue;
5918
 
5919
                // If we are at other site, we can't restore this alias.
5920
                } else {
5921
                    $this->notify_failure($info, 'restoring at another site');
5922
                    continue;
5923
                }
5924
            }
5925
        }
5926
        $progress->end_progress();
5927
        $rs->close();
5928
    }
5929
 
5930
    /**
5931
     * Choose the repository instance that should handle the alias.
5932
     *
5933
     * At the same site, we can rely on repository instance id and we just
5934
     * check it still exists. On other site, try to find matching Server files or
5935
     * Legacy course files repository instance. Return null if no matching
5936
     * repository instance can be found.
5937
     *
5938
     * @param stdClass $info
5939
     * @return repository|null
5940
     */
5941
    private function choose_repository(stdClass $info) {
5942
        global $DB, $CFG;
5943
        require_once($CFG->dirroot.'/repository/lib.php');
5944
 
5945
        if ($this->task->is_samesite()) {
5946
            // We can rely on repository instance id.
5947
 
5948
            if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) {
5949
                return $this->cachereposbyid[$info->oldfile->repositoryid];
5950
            }
5951
 
5952
            $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1);
5953
 
5954
            try {
5955
                $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID);
5956
                return $this->cachereposbyid[$info->oldfile->repositoryid];
5957
            } catch (Exception $e) {
5958
                $this->cachereposbyid[$info->oldfile->repositoryid] = null;
5959
                return null;
5960
            }
5961
 
5962
        } else {
5963
            // We can rely on repository type only.
5964
 
5965
            if (empty($info->oldfile->repositorytype)) {
5966
                return null;
5967
            }
5968
 
5969
            if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) {
5970
                return $this->cachereposbytype[$info->oldfile->repositorytype];
5971
            }
5972
 
5973
            $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1);
5974
 
5975
            // Both Server files and Legacy course files repositories have a single
5976
            // instance at the system context to use. Let us try to find it.
5977
            if ($info->oldfile->repositorytype === 'local' || $info->oldfile->repositorytype === 'coursefiles'
5978
                    || $info->oldfile->repositorytype === 'contentbank') {
5979
                $sql = "SELECT ri.id
5980
                          FROM {repository} r
5981
                          JOIN {repository_instances} ri ON ri.typeid = r.id
5982
                         WHERE r.type = ? AND ri.contextid = ?";
5983
                $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID));
5984
                if (empty($ris)) {
5985
                    return null;
5986
                }
5987
                $repoids = array_keys($ris);
5988
                $repoid = reset($repoids);
5989
                try {
5990
                    $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID);
5991
                    return $this->cachereposbytype[$info->oldfile->repositorytype];
5992
                } catch (Exception $e) {
5993
                    $this->cachereposbytype[$info->oldfile->repositorytype] = null;
5994
                    return null;
5995
                }
5996
            }
5997
 
5998
            $this->cachereposbytype[$info->oldfile->repositorytype] = null;
5999
            return null;
6000
        }
6001
    }
6002
 
6003
    /**
6004
     * Let the user know that the given alias was successfully restored
6005
     *
6006
     * @param stdClass $info
6007
     */
6008
    private function notify_success(stdClass $info) {
6009
        $filedesc = $this->describe_alias($info);
6010
        $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1);
6011
    }
6012
 
6013
    /**
6014
     * Let the user know that the given alias can't be restored
6015
     *
6016
     * @param stdClass $info
6017
     * @param string $reason detailed reason to be logged
6018
     */
6019
    private function notify_failure(stdClass $info, $reason = '') {
6020
        $filedesc = $this->describe_alias($info);
6021
        if ($reason) {
6022
            $reason = ' ('.$reason.')';
6023
        }
6024
        $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1);
6025
        $this->add_result_item('file_aliases_restore_failures', $filedesc);
6026
    }
6027
 
6028
    /**
6029
     * Return a human readable description of the alias file
6030
     *
6031
     * @param stdClass $info
6032
     * @return string
6033
     */
6034
    private function describe_alias(stdClass $info) {
6035
 
6036
        $filedesc = $this->expected_alias_location($info->newfile);
6037
 
6038
        if (!is_null($info->oldfile->source)) {
6039
            $filedesc .= ' ('.$info->oldfile->source.')';
6040
        }
6041
 
6042
        return $filedesc;
6043
    }
6044
 
6045
    /**
6046
     * Return the expected location of a file
6047
     *
6048
     * Please note this may and may not work as a part of URL to pluginfile.php
6049
     * (depends on how the given component/filearea deals with the itemid).
6050
     *
6051
     * @param stdClass $filerecord
6052
     * @return string
6053
     */
6054
    private function expected_alias_location($filerecord) {
6055
 
6056
        $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea;
6057
        if (!is_null($filerecord->itemid)) {
6058
            $filedesc .= '/'.$filerecord->itemid;
6059
        }
6060
        $filedesc .= $filerecord->filepath.$filerecord->filename;
6061
 
6062
        return $filedesc;
6063
    }
6064
 
6065
    /**
6066
     * Append a value to the given resultset
6067
     *
6068
     * @param string $name name of the result containing a list of values
6069
     * @param mixed $value value to add as another item in that result
6070
     */
6071
    private function add_result_item($name, $value) {
6072
 
6073
        $results = $this->task->get_results();
6074
 
6075
        if (isset($results[$name])) {
6076
            if (!is_array($results[$name])) {
6077
                throw new coding_exception('Unable to append a result item into a non-array structure.');
6078
            }
6079
            $current = $results[$name];
6080
            $current[] = $value;
6081
            $this->task->add_result(array($name => $current));
6082
 
6083
        } else {
6084
            $this->task->add_result(array($name => array($value)));
6085
        }
6086
    }
6087
}
6088
 
6089
 
6090
/**
6091
 * Helper code for use by any plugin that stores question attempt data that it needs to back up.
6092
 */
6093
trait restore_questions_attempt_data_trait {
6094
    /** @var array question_attempt->id to qtype. */
6095
    protected $qtypes = array();
6096
    /** @var array question_attempt->id to questionid. */
6097
    protected $newquestionids = array();
6098
 
6099
    /**
6100
     * Attach below $element (usually attempts) the needed restore_path_elements
6101
     * to restore question_usages and all they contain.
6102
     *
6103
     * If you use the $nameprefix parameter, then you will need to implement some
6104
     * extra methods in your class, like
6105
     *
6106
     * protected function process_{nameprefix}question_attempt($data) {
6107
     *     $this->restore_question_usage_worker($data, '{nameprefix}');
6108
     * }
6109
     * protected function process_{nameprefix}question_attempt($data) {
6110
     *     $this->restore_question_attempt_worker($data, '{nameprefix}');
6111
     * }
6112
     * protected function process_{nameprefix}question_attempt_step($data) {
6113
     *     $this->restore_question_attempt_step_worker($data, '{nameprefix}');
6114
     * }
6115
     *
6116
     * @param restore_path_element $element the parent element that the usages are stored inside.
6117
     * @param array $paths the paths array that is being built.
6118
     * @param string $nameprefix should match the prefix passed to the corresponding
6119
     *      backup_questions_activity_structure_step::add_question_usages call.
6120
     */
6121
    protected function add_question_usages($element, &$paths, $nameprefix = '') {
6122
        // Check $element is restore_path_element
6123
        if (! $element instanceof restore_path_element) {
6124
            throw new restore_step_exception('element_must_be_restore_path_element', $element);
6125
        }
6126
 
6127
        // Check $paths is one array
6128
        if (!is_array($paths)) {
6129
            throw new restore_step_exception('paths_must_be_array', $paths);
6130
        }
6131
        $paths[] = new restore_path_element($nameprefix . 'question_usage',
6132
                $element->get_path() . "/{$nameprefix}question_usage");
6133
        $paths[] = new restore_path_element($nameprefix . 'question_attempt',
6134
                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
6135
        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
6136
                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
6137
                true);
6138
        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
6139
                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
6140
    }
6141
 
6142
    /**
6143
     * Process question_usages
6144
     */
6145
    public function process_question_usage($data) {
6146
        $this->restore_question_usage_worker($data, '');
6147
    }
6148
 
6149
    /**
6150
     * Process question_attempts
6151
     */
6152
    public function process_question_attempt($data) {
6153
        $this->restore_question_attempt_worker($data, '');
6154
    }
6155
 
6156
    /**
6157
     * Process question_attempt_steps
6158
     */
6159
    public function process_question_attempt_step($data) {
6160
        $this->restore_question_attempt_step_worker($data, '');
6161
    }
6162
 
6163
    /**
6164
     * This method does the actual work for process_question_usage or
6165
     * process_{nameprefix}_question_usage.
6166
     * @param array $data the data from the XML file.
6167
     * @param string $nameprefix the element name prefix.
6168
     */
6169
    protected function restore_question_usage_worker($data, $nameprefix) {
6170
        global $DB;
6171
 
6172
        // Clear our caches.
6173
        $this->qtypes = array();
6174
        $this->newquestionids = array();
6175
 
6176
        $data = (object)$data;
6177
        $oldid = $data->id;
6178
 
6179
        $data->contextid  = $this->task->get_contextid();
6180
 
6181
        // Everything ready, insert (no mapping needed)
6182
        $newitemid = $DB->insert_record('question_usages', $data);
6183
 
6184
        $this->inform_new_usage_id($newitemid);
6185
 
6186
        $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
6187
    }
6188
 
6189
    /**
6190
     * When process_question_usage creates the new usage, it calls this method
6191
     * to let the activity link to the new usage. For example, the quiz uses
6192
     * this method to set quiz_attempts.uniqueid to the new usage id.
6193
     * @param integer $newusageid
6194
     */
6195
    abstract protected function inform_new_usage_id($newusageid);
6196
 
6197
    /**
6198
     * This method does the actual work for process_question_attempt or
6199
     * process_{nameprefix}_question_attempt.
6200
     * @param array $data the data from the XML file.
6201
     * @param string $nameprefix the element name prefix.
6202
     */
6203
    protected function restore_question_attempt_worker($data, $nameprefix) {
6204
        global $DB;
6205
 
6206
        $data = (object)$data;
6207
        $oldid = $data->id;
6208
 
6209
        $questioncreated = $this->get_mappingid('question_created', $data->questionid) ? true : false;
6210
        $question = $this->get_mapping('question', $data->questionid);
6211
        if ($questioncreated) {
6212
            $data->questionid = $question->newitemid;
6213
        }
6214
 
6215
        $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
6216
 
6217
        if (!property_exists($data, 'variant')) {
6218
            $data->variant = 1;
6219
        }
6220
 
6221
        if (!property_exists($data, 'maxfraction')) {
6222
            $data->maxfraction = 1;
6223
        }
6224
 
6225
        $newitemid = $DB->insert_record('question_attempts', $data);
6226
 
6227
        $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
6228
        if (isset($question->info->qtype)) {
6229
            $qtype = $question->info->qtype;
6230
        } else {
6231
            $qtype = $DB->get_record('question', ['id' => $data->questionid])->qtype;
6232
        }
6233
        $this->qtypes[$newitemid] = $qtype;
6234
        $this->newquestionids[$newitemid] = $data->questionid;
6235
    }
6236
 
6237
    /**
6238
     * This method does the actual work for process_question_attempt_step or
6239
     * process_{nameprefix}_question_attempt_step.
6240
     * @param array $data the data from the XML file.
6241
     * @param string $nameprefix the element name prefix.
6242
     */
6243
    protected function restore_question_attempt_step_worker($data, $nameprefix) {
6244
        global $DB;
6245
 
6246
        $data = (object)$data;
6247
        $oldid = $data->id;
6248
 
6249
        // Pull out the response data.
6250
        $response = array();
6251
        if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
6252
            foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
6253
                $response[$variable['name']] = $variable['value'];
6254
            }
6255
        }
6256
        unset($data->response);
6257
 
6258
        $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
6259
        $data->userid = $this->get_mappingid('user', $data->userid);
6260
 
6261
        // Everything ready, insert and create mapping (needed by question_sessions)
6262
        $newitemid = $DB->insert_record('question_attempt_steps', $data);
6263
        $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
6264
 
6265
        // Now process the response data.
6266
        $response = $this->questions_recode_response_data(
6267
                $this->qtypes[$data->questionattemptid],
6268
                $this->newquestionids[$data->questionattemptid],
6269
                $data->sequencenumber, $response);
6270
 
6271
        foreach ($response as $name => $value) {
6272
            $row = new stdClass();
6273
            $row->attemptstepid = $newitemid;
6274
            $row->name = $name;
6275
            $row->value = $value;
6276
            $DB->insert_record('question_attempt_step_data', $row, false);
6277
        }
6278
    }
6279
 
6280
    /**
6281
     * Recode the respones data for a particular step of an attempt at at particular question.
6282
     * @param string $qtype the question type.
6283
     * @param int $newquestionid the question id.
6284
     * @param int $sequencenumber the sequence number.
6285
     * @param array $response the response data to recode.
6286
     */
6287
    public function questions_recode_response_data(
6288
            $qtype, $newquestionid, $sequencenumber, array $response) {
6289
        $qtyperestorer = $this->get_qtype_restorer($qtype);
6290
        if ($qtyperestorer) {
6291
            $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response);
6292
        }
6293
        return $response;
6294
    }
6295
 
6296
    /**
6297
     * Given a list of question->ids, separated by commas, returns the
6298
     * recoded list, with all the restore question mappings applied.
6299
     * Note: Used by quiz->questions and quiz_attempts->layout
6300
     * Note: 0 = page break (unconverted)
6301
     */
6302
    protected function questions_recode_layout($layout) {
6303
        // Extracts question id from sequence
6304
        if ($questionids = explode(',', $layout)) {
6305
            foreach ($questionids as $id => $questionid) {
6306
                if ($questionid) { // If it is zero then this is a pagebreak, don't translate
6307
                    $newquestionid = $this->get_mappingid('question', $questionid);
6308
                    $questionids[$id] = $newquestionid;
6309
                }
6310
            }
6311
        }
6312
        return implode(',', $questionids);
6313
    }
6314
 
6315
    /**
6316
     * Get the restore_qtype_plugin subclass for a specific question type.
6317
     * @param string $qtype e.g. multichoice.
6318
     * @return restore_qtype_plugin instance.
6319
     */
6320
    protected function get_qtype_restorer($qtype) {
6321
        // Build one static cache to store {@link restore_qtype_plugin}
6322
        // while we are needing them, just to save zillions of instantiations
6323
        // or using static stuff that will break our nice API
6324
        static $qtypeplugins = array();
6325
 
6326
        if (!isset($qtypeplugins[$qtype])) {
6327
            $classname = 'restore_qtype_' . $qtype . '_plugin';
6328
            if (class_exists($classname)) {
6329
                $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
6330
            } else {
6331
                $qtypeplugins[$qtype] = null;
6332
            }
6333
        }
6334
        return $qtypeplugins[$qtype];
6335
    }
6336
 
6337
    protected function after_execute() {
6338
        parent::after_execute();
6339
 
6340
        // Restore any files belonging to responses.
6341
        foreach (question_engine::get_all_response_file_areas() as $filearea) {
6342
            $this->add_related_files('question', $filearea, 'question_attempt_step');
6343
        }
6344
    }
6345
}
6346
 
6347
/**
6348
 * Helper trait to restore question reference data.
6349
 */
6350
trait restore_question_reference_data_trait {
6351
 
6352
    /**
6353
     * Attach the question reference data to the restore.
6354
     *
6355
     * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
6356
     * @param array $paths the paths array that is being built to describe the structure.
6357
     */
6358
    protected function add_question_references($element, &$paths) {
6359
        // Check $element is restore_path_element.
6360
        if (! $element instanceof restore_path_element) {
6361
            throw new restore_step_exception('element_must_be_restore_path_element', $element);
6362
        }
6363
 
6364
        // Check $paths is one array.
6365
        if (!is_array($paths)) {
6366
            throw new restore_step_exception('paths_must_be_array', $paths);
6367
        }
6368
 
6369
        $paths[] = new restore_path_element('question_reference',
6370
            $element->get_path() . '/question_reference');
6371
    }
6372
 
6373
    /**
6374
     * Process question references which replaces the direct connection to quiz slots to question.
6375
     *
6376
     * @param array $data the data from the XML file.
6377
     */
6378
    public function process_question_reference($data) {
6379
        global $DB;
6380
        $data = (object) $data;
6381
        $data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
6382
        $data->itemid = $this->get_new_parentid('quiz_question_instance');
6383
        if ($entry = $this->get_mappingid('question_bank_entry', $data->questionbankentryid)) {
6384
            $data->questionbankentryid = $entry;
6385
        }
6386
        $DB->insert_record('question_references', $data);
6387
    }
6388
}
6389
 
6390
/**
6391
 * Helper trait to restore question set reference data.
6392
 */
6393
trait restore_question_set_reference_data_trait {
6394
 
6395
    /**
6396
     * Attach the question reference data to the restore.
6397
     *
6398
     * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
6399
     * @param array $paths the paths array that is being built to describe the structure.
6400
     */
6401
    protected function add_question_set_references($element, &$paths) {
6402
        // Check $element is restore_path_element.
6403
        if (! $element instanceof restore_path_element) {
6404
            throw new restore_step_exception('element_must_be_restore_path_element', $element);
6405
        }
6406
 
6407
        // Check $paths is one array.
6408
        if (!is_array($paths)) {
6409
            throw new restore_step_exception('paths_must_be_array', $paths);
6410
        }
6411
 
6412
        $paths[] = new restore_path_element('question_set_reference',
6413
            $element->get_path() . '/question_set_reference');
6414
    }
6415
 
6416
    /**
6417
     * Process question set references data which replaces the random qtype.
6418
     *
6419
     * @param array $data the data from the XML file.
6420
     */
6421
    public function process_question_set_reference($data) {
6422
        global $DB;
6423
        $data = (object) $data;
1441 ariadna 6424
        $owncontext = $data->usingcontextid == $data->questionscontextid;
1 efrain 6425
        $data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid);
6426
        $data->itemid = $this->get_new_parentid('quiz_question_instance');
6427
        $filtercondition = json_decode($data->filtercondition, true);
6428
 
6429
        if (!isset($filtercondition['filter'])) {
6430
            // Pre-4.3, convert the old filtercondition format to the new format.
6431
            $filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition(
6432
                    $filtercondition);
6433
        }
6434
 
6435
        // Map category id used for category filter condition and corresponding context id.
6436
        $oldcategoryid = $filtercondition['filter']['category']['values'][0];
1441 ariadna 6437
        // Decide if we're going to refer back to the original category, or to the new category.
6438
        // Are we restoring to a different site?
6439
        // Has the original context or category been deleted?
6440
        // Did the old category belong to the same context as the original set reference?
6441
        // Are we allowed to use its questions?
6442
        $questionscontext = context::instance_by_id($data->questionscontextid, IGNORE_MISSING);
6443
        if (
6444
            !$this->get_task()->is_samesite()
6445
            || !$questionscontext
6446
            || !$DB->record_exists('question_categories', ['id' => $oldcategoryid])
6447
            || $owncontext
6448
            || !has_capability('moodle/question:useall', $questionscontext)
6449
        ) {
6450
            $newcategoryid = $this->get_mappingid('question_category', $oldcategoryid);
6451
            $filtercondition['filter']['category']['values'][0] = $newcategoryid;
6452
        }
1 efrain 6453
 
6454
        if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
6455
            $data->questionscontextid = $context;
6456
        } else {
6457
            $this->log('question_set_reference with old id ' . $data->id .
6458
                ' referenced question context ' . $data->questionscontextid .
6459
                ' which was not included in the backup. Therefore, this has been ' .
6460
                ' restored with the old questionscontextid.', backup::LOG_WARNING);
6461
        }
6462
 
6463
        $filtercondition['cat'] = implode(',', [
6464
            $filtercondition['filter']['category']['values'][0],
6465
            $data->questionscontextid,
6466
        ]);
6467
 
6468
        $data->filtercondition = json_encode($filtercondition);
6469
 
6470
        $DB->insert_record('question_set_references', $data);
6471
    }
6472
}
6473
 
6474
 
6475
/**
6476
 * Abstract structure step to help activities that store question attempt data.
6477
 *
6478
 * @copyright 2011 The Open University
6479
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6480
 */
6481
abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
6482
    use restore_questions_attempt_data_trait;
6483
    use restore_question_reference_data_trait;
6484
    use restore_question_set_reference_data_trait;
6485
 
6486
    /** @var \question_engine_attempt_upgrader manages upgrading all the question attempts. */
6487
    private $attemptupgrader;
6488
 
6489
    /**
6490
     * Attach below $element (usually attempts) the needed restore_path_elements
6491
     * to restore question attempt data from Moodle 2.0.
6492
     *
6493
     * When using this method, the parent element ($element) must be defined with
6494
     * $grouped = true. Then, in that elements process method, you must call
6495
     * {@link process_legacy_attempt_data()} with the groupded data. See, for
6496
     * example, the usage of this method in {@link restore_quiz_activity_structure_step}.
6497
     * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
6498
     * @param array $paths the paths array that is being built to describe the
6499
     *      structure.
6500
     */
6501
    protected function add_legacy_question_attempt_data($element, &$paths) {
6502
        global $CFG;
6503
        require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php');
6504
 
6505
        // Check $element is restore_path_element
6506
        if (!($element instanceof restore_path_element)) {
6507
            throw new restore_step_exception('element_must_be_restore_path_element', $element);
6508
        }
6509
        // Check $paths is one array
6510
        if (!is_array($paths)) {
6511
            throw new restore_step_exception('paths_must_be_array', $paths);
6512
        }
6513
 
6514
        $paths[] = new restore_path_element('question_state',
6515
                $element->get_path() . '/states/state');
6516
        $paths[] = new restore_path_element('question_session',
6517
                $element->get_path() . '/sessions/session');
6518
    }
6519
 
6520
    protected function get_attempt_upgrader() {
6521
        if (empty($this->attemptupgrader)) {
6522
            $this->attemptupgrader = new question_engine_attempt_upgrader();
6523
            $this->attemptupgrader->prepare_to_restore();
6524
        }
6525
        return $this->attemptupgrader;
6526
    }
6527
 
6528
    /**
6529
     * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
6530
     * @param object $data contains all the grouped attempt data to process.
6531
     * @param object $quiz data about the activity the attempts belong to. Required
6532
     * fields are (basically this only works for the quiz module):
6533
     *      oldquestions => list of question ids in this activity - using old ids.
6534
     *      preferredbehaviour => the behaviour to use for questionattempts.
6535
     */
6536
    protected function process_legacy_quiz_attempt_data($data, $quiz) {
6537
        global $DB;
6538
        $upgrader = $this->get_attempt_upgrader();
6539
 
6540
        $data = (object)$data;
6541
 
6542
        $layout = explode(',', $data->layout);
6543
        $newlayout = $layout;
6544
 
6545
        // Convert each old question_session into a question_attempt.
6546
        $qas = array();
6547
        foreach (explode(',', $quiz->oldquestions) as $questionid) {
6548
            if ($questionid == 0) {
6549
                continue;
6550
            }
6551
 
6552
            $newquestionid = $this->get_mappingid('question', $questionid);
6553
            if (!$newquestionid) {
6554
                throw new restore_step_exception('questionattemptreferstomissingquestion',
6555
                        $questionid, $questionid);
6556
            }
6557
 
6558
            $question = $upgrader->load_question($newquestionid, $quiz->id);
6559
 
6560
            foreach ($layout as $key => $qid) {
6561
                if ($qid == $questionid) {
6562
                    $newlayout[$key] = $newquestionid;
6563
                }
6564
            }
6565
 
6566
            list($qsession, $qstates) = $this->find_question_session_and_states(
6567
                    $data, $questionid);
6568
 
6569
            if (empty($qsession) || empty($qstates)) {
6570
                throw new restore_step_exception('questionattemptdatamissing',
6571
                        $questionid, $questionid);
6572
            }
6573
 
6574
            list($qsession, $qstates) = $this->recode_legacy_response_data(
6575
                    $question, $qsession, $qstates);
6576
 
6577
            $data->layout = implode(',', $newlayout);
6578
            $qas[$newquestionid] = $upgrader->convert_question_attempt(
6579
                    $quiz, $data, $question, $qsession, $qstates);
6580
        }
6581
 
6582
        // Now create a new question_usage.
6583
        $usage = new stdClass();
6584
        $usage->component = 'mod_quiz';
6585
        $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid());
6586
        $usage->preferredbehaviour = $quiz->preferredbehaviour;
6587
        $usage->id = $DB->insert_record('question_usages', $usage);
6588
 
6589
        $this->inform_new_usage_id($usage->id);
6590
 
6591
        $data->uniqueid = $usage->id;
6592
        $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
6593
                $this->questions_recode_layout($quiz->oldquestions));
6594
    }
6595
 
6596
    protected function find_question_session_and_states($data, $questionid) {
6597
        $qsession = null;
6598
        foreach ($data->sessions['session'] as $session) {
6599
            if ($session['questionid'] == $questionid) {
6600
                $qsession = (object) $session;
6601
                break;
6602
            }
6603
        }
6604
 
6605
        $qstates = array();
6606
        foreach ($data->states['state'] as $state) {
6607
            if ($state['question'] == $questionid) {
6608
                // It would be natural to use $state['seq_number'] as the array-key
6609
                // here, but it seems that buggy behaviour in 2.0 and early can
6610
                // mean that that is not unique, so we use id, which is guaranteed
6611
                // to be unique.
6612
                $qstates[$state['id']] = (object) $state;
6613
            }
6614
        }
6615
        ksort($qstates);
6616
        $qstates = array_values($qstates);
6617
 
6618
        return array($qsession, $qstates);
6619
    }
6620
 
6621
    /**
6622
     * Recode any ids in the response data
6623
     * @param object $question the question data
6624
     * @param object $qsession the question sessions.
6625
     * @param array $qstates the question states.
6626
     */
6627
    protected function recode_legacy_response_data($question, $qsession, $qstates) {
6628
        $qsession->questionid = $question->id;
6629
 
6630
        foreach ($qstates as &$state) {
6631
            $state->question = $question->id;
6632
            $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype);
6633
        }
6634
 
6635
        return array($qsession, $qstates);
6636
    }
6637
 
6638
    /**
6639
     * Recode the legacy answer field.
6640
     * @param object $state the state to recode the answer of.
6641
     * @param string $qtype the question type.
6642
     */
6643
    public function restore_recode_legacy_answer($state, $qtype) {
6644
        $restorer = $this->get_qtype_restorer($qtype);
6645
        if ($restorer) {
6646
            return $restorer->recode_legacy_state_answer($state);
6647
        } else {
6648
            return $state->answer;
6649
        }
6650
    }
6651
}
6652
 
6653
 
6654
/**
6655
 * Restore completion defaults for each module type
6656
 *
6657
 * @package     core_backup
6658
 * @copyright   2017 Marina Glancy
6659
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6660
 */
6661
class restore_completion_defaults_structure_step extends restore_structure_step {
6662
    /**
6663
     * To conditionally decide if this step must be executed.
6664
     */
6665
    protected function execute_condition() {
6666
        // No completion on the front page.
6667
        if ($this->get_courseid() == SITEID) {
6668
            return false;
6669
        }
6670
 
6671
        // No default completion info found, don't execute.
6672
        $fullpath = $this->task->get_taskbasepath();
6673
        $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
6674
        if (!file_exists($fullpath)) {
6675
            return false;
6676
        }
6677
 
6678
        // Arrived here, execute the step.
6679
        return true;
6680
    }
6681
 
6682
    /**
6683
     * Function that will return the structure to be processed by this restore_step.
6684
     *
6685
     * @return restore_path_element[]
6686
     */
6687
    protected function define_structure() {
6688
        return [new restore_path_element('completion_defaults', '/course_completion_defaults/course_completion_default')];
6689
    }
6690
 
6691
    /**
6692
     * Processor for path element 'completion_defaults'
6693
     *
6694
     * @param stdClass|array $data
6695
     */
6696
    protected function process_completion_defaults($data) {
6697
        global $DB;
6698
 
6699
        $data = (array)$data;
6700
        $oldid = $data['id'];
6701
        unset($data['id']);
6702
 
6703
        // Find the module by name since id may be different in another site.
6704
        if (!$mod = $DB->get_record('modules', ['name' => $data['modulename']])) {
6705
            return;
6706
        }
6707
        unset($data['modulename']);
6708
 
6709
        // Find the existing record.
6710
        $newid = $DB->get_field('course_completion_defaults', 'id',
6711
            ['course' => $this->task->get_courseid(), 'module' => $mod->id]);
6712
        if (!$newid) {
6713
            $newid = $DB->insert_record('course_completion_defaults',
6714
                ['course' => $this->task->get_courseid(), 'module' => $mod->id] + $data);
6715
        } else {
6716
            $DB->update_record('course_completion_defaults', ['id' => $newid] + $data);
6717
        }
6718
 
6719
        // Save id mapping for restoring associated events.
6720
        $this->set_mapping('course_completion_defaults', $oldid, $newid);
6721
    }
6722
}
6723
 
6724
/**
6725
 * Index course after restore.
6726
 *
6727
 * @package core_backup
6728
 * @copyright 2017 The Open University
6729
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6730
 */
6731
class restore_course_search_index extends restore_execution_step {
6732
    /**
6733
     * When this step is executed, we add the course context to the queue for reindexing.
6734
     */
6735
    protected function define_execution() {
6736
        $context = \context_course::instance($this->task->get_courseid());
6737
        \core_search\manager::request_index($context);
6738
    }
6739
}
6740
 
6741
/**
6742
 * Index activity after restore (when not restoring whole course).
6743
 *
6744
 * @package core_backup
6745
 * @copyright 2017 The Open University
6746
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6747
 */
6748
class restore_activity_search_index extends restore_execution_step {
6749
    /**
6750
     * When this step is executed, we add the activity context to the queue for reindexing.
6751
     */
6752
    protected function define_execution() {
6753
        $context = \context::instance_by_id($this->task->get_contextid());
6754
        \core_search\manager::request_index($context);
6755
    }
6756
}
6757
 
6758
/**
6759
 * Index block after restore (when not restoring whole course).
6760
 *
6761
 * @package core_backup
6762
 * @copyright 2017 The Open University
6763
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6764
 */
6765
class restore_block_search_index extends restore_execution_step {
6766
    /**
6767
     * When this step is executed, we add the block context to the queue for reindexing.
6768
     */
6769
    protected function define_execution() {
6770
        // A block in the restore list may be skipped because a duplicate is detected.
6771
        // In this case, there is no new blockid (or context) to get.
6772
        if (!empty($this->task->get_blockid())) {
6773
            $context = \context_block::instance($this->task->get_blockid());
6774
            \core_search\manager::request_index($context);
6775
        }
6776
    }
6777
}
6778
 
6779
/**
6780
 * Restore action events.
6781
 *
6782
 * @package     core_backup
6783
 * @copyright   2017 onwards Ankit Agarwal
6784
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6785
 */
6786
class restore_calendar_action_events extends restore_execution_step {
6787
    /**
6788
     * What to do when this step is executed.
6789
     */
6790
    protected function define_execution() {
6791
        // We just queue the task here rather trying to recreate everything manually.
6792
        // The task will automatically populate all data.
6793
        $task = new \core\task\refresh_mod_calendar_events_task();
6794
        $task->set_custom_data(array('courseid' => $this->get_courseid()));
6795
        \core\task\manager::queue_adhoc_task($task, true);
6796
    }
6797
}