mirrored from git://git.moodle.org/moodle.git
/
restore_dbops.class.php
1607 lines (1444 loc) · 78.6 KB
/
restore_dbops.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Base abstract class for all the helper classes providing DB operations
*
* TODO: Finish phpdocs
*/
abstract class restore_dbops {
/**
* Keep cache of backup records.
* @var array
* @todo MDL-25290 static should be replaced with MUC code.
*/
private static $backupidscache = array();
/**
* Keep track of backup ids which are cached.
* @var array
* @todo MDL-25290 static should be replaced with MUC code.
*/
private static $backupidsexist = array();
/**
* Count is expensive, so manually keeping track of
* backupidscache, to avoid memory issues.
* @var int
* @todo MDL-25290 static should be replaced with MUC code.
*/
private static $backupidscachesize = 2048;
/**
* Count is expensive, so manually keeping track of
* backupidsexist, to avoid memory issues.
* @var int
* @todo MDL-25290 static should be replaced with MUC code.
*/
private static $backupidsexistsize = 10240;
/**
* Slice backupids cache to add more data.
* @var int
* @todo MDL-25290 static should be replaced with MUC code.
*/
private static $backupidsslice = 512;
/**
* Return one array containing all the tasks that have been included
* in the restore process. Note that these tasks aren't built (they
* haven't steps nor ids data available)
*/
public static function get_included_tasks($restoreid) {
$rc = restore_controller_dbops::load_controller($restoreid);
$tasks = $rc->get_plan()->get_tasks();
$includedtasks = array();
foreach ($tasks as $key => $task) {
// Calculate if the task is being included
$included = false;
// blocks, based in blocks setting and parent activity/course
if ($task instanceof restore_block_task) {
if (!$task->get_setting_value('blocks')) { // Blocks not included, continue
continue;
}
$parent = basename(dirname(dirname($task->get_taskbasepath())));
if ($parent == 'course') { // Parent is course, always included if present
$included = true;
} else { // Look for activity_included setting
$included = $task->get_setting_value($parent . '_included');
}
// ativities, based on included setting
} else if ($task instanceof restore_activity_task) {
$included = $task->get_setting_value('included');
// sections, based on included setting
} else if ($task instanceof restore_section_task) {
$included = $task->get_setting_value('included');
// course always included if present
} else if ($task instanceof restore_course_task) {
$included = true;
}
// If included, add it
if ($included) {
$includedtasks[] = $task;
}
}
return $includedtasks;
}
/**
* Load one inforef.xml file to backup_ids table for future reference
*/
public static function load_inforef_to_tempids($restoreid, $inforeffile) {
if (!file_exists($inforeffile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_inforef_xml_file', $inforeffile);
}
// Let's parse, custom processor will do its work, sending info to DB
$xmlparser = new progressive_parser();
$xmlparser->set_file($inforeffile);
$xmlprocessor = new restore_inforef_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
/**
* Load the needed role.xml file to backup_ids table for future reference
*/
public static function load_roles_to_tempids($restoreid, $rolesfile) {
if (!file_exists($rolesfile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_roles_xml_file', $rolesfile);
}
// Let's parse, custom processor will do its work, sending info to DB
$xmlparser = new progressive_parser();
$xmlparser->set_file($rolesfile);
$xmlprocessor = new restore_roles_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
/**
* Precheck the loaded roles, return empty array if everything is ok, and
* array with 'errors', 'warnings' elements (suitable to be used by restore_prechecks)
* with any problem found. At the same time, store all the mapping into backup_ids_temp
* and also put the information into $rolemappings (controller->info), so it can be reworked later by
* post-precheck stages while at the same time accept modified info in the same object coming from UI
*/
public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
global $DB;
$problems = array(); // To store warnings/errors
// Get loaded roles from backup_ids
$rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
foreach ($rs as $recrole) {
// If the rolemappings->modified flag is set, that means that we are coming from
// manually modified mappings (by UI), so accept those mappings an put them to backup_ids
if ($rolemappings->modified) {
$target = $rolemappings->mappings[$recrole->itemid]->targetroleid;
self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $target);
// Else, we haven't any info coming from UI, let's calculate the mappings, matching
// in multiple ways and checking permissions. Note mapping to 0 means "skip"
} else {
$role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
$match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
// Send match to backup_ids
self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
// Build the rolemappings element for controller
unset($role->id);
unset($role->nameincourse);
unset($role->nameincourse);
$role->targetroleid = $match;
$rolemappings->mappings[$recrole->itemid] = $role;
// Prepare warning if no match found
if (!$match) {
$problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name);
}
}
}
$rs->close();
return $problems;
}
/**
* Return cached backup id's
*
* @param int $restoreid id of backup
* @param string $itemname name of the item
* @param int $itemid id of item
* @return array backup id's
* @todo MDL-25290 replace static backupids* with MUC code
*/
protected static function get_backup_ids_cached($restoreid, $itemname, $itemid) {
global $DB;
$key = "$itemid $itemname $restoreid";
// If record exists in cache then return.
if (isset(self::$backupidsexist[$key]) && isset(self::$backupidscache[$key])) {
// Return a copy of cached data, to avoid any alterations in cached data.
return clone self::$backupidscache[$key];
}
// Clean cache, if it's full.
if (self::$backupidscachesize <= 0) {
// Remove some records, to keep memory in limit.
self::$backupidscache = array_slice(self::$backupidscache, self::$backupidsslice, null, true);
self::$backupidscachesize = self::$backupidscachesize + self::$backupidsslice;
}
if (self::$backupidsexistsize <= 0) {
self::$backupidsexist = array_slice(self::$backupidsexist, self::$backupidsslice, null, true);
self::$backupidsexistsize = self::$backupidsexistsize + self::$backupidsslice;
}
// Retrive record from database.
$record = array(
'backupid' => $restoreid,
'itemname' => $itemname,
'itemid' => $itemid
);
if ($dbrec = $DB->get_record('backup_ids_temp', $record)) {
self::$backupidsexist[$key] = $dbrec->id;
self::$backupidscache[$key] = $dbrec;
self::$backupidscachesize--;
self::$backupidsexistsize--;
return $dbrec;
} else {
return false;
}
}
/**
* Cache backup ids'
*
* @param int $restoreid id of backup
* @param string $itemname name of the item
* @param int $itemid id of item
* @param array $extrarecord extra record which needs to be updated
* @return void
* @todo MDL-25290 replace static BACKUP_IDS_* with MUC code
*/
protected static function set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord) {
global $DB;
$key = "$itemid $itemname $restoreid";
$record = array(
'backupid' => $restoreid,
'itemname' => $itemname,
'itemid' => $itemid,
);
// If record is not cached then add one.
if (!isset(self::$backupidsexist[$key])) {
// If we have this record in db, then just update this.
if ($existingrecord = $DB->get_record('backup_ids_temp', $record)) {
self::$backupidsexist[$key] = $existingrecord->id;
self::$backupidsexistsize--;
self::update_backup_cached_record($record, $extrarecord, $key, $existingrecord);
} else {
// Add new record to cache and db.
$recorddefault = array (
'newitemid' => 0,
'parentitemid' => null,
'info' => null);
$record = array_merge($record, $recorddefault, $extrarecord);
$record['id'] = $DB->insert_record('backup_ids_temp', $record);
self::$backupidsexist[$key] = $record['id'];
self::$backupidsexistsize--;
if (self::$backupidscachesize > 0) {
// Cache new records if we haven't got many yet.
self::$backupidscache[$key] = (object) $record;
self::$backupidscachesize--;
}
}
} else {
self::update_backup_cached_record($record, $extrarecord, $key);
}
}
/**
* Updates existing backup record
*
* @param array $record record which needs to be updated
* @param array $extrarecord extra record which needs to be updated
* @param string $key unique key which is used to identify cached record
* @param stdClass $existingrecord (optional) existing record
*/
protected static function update_backup_cached_record($record, $extrarecord, $key, $existingrecord = null) {
global $DB;
// Update only if extrarecord is not empty.
if (!empty($extrarecord)) {
$extrarecord['id'] = self::$backupidsexist[$key];
$DB->update_record('backup_ids_temp', $extrarecord);
// Update existing cache or add new record to cache.
if (isset(self::$backupidscache[$key])) {
$record = array_merge((array)self::$backupidscache[$key], $extrarecord);
self::$backupidscache[$key] = (object) $record;
} else if (self::$backupidscachesize > 0) {
if ($existingrecord) {
self::$backupidscache[$key] = $existingrecord;
} else {
// Retrive record from database and cache updated records.
self::$backupidscache[$key] = $DB->get_record('backup_ids_temp', $record);
}
$record = array_merge((array)self::$backupidscache[$key], $extrarecord);
self::$backupidscache[$key] = (object) $record;
self::$backupidscachesize--;
}
}
}
/**
* Reset the ids caches completely
*
* Any destructive operation (partial delete, truncate, drop or recreate) performed
* with the backup_ids table must cause the backup_ids caches to be
* invalidated by calling this method. See MDL-33630.
*
* Note that right now, the only operation of that type is the recreation
* (drop & restore) of the table that may happen once the prechecks have ended. All
* the rest of operations are always routed via {@link set_backup_ids_record()}, 1 by 1,
* keeping the caches on sync.
*
* @todo MDL-25290 static should be replaced with MUC code.
*/
public static function reset_backup_ids_cached() {
// Reset the ids cache.
$cachetoadd = count(self::$backupidscache);
self::$backupidscache = array();
self::$backupidscachesize = self::$backupidscachesize + $cachetoadd;
// Reset the exists cache.
$existstoadd = count(self::$backupidsexist);
self::$backupidsexist = array();
self::$backupidsexistsize = self::$backupidsexistsize + $existstoadd;
}
/**
* Given one role, as loaded from XML, perform the best possible matching against the assignable
* roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid)
* returning the id of the best matching role or 0 if no match is found
*/
protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) {
global $CFG, $DB;
// Gather various information about roles
$coursectx = context_course::instance($courseid);
$assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid);
// Note: under 1.9 we had one function restore_samerole() that performed one complete
// matching of roles (all caps) and if match was found the mapping was availabe bypassing
// any assignable_roles() security. IMO that was wrong and we must not allow such
// mappings anymore. So we have left that matching strategy out in 2.0
// Empty assignable roles, mean no match possible
if (empty($assignablerolesshortname)) {
return 0;
}
// Match by shortname
if ($match = array_search($role->shortname, $assignablerolesshortname)) {
return $match;
}
// Match by archetype
list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname));
$params = array_merge(array($role->archetype), $in_params);
if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) {
return $rec->id;
}
// Match editingteacher to teacher (happens a lot, from 1.9)
if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) {
return array_search('teacher', $assignablerolesshortname);
}
// No match, return 0
return 0;
}
/**
* Process the loaded roles, looking for their best mapping or skipping
* Any error will cause exception. Note this is one wrapper over
* precheck_included_roles, that contains all the logic, but returns
* errors/warnings instead and is executed as part of the restore prechecks
*/
public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
global $DB;
// Just let precheck_included_roles() to do all the hard work
$problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings);
// With problems of type error, throw exception, shouldn't happen if prechecks executed
if (array_key_exists('errors', $problems)) {
throw new restore_dbops_exception('restore_problems_processing_roles', null, implode(', ', $problems['errors']));
}
}
/**
* Load the needed users.xml file to backup_ids table for future reference
*/
public static function load_users_to_tempids($restoreid, $usersfile) {
if (!file_exists($usersfile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_users_xml_file', $usersfile);
}
// Let's parse, custom processor will do its work, sending info to DB
$xmlparser = new progressive_parser();
$xmlparser->set_file($usersfile);
$xmlprocessor = new restore_users_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
/**
* Load the needed questions.xml file to backup_ids table for future reference
*/
public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) {
if (!file_exists($questionsfile)) { // Shouldn't happen ever, but...
throw new backup_helper_exception('missing_questions_xml_file', $questionsfile);
}
// Let's parse, custom processor will do its work, sending info to DB
$xmlparser = new progressive_parser();
$xmlparser->set_file($questionsfile);
$xmlprocessor = new restore_questions_parser_processor($restoreid);
$xmlparser->set_processor($xmlprocessor);
$xmlparser->process();
}
/**
* Check all the included categories and questions, deciding the action to perform
* for each one (mapping / creation) and returning one array of problems in case
* something is wrong.
*
* There are some basic rules that the method below will always try to enforce:
*
* Rule1: Targets will be, always, calculated for *whole* question banks (a.k.a. contexid source),
* so, given 2 question categories belonging to the same bank, their target bank will be
* always the same. If not, we can be incurring into "fragmentation", leading to random/cloze
* problems (qtypes having "child" questions).
*
* Rule2: The 'moodle/question:managecategory' and 'moodle/question:add' capabilities will be
* checked before creating any category/question respectively and, if the cap is not allowed
* into upper contexts (system, coursecat)) but in lower ones (course), the *whole* question bank
* will be created there.
*
* Rule3: Coursecat question banks not existing in the target site will be created as course
* (lower ctx) question banks, never as "guessed" coursecat question banks base on depth or so.
*
* Rule4: System question banks will be created at system context if user has perms to do so. Else they
* will created as course (lower ctx) question banks (similary to rule3). In other words, course ctx
* if always a fallback for system and coursecat question banks.
*
* Also, there are some notes to clarify the scope of this method:
*
* Note1: This method won't create any question category nor question at all. It simply will calculate
* which actions (create/map) must be performed for each element and where, validating that all those
* actions are doable by the user executing the restore operation. Any problem found will be
* returned in the problems array, causing the restore process to stop with error.
*
* Note2: To decide if one question bank (all its question categories and questions) is going to be remapped,
* then all the categories and questions must exist in the same target bank. If able to do so, missing
* qcats and qs will be created (rule2). But if, at the end, something is missing, the whole question bank
* will be recreated at course ctx (rule1), no matter if that duplicates some categories/questions.
*
* Note3: We'll be using the newitemid column in the temp_ids table to store the action to be performed
* with each question category and question. newitemid = 0 means the qcat/q needs to be created and
* any other value means the qcat/q is mapped. Also, for qcats, parentitemid will contain the target
* context where the categories have to be created (but for module contexts where we'll keep the old
* one until the activity is created)
*
* Note4: All these "actions" will be "executed" later by {@link restore_create_categories_and_questions}
*/
public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) {
$problems = array();
// TODO: Check all qs, looking their qtypes are restorable
// Precheck all qcats and qs looking for target contexts / warnings / errors
list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM);
list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT);
list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE);
list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE);
// Acummulate and handle errors and warnings
$errors = array_merge($syserr, $caterr, $couerr, $moderr);
$warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn);
if (!empty($errors)) {
$problems['errors'] = $errors;
}
if (!empty($warnings)) {
$problems['warnings'] = $warnings;
}
return $problems;
}
/**
* This function will process all the question banks present in restore
* at some contextlevel (from CONTEXT_SYSTEM to CONTEXT_MODULE), finding
* the target contexts where each bank will be restored and returning
* warnings/errors as needed.
*
* Some contextlevels (system, coursecat), will delegate process to
* course level if any problem is found (lack of permissions, non-matching
* target context...). Other contextlevels (course, module) will
* cause return error if some problem is found.
*
* At the end, if no errors were found, all the categories in backup_temp_ids
* will be pointing (parentitemid) to the target context where they must be
* created later in the restore process.
*
* Note: at the time these prechecks are executed, activities haven't been
* created yet so, for CONTEXT_MODULE banks, we keep the old contextid
* in the parentitemid field. Once the activity (and its context) has been
* created, we'll update that context in the required qcats
*
* Caller {@link precheck_categories_and_questions} will, simply, execute
* this function for all the contextlevels, acting as a simple controller
* of warnings and errors.
*
* The function returns 2 arrays, one containing errors and another containing
* warnings. Both empty if no errors/warnings are found.
*/
public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
global $CFG, $DB;
// To return any errors and warnings found
$errors = array();
$warnings = array();
// Specify which fallbacks must be performed
$fallbacks = array(
CONTEXT_SYSTEM => CONTEXT_COURSE,
CONTEXT_COURSECAT => CONTEXT_COURSE);
// For any contextlevel, follow this process logic:
//
// 0) Iterate over each context (qbank)
// 1) Iterate over each qcat in the context, matching by stamp for the found target context
// 2a) No match, check if user can create qcat and q
// 3a) User can, mark the qcat and all dependent qs to be created in that target context
// 3b) User cannot, check if we are in some contextlevel with fallback
// 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
// 4b) No fallback, error. End qcat loop.
// 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
// 5a) No match, check if user can add q
// 6a) User can, mark the q to be created
// 6b) User cannot, check if we are in some contextlevel with fallback
// 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
// 7b) No fallback, error. End qcat loop
// 5b) Match, mark q to be mapped
// Get all the contexts (question banks) in restore for the given contextlevel
$contexts = self::restore_get_question_banks($restoreid, $contextlevel);
// 0) Iterate over each context (qbank)
foreach ($contexts as $contextid => $contextlevel) {
// Init some perms
$canmanagecategory = false;
$canadd = false;
// get categories in context (bank)
$categories = self::restore_get_question_categories($restoreid, $contextid);
// cache permissions if $targetcontext is found
if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
$canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
$canadd = has_capability('moodle/question:add', $targetcontext, $userid);
}
// 1) Iterate over each qcat in the context, matching by stamp for the found target context
foreach ($categories as $category) {
$matchcat = false;
if ($targetcontext) {
$matchcat = $DB->get_record('question_categories', array(
'contextid' => $targetcontext->id,
'stamp' => $category->stamp));
}
// 2a) No match, check if user can create qcat and q
if (!$matchcat) {
// 3a) User can, mark the qcat and all dependent qs to be created in that target context
if ($canmanagecategory && $canadd) {
// Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where
// we keep the source contextid unmodified (for easier matching later when the
// activities are created)
$parentitemid = $targetcontext->id;
if ($contextlevel == CONTEXT_MODULE) {
$parentitemid = null; // null means "not modify" a.k.a. leave original contextid
}
self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid);
// Nothing else to mark, newitemid = 0 means create
// 3b) User cannot, check if we are in some contextlevel with fallback
} else {
// 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
if (array_key_exists($contextlevel, $fallbacks)) {
foreach ($categories as $movedcat) {
$movedcat->contextlevel = $fallbacks[$contextlevel];
self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
// Warn about the performed fallback
$warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat);
}
// 4b) No fallback, error. End qcat loop.
} else {
$errors[] = get_string('qcategorycannotberestored', 'backup', $category);
}
break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank)
}
// 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
} else {
self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
$questions = self::restore_get_questions($restoreid, $category->id);
foreach ($questions as $question) {
$matchq = $DB->get_record('question', array(
'category' => $matchcat->id,
'stamp' => $question->stamp,
'version' => $question->version));
// 5a) No match, check if user can add q
if (!$matchq) {
// 6a) User can, mark the q to be created
if ($canadd) {
// Nothing to mark, newitemid means create
// 6b) User cannot, check if we are in some contextlevel with fallback
} else {
// 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo
if (array_key_exists($contextlevel, $fallbacks)) {
foreach ($categories as $movedcat) {
$movedcat->contextlevel = $fallbacks[$contextlevel];
self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
// Warn about the performed fallback
$warnings[] = get_string('question2coursefallback', 'backup', $movedcat);
}
// 7b) No fallback, error. End qcat loop
} else {
$errors[] = get_string('questioncannotberestored', 'backup', $question);
}
break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank)
}
// 5b) Match, mark q to be mapped
} else {
self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id);
}
}
}
}
}
return array($errors, $warnings);
}
/**
* Return one array of contextid => contextlevel pairs
* of question banks to be checked for one given restore operation
* ordered from CONTEXT_SYSTEM downto CONTEXT_MODULE
* If contextlevel is specified, then only banks corresponding to
* that level are returned
*/
public static function restore_get_question_banks($restoreid, $contextlevel = null) {
global $DB;
$results = array();
$qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'", array($restoreid));
foreach ($qcats as $qcat) {
// If this qcat context haven't been acummulated yet, do that
if (!isset($results[$qcat->contextid])) {
$temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
// Filter by contextlevel if necessary
if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
$results[$qcat->contextid] = $temprec->info->contextlevel;
}
}
}
// Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
asort($results);
return $results;
}
/**
* Return one array of question_category records for
* a given restore operation and one restore context (question bank)
*/
public static function restore_get_question_categories($restoreid, $contextid) {
global $DB;
$results = array();
$qcats = $DB->get_records_sql("SELECT itemid
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question_category'
AND parentitemid = ?", array($restoreid, $contextid));
foreach ($qcats as $qcat) {
$temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
$results[$qcat->itemid] = $temprec->info;
}
return $results;
}
/**
* Calculates the best context found to restore one collection of qcats,
* al them belonging to the same context (question bank), returning the
* target context found (object) or false
*/
public static function restore_find_best_target_context($categories, $courseid, $contextlevel) {
global $DB;
$targetcontext = false;
// Depending of $contextlevel, we perform different actions
switch ($contextlevel) {
// For system is easy, the best context is the system context
case CONTEXT_SYSTEM:
$targetcontext = context_system::instance();
break;
// For coursecat, we are going to look for stamps in all the
// course categories between CONTEXT_SYSTEM and CONTEXT_COURSE
// (i.e. in all the course categories in the path)
//
// And only will return one "best" target context if all the
// matches belong to ONE and ONLY ONE context. If multiple
// matches are found, that means that there is some annoying
// qbank "fragmentation" in the categories, so we'll fallback
// to create the qbank at course level
case CONTEXT_COURSECAT:
// Build the array of stamps we are going to match
$stamps = array();
foreach ($categories as $category) {
$stamps[] = $category->stamp;
}
$contexts = array();
// Build the array of contexts we are going to look
$systemctx = context_system::instance();
$coursectx = context_course::instance($courseid);
$parentctxs= get_parent_contexts($coursectx);
foreach ($parentctxs as $parentctx) {
// Exclude system context
if ($parentctx == $systemctx->id) {
continue;
}
$contexts[] = $parentctx;
}
if (!empty($stamps) && !empty($contexts)) {
// Prepare the query
list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps);
list($context_sql, $context_params) = $DB->get_in_or_equal($contexts);
$sql = "SELECT contextid
FROM {question_categories}
WHERE stamp $stamp_sql
AND contextid $context_sql";
$params = array_merge($stamp_params, $context_params);
$matchingcontexts = $DB->get_records_sql($sql, $params);
// Only if ONE and ONLY ONE context is found, use it as valid target
if (count($matchingcontexts) == 1) {
$targetcontext = context::instance_by_id(reset($matchingcontexts)->contextid);
}
}
break;
// For course is easy, the best context is the course context
case CONTEXT_COURSE:
$targetcontext = context_course::instance($courseid);
break;
// For module is easy, there is not best context, as far as the
// activity hasn't been created yet. So we return context course
// for them, so permission checks and friends will work. Note this
// case is handled by {@link prechek_precheck_qbanks_by_level}
// in an special way
case CONTEXT_MODULE:
$targetcontext = context_course::instance($courseid);
break;
}
return $targetcontext;
}
/**
* Return one array of question records for
* a given restore operation and one question category
*/
public static function restore_get_questions($restoreid, $qcatid) {
global $DB;
$results = array();
$qs = $DB->get_records_sql("SELECT itemid
FROM {backup_ids_temp}
WHERE backupid = ?
AND itemname = 'question'
AND parentitemid = ?", array($restoreid, $qcatid));
foreach ($qs as $q) {
$temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
$results[$q->itemid] = $temprec->info;
}
return $results;
}
/**
* Given one component/filearea/context and
* optionally one source itemname to match itemids
* put the corresponding files in the pool
*
* @param string $basepath the full path to the root of unzipped backup file
* @param string $restoreid the restore job's identification
* @param string $component
* @param string $filearea
* @param int $oldcontextid
* @param int $dfltuserid default $file->user if the old one can't be mapped
* @param string|null $itemname
* @param int|null $olditemid
* @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
* @param bool $skipparentitemidctxmatch
* @return array of result object
*/
public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
global $DB;
$results = array();
if ($forcenewcontextid) {
// Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings,
// with questions originally at system/coursecat context in source being restored to course context in target). So we need
// to be able to force the new contextid
$newcontextid = $forcenewcontextid;
} else {
// Get new context, must exist or this will fail
if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) {
throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid);
}
}
// Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid
// columns (because we have used them to store other information). This happens usually with
// all the question related backup_ids_temp records. In that case, it's safe to ignore that
// matching as far as we are always restoring for well known oldcontexts and olditemids
$parentitemctxmatchsql = ' AND i.parentitemid = f.contextid ';
if ($skipparentitemidctxmatch) {
$parentitemctxmatchsql = '';
}
// Important: remember how files have been loaded to backup_files_temp
// - info: contains the whole original object (times, names...)
// (all them being original ids as loaded from xml)
// itemname = null, we are going to match only by context, no need to use itemid (all them are 0)
if ($itemname == null) {
$sql = "SELECT id AS bftid, contextid, component, filearea, itemid, itemid AS newitemid, info
FROM {backup_files_temp}
WHERE backupid = ?
AND contextid = ?
AND component = ?
AND filearea = ?";
$params = array($restoreid, $oldcontextid, $component, $filearea);
// itemname not null, going to join with backup_ids to perform the old-new mapping of itemids
} else {
$sql = "SELECT f.id AS bftid, f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info
FROM {backup_files_temp} f
JOIN {backup_ids_temp} i ON i.backupid = f.backupid
$parentitemctxmatchsql
AND i.itemid = f.itemid
WHERE f.backupid = ?
AND f.contextid = ?
AND f.component = ?
AND f.filearea = ?
AND i.itemname = ?";
$params = array($restoreid, $oldcontextid, $component, $filearea, $itemname);
if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname
$sql .= ' AND i.itemid = ?';
$params[] = $olditemid;
}
}
$fs = get_file_storage(); // Get moodle file storage
$basepath = $basepath . '/files/';// Get backup file pool base
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $rec) {
$file = (object)unserialize(base64_decode($rec->info));
// ignore root dirs (they are created automatically)
if ($file->filepath == '/' && $file->filename == '.') {
continue;
}
// set the best possible user
$mappeduser = self::get_backup_ids_record($restoreid, 'user', $file->userid);
$mappeduserid = !empty($mappeduser) ? $mappeduser->newitemid : $dfltuserid;
// dir found (and not root one), let's create it
if ($file->filename == '.') {
$fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $mappeduserid);
continue;
}
if (empty($file->repositoryid)) {
// this is a regular file, it must be present in the backup pool
$backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash);
// The file is not found in the backup.
if (!file_exists($backuppath)) {
$result = new stdClass();
$result->code = 'file_missing_in_backup';
$result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
$result->level = backup::LOG_WARNING;
$results[] = $result;
continue;
}
// create the file in the filepool if it does not exist yet
if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
$file_record = array(
'contextid' => $newcontextid,
'component' => $component,
'filearea' => $filearea,
'itemid' => $rec->newitemid,
'filepath' => $file->filepath,
'filename' => $file->filename,
'timecreated' => $file->timecreated,
'timemodified'=> $file->timemodified,
'userid' => $mappeduserid,
'author' => $file->author,
'license' => $file->license,
'sortorder' => $file->sortorder
);
$fs->create_file_from_pathname($file_record, $backuppath);
}
// store the the new contextid and the new itemid in case we need to remap
// references to this file later
$DB->update_record('backup_files_temp', array(
'id' => $rec->bftid,
'newcontextid' => $newcontextid,
'newitemid' => $rec->newitemid), true);
} else {
// this is an alias - we can't create it yet so we stash it in a temp
// table and will let the final task to deal with it
if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
$info = new stdClass();
// oldfile holds the raw information stored in MBZ (including reference-related info)
$info->oldfile = $file;
// newfile holds the info for the new file_record with the context, user and itemid mapped
$info->newfile = (object)array(
'contextid' => $newcontextid,
'component' => $component,
'filearea' => $filearea,
'itemid' => $rec->newitemid,
'filepath' => $file->filepath,
'filename' => $file->filename,
'timecreated' => $file->timecreated,
'timemodified'=> $file->timemodified,
'userid' => $mappeduserid,
'author' => $file->author,
'license' => $file->license,
'sortorder' => $file->sortorder
);
restore_dbops::set_backup_ids_record($restoreid, 'file_aliases_queue', $file->id, 0, null, $info);
}
}
}
$rs->close();
return $results;
}
/**
* Given one restoreid, create in DB all the users present
* in backup_ids having newitemid = 0, as far as
* precheck_included_users() have left them there
* ready to be created. Also, annotate their newids
* once created for later reference
*/
public static function create_included_users($basepath, $restoreid, $userid) {
global $CFG, $DB;
$authcache = array(); // Cache to get some bits from authentication plugins
$languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search later
$themes = get_list_of_themes(); // Get themes for quick search later
// Iterate over all the included users with newitemid = 0, have to create them
$rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
foreach ($rs as $recuser) {
$user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
// if user lang doesn't exist here, use site default
if (!array_key_exists($user->lang, $languages)) {
$user->lang = $CFG->lang;
}
// if user theme isn't available on target site or they are disabled, reset theme
if (!empty($user->theme)) {
if (empty($CFG->allowuserthemes) || !in_array($user->theme, $themes)) {