MDL-27120 backup: fixed docblock and code cleanup
[moodle.git] / backup / util / dbops / restore_dbops.class.php
CommitLineData
fbc2778d
EL
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 * @package moodlecore
20 * @subpackage backup-dbops
21 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25/**
26 * Base abstract class for all the helper classes providing DB operations
27 *
28 * TODO: Finish phpdocs
29 */
482aac65 30abstract class restore_dbops {
f2bfe26a
RT
31 /**
32 * Keep cache of backup records.
33 * @var array
34 * @todo MDL-25290 static should be replaced with MUC code.
35 */
36 private static $backupidscache = array();
37 /**
38 * Keep track of backup ids which are cached.
39 * @var array
40 * @todo MDL-25290 static should be replaced with MUC code.
41 */
42 private static $backupidsexist = array();
43 /**
44 * Count is expensive, so manually keeping track of
45 * backupidscache, to avoid memory issues.
46 * @var int
47 * @todo MDL-25290 static should be replaced with MUC code.
48 */
49 private static $backupidscachesize = 2048;
50 /**
51 * Count is expensive, so manually keeping track of
52 * backupidsexist, to avoid memory issues.
53 * @var int
54 * @todo MDL-25290 static should be replaced with MUC code.
55 */
56 private static $backupidsexistsize = 10240;
57 /**
58 * Slice backupids cache to add more data.
59 * @var int
60 * @todo MDL-25290 static should be replaced with MUC code.
61 */
62 private static $backupidsslice = 512;
482aac65
EL
63
64 /**
648a575e
EL
65 * Return one array containing all the tasks that have been included
66 * in the restore process. Note that these tasks aren't built (they
67 * haven't steps nor ids data available)
482aac65 68 */
648a575e 69 public static function get_included_tasks($restoreid) {
482aac65
EL
70 $rc = restore_controller_dbops::load_controller($restoreid);
71 $tasks = $rc->get_plan()->get_tasks();
648a575e
EL
72 $includedtasks = array();
73 foreach ($tasks as $key => $task) {
482aac65
EL
74 // Calculate if the task is being included
75 $included = false;
76 // blocks, based in blocks setting and parent activity/course
77 if ($task instanceof restore_block_task) {
4a15bb76 78 if (!$task->get_setting_value('blocks')) { // Blocks not included, continue
482aac65
EL
79 continue;
80 }
81 $parent = basename(dirname(dirname($task->get_taskbasepath())));
82 if ($parent == 'course') { // Parent is course, always included if present
83 $included = true;
84
85 } else { // Look for activity_included setting
86 $included = $task->get_setting_value($parent . '_included');
87 }
88
89 // ativities, based on included setting
90 } else if ($task instanceof restore_activity_task) {
91 $included = $task->get_setting_value('included');
92
93 // sections, based on included setting
94 } else if ($task instanceof restore_section_task) {
95 $included = $task->get_setting_value('included');
96
97 // course always included if present
98 } else if ($task instanceof restore_course_task) {
99 $included = true;
100 }
101
648a575e 102 // If included, add it
482aac65 103 if ($included) {
648a575e 104 $includedtasks[] = $task;
482aac65
EL
105 }
106 }
648a575e 107 return $includedtasks;
482aac65
EL
108 }
109
110 /**
111 * Load one inforef.xml file to backup_ids table for future reference
112 */
113 public static function load_inforef_to_tempids($restoreid, $inforeffile) {
114
115 if (!file_exists($inforeffile)) { // Shouldn't happen ever, but...
116 throw new backup_helper_exception('missing_inforef_xml_file', $inforeffile);
117 }
118 // Let's parse, custom processor will do its work, sending info to DB
119 $xmlparser = new progressive_parser();
120 $xmlparser->set_file($inforeffile);
121 $xmlprocessor = new restore_inforef_parser_processor($restoreid);
122 $xmlparser->set_processor($xmlprocessor);
123 $xmlparser->process();
124 }
125
71a50b13
EL
126 /**
127 * Load the needed role.xml file to backup_ids table for future reference
128 */
129 public static function load_roles_to_tempids($restoreid, $rolesfile) {
130
131 if (!file_exists($rolesfile)) { // Shouldn't happen ever, but...
132 throw new backup_helper_exception('missing_roles_xml_file', $rolesfile);
133 }
134 // Let's parse, custom processor will do its work, sending info to DB
135 $xmlparser = new progressive_parser();
136 $xmlparser->set_file($rolesfile);
137 $xmlprocessor = new restore_roles_parser_processor($restoreid);
138 $xmlparser->set_processor($xmlprocessor);
139 $xmlparser->process();
140 }
141
142 /**
143 * Precheck the loaded roles, return empty array if everything is ok, and
144 * array with 'errors', 'warnings' elements (suitable to be used by restore_prechecks)
8d4e41f4
EL
145 * with any problem found. At the same time, store all the mapping into backup_ids_temp
146 * and also put the information into $rolemappings (controller->info), so it can be reworked later by
147 * post-precheck stages while at the same time accept modified info in the same object coming from UI
71a50b13 148 */
8d4e41f4
EL
149 public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
150 global $DB;
151
152 $problems = array(); // To store warnings/errors
153
154 // Get loaded roles from backup_ids
155 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
156 foreach ($rs as $recrole) {
157 // If the rolemappings->modified flag is set, that means that we are coming from
158 // manually modified mappings (by UI), so accept those mappings an put them to backup_ids
159 if ($rolemappings->modified) {
160 $target = $rolemappings->mappings[$recrole->itemid]->targetroleid;
161 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $target);
162
163 // Else, we haven't any info coming from UI, let's calculate the mappings, matching
164 // in multiple ways and checking permissions. Note mapping to 0 means "skip"
165 } else {
166 $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info;
167 $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite);
168 // Send match to backup_ids
169 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match);
170 // Build the rolemappings element for controller
171 unset($role->id);
172 unset($role->nameincourse);
173 unset($role->nameincourse);
174 $role->targetroleid = $match;
175 $rolemappings->mappings[$recrole->itemid] = $role;
176 // Prepare warning if no match found
177 if (!$match) {
178 $problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name);
179 }
180 }
181 }
182 $rs->close();
183 return $problems;
184 }
185
f2bfe26a
RT
186 /**
187 * Return cached backup id's
188 *
189 * @param int $restoreid id of backup
190 * @param string $itemname name of the item
191 * @param int $itemid id of item
192 * @return array backup id's
193 * @todo MDL-25290 replace static backupids* with MUC code
194 */
0c99b722
TL
195 protected static function get_backup_ids_cached($restoreid, $itemname, $itemid) {
196 global $DB;
197
198 $key = "$itemid $itemname $restoreid";
199
f2bfe26a
RT
200 // If record exists in cache then return.
201 if (isset(self::$backupidsexist[$key]) && isset(self::$backupidscache[$key])) {
202 return self::$backupidscache[$key];
0c99b722
TL
203 }
204
f2bfe26a
RT
205 // Clean cache, if it's full.
206 if (self::$backupidscachesize <= 0) {
207 // Remove some records, to keep memory in limit.
208 self::$backupidscache = array_slice(self::$backupidscache, self::$backupidsslice, null, true);
209 self::$backupidscachesize = self::$backupidscachesize + self::$backupidsslice;
210 }
211 if (self::$backupidsexistsize <= 0) {
212 self::$backupidsexist = array_slice(self::$backupidsexist, self::$backupidsslice, null, true);
213 self::$backupidsexistsize = self::$backupidsexistsize + self::$backupidsslice;
0c99b722
TL
214 }
215
f2bfe26a 216 // Retrive record from database.
0c99b722
TL
217 $record = array(
218 'backupid' => $restoreid,
219 'itemname' => $itemname,
220 'itemid' => $itemid
221 );
f2bfe26a
RT
222 if ($dbrec = $DB->get_record('backup_ids_temp', $record)) {
223 self::$backupidsexist[$key] = $dbrec->id;
224 self::$backupidscache[$key] = $dbrec;
225 self::$backupidscachesize--;
226 self::$backupidsexistsize--;
227 return self::$backupidscache[$key];
228 } else {
229 return false;
0c99b722 230 }
0c99b722
TL
231 }
232
f2bfe26a
RT
233 /**
234 * Cache backup ids'
235 *
236 * @param int $restoreid id of backup
237 * @param string $itemname name of the item
238 * @param int $itemid id of item
239 * @param array $extrarecord extra record which needs to be updated
240 * @return void
241 * @todo MDL-25290 replace static BACKUP_IDS_* with MUC code
242 */
0c99b722
TL
243 protected static function set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord) {
244 global $DB;
245
246 $key = "$itemid $itemname $restoreid";
247
f2bfe26a
RT
248 $record = array(
249 'backupid' => $restoreid,
250 'itemname' => $itemname,
251 'itemid' => $itemid,
252 );
253
254 // If record is not cached then add one.
255 if (!isset(self::$backupidsexist[$key])) {
256 // If we have this record in db, then just update this.
257 if ($existingrecord = $DB->get_record('backup_ids_temp', $record)) {
258 self::$backupidsexist[$key] = $existingrecord->id;
259 self::$backupidsexistsize--;
260 self::update_backup_cached_record($record, $extrarecord, $key, $existingrecord);
261 } else {
262 // Add new record to cache and db.
263 $recorddefault = array (
264 'newitemid' => 0,
265 'parentitemid' => null,
266 'info' => null);
267 $record = array_merge($record, $recorddefault, $extrarecord);
268 $record['id'] = $DB->insert_record('backup_ids_temp', $record);
269 self::$backupidsexist[$key] = $record['id'];
270 self::$backupidsexistsize--;
271 if (self::$backupidscachesize > 0) {
272 // Cache new records if we haven't got many yet.
273 self::$backupidscache[$key] = (object) $record;
274 self::$backupidscachesize--;
275 }
0c99b722
TL
276 }
277 } else {
f2bfe26a
RT
278 self::update_backup_cached_record($record, $extrarecord, $key);
279 }
280 }
281
282 /**
283 * Updates existing backup record
284 *
285 * @param array $record record which needs to be updated
286 * @param array $extrarecord extra record which needs to be updated
287 * @param string $key unique key which is used to identify cached record
288 * @param stdClass $existingrecord (optional) existing record
289 */
290 protected static function update_backup_cached_record($record, $extrarecord, $key, $existingrecord = null) {
291 global $DB;
292 // Update only if extrarecord is not empty.
293 if (!empty($extrarecord)) {
294 $extrarecord['id'] = self::$backupidsexist[$key];
295 $DB->update_record('backup_ids_temp', $extrarecord);
296 // Update existing cache or add new record to cache.
297 if (isset(self::$backupidscache[$key])) {
298 $record = array_merge((array)self::$backupidscache[$key], $extrarecord);
299 self::$backupidscache[$key] = (object) $record;
300 } else if (self::$backupidscachesize > 0) {
301 if ($existingrecord) {
302 self::$backupidscache[$key] = $existingrecord;
303 } else {
304 // Retrive record from database and cache updated records.
305 self::$backupidscache[$key] = $DB->get_record('backup_ids_temp', $record);
0c99b722 306 }
f2bfe26a
RT
307 $record = array_merge((array)self::$backupidscache[$key], $extrarecord);
308 self::$backupidscache[$key] = (object) $record;
309 self::$backupidscachesize--;
0c99b722
TL
310 }
311 }
312 }
313
8d4e41f4
EL
314 /**
315 * Given one role, as loaded from XML, perform the best possible matching against the assignable
316 * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid)
317 * returning the id of the best matching role or 0 if no match is found
318 */
319 protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) {
320 global $CFG, $DB;
321
322 // Gather various information about roles
323 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid);
324 $allroles = $DB->get_records('role');
325 $assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid);
326
327 // Note: under 1.9 we had one function restore_samerole() that performed one complete
328 // matching of roles (all caps) and if match was found the mapping was availabe bypassing
329 // any assignable_roles() security. IMO that was wrong and we must not allow such
330 // mappings anymore. So we have left that matching strategy out in 2.0
331
332 // Empty assignable roles, mean no match possible
333 if (empty($assignablerolesshortname)) {
334 return 0;
335 }
336
337 // Match by shortname
338 if ($match = array_search($role->shortname, $assignablerolesshortname)) {
339 return $match;
340 }
341
342 // Match by archetype
343 list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname));
344 $params = array_merge(array($role->archetype), $in_params);
345 if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) {
346 return $rec->id;
347 }
348
349 // Match editingteacher to teacher (happens a lot, from 1.9)
350 if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) {
351 return array_search('teacher', $assignablerolesshortname);
352 }
353
354 // No match, return 0
355 return 0;
71a50b13
EL
356 }
357
8d4e41f4 358
71a50b13
EL
359 /**
360 * Process the loaded roles, looking for their best mapping or skipping
361 * Any error will cause exception. Note this is one wrapper over
362 * precheck_included_roles, that contains all the logic, but returns
363 * errors/warnings instead and is executed as part of the restore prechecks
364 */
8d4e41f4 365 public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
71a50b13
EL
366 global $DB;
367
368 // Just let precheck_included_roles() to do all the hard work
8d4e41f4 369 $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings);
71a50b13
EL
370
371 // With problems of type error, throw exception, shouldn't happen if prechecks executed
372 if (array_key_exists('errors', $problems)) {
373 throw new restore_dbops_exception('restore_problems_processing_roles', null, implode(', ', $problems['errors']));
374 }
375 }
376
482aac65
EL
377 /**
378 * Load the needed users.xml file to backup_ids table for future reference
379 */
380 public static function load_users_to_tempids($restoreid, $usersfile) {
381
382 if (!file_exists($usersfile)) { // Shouldn't happen ever, but...
2df0f295 383 throw new backup_helper_exception('missing_users_xml_file', $usersfile);
482aac65
EL
384 }
385 // Let's parse, custom processor will do its work, sending info to DB
386 $xmlparser = new progressive_parser();
387 $xmlparser->set_file($usersfile);
388 $xmlprocessor = new restore_users_parser_processor($restoreid);
389 $xmlparser->set_processor($xmlprocessor);
390 $xmlparser->process();
391 }
392
41941110
EL
393 /**
394 * Load the needed questions.xml file to backup_ids table for future reference
395 */
396 public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) {
397
398 if (!file_exists($questionsfile)) { // Shouldn't happen ever, but...
399 throw new backup_helper_exception('missing_questions_xml_file', $questionsfile);
400 }
401 // Let's parse, custom processor will do its work, sending info to DB
402 $xmlparser = new progressive_parser();
403 $xmlparser->set_file($questionsfile);
404 $xmlprocessor = new restore_questions_parser_processor($restoreid);
405 $xmlparser->set_processor($xmlprocessor);
406 $xmlparser->process();
407 }
408
409 /**
410 * Check all the included categories and questions, deciding the action to perform
411 * for each one (mapping / creation) and returning one array of problems in case
412 * something is wrong.
413 *
414 * There are some basic rules that the method below will always try to enforce:
415 *
416 * Rule1: Targets will be, always, calculated for *whole* question banks (a.k.a. contexid source),
417 * so, given 2 question categories belonging to the same bank, their target bank will be
418 * always the same. If not, we can be incurring into "fragmentation", leading to random/cloze
419 * problems (qtypes having "child" questions).
420 *
421 * Rule2: The 'moodle/question:managecategory' and 'moodle/question:add' capabilities will be
422 * checked before creating any category/question respectively and, if the cap is not allowed
423 * into upper contexts (system, coursecat)) but in lower ones (course), the *whole* question bank
424 * will be created there.
425 *
426 * Rule3: Coursecat question banks not existing in the target site will be created as course
427 * (lower ctx) question banks, never as "guessed" coursecat question banks base on depth or so.
428 *
429 * Rule4: System question banks will be created at system context if user has perms to do so. Else they
430 * will created as course (lower ctx) question banks (similary to rule3). In other words, course ctx
431 * if always a fallback for system and coursecat question banks.
432 *
433 * Also, there are some notes to clarify the scope of this method:
434 *
435 * Note1: This method won't create any question category nor question at all. It simply will calculate
436 * which actions (create/map) must be performed for each element and where, validating that all those
437 * actions are doable by the user executing the restore operation. Any problem found will be
438 * returned in the problems array, causing the restore process to stop with error.
439 *
440 * Note2: To decide if one question bank (all its question categories and questions) is going to be remapped,
441 * then all the categories and questions must exist in the same target bank. If able to do so, missing
442 * qcats and qs will be created (rule2). But if, at the end, something is missing, the whole question bank
443 * will be recreated at course ctx (rule1), no matter if that duplicates some categories/questions.
444 *
445 * Note3: We'll be using the newitemid column in the temp_ids table to store the action to be performed
446 * with each question category and question. newitemid = 0 means the qcat/q needs to be created and
447 * any other value means the qcat/q is mapped. Also, for qcats, parentitemid will contain the target
448 * context where the categories have to be created (but for module contexts where we'll keep the old
449 * one until the activity is created)
450 *
451 * Note4: All these "actions" will be "executed" later by {@link restore_create_categories_and_questions}
452 */
453 public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) {
454
455 $problems = array();
456
457 // TODO: Check all qs, looking their qtypes are restorable
458
459 // Precheck all qcats and qs looking for target contexts / warnings / errors
460 list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM);
461 list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT);
462 list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE);
463 list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE);
464
465 // Acummulate and handle errors and warnings
466 $errors = array_merge($syserr, $caterr, $couerr, $moderr);
467 $warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn);
468 if (!empty($errors)) {
469 $problems['errors'] = $errors;
470 }
471 if (!empty($warnings)) {
472 $problems['warnings'] = $warnings;
473 }
474 return $problems;
475 }
476
477 /**
478 * This function will process all the question banks present in restore
479 * at some contextlevel (from CONTEXT_SYSTEM to CONTEXT_MODULE), finding
480 * the target contexts where each bank will be restored and returning
481 * warnings/errors as needed.
482 *
483 * Some contextlevels (system, coursecat), will delegate process to
484 * course level if any problem is found (lack of permissions, non-matching
485 * target context...). Other contextlevels (course, module) will
486 * cause return error if some problem is found.
487 *
488 * At the end, if no errors were found, all the categories in backup_temp_ids
489 * will be pointing (parentitemid) to the target context where they must be
490 * created later in the restore process.
491 *
492 * Note: at the time these prechecks are executed, activities haven't been
493 * created yet so, for CONTEXT_MODULE banks, we keep the old contextid
494 * in the parentitemid field. Once the activity (and its context) has been
495 * created, we'll update that context in the required qcats
496 *
497 * Caller {@link precheck_categories_and_questions} will, simply, execute
498 * this function for all the contextlevels, acting as a simple controller
499 * of warnings and errors.
500 *
501 * The function returns 2 arrays, one containing errors and another containing
502 * warnings. Both empty if no errors/warnings are found.
503 */
504 public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
505 global $CFG, $DB;
506
507 // To return any errors and warnings found
508 $errors = array();
509 $warnings = array();
510
511 // Specify which fallbacks must be performed
512 $fallbacks = array(
513 CONTEXT_SYSTEM => CONTEXT_COURSE,
514 CONTEXT_COURSECAT => CONTEXT_COURSE);
515
516 // For any contextlevel, follow this process logic:
517 //
518 // 0) Iterate over each context (qbank)
519 // 1) Iterate over each qcat in the context, matching by stamp for the found target context
520 // 2a) No match, check if user can create qcat and q
521 // 3a) User can, mark the qcat and all dependent qs to be created in that target context
522 // 3b) User cannot, check if we are in some contextlevel with fallback
523 // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
524 // 4b) No fallback, error. End qcat loop.
525 // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
526 // 5a) No match, check if user can add q
527 // 6a) User can, mark the q to be created
528 // 6b) User cannot, check if we are in some contextlevel with fallback
529 // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
530 // 7b) No fallback, error. End qcat loop
531 // 5b) Match, mark q to be mapped
532
533 // Get all the contexts (question banks) in restore for the given contextlevel
534 $contexts = self::restore_get_question_banks($restoreid, $contextlevel);
535
536 // 0) Iterate over each context (qbank)
537 foreach ($contexts as $contextid => $contextlevel) {
538 // Init some perms
539 $canmanagecategory = false;
540 $canadd = false;
541 // get categories in context (bank)
542 $categories = self::restore_get_question_categories($restoreid, $contextid);
543 // cache permissions if $targetcontext is found
544 if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
545 $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
546 $canadd = has_capability('moodle/question:add', $targetcontext, $userid);
547 }
548 // 1) Iterate over each qcat in the context, matching by stamp for the found target context
549 foreach ($categories as $category) {
550 $matchcat = false;
551 if ($targetcontext) {
552 $matchcat = $DB->get_record('question_categories', array(
553 'contextid' => $targetcontext->id,
554 'stamp' => $category->stamp));
555 }
556 // 2a) No match, check if user can create qcat and q
557 if (!$matchcat) {
558 // 3a) User can, mark the qcat and all dependent qs to be created in that target context
559 if ($canmanagecategory && $canadd) {
560 // Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where
561 // we keep the source contextid unmodified (for easier matching later when the
562 // activities are created)
563 $parentitemid = $targetcontext->id;
564 if ($contextlevel == CONTEXT_MODULE) {
565 $parentitemid = null; // null means "not modify" a.k.a. leave original contextid
566 }
567 self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid);
568 // Nothing else to mark, newitemid = 0 means create
569
570 // 3b) User cannot, check if we are in some contextlevel with fallback
571 } else {
572 // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
573 if (array_key_exists($contextlevel, $fallbacks)) {
574 foreach ($categories as $movedcat) {
575 $movedcat->contextlevel = $fallbacks[$contextlevel];
576 self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
577 // Warn about the performed fallback
578 $warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat);
579 }
580
581 // 4b) No fallback, error. End qcat loop.
582 } else {
583 $errors[] = get_string('qcategorycannotberestored', 'backup', $category);
584 }
585 break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank)
586 }
587
588 // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
589 } else {
590 self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
591 $questions = self::restore_get_questions($restoreid, $category->id);
592 foreach ($questions as $question) {
593 $matchq = $DB->get_record('question', array(
594 'category' => $matchcat->id,
595 'stamp' => $question->stamp,
596 'version' => $question->version));
597 // 5a) No match, check if user can add q
598 if (!$matchq) {
599 // 6a) User can, mark the q to be created
600 if ($canadd) {
601 // Nothing to mark, newitemid means create
602
603 // 6b) User cannot, check if we are in some contextlevel with fallback
604 } else {
605 // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo
606 if (array_key_exists($contextlevel, $fallbacks)) {
607 foreach ($categories as $movedcat) {
608 $movedcat->contextlevel = $fallbacks[$contextlevel];
609 self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
610 // Warn about the performed fallback
611 $warnings[] = get_string('question2coursefallback', 'backup', $movedcat);
612 }
613
614 // 7b) No fallback, error. End qcat loop
615 } else {
616 $errors[] = get_string('questioncannotberestored', 'backup', $question);
617 }
618 break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank)
619 }
620
621 // 5b) Match, mark q to be mapped
622 } else {
623 self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id);
624 }
625 }
626 }
627 }
628 }
629
630 return array($errors, $warnings);
631 }
632
633 /**
634 * Return one array of contextid => contextlevel pairs
635 * of question banks to be checked for one given restore operation
636 * ordered from CONTEXT_SYSTEM downto CONTEXT_MODULE
637 * If contextlevel is specified, then only banks corresponding to
638 * that level are returned
639 */
640 public static function restore_get_question_banks($restoreid, $contextlevel = null) {
641 global $DB;
642
643 $results = array();
644 $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid
645 FROM {backup_ids_temp}
646 WHERE backupid = ?
647 AND itemname = 'question_category'", array($restoreid));
648 foreach ($qcats as $qcat) {
649 // If this qcat context haven't been acummulated yet, do that
650 if (!isset($results[$qcat->contextid])) {
651 $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
652 // Filter by contextlevel if necessary
653 if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) {
654 $results[$qcat->contextid] = $temprec->info->contextlevel;
655 }
656 }
657 }
658 // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
659 asort($results);
660 return $results;
661 }
662
663 /**
664 * Return one array of question_category records for
665 * a given restore operation and one restore context (question bank)
666 */
667 public static function restore_get_question_categories($restoreid, $contextid) {
668 global $DB;
669
670 $results = array();
671 $qcats = $DB->get_records_sql("SELECT itemid
672 FROM {backup_ids_temp}
673 WHERE backupid = ?
674 AND itemname = 'question_category'
675 AND parentitemid = ?", array($restoreid, $contextid));
676 foreach ($qcats as $qcat) {
677 $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid);
678 $results[$qcat->itemid] = $temprec->info;
679 }
680 return $results;
681 }
682
683 /**
684 * Calculates the best context found to restore one collection of qcats,
685 * al them belonging to the same context (question bank), returning the
686 * target context found (object) or false
687 */
688 public static function restore_find_best_target_context($categories, $courseid, $contextlevel) {
689 global $DB;
690
691 $targetcontext = false;
692
693 // Depending of $contextlevel, we perform different actions
694 switch ($contextlevel) {
695 // For system is easy, the best context is the system context
696 case CONTEXT_SYSTEM:
697 $targetcontext = get_context_instance(CONTEXT_SYSTEM);
698 break;
699
700 // For coursecat, we are going to look for stamps in all the
701 // course categories between CONTEXT_SYSTEM and CONTEXT_COURSE
702 // (i.e. in all the course categories in the path)
703 //
704 // And only will return one "best" target context if all the
705 // matches belong to ONE and ONLY ONE context. If multiple
706 // matches are found, that means that there is some annoying
707 // qbank "fragmentation" in the categories, so we'll fallback
708 // to create the qbank at course level
709 case CONTEXT_COURSECAT:
710 // Build the array of stamps we are going to match
711 $stamps = array();
712 foreach ($categories as $category) {
713 $stamps[] = $category->stamp;
714 }
715 $contexts = array();
716 // Build the array of contexts we are going to look
717 $systemctx = get_context_instance(CONTEXT_SYSTEM);
718 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid);
719 $parentctxs= get_parent_contexts($coursectx);
720 foreach ($parentctxs as $parentctx) {
721 // Exclude system context
722 if ($parentctx == $systemctx->id) {
723 continue;
724 }
725 $contexts[] = $parentctx;
726 }
727 if (!empty($stamps) && !empty($contexts)) {
728 // Prepare the query
729 list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps);
730 list($context_sql, $context_params) = $DB->get_in_or_equal($contexts);
731 $sql = "SELECT contextid
732 FROM {question_categories}
733 WHERE stamp $stamp_sql
734 AND contextid $context_sql";
735 $params = array_merge($stamp_params, $context_params);
736 $matchingcontexts = $DB->get_records_sql($sql, $params);
737 // Only if ONE and ONLY ONE context is found, use it as valid target
738 if (count($matchingcontexts) == 1) {
739 $targetcontext = get_context_instance_by_id(reset($matchingcontexts)->contextid);
740 }
741 }
742 break;
743
744 // For course is easy, the best context is the course context
745 case CONTEXT_COURSE:
746 $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid);
747 break;
748
749 // For module is easy, there is not best context, as far as the
750 // activity hasn't been created yet. So we return context course
751 // for them, so permission checks and friends will work. Note this
752 // case is handled by {@link prechek_precheck_qbanks_by_level}
753 // in an special way
754 case CONTEXT_MODULE:
755 $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid);
756 break;
757 }
758 return $targetcontext;
759 }
760
761 /**
762 * Return one array of question records for
763 * a given restore operation and one question category
764 */
765 public static function restore_get_questions($restoreid, $qcatid) {
766 global $DB;
767
768 $results = array();
769 $qs = $DB->get_records_sql("SELECT itemid
770 FROM {backup_ids_temp}
771 WHERE backupid = ?
772 AND itemname = 'question'
773 AND parentitemid = ?", array($restoreid, $qcatid));
774 foreach ($qs as $q) {
775 $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid);
776 $results[$q->itemid] = $temprec->info;
777 }
778 return $results;
779 }
780
2df0f295
EL
781 /**
782 * Given one component/filearea/context and
783 * optionally one source itemname to match itemids
784 * put the corresponding files in the pool
785 */
41941110 786 public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
2df0f295
EL
787 global $DB;
788
41941110
EL
789 if ($forcenewcontextid) {
790 // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings,
791 // with questions originally at system/coursecat context in source being restored to course context in target). So we need
792 // to be able to force the new contextid
793 $newcontextid = $forcenewcontextid;
794 } else {
795 // Get new context, must exist or this will fail
796 if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) {
797 throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid);
798 }
799 }
800
801 // Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid
802 // columns (because we have used them to store other information). This happens usually with
803 // all the question related backup_ids_temp records. In that case, it's safe to ignore that
804 // matching as far as we are always restoring for well known oldcontexts and olditemids
805 $parentitemctxmatchsql = ' AND i.parentitemid = f.contextid ';
806 if ($skipparentitemidctxmatch) {
807 $parentitemctxmatchsql = '';
2df0f295
EL
808 }
809
b212f87e 810 // Important: remember how files have been loaded to backup_files_temp
2df0f295
EL
811 // - info: contains the whole original object (times, names...)
812 // (all them being original ids as loaded from xml)
813
814 // itemname = null, we are going to match only by context, no need to use itemid (all them are 0)
815 if ($itemname == null) {
70c1ad58 816 $sql = 'SELECT contextid, component, filearea, itemid, itemid AS newitemid, info
b8bb45b0 817 FROM {backup_files_temp}
2df0f295 818 WHERE backupid = ?
b8bb45b0
EL
819 AND contextid = ?
820 AND component = ?
821 AND filearea = ?';
822 $params = array($restoreid, $oldcontextid, $component, $filearea);
2df0f295 823
b8bb45b0 824 // itemname not null, going to join with backup_ids to perform the old-new mapping of itemids
2df0f295 825 } else {
41941110 826 $sql = "SELECT f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info
b8bb45b0 827 FROM {backup_files_temp} f
2df0f295 828 JOIN {backup_ids_temp} i ON i.backupid = f.backupid
41941110 829 $parentitemctxmatchsql
b8bb45b0 830 AND i.itemid = f.itemid
2df0f295 831 WHERE f.backupid = ?
b8bb45b0
EL
832 AND f.contextid = ?
833 AND f.component = ?
834 AND f.filearea = ?
41941110 835 AND i.itemname = ?";
b8bb45b0 836 $params = array($restoreid, $oldcontextid, $component, $filearea, $itemname);
f2745cbe
EL
837 if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname
838 $sql .= ' AND i.itemid = ?';
839 $params[] = $olditemid;
840 }
2df0f295
EL
841 }
842
2df0f295
EL
843 $fs = get_file_storage(); // Get moodle file storage
844 $basepath = $basepath . '/files/';// Get backup file pool base
71a50b13 845 $rs = $DB->get_recordset_sql($sql, $params);
2df0f295 846 foreach ($rs as $rec) {
b8bb45b0 847 $file = (object)unserialize(base64_decode($rec->info));
2df0f295
EL
848 // ignore root dirs (they are created automatically)
849 if ($file->filepath == '/' && $file->filename == '.') {
850 continue;
851 }
84cdf7de
EL
852 // set the best possible user
853 $mappeduser = self::get_backup_ids_record($restoreid, 'user', $file->userid);
854 $file->userid = !empty($mappeduser) ? $mappeduser->newitemid : $dfltuserid;
2df0f295
EL
855 // dir found (and not root one), let's create if
856 if ($file->filename == '.') {
84cdf7de 857 $fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->userid);
2df0f295
EL
858 continue;
859 }
860 // arrived here, file found
861 // Find file in backup pool
2b199e7c 862 $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash);
2df0f295
EL
863 if (!file_exists($backuppath)) {
864 throw new restore_dbops_exception('file_not_found_in_pool', $file);
865 }
866 if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
867 $file_record = array(
868 'contextid' => $newcontextid,
869 'component' => $component,
870 'filearea' => $filearea,
871 'itemid' => $rec->newitemid,
872 'filepath' => $file->filepath,
873 'filename' => $file->filename,
874 'timecreated' => $file->timecreated,
875 'timemodified'=> $file->timemodified,
84cdf7de 876 'userid' => $file->userid,
2df0f295 877 'author' => $file->author,
84cdf7de
EL
878 'license' => $file->license,
879 'sortorder' => $file->sortorder);
2df0f295
EL
880 $fs->create_file_from_pathname($file_record, $backuppath);
881 }
882 }
883 $rs->close();
884 }
885
482aac65
EL
886 /**
887 * Given one restoreid, create in DB all the users present
888 * in backup_ids having newitemid = 0, as far as
889 * precheck_included_users() have left them there
890 * ready to be created. Also, annotate their newids
891 * once created for later reference
892 */
ac6dc09c 893 public static function create_included_users($basepath, $restoreid, $userid) {
482aac65
EL
894 global $CFG, $DB;
895
896 $authcache = array(); // Cache to get some bits from authentication plugins
897 $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search later
898 $themes = get_list_of_themes(); // Get themes for quick search later
899
900 // Iterate over all the included users with newitemid = 0, have to create them
901 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid');
902 foreach ($rs as $recuser) {
903 $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
904
905 // if user lang doesn't exist here, use site default
906 if (!array_key_exists($user->lang, $languages)) {
907 $user->lang = $CFG->lang;
908 }
909
910 // if user theme isn't available on target site or they are disabled, reset theme
911 if (!empty($user->theme)) {
912 if (empty($CFG->allowuserthemes) || !in_array($user->theme, $themes)) {
913 $user->theme = '';
914 }
915 }
916
917 // if user to be created has mnet auth and its mnethostid is $CFG->mnet_localhost_id
918 // that's 100% impossible as own server cannot be accesed over mnet. Change auth to email/manual
919 if ($user->auth == 'mnet' && $user->mnethostid == $CFG->mnet_localhost_id) {
920 // Respect registerauth
921 if ($CFG->registerauth == 'email') {
922 $user->auth = 'email';
923 } else {
924 $user->auth = 'manual';
925 }
926 }
927 unset($user->mnethosturl); // Not needed anymore
928
929 // Disable pictures based on global setting
930 if (!empty($CFG->disableuserimages)) {
931 $user->picture = 0;
932 }
933
934 // We need to analyse the AUTH field to recode it:
935 // - if the auth isn't enabled in target site, $CFG->registerauth will decide
936 // - finally, if the auth resulting isn't enabled, default to 'manual'
937 if (!is_enabled_auth($user->auth)) {
938 if ($CFG->registerauth == 'email') {
939 $user->auth = 'email';
940 } else {
941 $user->auth = 'manual';
942 }
943 }
944 if (!is_enabled_auth($user->auth)) { // Final auth check verify, default to manual if not enabled
945 $user->auth = 'manual';
946 }
947
948 // Now that we know the auth method, for users to be created without pass
949 // if password handling is internal and reset password is available
950 // we set the password to "restored" (plain text), so the login process
951 // will know how to handle that situation in order to allow the user to
952 // recover the password. MDL-20846
953 if (empty($user->password)) { // Only if restore comes without password
954 if (!array_key_exists($user->auth, $authcache)) { // Not in cache
955 $userauth = new stdClass();
956 $authplugin = get_auth_plugin($user->auth);
957 $userauth->preventpassindb = $authplugin->prevent_local_passwords();
958 $userauth->isinternal = $authplugin->is_internal();
959 $userauth->canresetpwd = $authplugin->can_reset_password();
960 $authcache[$user->auth] = $userauth;
961 } else {
962 $userauth = $authcache[$user->auth]; // Get from cache
963 }
964
965 // Most external plugins do not store passwords locally
966 if (!empty($userauth->preventpassindb)) {
967 $user->password = 'not cached';
968
969 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark
970 } else if ($userauth->isinternal and $userauth->canresetpwd) {
971 $user->password = 'restored';
972 }
973 }
974
975 // Creating new user, we must reset the policyagreed always
976 $user->policyagreed = 0;
977
978 // Set time created if empty
979 if (empty($user->timecreated)) {
980 $user->timecreated = time();
981 }
982
983 // Done, let's create the user and annotate its id
984 $newuserid = $DB->insert_record('user', $user);
985 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $newuserid);
986 // Let's create the user context and annotate it (we need it for sure at least for files)
1acad8ee
EL
987 // but for deleted users that don't have a context anymore (MDL-30192). We are done for them
988 // and nothing else (custom fields, prefs, tags, files...) will be created.
989 if (empty($user->deleted)) {
990 $newuserctxid = $user->deleted ? 0 : get_context_instance(CONTEXT_USER, $newuserid)->id;
991 self::set_backup_ids_record($restoreid, 'context', $recuser->parentitemid, $newuserctxid);
992
993 // Process custom fields
994 if (isset($user->custom_fields)) { // if present in backup
995 foreach($user->custom_fields['custom_field'] as $udata) {
996 $udata = (object)$udata;
997 // If the profile field has data and the profile shortname-datatype is defined in server
998 if ($udata->field_data) {
999 if ($field = $DB->get_record('user_info_field', array('shortname'=>$udata->field_name, 'datatype'=>$udata->field_type))) {
1000 /// Insert the user_custom_profile_field
1001 $rec = new stdClass();
1002 $rec->userid = $newuserid;
1003 $rec->fieldid = $field->id;
1004 $rec->data = $udata->field_data;
1005 $DB->insert_record('user_info_data', $rec);
1006 }
482aac65
EL
1007 }
1008 }
1009 }
482aac65 1010
1acad8ee
EL
1011 // Process tags
1012 if (!empty($CFG->usetags) && isset($user->tags)) { // if enabled in server and present in backup
1013 $tags = array();
1014 foreach($user->tags['tag'] as $usertag) {
1015 $usertag = (object)$usertag;
1016 $tags[] = $usertag->rawname;
1017 }
1018 tag_set('user', $newuserid, $tags);
482aac65 1019 }
482aac65 1020
1acad8ee
EL
1021 // Process preferences
1022 if (isset($user->preferences)) { // if present in backup
1023 foreach($user->preferences['preference'] as $preference) {
1024 $preference = (object)$preference;
1025 // Prepare the record and insert it
1026 $preference->userid = $newuserid;
1027 $status = $DB->insert_record('user_preferences', $preference);
1028 }
482aac65 1029 }
482aac65 1030
1acad8ee
EL
1031 // Create user files in pool (profile, icon, private) by context
1032 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'icon', $recuser->parentitemid, $userid);
1033 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'profile', $recuser->parentitemid, $userid);
1acad8ee 1034 }
482aac65
EL
1035 }
1036 $rs->close();
1037 }
1038
1039 /**
1040 * Given one user object (from backup file), perform all the neccesary
1041 * checks is order to decide how that user will be handled on restore.
1042 *
1043 * Note the function requires $user->mnethostid to be already calculated
1044 * so it's caller responsibility to set it
1045 *
1046 * This function is used both by @restore_precheck_users() and
1047 * @restore_create_users() to get consistent results in both places
1048 *
1049 * It returns:
1050 * - one user object (from DB), if match has been found and user will be remapped
1051 * - boolean true if the user needs to be created
1052 * - boolean false if some conflict happened and the user cannot be handled
1053 *
1054 * Each test is responsible for returning its results and interrupt
1055 * execution. At the end, boolean true (user needs to be created) will be
1056 * returned if no test has interrupted that.
1057 *
1058 * Here it's the logic applied, keep it updated:
1059 *
1060 * If restoring users from same site backup:
1061 * 1A - Normal check: If match by id and username and mnethost => ok, return target user
1062 * 1B - Handle users deleted in DB and "alive" in backup file:
1063 * If match by id and mnethost and user is deleted in DB and
1064 * (match by username LIKE 'backup_email.%' or by non empty email = md5(username)) => ok, return target user
1065 * 1C - Handle users deleted in backup file and "alive" in DB:
1066 * If match by id and mnethost and user is deleted in backup file
1067 * and match by email = email_without_time(backup_email) => ok, return target user
1068 * 1D - Conflict: If match by username and mnethost and doesn't match by id => conflict, return false
1069 * 1E - None of the above, return true => User needs to be created
1070 *
1071 * if restoring from another site backup (cannot match by id here, replace it by email/firstaccess combination):
1072 * 2A - Normal check: If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user
1073 * 2B - Handle users deleted in DB and "alive" in backup file:
1074 * 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
1075 * (username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
1076 * 2B2 - If match by mnethost and user is deleted in DB and
1077 * username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
1078 * (to cover situations were md5(username) wasn't implemented on delete we requiere both)
1079 * 2C - Handle users deleted in backup file and "alive" in DB:
1080 * If match mnethost and user is deleted in backup file
1081 * and by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
1082 * 2D - Conflict: If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
1083 * 2E - None of the above, return true => User needs to be created
1084 *
1085 * Note: for DB deleted users email is stored in username field, hence we
1086 * are looking there for emails. See delete_user()
1087 * Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1088 * hence we are looking there for usernames if not empty. See delete_user()
1089 */
1090 protected static function precheck_user($user, $samesite) {
1091 global $CFG, $DB;
1092
1093 // Handle checks from same site backups
1094 if ($samesite && empty($CFG->forcedifferentsitecheckingusersonrestore)) {
1095
1096 // 1A - If match by id and username and mnethost => ok, return target user
1097 if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
1098 return $rec; // Matching user found, return it
1099 }
1100
1101 // 1B - Handle users deleted in DB and "alive" in backup file
1102 // Note: for DB deleted users email is stored in username field, hence we
1103 // are looking there for emails. See delete_user()
1104 // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1105 // hence we are looking there for usernames if not empty. See delete_user()
1106 // If match by id and mnethost and user is deleted in DB and
1107 // match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
1108 if ($rec = $DB->get_record_sql("SELECT *
1109 FROM {user} u
1110 WHERE id = ?
1111 AND mnethostid = ?
1112 AND deleted = 1
1113 AND (
ef6b3ba1 1114 UPPER(username) LIKE UPPER(?)
482aac65
EL
1115 OR (
1116 ".$DB->sql_isnotempty('user', 'email', false, false)."
1117 AND email = ?
1118 )
1119 )",
1120 array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
1121 return $rec; // Matching user, deleted in DB found, return it
1122 }
1123
1124 // 1C - Handle users deleted in backup file and "alive" in DB
1125 // If match by id and mnethost and user is deleted in backup file
1126 // and match by email = email_without_time(backup_email) => ok, return target user
1127 if ($user->deleted) {
1128 // Note: for DB deleted users email is stored in username field, hence we
1129 // are looking there for emails. See delete_user()
1130 // Trim time() from email
1131 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
1132 if ($rec = $DB->get_record_sql("SELECT *
1133 FROM {user} u
1134 WHERE id = ?
1135 AND mnethostid = ?
ef6b3ba1 1136 AND UPPER(email) = UPPER(?)",
482aac65
EL
1137 array($user->id, $user->mnethostid, $trimemail))) {
1138 return $rec; // Matching user, deleted in backup file found, return it
1139 }
1140 }
1141
1142 // 1D - If match by username and mnethost and doesn't match by id => conflict, return false
1143 if ($rec = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
1144 if ($user->id != $rec->id) {
1145 return false; // Conflict, username already exists and belongs to another id
1146 }
1147 }
1148
1149 // Handle checks from different site backups
1150 } else {
1151
1152 // 2A - If match by username and mnethost and
1153 // (email or non-zero firstaccess) => ok, return target user
1154 if ($rec = $DB->get_record_sql("SELECT *
1155 FROM {user} u
1156 WHERE username = ?
1157 AND mnethostid = ?
1158 AND (
ef6b3ba1 1159 UPPER(email) = UPPER(?)
482aac65
EL
1160 OR (
1161 firstaccess != 0
1162 AND firstaccess = ?
1163 )
1164 )",
1165 array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
1166 return $rec; // Matching user found, return it
1167 }
1168
1169 // 2B - Handle users deleted in DB and "alive" in backup file
1170 // Note: for DB deleted users email is stored in username field, hence we
1171 // are looking there for emails. See delete_user()
1172 // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1173 // hence we are looking there for usernames if not empty. See delete_user()
1174 // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
1175 // (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
1176 if ($rec = $DB->get_record_sql("SELECT *
1177 FROM {user} u
1178 WHERE mnethostid = ?
1179 AND deleted = 1
1180 AND ".$DB->sql_isnotempty('user', 'email', false, false)."
1181 AND email = ?
1182 AND (
ef6b3ba1 1183 UPPER(username) LIKE UPPER(?)
482aac65
EL
1184 OR (
1185 firstaccess != 0
1186 AND firstaccess = ?
1187 )
1188 )",
1189 array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
1190 return $rec; // Matching user found, return it
1191 }
1192
1193 // 2B2 - If match by mnethost and user is deleted in DB and
1194 // username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
1195 // (this covers situations where md5(username) wasn't being stored so we require both
1196 // the email & non-zero firstaccess to match)
1197 if ($rec = $DB->get_record_sql("SELECT *
1198 FROM {user} u
1199 WHERE mnethostid = ?
1200 AND deleted = 1
ef6b3ba1 1201 AND UPPER(username) LIKE UPPER(?)
482aac65
EL
1202 AND firstaccess != 0
1203 AND firstaccess = ?",
1204 array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
1205 return $rec; // Matching user found, return it
1206 }
1207
1208 // 2C - Handle users deleted in backup file and "alive" in DB
1209 // If match mnethost and user is deleted in backup file
1210 // and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
1211 if ($user->deleted) {
1212 // Note: for DB deleted users email is stored in username field, hence we
1213 // are looking there for emails. See delete_user()
1214 // Trim time() from email
1215 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
1216 if ($rec = $DB->get_record_sql("SELECT *
1217 FROM {user} u
1218 WHERE mnethostid = ?
ef6b3ba1 1219 AND UPPER(email) = UPPER(?)
482aac65
EL
1220 AND firstaccess != 0
1221 AND firstaccess = ?",
1222 array($user->mnethostid, $trimemail, $user->firstaccess))) {
1223 return $rec; // Matching user, deleted in backup file found, return it
1224 }
1225 }
1226
1227 // 2D - If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
1228 if ($rec = $DB->get_record_sql("SELECT *
1229 FROM {user} u
1230 WHERE username = ?
1231 AND mnethostid = ?
1232 AND NOT (
ef6b3ba1 1233 UPPER(email) = UPPER(?)
482aac65
EL
1234 OR (
1235 firstaccess != 0
1236 AND firstaccess = ?
1237 )
1238 )",
1239 array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
1240 return false; // Conflict, username/mnethostid already exist and belong to another user (by email/firstaccess)
1241 }
1242 }
1243
1244 // Arrived here, return true as the user will need to be created and no
1245 // conflicts have been found in the logic above. This covers:
1246 // 1E - else => user needs to be created, return true
1247 // 2E - else => user needs to be created, return true
1248 return true;
1249 }
1250
1251 /**
1252 * Check all the included users, deciding the action to perform
1253 * for each one (mapping / creation) and returning one array
1254 * of problems in case something is wrong (lack of permissions,
1255 * conficts)
1256 */
1257 public static function precheck_included_users($restoreid, $courseid, $userid, $samesite) {
1258 global $CFG, $DB;
1259
1260 // To return any problem found
1261 $problems = array();
1262
1263 // We are going to map mnethostid, so load all the available ones
1264 $mnethosts = $DB->get_records('mnet_host', array(), 'wwwroot', 'wwwroot, id');
1265
1266 // Calculate the context we are going to use for capability checking
1267 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1268
1269 // Calculate if we have perms to create users, by checking:
1270 // to 'moodle/restore:createuser' and 'moodle/restore:userinfo'
1271 // and also observe $CFG->disableusercreationonrestore
1272 $cancreateuser = false;
1273 if (has_capability('moodle/restore:createuser', $context, $userid) and
1274 has_capability('moodle/restore:userinfo', $context, $userid) and
1275 empty($CFG->disableusercreationonrestore)) { // Can create users
1276
1277 $cancreateuser = true;
1278 }
1279
1280 // Iterate over all the included users
1281 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid');
1282 foreach ($rs as $recuser) {
1283 $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info;
1284
1285 // Find the correct mnethostid for user before performing any further check
1286 if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
1287 $user->mnethostid = $CFG->mnet_localhost_id;
1288 } else {
1289 // fast url-to-id lookups
1290 if (isset($mnethosts[$user->mnethosturl])) {
1291 $user->mnethostid = $mnethosts[$user->mnethosturl]->id;
1292 } else {
1293 $user->mnethostid = $CFG->mnet_localhost_id;
1294 }
1295 }
1296
1297 // Now, precheck that user and, based on returned results, annotate action/problem
1298 $usercheck = self::precheck_user($user, $samesite);
1299
1300 if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to
1301 // Annotate it, for later process. Set newitemid to mapping user->id
1302 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $usercheck->id);
1303
1304 } else if ($usercheck === false) { // Found conflict, report it as problem
1305 $problems[] = get_string('restoreuserconflict', '', $user->username);
1306
1307 } else if ($usercheck === true) { // User needs to be created, check if we are able
1308 if ($cancreateuser) { // Can create user, set newitemid to 0 so will be created later
1309 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user);
1310
1311 } else { // Cannot create user, report it as problem
1312 $problems[] = get_string('restorecannotcreateuser', '', $user->username);
1313 }
1314
1315 } else { // Shouldn't arrive here ever, something is for sure wrong. Exception
1316 throw new restore_dbops_exception('restore_error_processing_user', $user->username);
1317 }
1318 }
1319 $rs->close();
1320 return $problems;
1321 }
1322
1323 /**
41941110
EL
1324 * Process the needed users in order to decide
1325 * which action to perform with them (create/map)
482aac65
EL
1326 *
1327 * Just wrap over precheck_included_users(), returning
41941110 1328 * exception if any problem is found
482aac65
EL
1329 */
1330 public static function process_included_users($restoreid, $courseid, $userid, $samesite) {
1331 global $DB;
1332
1333 // Just let precheck_included_users() to do all the hard work
1334 $problems = self::precheck_included_users($restoreid, $courseid, $userid, $samesite);
1335
1336 // With problems, throw exception, shouldn't happen if prechecks were originally
1337 // executed, so be radical here.
1338 if (!empty($problems)) {
1339 throw new restore_dbops_exception('restore_problems_processing_users', null, implode(', ', $problems));
1340 }
482aac65
EL
1341 }
1342
41941110
EL
1343 /**
1344 * Process the needed question categories and questions
1345 * to check all them, deciding about the action to perform
1346 * (create/map) and target.
1347 *
1348 * Just wrap over precheck_categories_and_questions(), returning
1349 * exception if any problem is found
1350 */
1351 public static function process_categories_and_questions($restoreid, $courseid, $userid, $samesite) {
1352 global $DB;
1353
1354 // Just let precheck_included_users() to do all the hard work
1355 $problems = self::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite);
1356
1357 // With problems of type error, throw exception, shouldn't happen if prechecks were originally
1358 // executed, so be radical here.
1359 if (array_key_exists('errors', $problems)) {
ede7105f 1360 throw new restore_dbops_exception('restore_problems_processing_questions', null, implode(', ', $problems['errors']));
41941110
EL
1361 }
1362 }
1363
b8bb45b0
EL
1364 public static function set_backup_files_record($restoreid, $filerec) {
1365 global $DB;
1366
1367 $filerec->info = base64_encode(serialize($filerec)); // Serialize the whole rec in info
1368 $filerec->backupid = $restoreid;
1369 $DB->insert_record('backup_files_temp', $filerec);
1370 }
1371
482aac65 1372 public static function set_backup_ids_record($restoreid, $itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) {
482aac65
EL
1373 // Build conditionally the extra record info
1374 $extrarecord = array();
1375 if ($newitemid != 0) {
1376 $extrarecord['newitemid'] = $newitemid;
1377 }
1378 if ($parentitemid != null) {
1379 $extrarecord['parentitemid'] = $parentitemid;
1380 }
1381 if ($info != null) {
1382 $extrarecord['info'] = base64_encode(serialize($info));
1383 }
1384
0c99b722 1385 self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
482aac65
EL
1386 }
1387
1388 public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
0c99b722 1389 $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
482aac65 1390
0c99b722
TL
1391 if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
1392 $dbrec->info = unserialize(base64_decode($dbrec->info));
482aac65 1393 }
0c99b722 1394
482aac65
EL
1395 return $dbrec;
1396 }
4bca307a
EL
1397
1398 /**
1399 * Given on courseid, fullname and shortname, calculate the correct fullname/shortname to avoid dupes
1400 */
1401 public static function calculate_course_names($courseid, $fullname, $shortname) {
1402 global $CFG, $DB;
1403
1404 $currentfullname = '';
1405 $currentshortname = '';
1406 $counter = 0;
1407 // Iteratere while the name exists
1408 do {
1409 if ($counter) {
1410 $suffixfull = ' ' . get_string('copyasnoun') . ' ' . $counter;
1411 $suffixshort = '_' . $counter;
1412 } else {
1413 $suffixfull = '';
1414 $suffixshort = '';
1415 }
1416 $currentfullname = $fullname.$suffixfull;
1417 $currentshortname = substr($shortname, 0, 100 - strlen($suffixshort)).$suffixshort; // < 100cc
1418 $coursefull = $DB->get_record_select('course', 'fullname = ? AND id != ?', array($currentfullname, $courseid));
1419 $courseshort = $DB->get_record_select('course', 'shortname = ? AND id != ?', array($currentshortname, $courseid));
1420 $counter++;
1421 } while ($coursefull || $courseshort);
1422
1423 // Return results
1424 return array($currentfullname, $currentshortname);
1425 }
1426
1427 /**
1428 * For the target course context, put as many custom role names as possible
1429 */
1430 public static function set_course_role_names($restoreid, $courseid) {
1431 global $DB;
1432
1433 // Get the course context
1434 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid);
1435 // Get all the mapped roles we have
1436 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid');
1437 foreach ($rs as $recrole) {
1438 // Get the complete temp_ids record
1439 $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid);
024c288d
EL
1440 // If it's one mapped role and we have one name for it
1441 if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) {
4bca307a
EL
1442 // If role name doesn't exist, add it
1443 $rolename = new stdclass();
1444 $rolename->roleid = $role->newitemid;
1445 $rolename->contextid = $coursectx->id;
1446 if (!$DB->record_exists('role_names', (array)$rolename)) {
1447 $rolename->name = $role->info['nameincourse'];
1448 $DB->insert_record('role_names', $rolename);
1449 }
1450 }
1451 }
1452 $rs->close();
1453 }
785d6603 1454
3acc9b81
SH
1455 /**
1456 * Creates a skeleton record within the database using the passed parameters
1457 * and returns the new course id.
1458 *
1459 * @global moodle_database $DB
1460 * @param string $fullname
1461 * @param string $shortname
1462 * @param int $categoryid
1463 * @return int The new course id
1464 */
785d6603
SH
1465 public static function create_new_course($fullname, $shortname, $categoryid) {
1466 global $DB;
1467 $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST);
1468
1469 $course = new stdClass;
1470 $course->fullname = $fullname;
1471 $course->shortname = $shortname;
1472 $course->category = $category->id;
1473 $course->sortorder = 0;
1474 $course->timecreated = time();
1475 $course->timemodified = $course->timecreated;
ed6a14eb
AB
1476 // forcing skeleton courses to be hidden instead of going by $category->visible , until MDL-27790 is resolved.
1477 $course->visible = 0;
785d6603 1478
593fc4a9
SH
1479 $courseid = $DB->insert_record('course', $course);
1480
1481 $category->coursecount++;
1482 $DB->update_record('course_categories', $category);
1483
1484 return $courseid;
785d6603
SH
1485 }
1486
3acc9b81
SH
1487 /**
1488 * Deletes all of the content associated with the given course (courseid)
1489 * @param int $courseid
d53e3298 1490 * @param array $options
3acc9b81
SH
1491 * @return bool True for success
1492 */
d53e3298
PS
1493 public static function delete_course_content($courseid, array $options = null) {
1494 return remove_course_contents($courseid, false, $options);
785d6603 1495 }
482aac65 1496}
fbc2778d
EL
1497
1498/*
1499 * Exception class used by all the @dbops stuff
1500 */
1501class restore_dbops_exception extends backup_exception {
1502
1503 public function __construct($errorcode, $a=NULL, $debuginfo=null) {
1504 parent::__construct($errorcode, 'error', '', $a, null, $debuginfo);
1505 }
1506}