0e5fabc7b70763a67805428fd4b1790f8ed04080
[moodle.git] / backup / util / dbops / restore_dbops.class.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * @package    moodlecore
20  * @subpackage backup-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  */
25 /**
26  * Base abstract class for all the helper classes providing DB operations
27  *
28  * TODO: Finish phpdocs
29  */
30 abstract class restore_dbops {
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;
64     /**
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)
68      */
69     public static function get_included_tasks($restoreid) {
70         $rc = restore_controller_dbops::load_controller($restoreid);
71         $tasks = $rc->get_plan()->get_tasks();
72         $includedtasks = array();
73         foreach ($tasks as $key => $task) {
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) {
78                 if (!$task->get_setting_value('blocks')) { // Blocks not included, continue
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;
85                 } else { // Look for activity_included setting
86                     $included = $task->get_setting_value($parent . '_included');
87                 }
89             // ativities, based on included setting
90             } else if ($task instanceof restore_activity_task) {
91                 $included = $task->get_setting_value('included');
93             // sections, based on included setting
94             } else if ($task instanceof restore_section_task) {
95                 $included = $task->get_setting_value('included');
97             // course always included if present
98             } else if ($task instanceof restore_course_task) {
99                 $included = true;
100             }
102             // If included, add it
103             if ($included) {
104                 $includedtasks[] = $task;
105             }
106         }
107         return $includedtasks;
108     }
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) {
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     }
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) {
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     }
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)
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
148      */
149     public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
150         global $DB;
152         $problems = array(); // To store warnings/errors
154         // Get loaded roles from backup_ids
155         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info');
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);
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)backup_controller_dbops::decode_backup_temp_info($recrole->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                 $role->targetroleid = $match;
174                 $rolemappings->mappings[$recrole->itemid] = $role;
175                 // Prepare warning if no match found
176                 if (!$match) {
177                     $problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name);
178                 }
179             }
180         }
181         $rs->close();
182         return $problems;
183     }
185     /**
186      * Return cached backup id's
187      *
188      * @param int $restoreid id of backup
189      * @param string $itemname name of the item
190      * @param int $itemid id of item
191      * @return array backup id's
192      * @todo MDL-25290 replace static backupids* with MUC code
193      */
194     protected static function get_backup_ids_cached($restoreid, $itemname, $itemid) {
195         global $DB;
197         $key = "$itemid $itemname $restoreid";
199         // If record exists in cache then return.
200         if (isset(self::$backupidsexist[$key]) && isset(self::$backupidscache[$key])) {
201             // Return a copy of cached data, to avoid any alterations in cached data.
202             return clone self::$backupidscache[$key];
203         }
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;
214         }
216         // Retrive record from database.
217         $record = array(
218             'backupid' => $restoreid,
219             'itemname' => $itemname,
220             'itemid'   => $itemid
221         );
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 $dbrec;
228         } else {
229             return false;
230         }
231     }
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      */
243     protected static function set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord) {
244         global $DB;
246         $key = "$itemid $itemname $restoreid";
248         $record = array(
249             'backupid' => $restoreid,
250             'itemname' => $itemname,
251             'itemid'   => $itemid,
252         );
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                 }
276             }
277         } else {
278             self::update_backup_cached_record($record, $extrarecord, $key);
279         }
280     }
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);
306                 }
307                 $record = array_merge((array)self::$backupidscache[$key], $extrarecord);
308                 self::$backupidscache[$key] = (object) $record;
309                 self::$backupidscachesize--;
310             }
311         }
312     }
314     /**
315      * Reset the ids caches completely
316      *
317      * Any destructive operation (partial delete, truncate, drop or recreate) performed
318      * with the backup_ids table must cause the backup_ids caches to be
319      * invalidated by calling this method. See MDL-33630.
320      *
321      * Note that right now, the only operation of that type is the recreation
322      * (drop & restore) of the table that may happen once the prechecks have ended. All
323      * the rest of operations are always routed via {@link set_backup_ids_record()}, 1 by 1,
324      * keeping the caches on sync.
325      *
326      * @todo MDL-25290 static should be replaced with MUC code.
327      */
328     public static function reset_backup_ids_cached() {
329         // Reset the ids cache.
330         $cachetoadd = count(self::$backupidscache);
331         self::$backupidscache = array();
332         self::$backupidscachesize = self::$backupidscachesize + $cachetoadd;
333         // Reset the exists cache.
334         $existstoadd = count(self::$backupidsexist);
335         self::$backupidsexist = array();
336         self::$backupidsexistsize = self::$backupidsexistsize + $existstoadd;
337     }
339     /**
340      * Given one role, as loaded from XML, perform the best possible matching against the assignable
341      * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid)
342      * returning the id of the best matching role or 0 if no match is found
343      */
344     protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) {
345         global $CFG, $DB;
347         // Gather various information about roles
348         $coursectx = context_course::instance($courseid);
349         $assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid);
351         // Note: under 1.9 we had one function restore_samerole() that performed one complete
352         // matching of roles (all caps) and if match was found the mapping was availabe bypassing
353         // any assignable_roles() security. IMO that was wrong and we must not allow such
354         // mappings anymore. So we have left that matching strategy out in 2.0
356         // Empty assignable roles, mean no match possible
357         if (empty($assignablerolesshortname)) {
358             return 0;
359         }
361         // Match by shortname
362         if ($match = array_search($role->shortname, $assignablerolesshortname)) {
363             return $match;
364         }
366         // Match by archetype
367         list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname));
368         $params = array_merge(array($role->archetype), $in_params);
369         if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) {
370             return $rec->id;
371         }
373         // Match editingteacher to teacher (happens a lot, from 1.9)
374         if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) {
375             return array_search('teacher', $assignablerolesshortname);
376         }
378         // No match, return 0
379         return 0;
380     }
383     /**
384      * Process the loaded roles, looking for their best mapping or skipping
385      * Any error will cause exception. Note this is one wrapper over
386      * precheck_included_roles, that contains all the logic, but returns
387      * errors/warnings instead and is executed as part of the restore prechecks
388      */
389      public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) {
390         global $DB;
392         // Just let precheck_included_roles() to do all the hard work
393         $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings);
395         // With problems of type error, throw exception, shouldn't happen if prechecks executed
396         if (array_key_exists('errors', $problems)) {
397             throw new restore_dbops_exception('restore_problems_processing_roles', null, implode(', ', $problems['errors']));
398         }
399     }
401     /**
402      * Load the needed users.xml file to backup_ids table for future reference
403      */
404     public static function load_users_to_tempids($restoreid, $usersfile) {
406         if (!file_exists($usersfile)) { // Shouldn't happen ever, but...
407             throw new backup_helper_exception('missing_users_xml_file', $usersfile);
408         }
409         // Let's parse, custom processor will do its work, sending info to DB
410         $xmlparser = new progressive_parser();
411         $xmlparser->set_file($usersfile);
412         $xmlprocessor = new restore_users_parser_processor($restoreid);
413         $xmlparser->set_processor($xmlprocessor);
414         $xmlparser->process();
415     }
417     /**
418      * Load the needed questions.xml file to backup_ids table for future reference
419      */
420     public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) {
422         if (!file_exists($questionsfile)) { // Shouldn't happen ever, but...
423             throw new backup_helper_exception('missing_questions_xml_file', $questionsfile);
424         }
425         // Let's parse, custom processor will do its work, sending info to DB
426         $xmlparser = new progressive_parser();
427         $xmlparser->set_file($questionsfile);
428         $xmlprocessor = new restore_questions_parser_processor($restoreid);
429         $xmlparser->set_processor($xmlprocessor);
430         $xmlparser->process();
431     }
433     /**
434      * Check all the included categories and questions, deciding the action to perform
435      * for each one (mapping / creation) and returning one array of problems in case
436      * something is wrong.
437      *
438      * There are some basic rules that the method below will always try to enforce:
439      *
440      * Rule1: Targets will be, always, calculated for *whole* question banks (a.k.a. contexid source),
441      *     so, given 2 question categories belonging to the same bank, their target bank will be
442      *     always the same. If not, we can be incurring into "fragmentation", leading to random/cloze
443      *     problems (qtypes having "child" questions).
444      *
445      * Rule2: The 'moodle/question:managecategory' and 'moodle/question:add' capabilities will be
446      *     checked before creating any category/question respectively and, if the cap is not allowed
447      *     into upper contexts (system, coursecat)) but in lower ones (course), the *whole* question bank
448      *     will be created there.
449      *
450      * Rule3: Coursecat question banks not existing in the target site will be created as course
451      *     (lower ctx) question banks, never as "guessed" coursecat question banks base on depth or so.
452      *
453      * Rule4: System question banks will be created at system context if user has perms to do so. Else they
454      *     will created as course (lower ctx) question banks (similary to rule3). In other words, course ctx
455      *     if always a fallback for system and coursecat question banks.
456      *
457      * Also, there are some notes to clarify the scope of this method:
458      *
459      * Note1: This method won't create any question category nor question at all. It simply will calculate
460      *     which actions (create/map) must be performed for each element and where, validating that all those
461      *     actions are doable by the user executing the restore operation. Any problem found will be
462      *     returned in the problems array, causing the restore process to stop with error.
463      *
464      * Note2: To decide if one question bank (all its question categories and questions) is going to be remapped,
465      *     then all the categories and questions must exist in the same target bank. If able to do so, missing
466      *     qcats and qs will be created (rule2). But if, at the end, something is missing, the whole question bank
467      *     will be recreated at course ctx (rule1), no matter if that duplicates some categories/questions.
468      *
469      * Note3: We'll be using the newitemid column in the temp_ids table to store the action to be performed
470      *     with each question category and question. newitemid = 0 means the qcat/q needs to be created and
471      *     any other value means the qcat/q is mapped. Also, for qcats, parentitemid will contain the target
472      *     context where the categories have to be created (but for module contexts where we'll keep the old
473      *     one until the activity is created)
474      *
475      * Note4: All these "actions" will be "executed" later by {@link restore_create_categories_and_questions}
476      */
477     public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) {
479         $problems = array();
481         // TODO: Check all qs, looking their qtypes are restorable
483         // Precheck all qcats and qs looking for target contexts / warnings / errors
484         list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM);
485         list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT);
486         list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE);
487         list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE);
489         // Acummulate and handle errors and warnings
490         $errors   = array_merge($syserr, $caterr, $couerr, $moderr);
491         $warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn);
492         if (!empty($errors)) {
493             $problems['errors'] = $errors;
494         }
495         if (!empty($warnings)) {
496             $problems['warnings'] = $warnings;
497         }
498         return $problems;
499     }
501     /**
502      * This function will process all the question banks present in restore
503      * at some contextlevel (from CONTEXT_SYSTEM to CONTEXT_MODULE), finding
504      * the target contexts where each bank will be restored and returning
505      * warnings/errors as needed.
506      *
507      * Some contextlevels (system, coursecat), will delegate process to
508      * course level if any problem is found (lack of permissions, non-matching
509      * target context...). Other contextlevels (course, module) will
510      * cause return error if some problem is found.
511      *
512      * At the end, if no errors were found, all the categories in backup_temp_ids
513      * will be pointing (parentitemid) to the target context where they must be
514      * created later in the restore process.
515      *
516      * Note: at the time these prechecks are executed, activities haven't been
517      * created yet so, for CONTEXT_MODULE banks, we keep the old contextid
518      * in the parentitemid field. Once the activity (and its context) has been
519      * created, we'll update that context in the required qcats
520      *
521      * Caller {@link precheck_categories_and_questions} will, simply, execute
522      * this function for all the contextlevels, acting as a simple controller
523      * of warnings and errors.
524      *
525      * The function returns 2 arrays, one containing errors and another containing
526      * warnings. Both empty if no errors/warnings are found.
527      */
528     public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
529         global $CFG, $DB;
531         // To return any errors and warnings found
532         $errors   = array();
533         $warnings = array();
535         // Specify which fallbacks must be performed
536         $fallbacks = array(
537             CONTEXT_SYSTEM => CONTEXT_COURSE,
538             CONTEXT_COURSECAT => CONTEXT_COURSE);
540         // For any contextlevel, follow this process logic:
541         //
542         // 0) Iterate over each context (qbank)
543         // 1) Iterate over each qcat in the context, matching by stamp for the found target context
544         //     2a) No match, check if user can create qcat and q
545         //         3a) User can, mark the qcat and all dependent qs to be created in that target context
546         //         3b) User cannot, check if we are in some contextlevel with fallback
547         //             4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
548         //             4b) No fallback, error. End qcat loop.
549         //     2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
550         //         5a) No match, check if user can add q
551         //             6a) User can, mark the q to be created
552         //             6b) User cannot, check if we are in some contextlevel with fallback
553         //                 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
554         //                 7b) No fallback, error. End qcat loop
555         //         5b) Match, mark q to be mapped
557         // Get all the contexts (question banks) in restore for the given contextlevel
558         $contexts = self::restore_get_question_banks($restoreid, $contextlevel);
560         // 0) Iterate over each context (qbank)
561         foreach ($contexts as $contextid => $contextlevel) {
562             // Init some perms
563             $canmanagecategory = false;
564             $canadd            = false;
565             // get categories in context (bank)
566             $categories = self::restore_get_question_categories($restoreid, $contextid);
567             // cache permissions if $targetcontext is found
568             if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) {
569                 $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid);
570                 $canadd            = has_capability('moodle/question:add', $targetcontext, $userid);
571             }
572             // 1) Iterate over each qcat in the context, matching by stamp for the found target context
573             foreach ($categories as $category) {
574                 $matchcat = false;
575                 if ($targetcontext) {
576                     $matchcat = $DB->get_record('question_categories', array(
577                                     'contextid' => $targetcontext->id,
578                                     'stamp' => $category->stamp));
579                 }
580                 // 2a) No match, check if user can create qcat and q
581                 if (!$matchcat) {
582                     // 3a) User can, mark the qcat and all dependent qs to be created in that target context
583                     if ($canmanagecategory && $canadd) {
584                         // Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where
585                         // we keep the source contextid unmodified (for easier matching later when the
586                         // activities are created)
587                         $parentitemid = $targetcontext->id;
588                         if ($contextlevel == CONTEXT_MODULE) {
589                             $parentitemid = null; // null means "not modify" a.k.a. leave original contextid
590                         }
591                         self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid);
592                         // Nothing else to mark, newitemid = 0 means create
594                     // 3b) User cannot, check if we are in some contextlevel with fallback
595                     } else {
596                         // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop
597                         if (array_key_exists($contextlevel, $fallbacks)) {
598                             foreach ($categories as $movedcat) {
599                                 $movedcat->contextlevel = $fallbacks[$contextlevel];
600                                 self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
601                                 // Warn about the performed fallback
602                                 $warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat);
603                             }
605                         // 4b) No fallback, error. End qcat loop.
606                         } else {
607                             $errors[] = get_string('qcategorycannotberestored', 'backup', $category);
608                         }
609                         break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank)
610                     }
612                 // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version
613                 } else {
614                     self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
615                     $questions = self::restore_get_questions($restoreid, $category->id);
617                     // Collect all the questions for this category into memory so we only talk to the DB once.
618                     $questioncache = $DB->get_records_sql_menu("SELECT ".$DB->sql_concat('stamp', "' '", 'version').", id
619                                                                   FROM {question}
620                                                                  WHERE category = ?", array($matchcat->id));
622                     foreach ($questions as $question) {
623                         if (isset($questioncache[$question->stamp." ".$question->version])) {
624                             $matchqid = $questioncache[$question->stamp." ".$question->version];
625                         } else {
626                             $matchqid = false;
627                         }
628                         // 5a) No match, check if user can add q
629                         if (!$matchqid) {
630                             // 6a) User can, mark the q to be created
631                             if ($canadd) {
632                                 // Nothing to mark, newitemid means create
634                              // 6b) User cannot, check if we are in some contextlevel with fallback
635                             } else {
636                                 // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo
637                                 if (array_key_exists($contextlevel, $fallbacks)) {
638                                     foreach ($categories as $movedcat) {
639                                         $movedcat->contextlevel = $fallbacks[$contextlevel];
640                                         self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat);
641                                         // Warn about the performed fallback
642                                         $warnings[] = get_string('question2coursefallback', 'backup', $movedcat);
643                                     }
645                                 // 7b) No fallback, error. End qcat loop
646                                 } else {
647                                     $errors[] = get_string('questioncannotberestored', 'backup', $question);
648                                 }
649                                 break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank)
650                             }
652                         // 5b) Match, mark q to be mapped
653                         } else {
654                             self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid);
655                         }
656                     }
657                 }
658             }
659         }
661         return array($errors, $warnings);
662     }
664     /**
665      * Return one array of contextid => contextlevel pairs
666      * of question banks to be checked for one given restore operation
667      * ordered from CONTEXT_SYSTEM downto CONTEXT_MODULE
668      * If contextlevel is specified, then only banks corresponding to
669      * that level are returned
670      */
671     public static function restore_get_question_banks($restoreid, $contextlevel = null) {
672         global $DB;
674         $results = array();
675         $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info
676                                          FROM {backup_ids_temp}
677                                        WHERE backupid = ?
678                                          AND itemname = 'question_category'", array($restoreid));
679         foreach ($qcats as $qcat) {
680             // If this qcat context haven't been acummulated yet, do that
681             if (!isset($results[$qcat->contextid])) {
682                 $info = backup_controller_dbops::decode_backup_temp_info($qcat->info);
683                 // Filter by contextlevel if necessary
684                 if (is_null($contextlevel) || $contextlevel == $info->contextlevel) {
685                     $results[$qcat->contextid] = $info->contextlevel;
686                 }
687             }
688         }
689         $qcats->close();
690         // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE)
691         asort($results);
692         return $results;
693     }
695     /**
696      * Return one array of question_category records for
697      * a given restore operation and one restore context (question bank)
698      */
699     public static function restore_get_question_categories($restoreid, $contextid) {
700         global $DB;
702         $results = array();
703         $qcats = $DB->get_recordset_sql("SELECT itemid, info
704                                          FROM {backup_ids_temp}
705                                         WHERE backupid = ?
706                                           AND itemname = 'question_category'
707                                           AND parentitemid = ?", array($restoreid, $contextid));
708         foreach ($qcats as $qcat) {
709             $results[$qcat->itemid] = backup_controller_dbops::decode_backup_temp_info($qcat->info);
710         }
711         $qcats->close();
713         return $results;
714     }
716     /**
717      * Calculates the best context found to restore one collection of qcats,
718      * al them belonging to the same context (question bank), returning the
719      * target context found (object) or false
720      */
721     public static function restore_find_best_target_context($categories, $courseid, $contextlevel) {
722         global $DB;
724         $targetcontext = false;
726         // Depending of $contextlevel, we perform different actions
727         switch ($contextlevel) {
728              // For system is easy, the best context is the system context
729              case CONTEXT_SYSTEM:
730                  $targetcontext = context_system::instance();
731                  break;
733              // For coursecat, we are going to look for stamps in all the
734              // course categories between CONTEXT_SYSTEM and CONTEXT_COURSE
735              // (i.e. in all the course categories in the path)
736              //
737              // And only will return one "best" target context if all the
738              // matches belong to ONE and ONLY ONE context. If multiple
739              // matches are found, that means that there is some annoying
740              // qbank "fragmentation" in the categories, so we'll fallback
741              // to create the qbank at course level
742              case CONTEXT_COURSECAT:
743                  // Build the array of stamps we are going to match
744                  $stamps = array();
745                  foreach ($categories as $category) {
746                      $stamps[] = $category->stamp;
747                  }
748                  $contexts = array();
749                  // Build the array of contexts we are going to look
750                  $systemctx = context_system::instance();
751                  $coursectx = context_course::instance($courseid);
752                  $parentctxs = $coursectx->get_parent_context_ids();
753                  foreach ($parentctxs as $parentctx) {
754                      // Exclude system context
755                      if ($parentctx == $systemctx->id) {
756                          continue;
757                      }
758                      $contexts[] = $parentctx;
759                  }
760                  if (!empty($stamps) && !empty($contexts)) {
761                      // Prepare the query
762                      list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps);
763                      list($context_sql, $context_params) = $DB->get_in_or_equal($contexts);
764                      $sql = "SELECT contextid
765                                FROM {question_categories}
766                               WHERE stamp $stamp_sql
767                                 AND contextid $context_sql";
768                      $params = array_merge($stamp_params, $context_params);
769                      $matchingcontexts = $DB->get_records_sql($sql, $params);
770                      // Only if ONE and ONLY ONE context is found, use it as valid target
771                      if (count($matchingcontexts) == 1) {
772                          $targetcontext = context::instance_by_id(reset($matchingcontexts)->contextid);
773                      }
774                  }
775                  break;
777              // For course is easy, the best context is the course context
778              case CONTEXT_COURSE:
779                  $targetcontext = context_course::instance($courseid);
780                  break;
782              // For module is easy, there is not best context, as far as the
783              // activity hasn't been created yet. So we return context course
784              // for them, so permission checks and friends will work. Note this
785              // case is handled by {@link prechek_precheck_qbanks_by_level}
786              // in an special way
787              case CONTEXT_MODULE:
788                  $targetcontext = context_course::instance($courseid);
789                  break;
790         }
791         return $targetcontext;
792     }
794     /**
795      * Return one array of question records for
796      * a given restore operation and one question category
797      */
798     public static function restore_get_questions($restoreid, $qcatid) {
799         global $DB;
801         $results = array();
802         $qs = $DB->get_recordset_sql("SELECT itemid, info
803                                       FROM {backup_ids_temp}
804                                      WHERE backupid = ?
805                                        AND itemname = 'question'
806                                        AND parentitemid = ?", array($restoreid, $qcatid));
807         foreach ($qs as $q) {
808             $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info);
809         }
810         $qs->close();
811         return $results;
812     }
814     /**
815      * Given one component/filearea/context and
816      * optionally one source itemname to match itemids
817      * put the corresponding files in the pool
818      *
819      * @param string $basepath the full path to the root of unzipped backup file
820      * @param string $restoreid the restore job's identification
821      * @param string $component
822      * @param string $filearea
823      * @param int $oldcontextid
824      * @param int $dfltuserid default $file->user if the old one can't be mapped
825      * @param string|null $itemname
826      * @param int|null $olditemid
827      * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
828      * @param bool $skipparentitemidctxmatch
829      * @return array of result object
830      */
831     public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
832         global $DB, $CFG;
834         $backupinfo = backup_general_helper::get_backup_information(basename($basepath));
835         $includesfiles = $backupinfo->include_files;
837         $results = array();
839         if ($forcenewcontextid) {
840             // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings,
841             // with questions originally at system/coursecat context in source being restored to course context in target). So we need
842             // to be able to force the new contextid
843             $newcontextid = $forcenewcontextid;
844         } else {
845             // Get new context, must exist or this will fail
846             if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) {
847                 throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid);
848             }
849         }
851         // Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid
852         // columns (because we have used them to store other information). This happens usually with
853         // all the question related backup_ids_temp records. In that case, it's safe to ignore that
854         // matching as far as we are always restoring for well known oldcontexts and olditemids
855         $parentitemctxmatchsql = ' AND i.parentitemid = f.contextid ';
856         if ($skipparentitemidctxmatch) {
857             $parentitemctxmatchsql = '';
858         }
860         // Important: remember how files have been loaded to backup_files_temp
861         //   - info: contains the whole original object (times, names...)
862         //   (all them being original ids as loaded from xml)
864         // itemname = null, we are going to match only by context, no need to use itemid (all them are 0)
865         if ($itemname == null) {
866             $sql = "SELECT id AS bftid, contextid, component, filearea, itemid, itemid AS newitemid, info
867                       FROM {backup_files_temp}
868                      WHERE backupid = ?
869                        AND contextid = ?
870                        AND component = ?
871                        AND filearea  = ?";
872             $params = array($restoreid, $oldcontextid, $component, $filearea);
874         // itemname not null, going to join with backup_ids to perform the old-new mapping of itemids
875         } else {
876             $sql = "SELECT f.id AS bftid, f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info
877                       FROM {backup_files_temp} f
878                       JOIN {backup_ids_temp} i ON i.backupid = f.backupid
879                                               $parentitemctxmatchsql
880                                               AND i.itemid = f.itemid
881                      WHERE f.backupid = ?
882                        AND f.contextid = ?
883                        AND f.component = ?
884                        AND f.filearea = ?
885                        AND i.itemname = ?";
886             $params = array($restoreid, $oldcontextid, $component, $filearea, $itemname);
887             if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname
888                 $sql .= ' AND i.itemid = ?';
889                 $params[] = $olditemid;
890             }
891         }
893         $fs = get_file_storage();         // Get moodle file storage
894         $basepath = $basepath . '/files/';// Get backup file pool base
895         $rs = $DB->get_recordset_sql($sql, $params);
896         foreach ($rs as $rec) {
897             $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
899             // ignore root dirs (they are created automatically)
900             if ($file->filepath == '/' && $file->filename == '.') {
901                 continue;
902             }
904             // set the best possible user
905             $mappeduser = self::get_backup_ids_record($restoreid, 'user', $file->userid);
906             $mappeduserid = !empty($mappeduser) ? $mappeduser->newitemid : $dfltuserid;
908             // dir found (and not root one), let's create it
909             if ($file->filename == '.') {
910                 $fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $mappeduserid);
911                 continue;
912             }
914             // The file record to restore.
915             $file_record = array(
916                 'contextid'   => $newcontextid,
917                 'component'   => $component,
918                 'filearea'    => $filearea,
919                 'itemid'      => $rec->newitemid,
920                 'filepath'    => $file->filepath,
921                 'filename'    => $file->filename,
922                 'timecreated' => $file->timecreated,
923                 'timemodified'=> $file->timemodified,
924                 'userid'      => $mappeduserid,
925                 'author'      => $file->author,
926                 'license'     => $file->license,
927                 'sortorder'   => $file->sortorder
928             );
930             if (empty($file->repositoryid)) {
931                 // this is a regular file, it must be present in the backup pool
932                 $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash);
934                 // Some file types do not include the files as they should already be
935                 // present. We still need to create entries into the files table.
936                 if ($includesfiles) {
937                     // The file is not found in the backup.
938                     if (!file_exists($backuppath)) {
939                         $result = new stdClass();
940                         $result->code = 'file_missing_in_backup';
941                         $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
942                         $result->level = backup::LOG_WARNING;
943                         $results[] = $result;
944                         continue;
945                     }
947                     // create the file in the filepool if it does not exist yet
948                     if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
950                         // If no license found, use default.
951                         if ($file->license == null){
952                             $file->license = $CFG->sitedefaultlicense;
953                         }
955                         $fs->create_file_from_pathname($file_record, $backuppath);
956                     }
957                 } else {
958                     // This backup does not include the files - they should be available in moodle filestorage already.
960                     // Create the file in the filepool if it does not exist yet.
961                     if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
963                         // Even if a file has been deleted since the backup was made, the file metadata will remain in the
964                         // files table, and the file will not be moved to the trashdir.
965                         // Files are not cleared from the files table by cron until several days after deletion.
966                         if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
967                             // Only grab one of the foundfiles - the file content should be the same for all entries.
968                             $foundfile = reset($foundfiles);
969                             $fs->create_file_from_storedfile($file_record, $foundfile->id);
970                         } else {
971                             // A matching existing file record was not found in the database.
972                             $result = new stdClass();
973                             $result->code = 'file_missing_in_backup';
974                             $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
975                             $result->level = backup::LOG_WARNING;
976                             $results[] = $result;
977                             continue;
978                         }
979                     }
980                 }
982                 // store the the new contextid and the new itemid in case we need to remap
983                 // references to this file later
984                 $DB->update_record('backup_files_temp', array(
985                     'id' => $rec->bftid,
986                     'newcontextid' => $newcontextid,
987                     'newitemid' => $rec->newitemid), true);
989             } else {
990                 // this is an alias - we can't create it yet so we stash it in a temp
991                 // table and will let the final task to deal with it
992                 if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
993                     $info = new stdClass();
994                     // oldfile holds the raw information stored in MBZ (including reference-related info)
995                     $info->oldfile = $file;
996                     // newfile holds the info for the new file_record with the context, user and itemid mapped
997                     $info->newfile = (object) $file_record;
999                     restore_dbops::set_backup_ids_record($restoreid, 'file_aliases_queue', $file->id, 0, null, $info);
1000                 }
1001             }
1002         }
1003         $rs->close();
1004         return $results;
1005     }
1007     /**
1008      * Given one restoreid, create in DB all the users present
1009      * in backup_ids having newitemid = 0, as far as
1010      * precheck_included_users() have left them there
1011      * ready to be created. Also, annotate their newids
1012      * once created for later reference
1013      */
1014     public static function create_included_users($basepath, $restoreid, $userid) {
1015         global $CFG, $DB;
1017         $authcache = array(); // Cache to get some bits from authentication plugins
1018         $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search later
1019         $themes    = get_list_of_themes(); // Get themes for quick search later
1021         // Iterate over all the included users with newitemid = 0, have to create them
1022         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info');
1023         foreach ($rs as $recuser) {
1024             $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
1026             // if user lang doesn't exist here, use site default
1027             if (!array_key_exists($user->lang, $languages)) {
1028                 $user->lang = $CFG->lang;
1029             }
1031             // if user theme isn't available on target site or they are disabled, reset theme
1032             if (!empty($user->theme)) {
1033                 if (empty($CFG->allowuserthemes) || !in_array($user->theme, $themes)) {
1034                     $user->theme = '';
1035                 }
1036             }
1038             // if user to be created has mnet auth and its mnethostid is $CFG->mnet_localhost_id
1039             // that's 100% impossible as own server cannot be accesed over mnet. Change auth to email/manual
1040             if ($user->auth == 'mnet' && $user->mnethostid == $CFG->mnet_localhost_id) {
1041                 // Respect registerauth
1042                 if ($CFG->registerauth == 'email') {
1043                     $user->auth = 'email';
1044                 } else {
1045                     $user->auth = 'manual';
1046                 }
1047             }
1048             unset($user->mnethosturl); // Not needed anymore
1050             // Disable pictures based on global setting
1051             if (!empty($CFG->disableuserimages)) {
1052                 $user->picture = 0;
1053             }
1055             // We need to analyse the AUTH field to recode it:
1056             //   - if the auth isn't enabled in target site, $CFG->registerauth will decide
1057             //   - finally, if the auth resulting isn't enabled, default to 'manual'
1058             if (!is_enabled_auth($user->auth)) {
1059                 if ($CFG->registerauth == 'email') {
1060                     $user->auth = 'email';
1061                 } else {
1062                     $user->auth = 'manual';
1063                 }
1064             }
1065             if (!is_enabled_auth($user->auth)) { // Final auth check verify, default to manual if not enabled
1066                 $user->auth = 'manual';
1067             }
1069             // Now that we know the auth method, for users to be created without pass
1070             // if password handling is internal and reset password is available
1071             // we set the password to "restored" (plain text), so the login process
1072             // will know how to handle that situation in order to allow the user to
1073             // recover the password. MDL-20846
1074             if (empty($user->password)) { // Only if restore comes without password
1075                 if (!array_key_exists($user->auth, $authcache)) { // Not in cache
1076                     $userauth = new stdClass();
1077                     $authplugin = get_auth_plugin($user->auth);
1078                     $userauth->preventpassindb = $authplugin->prevent_local_passwords();
1079                     $userauth->isinternal      = $authplugin->is_internal();
1080                     $userauth->canresetpwd     = $authplugin->can_reset_password();
1081                     $authcache[$user->auth] = $userauth;
1082                 } else {
1083                     $userauth = $authcache[$user->auth]; // Get from cache
1084                 }
1086                 // Most external plugins do not store passwords locally
1087                 if (!empty($userauth->preventpassindb)) {
1088                     $user->password = AUTH_PASSWORD_NOT_CACHED;
1090                 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark
1091                 } else if ($userauth->isinternal and $userauth->canresetpwd) {
1092                     $user->password = 'restored';
1093                 }
1094             }
1096             // Creating new user, we must reset the policyagreed always
1097             $user->policyagreed = 0;
1099             // Set time created if empty
1100             if (empty($user->timecreated)) {
1101                 $user->timecreated = time();
1102             }
1104             // Done, let's create the user and annotate its id
1105             $newuserid = $DB->insert_record('user', $user);
1106             self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $newuserid);
1107             // Let's create the user context and annotate it (we need it for sure at least for files)
1108             // but for deleted users that don't have a context anymore (MDL-30192). We are done for them
1109             // and nothing else (custom fields, prefs, tags, files...) will be created.
1110             if (empty($user->deleted)) {
1111                 $newuserctxid = $user->deleted ? 0 : context_user::instance($newuserid)->id;
1112                 self::set_backup_ids_record($restoreid, 'context', $recuser->parentitemid, $newuserctxid);
1114                 // Process custom fields
1115                 if (isset($user->custom_fields)) { // if present in backup
1116                     foreach($user->custom_fields['custom_field'] as $udata) {
1117                         $udata = (object)$udata;
1118                         // If the profile field has data and the profile shortname-datatype is defined in server
1119                         if ($udata->field_data) {
1120                             if ($field = $DB->get_record('user_info_field', array('shortname'=>$udata->field_name, 'datatype'=>$udata->field_type))) {
1121                             /// Insert the user_custom_profile_field
1122                                 $rec = new stdClass();
1123                                 $rec->userid  = $newuserid;
1124                                 $rec->fieldid = $field->id;
1125                                 $rec->data    = $udata->field_data;
1126                                 $DB->insert_record('user_info_data', $rec);
1127                             }
1128                         }
1129                     }
1130                 }
1132                 // Process tags
1133                 if (!empty($CFG->usetags) && isset($user->tags)) { // if enabled in server and present in backup
1134                     $tags = array();
1135                     foreach($user->tags['tag'] as $usertag) {
1136                         $usertag = (object)$usertag;
1137                         $tags[] = $usertag->rawname;
1138                     }
1139                     tag_set('user', $newuserid, $tags);
1140                 }
1142                 // Process preferences
1143                 if (isset($user->preferences)) { // if present in backup
1144                     foreach($user->preferences['preference'] as $preference) {
1145                         $preference = (object)$preference;
1146                         // Prepare the record and insert it
1147                         $preference->userid = $newuserid;
1148                         $status = $DB->insert_record('user_preferences', $preference);
1149                     }
1150                 }
1152                 // Create user files in pool (profile, icon, private) by context
1153                 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'icon', $recuser->parentitemid, $userid);
1154                 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'profile', $recuser->parentitemid, $userid);
1155             }
1156         }
1157         $rs->close();
1158     }
1160     /**
1161     * Given one user object (from backup file), perform all the neccesary
1162     * checks is order to decide how that user will be handled on restore.
1163     *
1164     * Note the function requires $user->mnethostid to be already calculated
1165     * so it's caller responsibility to set it
1166     *
1167     * This function is used both by @restore_precheck_users() and
1168     * @restore_create_users() to get consistent results in both places
1169     *
1170     * It returns:
1171     *   - one user object (from DB), if match has been found and user will be remapped
1172     *   - boolean true if the user needs to be created
1173     *   - boolean false if some conflict happened and the user cannot be handled
1174     *
1175     * Each test is responsible for returning its results and interrupt
1176     * execution. At the end, boolean true (user needs to be created) will be
1177     * returned if no test has interrupted that.
1178     *
1179     * Here it's the logic applied, keep it updated:
1180     *
1181     *  If restoring users from same site backup:
1182     *      1A - Normal check: If match by id and username and mnethost  => ok, return target user
1183     *      1B - If restoring an 'anonymous' user (created via the 'Anonymize user information' option) try to find a
1184     *           match by username only => ok, return target user MDL-31484
1185     *      1C - Handle users deleted in DB and "alive" in backup file:
1186     *           If match by id and mnethost and user is deleted in DB and
1187     *           (match by username LIKE 'backup_email.%' or by non empty email = md5(username)) => ok, return target user
1188     *      1D - Handle users deleted in backup file and "alive" in DB:
1189     *           If match by id and mnethost and user is deleted in backup file
1190     *           and match by email = email_without_time(backup_email) => ok, return target user
1191     *      1E - Conflict: If match by username and mnethost and doesn't match by id => conflict, return false
1192     *      1F - None of the above, return true => User needs to be created
1193     *
1194     *  if restoring from another site backup (cannot match by id here, replace it by email/firstaccess combination):
1195     *      2A - Normal check: If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user
1196     *      2B - Handle users deleted in DB and "alive" in backup file:
1197     *           2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
1198     *                 (username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
1199     *           2B2 - If match by mnethost and user is deleted in DB and
1200     *                 username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
1201     *                 (to cover situations were md5(username) wasn't implemented on delete we requiere both)
1202     *      2C - Handle users deleted in backup file and "alive" in DB:
1203     *           If match mnethost and user is deleted in backup file
1204     *           and by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
1205     *      2D - Conflict: If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
1206     *      2E - None of the above, return true => User needs to be created
1207     *
1208     * Note: for DB deleted users email is stored in username field, hence we
1209     *       are looking there for emails. See delete_user()
1210     * Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1211     *       hence we are looking there for usernames if not empty. See delete_user()
1212     */
1213     protected static function precheck_user($user, $samesite) {
1214         global $CFG, $DB;
1216         // Handle checks from same site backups
1217         if ($samesite && empty($CFG->forcedifferentsitecheckingusersonrestore)) {
1219             // 1A - If match by id and username and mnethost => ok, return target user
1220             if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
1221                 return $rec; // Matching user found, return it
1222             }
1224             // 1B - If restoring an 'anonymous' user (created via the 'Anonymize user information' option) try to find a
1225             // match by username only => ok, return target user MDL-31484
1226             // This avoids username / id mis-match problems when restoring subsequent anonymized backups.
1227             if (backup_anonymizer_helper::is_anonymous_user($user)) {
1228                 if ($rec = $DB->get_record('user', array('username' => $user->username))) {
1229                     return $rec; // Matching anonymous user found - return it
1230                 }
1231             }
1233             // 1C - Handle users deleted in DB and "alive" in backup file
1234             // Note: for DB deleted users email is stored in username field, hence we
1235             //       are looking there for emails. See delete_user()
1236             // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1237             //       hence we are looking there for usernames if not empty. See delete_user()
1238             // If match by id and mnethost and user is deleted in DB and
1239             // match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
1240             if ($rec = $DB->get_record_sql("SELECT *
1241                                               FROM {user} u
1242                                              WHERE id = ?
1243                                                AND mnethostid = ?
1244                                                AND deleted = 1
1245                                                AND (
1246                                                        UPPER(username) LIKE UPPER(?)
1247                                                     OR (
1248                                                            ".$DB->sql_isnotempty('user', 'email', false, false)."
1249                                                        AND email = ?
1250                                                        )
1251                                                    )",
1252                                            array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
1253                 return $rec; // Matching user, deleted in DB found, return it
1254             }
1256             // 1D - Handle users deleted in backup file and "alive" in DB
1257             // If match by id and mnethost and user is deleted in backup file
1258             // and match by email = email_without_time(backup_email) => ok, return target user
1259             if ($user->deleted) {
1260                 // Note: for DB deleted users email is stored in username field, hence we
1261                 //       are looking there for emails. See delete_user()
1262                 // Trim time() from email
1263                 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
1264                 if ($rec = $DB->get_record_sql("SELECT *
1265                                                   FROM {user} u
1266                                                  WHERE id = ?
1267                                                    AND mnethostid = ?
1268                                                    AND UPPER(email) = UPPER(?)",
1269                                                array($user->id, $user->mnethostid, $trimemail))) {
1270                     return $rec; // Matching user, deleted in backup file found, return it
1271                 }
1272             }
1274             // 1E - If match by username and mnethost and doesn't match by id => conflict, return false
1275             if ($rec = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid))) {
1276                 if ($user->id != $rec->id) {
1277                     return false; // Conflict, username already exists and belongs to another id
1278                 }
1279             }
1281         // Handle checks from different site backups
1282         } else {
1284             // 2A - If match by username and mnethost and
1285             //     (email or non-zero firstaccess) => ok, return target user
1286             if ($rec = $DB->get_record_sql("SELECT *
1287                                               FROM {user} u
1288                                              WHERE username = ?
1289                                                AND mnethostid = ?
1290                                                AND (
1291                                                        UPPER(email) = UPPER(?)
1292                                                     OR (
1293                                                            firstaccess != 0
1294                                                        AND firstaccess = ?
1295                                                        )
1296                                                    )",
1297                                            array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
1298                 return $rec; // Matching user found, return it
1299             }
1301             // 2B - Handle users deleted in DB and "alive" in backup file
1302             // Note: for DB deleted users email is stored in username field, hence we
1303             //       are looking there for emails. See delete_user()
1304             // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
1305             //       hence we are looking there for usernames if not empty. See delete_user()
1306             // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
1307             //       (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
1308             if ($rec = $DB->get_record_sql("SELECT *
1309                                               FROM {user} u
1310                                              WHERE mnethostid = ?
1311                                                AND deleted = 1
1312                                                AND ".$DB->sql_isnotempty('user', 'email', false, false)."
1313                                                AND email = ?
1314                                                AND (
1315                                                        UPPER(username) LIKE UPPER(?)
1316                                                     OR (
1317                                                            firstaccess != 0
1318                                                        AND firstaccess = ?
1319                                                        )
1320                                                    )",
1321                                            array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
1322                 return $rec; // Matching user found, return it
1323             }
1325             // 2B2 - If match by mnethost and user is deleted in DB and
1326             //       username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
1327             //       (this covers situations where md5(username) wasn't being stored so we require both
1328             //        the email & non-zero firstaccess to match)
1329             if ($rec = $DB->get_record_sql("SELECT *
1330                                               FROM {user} u
1331                                              WHERE mnethostid = ?
1332                                                AND deleted = 1
1333                                                AND UPPER(username) LIKE UPPER(?)
1334                                                AND firstaccess != 0
1335                                                AND firstaccess = ?",
1336                                            array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
1337                 return $rec; // Matching user found, return it
1338             }
1340             // 2C - Handle users deleted in backup file and "alive" in DB
1341             // If match mnethost and user is deleted in backup file
1342             // and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
1343             if ($user->deleted) {
1344                 // Note: for DB deleted users email is stored in username field, hence we
1345                 //       are looking there for emails. See delete_user()
1346                 // Trim time() from email
1347                 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username);
1348                 if ($rec = $DB->get_record_sql("SELECT *
1349                                                   FROM {user} u
1350                                                  WHERE mnethostid = ?
1351                                                    AND UPPER(email) = UPPER(?)
1352                                                    AND firstaccess != 0
1353                                                    AND firstaccess = ?",
1354                                                array($user->mnethostid, $trimemail, $user->firstaccess))) {
1355                     return $rec; // Matching user, deleted in backup file found, return it
1356                 }
1357             }
1359             // 2D - If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false
1360             if ($rec = $DB->get_record_sql("SELECT *
1361                                               FROM {user} u
1362                                              WHERE username = ?
1363                                                AND mnethostid = ?
1364                                            AND NOT (
1365                                                        UPPER(email) = UPPER(?)
1366                                                     OR (
1367                                                            firstaccess != 0
1368                                                        AND firstaccess = ?
1369                                                        )
1370                                                    )",
1371                                            array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) {
1372                 return false; // Conflict, username/mnethostid already exist and belong to another user (by email/firstaccess)
1373             }
1374         }
1376         // Arrived here, return true as the user will need to be created and no
1377         // conflicts have been found in the logic above. This covers:
1378         // 1E - else => user needs to be created, return true
1379         // 2E - else => user needs to be created, return true
1380         return true;
1381     }
1383     /**
1384      * Check all the included users, deciding the action to perform
1385      * for each one (mapping / creation) and returning one array
1386      * of problems in case something is wrong (lack of permissions,
1387      * conficts)
1388      */
1389     public static function precheck_included_users($restoreid, $courseid, $userid, $samesite) {
1390         global $CFG, $DB;
1392         // To return any problem found
1393         $problems = array();
1395         // We are going to map mnethostid, so load all the available ones
1396         $mnethosts = $DB->get_records('mnet_host', array(), 'wwwroot', 'wwwroot, id');
1398         // Calculate the context we are going to use for capability checking
1399         $context = context_course::instance($courseid);
1401         // Calculate if we have perms to create users, by checking:
1402         // to 'moodle/restore:createuser' and 'moodle/restore:userinfo'
1403         // and also observe $CFG->disableusercreationonrestore
1404         $cancreateuser = false;
1405         if (has_capability('moodle/restore:createuser', $context, $userid) and
1406             has_capability('moodle/restore:userinfo', $context, $userid) and
1407             empty($CFG->disableusercreationonrestore)) { // Can create users
1409             $cancreateuser = true;
1410         }
1412         // Iterate over all the included users
1413         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid, info');
1414         foreach ($rs as $recuser) {
1415             $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info);
1417             // Find the correct mnethostid for user before performing any further check
1418             if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) {
1419                 $user->mnethostid = $CFG->mnet_localhost_id;
1420             } else {
1421                 // fast url-to-id lookups
1422                 if (isset($mnethosts[$user->mnethosturl])) {
1423                     $user->mnethostid = $mnethosts[$user->mnethosturl]->id;
1424                 } else {
1425                     $user->mnethostid = $CFG->mnet_localhost_id;
1426                 }
1427             }
1429             // Now, precheck that user and, based on returned results, annotate action/problem
1430             $usercheck = self::precheck_user($user, $samesite);
1432             if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to
1433                 // Annotate it, for later process. Set newitemid to mapping user->id
1434                 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $usercheck->id);
1436             } else if ($usercheck === false) { // Found conflict, report it as problem
1437                  $problems[] = get_string('restoreuserconflict', '', $user->username);
1439             } else if ($usercheck === true) { // User needs to be created, check if we are able
1440                 if ($cancreateuser) { // Can create user, set newitemid to 0 so will be created later
1441                     self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user);
1443                 } else { // Cannot create user, report it as problem
1444                     $problems[] = get_string('restorecannotcreateuser', '', $user->username);
1445                 }
1447             } else { // Shouldn't arrive here ever, something is for sure wrong. Exception
1448                 throw new restore_dbops_exception('restore_error_processing_user', $user->username);
1449             }
1450         }
1451         $rs->close();
1452         return $problems;
1453     }
1455     /**
1456      * Process the needed users in order to decide
1457      * which action to perform with them (create/map)
1458      *
1459      * Just wrap over precheck_included_users(), returning
1460      * exception if any problem is found
1461      */
1462     public static function process_included_users($restoreid, $courseid, $userid, $samesite) {
1463         global $DB;
1465         // Just let precheck_included_users() to do all the hard work
1466         $problems = self::precheck_included_users($restoreid, $courseid, $userid, $samesite);
1468         // With problems, throw exception, shouldn't happen if prechecks were originally
1469         // executed, so be radical here.
1470         if (!empty($problems)) {
1471             throw new restore_dbops_exception('restore_problems_processing_users', null, implode(', ', $problems));
1472         }
1473     }
1475     /**
1476      * Process the needed question categories and questions
1477      * to check all them, deciding about the action to perform
1478      * (create/map) and target.
1479      *
1480      * Just wrap over precheck_categories_and_questions(), returning
1481      * exception if any problem is found
1482      */
1483     public static function process_categories_and_questions($restoreid, $courseid, $userid, $samesite) {
1484         global $DB;
1486         // Just let precheck_included_users() to do all the hard work
1487         $problems = self::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite);
1489         // With problems of type error, throw exception, shouldn't happen if prechecks were originally
1490         // executed, so be radical here.
1491         if (array_key_exists('errors', $problems)) {
1492             throw new restore_dbops_exception('restore_problems_processing_questions', null, implode(', ', $problems['errors']));
1493         }
1494     }
1496     public static function set_backup_files_record($restoreid, $filerec) {
1497         global $DB;
1499         // Store external files info in `info` field
1500         $filerec->info     = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info.
1501         $filerec->backupid = $restoreid;
1502         $DB->insert_record('backup_files_temp', $filerec);
1503     }
1505     public static function set_backup_ids_record($restoreid, $itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) {
1506         // Build conditionally the extra record info
1507         $extrarecord = array();
1508         if ($newitemid != 0) {
1509             $extrarecord['newitemid'] = $newitemid;
1510         }
1511         if ($parentitemid != null) {
1512             $extrarecord['parentitemid'] = $parentitemid;
1513         }
1514         if ($info != null) {
1515             $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info);
1516         }
1518         self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord);
1519     }
1521     public static function get_backup_ids_record($restoreid, $itemname, $itemid) {
1522         $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid);
1524         // We must test if info is a string, as the cache stores info in object form.
1525         if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) {
1526             $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info);
1527         }
1529         return $dbrec;
1530     }
1532     /**
1533      * Given on courseid, fullname and shortname, calculate the correct fullname/shortname to avoid dupes
1534      */
1535     public static function calculate_course_names($courseid, $fullname, $shortname) {
1536         global $CFG, $DB;
1538         $currentfullname = '';
1539         $currentshortname = '';
1540         $counter = 0;
1541         // Iteratere while the name exists
1542         do {
1543             if ($counter) {
1544                 $suffixfull  = ' ' . get_string('copyasnoun') . ' ' . $counter;
1545                 $suffixshort = '_' . $counter;
1546             } else {
1547                 $suffixfull  = '';
1548                 $suffixshort = '';
1549             }
1550             $currentfullname = $fullname.$suffixfull;
1551             $currentshortname = substr($shortname, 0, 100 - strlen($suffixshort)).$suffixshort; // < 100cc
1552             $coursefull  = $DB->get_record_select('course', 'fullname = ? AND id != ?',
1553                     array($currentfullname, $courseid), '*', IGNORE_MULTIPLE);
1554             $courseshort = $DB->get_record_select('course', 'shortname = ? AND id != ?', array($currentshortname, $courseid));
1555             $counter++;
1556         } while ($coursefull || $courseshort);
1558         // Return results
1559         return array($currentfullname, $currentshortname);
1560     }
1562     /**
1563      * For the target course context, put as many custom role names as possible
1564      */
1565     public static function set_course_role_names($restoreid, $courseid) {
1566         global $DB;
1568         // Get the course context
1569         $coursectx = context_course::instance($courseid);
1570         // Get all the mapped roles we have
1571         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid');
1572         foreach ($rs as $recrole) {
1573             $info = backup_controller_dbops::decode_backup_temp_info($recrole->info);
1574             // If it's one mapped role and we have one name for it
1575             if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) {
1576                 // If role name doesn't exist, add it
1577                 $rolename = new stdclass();
1578                 $rolename->roleid = $recrole->newitemid;
1579                 $rolename->contextid = $coursectx->id;
1580                 if (!$DB->record_exists('role_names', (array)$rolename)) {
1581                     $rolename->name = $info['nameincourse'];
1582                     $DB->insert_record('role_names', $rolename);
1583                 }
1584             }
1585         }
1586         $rs->close();
1587     }
1589     /**
1590      * Creates a skeleton record within the database using the passed parameters
1591      * and returns the new course id.
1592      *
1593      * @global moodle_database $DB
1594      * @param string $fullname
1595      * @param string $shortname
1596      * @param int $categoryid
1597      * @return int The new course id
1598      */
1599     public static function create_new_course($fullname, $shortname, $categoryid) {
1600         global $DB;
1601         $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST);
1603         $course = new stdClass;
1604         $course->fullname = $fullname;
1605         $course->shortname = $shortname;
1606         $course->category = $category->id;
1607         $course->sortorder = 0;
1608         $course->timecreated  = time();
1609         $course->timemodified = $course->timecreated;
1610         // forcing skeleton courses to be hidden instead of going by $category->visible , until MDL-27790 is resolved.
1611         $course->visible = 0;
1613         $courseid = $DB->insert_record('course', $course);
1615         $category->coursecount++;
1616         $DB->update_record('course_categories', $category);
1618         return $courseid;
1619     }
1621     /**
1622      * Deletes all of the content associated with the given course (courseid)
1623      * @param int $courseid
1624      * @param array $options
1625      * @return bool True for success
1626      */
1627     public static function delete_course_content($courseid, array $options = null) {
1628         return remove_course_contents($courseid, false, $options);
1629     }
1632 /*
1633  * Exception class used by all the @dbops stuff
1634  */
1635 class restore_dbops_exception extends backup_exception {
1637     public function __construct($errorcode, $a=NULL, $debuginfo=null) {
1638         parent::__construct($errorcode, 'error', '', $a, null, $debuginfo);
1639     }