|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 00003 // This file is part of Moodle - http://moodle.org/ 00004 // 00005 // Moodle is free software: you can redistribute it and/or modify 00006 // it under the terms of the GNU General Public License as published by 00007 // the Free Software Foundation, either version 3 of the License, or 00008 // (at your option) any later version. 00009 // 00010 // Moodle is distributed in the hope that it will be useful, 00011 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00012 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00013 // GNU General Public License for more details. 00014 // 00015 // You should have received a copy of the GNU General Public License 00016 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00017 00030 abstract class restore_dbops { 00031 00037 public static function get_included_tasks($restoreid) { 00038 $rc = restore_controller_dbops::load_controller($restoreid); 00039 $tasks = $rc->get_plan()->get_tasks(); 00040 $includedtasks = array(); 00041 foreach ($tasks as $key => $task) { 00042 // Calculate if the task is being included 00043 $included = false; 00044 // blocks, based in blocks setting and parent activity/course 00045 if ($task instanceof restore_block_task) { 00046 if (!$task->get_setting_value('blocks')) { // Blocks not included, continue 00047 continue; 00048 } 00049 $parent = basename(dirname(dirname($task->get_taskbasepath()))); 00050 if ($parent == 'course') { // Parent is course, always included if present 00051 $included = true; 00052 00053 } else { // Look for activity_included setting 00054 $included = $task->get_setting_value($parent . '_included'); 00055 } 00056 00057 // ativities, based on included setting 00058 } else if ($task instanceof restore_activity_task) { 00059 $included = $task->get_setting_value('included'); 00060 00061 // sections, based on included setting 00062 } else if ($task instanceof restore_section_task) { 00063 $included = $task->get_setting_value('included'); 00064 00065 // course always included if present 00066 } else if ($task instanceof restore_course_task) { 00067 $included = true; 00068 } 00069 00070 // If included, add it 00071 if ($included) { 00072 $includedtasks[] = $task; 00073 } 00074 } 00075 return $includedtasks; 00076 } 00077 00081 public static function load_inforef_to_tempids($restoreid, $inforeffile) { 00082 00083 if (!file_exists($inforeffile)) { // Shouldn't happen ever, but... 00084 throw new backup_helper_exception('missing_inforef_xml_file', $inforeffile); 00085 } 00086 // Let's parse, custom processor will do its work, sending info to DB 00087 $xmlparser = new progressive_parser(); 00088 $xmlparser->set_file($inforeffile); 00089 $xmlprocessor = new restore_inforef_parser_processor($restoreid); 00090 $xmlparser->set_processor($xmlprocessor); 00091 $xmlparser->process(); 00092 } 00093 00097 public static function load_roles_to_tempids($restoreid, $rolesfile) { 00098 00099 if (!file_exists($rolesfile)) { // Shouldn't happen ever, but... 00100 throw new backup_helper_exception('missing_roles_xml_file', $rolesfile); 00101 } 00102 // Let's parse, custom processor will do its work, sending info to DB 00103 $xmlparser = new progressive_parser(); 00104 $xmlparser->set_file($rolesfile); 00105 $xmlprocessor = new restore_roles_parser_processor($restoreid); 00106 $xmlparser->set_processor($xmlprocessor); 00107 $xmlparser->process(); 00108 } 00109 00117 public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { 00118 global $DB; 00119 00120 $problems = array(); // To store warnings/errors 00121 00122 // Get loaded roles from backup_ids 00123 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid'); 00124 foreach ($rs as $recrole) { 00125 // If the rolemappings->modified flag is set, that means that we are coming from 00126 // manually modified mappings (by UI), so accept those mappings an put them to backup_ids 00127 if ($rolemappings->modified) { 00128 $target = $rolemappings->mappings[$recrole->itemid]->targetroleid; 00129 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $target); 00130 00131 // Else, we haven't any info coming from UI, let's calculate the mappings, matching 00132 // in multiple ways and checking permissions. Note mapping to 0 means "skip" 00133 } else { 00134 $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid)->info; 00135 $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite); 00136 // Send match to backup_ids 00137 self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match); 00138 // Build the rolemappings element for controller 00139 unset($role->id); 00140 unset($role->nameincourse); 00141 unset($role->nameincourse); 00142 $role->targetroleid = $match; 00143 $rolemappings->mappings[$recrole->itemid] = $role; 00144 // Prepare warning if no match found 00145 if (!$match) { 00146 $problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name); 00147 } 00148 } 00149 } 00150 $rs->close(); 00151 return $problems; 00152 } 00153 00159 protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) { 00160 global $CFG, $DB; 00161 00162 // Gather various information about roles 00163 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid); 00164 $allroles = $DB->get_records('role'); 00165 $assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid); 00166 00167 // Note: under 1.9 we had one function restore_samerole() that performed one complete 00168 // matching of roles (all caps) and if match was found the mapping was availabe bypassing 00169 // any assignable_roles() security. IMO that was wrong and we must not allow such 00170 // mappings anymore. So we have left that matching strategy out in 2.0 00171 00172 // Empty assignable roles, mean no match possible 00173 if (empty($assignablerolesshortname)) { 00174 return 0; 00175 } 00176 00177 // Match by shortname 00178 if ($match = array_search($role->shortname, $assignablerolesshortname)) { 00179 return $match; 00180 } 00181 00182 // Match by archetype 00183 list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname)); 00184 $params = array_merge(array($role->archetype), $in_params); 00185 if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) { 00186 return $rec->id; 00187 } 00188 00189 // Match editingteacher to teacher (happens a lot, from 1.9) 00190 if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) { 00191 return array_search('teacher', $assignablerolesshortname); 00192 } 00193 00194 // No match, return 0 00195 return 0; 00196 } 00197 00198 00205 public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { 00206 global $DB; 00207 00208 // Just let precheck_included_roles() to do all the hard work 00209 $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings); 00210 00211 // With problems of type error, throw exception, shouldn't happen if prechecks executed 00212 if (array_key_exists('errors', $problems)) { 00213 throw new restore_dbops_exception('restore_problems_processing_roles', null, implode(', ', $problems['errors'])); 00214 } 00215 } 00216 00220 public static function load_users_to_tempids($restoreid, $usersfile) { 00221 00222 if (!file_exists($usersfile)) { // Shouldn't happen ever, but... 00223 throw new backup_helper_exception('missing_users_xml_file', $usersfile); 00224 } 00225 // Let's parse, custom processor will do its work, sending info to DB 00226 $xmlparser = new progressive_parser(); 00227 $xmlparser->set_file($usersfile); 00228 $xmlprocessor = new restore_users_parser_processor($restoreid); 00229 $xmlparser->set_processor($xmlprocessor); 00230 $xmlparser->process(); 00231 } 00232 00236 public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) { 00237 00238 if (!file_exists($questionsfile)) { // Shouldn't happen ever, but... 00239 throw new backup_helper_exception('missing_questions_xml_file', $questionsfile); 00240 } 00241 // Let's parse, custom processor will do its work, sending info to DB 00242 $xmlparser = new progressive_parser(); 00243 $xmlparser->set_file($questionsfile); 00244 $xmlprocessor = new restore_questions_parser_processor($restoreid); 00245 $xmlparser->set_processor($xmlprocessor); 00246 $xmlparser->process(); 00247 } 00248 00293 public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) { 00294 00295 $problems = array(); 00296 00297 // TODO: Check all qs, looking their qtypes are restorable 00298 00299 // Precheck all qcats and qs looking for target contexts / warnings / errors 00300 list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM); 00301 list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT); 00302 list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE); 00303 list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE); 00304 00305 // Acummulate and handle errors and warnings 00306 $errors = array_merge($syserr, $caterr, $couerr, $moderr); 00307 $warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn); 00308 if (!empty($errors)) { 00309 $problems['errors'] = $errors; 00310 } 00311 if (!empty($warnings)) { 00312 $problems['warnings'] = $warnings; 00313 } 00314 return $problems; 00315 } 00316 00344 public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) { 00345 global $CFG, $DB; 00346 00347 // To return any errors and warnings found 00348 $errors = array(); 00349 $warnings = array(); 00350 00351 // Specify which fallbacks must be performed 00352 $fallbacks = array( 00353 CONTEXT_SYSTEM => CONTEXT_COURSE, 00354 CONTEXT_COURSECAT => CONTEXT_COURSE); 00355 00356 // For any contextlevel, follow this process logic: 00357 // 00358 // 0) Iterate over each context (qbank) 00359 // 1) Iterate over each qcat in the context, matching by stamp for the found target context 00360 // 2a) No match, check if user can create qcat and q 00361 // 3a) User can, mark the qcat and all dependent qs to be created in that target context 00362 // 3b) User cannot, check if we are in some contextlevel with fallback 00363 // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop 00364 // 4b) No fallback, error. End qcat loop. 00365 // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version 00366 // 5a) No match, check if user can add q 00367 // 6a) User can, mark the q to be created 00368 // 6b) User cannot, check if we are in some contextlevel with fallback 00369 // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop 00370 // 7b) No fallback, error. End qcat loop 00371 // 5b) Match, mark q to be mapped 00372 00373 // Get all the contexts (question banks) in restore for the given contextlevel 00374 $contexts = self::restore_get_question_banks($restoreid, $contextlevel); 00375 00376 // 0) Iterate over each context (qbank) 00377 foreach ($contexts as $contextid => $contextlevel) { 00378 // Init some perms 00379 $canmanagecategory = false; 00380 $canadd = false; 00381 // get categories in context (bank) 00382 $categories = self::restore_get_question_categories($restoreid, $contextid); 00383 // cache permissions if $targetcontext is found 00384 if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) { 00385 $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid); 00386 $canadd = has_capability('moodle/question:add', $targetcontext, $userid); 00387 } 00388 // 1) Iterate over each qcat in the context, matching by stamp for the found target context 00389 foreach ($categories as $category) { 00390 $matchcat = false; 00391 if ($targetcontext) { 00392 $matchcat = $DB->get_record('question_categories', array( 00393 'contextid' => $targetcontext->id, 00394 'stamp' => $category->stamp)); 00395 } 00396 // 2a) No match, check if user can create qcat and q 00397 if (!$matchcat) { 00398 // 3a) User can, mark the qcat and all dependent qs to be created in that target context 00399 if ($canmanagecategory && $canadd) { 00400 // Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where 00401 // we keep the source contextid unmodified (for easier matching later when the 00402 // activities are created) 00403 $parentitemid = $targetcontext->id; 00404 if ($contextlevel == CONTEXT_MODULE) { 00405 $parentitemid = null; // null means "not modify" a.k.a. leave original contextid 00406 } 00407 self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid); 00408 // Nothing else to mark, newitemid = 0 means create 00409 00410 // 3b) User cannot, check if we are in some contextlevel with fallback 00411 } else { 00412 // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop 00413 if (array_key_exists($contextlevel, $fallbacks)) { 00414 foreach ($categories as $movedcat) { 00415 $movedcat->contextlevel = $fallbacks[$contextlevel]; 00416 self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); 00417 // Warn about the performed fallback 00418 $warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat); 00419 } 00420 00421 // 4b) No fallback, error. End qcat loop. 00422 } else { 00423 $errors[] = get_string('qcategorycannotberestored', 'backup', $category); 00424 } 00425 break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank) 00426 } 00427 00428 // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version 00429 } else { 00430 self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id); 00431 $questions = self::restore_get_questions($restoreid, $category->id); 00432 foreach ($questions as $question) { 00433 $matchq = $DB->get_record('question', array( 00434 'category' => $matchcat->id, 00435 'stamp' => $question->stamp, 00436 'version' => $question->version)); 00437 // 5a) No match, check if user can add q 00438 if (!$matchq) { 00439 // 6a) User can, mark the q to be created 00440 if ($canadd) { 00441 // Nothing to mark, newitemid means create 00442 00443 // 6b) User cannot, check if we are in some contextlevel with fallback 00444 } else { 00445 // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo 00446 if (array_key_exists($contextlevel, $fallbacks)) { 00447 foreach ($categories as $movedcat) { 00448 $movedcat->contextlevel = $fallbacks[$contextlevel]; 00449 self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); 00450 // Warn about the performed fallback 00451 $warnings[] = get_string('question2coursefallback', 'backup', $movedcat); 00452 } 00453 00454 // 7b) No fallback, error. End qcat loop 00455 } else { 00456 $errors[] = get_string('questioncannotberestored', 'backup', $question); 00457 } 00458 break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank) 00459 } 00460 00461 // 5b) Match, mark q to be mapped 00462 } else { 00463 self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id); 00464 } 00465 } 00466 } 00467 } 00468 } 00469 00470 return array($errors, $warnings); 00471 } 00472 00480 public static function restore_get_question_banks($restoreid, $contextlevel = null) { 00481 global $DB; 00482 00483 $results = array(); 00484 $qcats = $DB->get_records_sql("SELECT itemid, parentitemid AS contextid 00485 FROM {backup_ids_temp} 00486 WHERE backupid = ? 00487 AND itemname = 'question_category'", array($restoreid)); 00488 foreach ($qcats as $qcat) { 00489 // If this qcat context haven't been acummulated yet, do that 00490 if (!isset($results[$qcat->contextid])) { 00491 $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid); 00492 // Filter by contextlevel if necessary 00493 if (is_null($contextlevel) || $contextlevel == $temprec->info->contextlevel) { 00494 $results[$qcat->contextid] = $temprec->info->contextlevel; 00495 } 00496 } 00497 } 00498 // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE) 00499 asort($results); 00500 return $results; 00501 } 00502 00507 public static function restore_get_question_categories($restoreid, $contextid) { 00508 global $DB; 00509 00510 $results = array(); 00511 $qcats = $DB->get_records_sql("SELECT itemid 00512 FROM {backup_ids_temp} 00513 WHERE backupid = ? 00514 AND itemname = 'question_category' 00515 AND parentitemid = ?", array($restoreid, $contextid)); 00516 foreach ($qcats as $qcat) { 00517 $temprec = self::get_backup_ids_record($restoreid, 'question_category', $qcat->itemid); 00518 $results[$qcat->itemid] = $temprec->info; 00519 } 00520 return $results; 00521 } 00522 00528 public static function restore_find_best_target_context($categories, $courseid, $contextlevel) { 00529 global $DB; 00530 00531 $targetcontext = false; 00532 00533 // Depending of $contextlevel, we perform different actions 00534 switch ($contextlevel) { 00535 // For system is easy, the best context is the system context 00536 case CONTEXT_SYSTEM: 00537 $targetcontext = get_context_instance(CONTEXT_SYSTEM); 00538 break; 00539 00540 // For coursecat, we are going to look for stamps in all the 00541 // course categories between CONTEXT_SYSTEM and CONTEXT_COURSE 00542 // (i.e. in all the course categories in the path) 00543 // 00544 // And only will return one "best" target context if all the 00545 // matches belong to ONE and ONLY ONE context. If multiple 00546 // matches are found, that means that there is some annoying 00547 // qbank "fragmentation" in the categories, so we'll fallback 00548 // to create the qbank at course level 00549 case CONTEXT_COURSECAT: 00550 // Build the array of stamps we are going to match 00551 $stamps = array(); 00552 foreach ($categories as $category) { 00553 $stamps[] = $category->stamp; 00554 } 00555 $contexts = array(); 00556 // Build the array of contexts we are going to look 00557 $systemctx = get_context_instance(CONTEXT_SYSTEM); 00558 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid); 00559 $parentctxs= get_parent_contexts($coursectx); 00560 foreach ($parentctxs as $parentctx) { 00561 // Exclude system context 00562 if ($parentctx == $systemctx->id) { 00563 continue; 00564 } 00565 $contexts[] = $parentctx; 00566 } 00567 if (!empty($stamps) && !empty($contexts)) { 00568 // Prepare the query 00569 list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps); 00570 list($context_sql, $context_params) = $DB->get_in_or_equal($contexts); 00571 $sql = "SELECT contextid 00572 FROM {question_categories} 00573 WHERE stamp $stamp_sql 00574 AND contextid $context_sql"; 00575 $params = array_merge($stamp_params, $context_params); 00576 $matchingcontexts = $DB->get_records_sql($sql, $params); 00577 // Only if ONE and ONLY ONE context is found, use it as valid target 00578 if (count($matchingcontexts) == 1) { 00579 $targetcontext = get_context_instance_by_id(reset($matchingcontexts)->contextid); 00580 } 00581 } 00582 break; 00583 00584 // For course is easy, the best context is the course context 00585 case CONTEXT_COURSE: 00586 $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid); 00587 break; 00588 00589 // For module is easy, there is not best context, as far as the 00590 // activity hasn't been created yet. So we return context course 00591 // for them, so permission checks and friends will work. Note this 00592 // case is handled by {@link prechek_precheck_qbanks_by_level} 00593 // in an special way 00594 case CONTEXT_MODULE: 00595 $targetcontext = get_context_instance(CONTEXT_COURSE, $courseid); 00596 break; 00597 } 00598 return $targetcontext; 00599 } 00600 00605 public static function restore_get_questions($restoreid, $qcatid) { 00606 global $DB; 00607 00608 $results = array(); 00609 $qs = $DB->get_records_sql("SELECT itemid 00610 FROM {backup_ids_temp} 00611 WHERE backupid = ? 00612 AND itemname = 'question' 00613 AND parentitemid = ?", array($restoreid, $qcatid)); 00614 foreach ($qs as $q) { 00615 $temprec = self::get_backup_ids_record($restoreid, 'question', $q->itemid); 00616 $results[$q->itemid] = $temprec->info; 00617 } 00618 return $results; 00619 } 00620 00626 public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) { 00627 global $DB; 00628 00629 if ($forcenewcontextid) { 00630 // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings, 00631 // with questions originally at system/coursecat context in source being restored to course context in target). So we need 00632 // to be able to force the new contextid 00633 $newcontextid = $forcenewcontextid; 00634 } else { 00635 // Get new context, must exist or this will fail 00636 if (!$newcontextid = self::get_backup_ids_record($restoreid, 'context', $oldcontextid)->newitemid) { 00637 throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid); 00638 } 00639 } 00640 00641 // Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid 00642 // columns (because we have used them to store other information). This happens usually with 00643 // all the question related backup_ids_temp records. In that case, it's safe to ignore that 00644 // matching as far as we are always restoring for well known oldcontexts and olditemids 00645 $parentitemctxmatchsql = ' AND i.parentitemid = f.contextid '; 00646 if ($skipparentitemidctxmatch) { 00647 $parentitemctxmatchsql = ''; 00648 } 00649 00650 // Important: remember how files have been loaded to backup_files_temp 00651 // - info: contains the whole original object (times, names...) 00652 // (all them being original ids as loaded from xml) 00653 00654 // itemname = null, we are going to match only by context, no need to use itemid (all them are 0) 00655 if ($itemname == null) { 00656 $sql = 'SELECT contextid, component, filearea, itemid, itemid AS newitemid, info 00657 FROM {backup_files_temp} 00658 WHERE backupid = ? 00659 AND contextid = ? 00660 AND component = ? 00661 AND filearea = ?'; 00662 $params = array($restoreid, $oldcontextid, $component, $filearea); 00663 00664 // itemname not null, going to join with backup_ids to perform the old-new mapping of itemids 00665 } else { 00666 $sql = "SELECT f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info 00667 FROM {backup_files_temp} f 00668 JOIN {backup_ids_temp} i ON i.backupid = f.backupid 00669 $parentitemctxmatchsql 00670 AND i.itemid = f.itemid 00671 WHERE f.backupid = ? 00672 AND f.contextid = ? 00673 AND f.component = ? 00674 AND f.filearea = ? 00675 AND i.itemname = ?"; 00676 $params = array($restoreid, $oldcontextid, $component, $filearea, $itemname); 00677 if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname 00678 $sql .= ' AND i.itemid = ?'; 00679 $params[] = $olditemid; 00680 } 00681 } 00682 00683 $fs = get_file_storage(); // Get moodle file storage 00684 $basepath = $basepath . '/files/';// Get backup file pool base 00685 $rs = $DB->get_recordset_sql($sql, $params); 00686 foreach ($rs as $rec) { 00687 $file = (object)unserialize(base64_decode($rec->info)); 00688 // ignore root dirs (they are created automatically) 00689 if ($file->filepath == '/' && $file->filename == '.') { 00690 continue; 00691 } 00692 // set the best possible user 00693 $mappeduser = self::get_backup_ids_record($restoreid, 'user', $file->userid); 00694 $file->userid = !empty($mappeduser) ? $mappeduser->newitemid : $dfltuserid; 00695 // dir found (and not root one), let's create if 00696 if ($file->filename == '.') { 00697 $fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->userid); 00698 continue; 00699 } 00700 // arrived here, file found 00701 // Find file in backup pool 00702 $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash); 00703 if (!file_exists($backuppath)) { 00704 throw new restore_dbops_exception('file_not_found_in_pool', $file); 00705 } 00706 if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) { 00707 $file_record = array( 00708 'contextid' => $newcontextid, 00709 'component' => $component, 00710 'filearea' => $filearea, 00711 'itemid' => $rec->newitemid, 00712 'filepath' => $file->filepath, 00713 'filename' => $file->filename, 00714 'timecreated' => $file->timecreated, 00715 'timemodified'=> $file->timemodified, 00716 'userid' => $file->userid, 00717 'author' => $file->author, 00718 'license' => $file->license, 00719 'sortorder' => $file->sortorder); 00720 $fs->create_file_from_pathname($file_record, $backuppath); 00721 } 00722 } 00723 $rs->close(); 00724 } 00725 00733 public static function create_included_users($basepath, $restoreid, $userfiles, $userid) { 00734 global $CFG, $DB; 00735 00736 $authcache = array(); // Cache to get some bits from authentication plugins 00737 $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search later 00738 $themes = get_list_of_themes(); // Get themes for quick search later 00739 00740 // Iterate over all the included users with newitemid = 0, have to create them 00741 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid'); 00742 foreach ($rs as $recuser) { 00743 $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info; 00744 00745 // if user lang doesn't exist here, use site default 00746 if (!array_key_exists($user->lang, $languages)) { 00747 $user->lang = $CFG->lang; 00748 } 00749 00750 // if user theme isn't available on target site or they are disabled, reset theme 00751 if (!empty($user->theme)) { 00752 if (empty($CFG->allowuserthemes) || !in_array($user->theme, $themes)) { 00753 $user->theme = ''; 00754 } 00755 } 00756 00757 // if user to be created has mnet auth and its mnethostid is $CFG->mnet_localhost_id 00758 // that's 100% impossible as own server cannot be accesed over mnet. Change auth to email/manual 00759 if ($user->auth == 'mnet' && $user->mnethostid == $CFG->mnet_localhost_id) { 00760 // Respect registerauth 00761 if ($CFG->registerauth == 'email') { 00762 $user->auth = 'email'; 00763 } else { 00764 $user->auth = 'manual'; 00765 } 00766 } 00767 unset($user->mnethosturl); // Not needed anymore 00768 00769 // Disable pictures based on global setting 00770 if (!empty($CFG->disableuserimages)) { 00771 $user->picture = 0; 00772 } 00773 00774 // We need to analyse the AUTH field to recode it: 00775 // - if the auth isn't enabled in target site, $CFG->registerauth will decide 00776 // - finally, if the auth resulting isn't enabled, default to 'manual' 00777 if (!is_enabled_auth($user->auth)) { 00778 if ($CFG->registerauth == 'email') { 00779 $user->auth = 'email'; 00780 } else { 00781 $user->auth = 'manual'; 00782 } 00783 } 00784 if (!is_enabled_auth($user->auth)) { // Final auth check verify, default to manual if not enabled 00785 $user->auth = 'manual'; 00786 } 00787 00788 // Now that we know the auth method, for users to be created without pass 00789 // if password handling is internal and reset password is available 00790 // we set the password to "restored" (plain text), so the login process 00791 // will know how to handle that situation in order to allow the user to 00792 // recover the password. MDL-20846 00793 if (empty($user->password)) { // Only if restore comes without password 00794 if (!array_key_exists($user->auth, $authcache)) { // Not in cache 00795 $userauth = new stdClass(); 00796 $authplugin = get_auth_plugin($user->auth); 00797 $userauth->preventpassindb = $authplugin->prevent_local_passwords(); 00798 $userauth->isinternal = $authplugin->is_internal(); 00799 $userauth->canresetpwd = $authplugin->can_reset_password(); 00800 $authcache[$user->auth] = $userauth; 00801 } else { 00802 $userauth = $authcache[$user->auth]; // Get from cache 00803 } 00804 00805 // Most external plugins do not store passwords locally 00806 if (!empty($userauth->preventpassindb)) { 00807 $user->password = 'not cached'; 00808 00809 // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark 00810 } else if ($userauth->isinternal and $userauth->canresetpwd) { 00811 $user->password = 'restored'; 00812 } 00813 } 00814 00815 // Creating new user, we must reset the policyagreed always 00816 $user->policyagreed = 0; 00817 00818 // Set time created if empty 00819 if (empty($user->timecreated)) { 00820 $user->timecreated = time(); 00821 } 00822 00823 // Done, let's create the user and annotate its id 00824 $newuserid = $DB->insert_record('user', $user); 00825 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $newuserid); 00826 // Let's create the user context and annotate it (we need it for sure at least for files) 00827 // but for deleted users that don't have a context anymore (MDL-30192). We are done for them 00828 // and nothing else (custom fields, prefs, tags, files...) will be created. 00829 if (empty($user->deleted)) { 00830 $newuserctxid = $user->deleted ? 0 : get_context_instance(CONTEXT_USER, $newuserid)->id; 00831 self::set_backup_ids_record($restoreid, 'context', $recuser->parentitemid, $newuserctxid); 00832 00833 // Process custom fields 00834 if (isset($user->custom_fields)) { // if present in backup 00835 foreach($user->custom_fields['custom_field'] as $udata) { 00836 $udata = (object)$udata; 00837 // If the profile field has data and the profile shortname-datatype is defined in server 00838 if ($udata->field_data) { 00839 if ($field = $DB->get_record('user_info_field', array('shortname'=>$udata->field_name, 'datatype'=>$udata->field_type))) { 00841 $rec = new stdClass(); 00842 $rec->userid = $newuserid; 00843 $rec->fieldid = $field->id; 00844 $rec->data = $udata->field_data; 00845 $DB->insert_record('user_info_data', $rec); 00846 } 00847 } 00848 } 00849 } 00850 00851 // Process tags 00852 if (!empty($CFG->usetags) && isset($user->tags)) { // if enabled in server and present in backup 00853 $tags = array(); 00854 foreach($user->tags['tag'] as $usertag) { 00855 $usertag = (object)$usertag; 00856 $tags[] = $usertag->rawname; 00857 } 00858 tag_set('user', $newuserid, $tags); 00859 } 00860 00861 // Process preferences 00862 if (isset($user->preferences)) { // if present in backup 00863 foreach($user->preferences['preference'] as $preference) { 00864 $preference = (object)$preference; 00865 // Prepare the record and insert it 00866 $preference->userid = $newuserid; 00867 $status = $DB->insert_record('user_preferences', $preference); 00868 } 00869 } 00870 00871 // Create user files in pool (profile, icon, private) by context 00872 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'icon', $recuser->parentitemid, $userid); 00873 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'profile', $recuser->parentitemid, $userid); 00874 if ($userfiles) { // private files only if enabled in settings 00875 restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'private', $recuser->parentitemid, $userid); 00876 } 00877 00878 } 00879 } 00880 $rs->close(); 00881 } 00882 00934 protected static function precheck_user($user, $samesite) { 00935 global $CFG, $DB; 00936 00937 // Handle checks from same site backups 00938 if ($samesite && empty($CFG->forcedifferentsitecheckingusersonrestore)) { 00939 00940 // 1A - If match by id and username and mnethost => ok, return target user 00941 if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) { 00942 return $rec; // Matching user found, return it 00943 } 00944 00945 // 1B - Handle users deleted in DB and "alive" in backup file 00946 // Note: for DB deleted users email is stored in username field, hence we 00947 // are looking there for emails. See delete_user() 00948 // Note: for DB deleted users md5(username) is stored *sometimes* in the email field, 00949 // hence we are looking there for usernames if not empty. See delete_user() 00950 // If match by id and mnethost and user is deleted in DB and 00951 // match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user 00952 if ($rec = $DB->get_record_sql("SELECT * 00953 FROM {user} u 00954 WHERE id = ? 00955 AND mnethostid = ? 00956 AND deleted = 1 00957 AND ( 00958 UPPER(username) LIKE UPPER(?) 00959 OR ( 00960 ".$DB->sql_isnotempty('user', 'email', false, false)." 00961 AND email = ? 00962 ) 00963 )", 00964 array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) { 00965 return $rec; // Matching user, deleted in DB found, return it 00966 } 00967 00968 // 1C - Handle users deleted in backup file and "alive" in DB 00969 // If match by id and mnethost and user is deleted in backup file 00970 // and match by email = email_without_time(backup_email) => ok, return target user 00971 if ($user->deleted) { 00972 // Note: for DB deleted users email is stored in username field, hence we 00973 // are looking there for emails. See delete_user() 00974 // Trim time() from email 00975 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username); 00976 if ($rec = $DB->get_record_sql("SELECT * 00977 FROM {user} u 00978 WHERE id = ? 00979 AND mnethostid = ? 00980 AND UPPER(email) = UPPER(?)", 00981 array($user->id, $user->mnethostid, $trimemail))) { 00982 return $rec; // Matching user, deleted in backup file found, return it 00983 } 00984 } 00985 00986 // 1D - If match by username and mnethost and doesn't match by id => conflict, return false 00987 if ($rec = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid))) { 00988 if ($user->id != $rec->id) { 00989 return false; // Conflict, username already exists and belongs to another id 00990 } 00991 } 00992 00993 // Handle checks from different site backups 00994 } else { 00995 00996 // 2A - If match by username and mnethost and 00997 // (email or non-zero firstaccess) => ok, return target user 00998 if ($rec = $DB->get_record_sql("SELECT * 00999 FROM {user} u 01000 WHERE username = ? 01001 AND mnethostid = ? 01002 AND ( 01003 UPPER(email) = UPPER(?) 01004 OR ( 01005 firstaccess != 0 01006 AND firstaccess = ? 01007 ) 01008 )", 01009 array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) { 01010 return $rec; // Matching user found, return it 01011 } 01012 01013 // 2B - Handle users deleted in DB and "alive" in backup file 01014 // Note: for DB deleted users email is stored in username field, hence we 01015 // are looking there for emails. See delete_user() 01016 // Note: for DB deleted users md5(username) is stored *sometimes* in the email field, 01017 // hence we are looking there for usernames if not empty. See delete_user() 01018 // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and 01019 // (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user 01020 if ($rec = $DB->get_record_sql("SELECT * 01021 FROM {user} u 01022 WHERE mnethostid = ? 01023 AND deleted = 1 01024 AND ".$DB->sql_isnotempty('user', 'email', false, false)." 01025 AND email = ? 01026 AND ( 01027 UPPER(username) LIKE UPPER(?) 01028 OR ( 01029 firstaccess != 0 01030 AND firstaccess = ? 01031 ) 01032 )", 01033 array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) { 01034 return $rec; // Matching user found, return it 01035 } 01036 01037 // 2B2 - If match by mnethost and user is deleted in DB and 01038 // username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user 01039 // (this covers situations where md5(username) wasn't being stored so we require both 01040 // the email & non-zero firstaccess to match) 01041 if ($rec = $DB->get_record_sql("SELECT * 01042 FROM {user} u 01043 WHERE mnethostid = ? 01044 AND deleted = 1 01045 AND UPPER(username) LIKE UPPER(?) 01046 AND firstaccess != 0 01047 AND firstaccess = ?", 01048 array($user->mnethostid, $user->email.'.%', $user->firstaccess))) { 01049 return $rec; // Matching user found, return it 01050 } 01051 01052 // 2C - Handle users deleted in backup file and "alive" in DB 01053 // If match mnethost and user is deleted in backup file 01054 // and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user 01055 if ($user->deleted) { 01056 // Note: for DB deleted users email is stored in username field, hence we 01057 // are looking there for emails. See delete_user() 01058 // Trim time() from email 01059 $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username); 01060 if ($rec = $DB->get_record_sql("SELECT * 01061 FROM {user} u 01062 WHERE mnethostid = ? 01063 AND UPPER(email) = UPPER(?) 01064 AND firstaccess != 0 01065 AND firstaccess = ?", 01066 array($user->mnethostid, $trimemail, $user->firstaccess))) { 01067 return $rec; // Matching user, deleted in backup file found, return it 01068 } 01069 } 01070 01071 // 2D - If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false 01072 if ($rec = $DB->get_record_sql("SELECT * 01073 FROM {user} u 01074 WHERE username = ? 01075 AND mnethostid = ? 01076 AND NOT ( 01077 UPPER(email) = UPPER(?) 01078 OR ( 01079 firstaccess != 0 01080 AND firstaccess = ? 01081 ) 01082 )", 01083 array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) { 01084 return false; // Conflict, username/mnethostid already exist and belong to another user (by email/firstaccess) 01085 } 01086 } 01087 01088 // Arrived here, return true as the user will need to be created and no 01089 // conflicts have been found in the logic above. This covers: 01090 // 1E - else => user needs to be created, return true 01091 // 2E - else => user needs to be created, return true 01092 return true; 01093 } 01094 01101 public static function precheck_included_users($restoreid, $courseid, $userid, $samesite) { 01102 global $CFG, $DB; 01103 01104 // To return any problem found 01105 $problems = array(); 01106 01107 // We are going to map mnethostid, so load all the available ones 01108 $mnethosts = $DB->get_records('mnet_host', array(), 'wwwroot', 'wwwroot, id'); 01109 01110 // Calculate the context we are going to use for capability checking 01111 $context = get_context_instance(CONTEXT_COURSE, $courseid); 01112 01113 // Calculate if we have perms to create users, by checking: 01114 // to 'moodle/restore:createuser' and 'moodle/restore:userinfo' 01115 // and also observe $CFG->disableusercreationonrestore 01116 $cancreateuser = false; 01117 if (has_capability('moodle/restore:createuser', $context, $userid) and 01118 has_capability('moodle/restore:userinfo', $context, $userid) and 01119 empty($CFG->disableusercreationonrestore)) { // Can create users 01120 01121 $cancreateuser = true; 01122 } 01123 01124 // Iterate over all the included users 01125 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user'), '', 'itemid'); 01126 foreach ($rs as $recuser) { 01127 $user = (object)self::get_backup_ids_record($restoreid, 'user', $recuser->itemid)->info; 01128 01129 // Find the correct mnethostid for user before performing any further check 01130 if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) { 01131 $user->mnethostid = $CFG->mnet_localhost_id; 01132 } else { 01133 // fast url-to-id lookups 01134 if (isset($mnethosts[$user->mnethosturl])) { 01135 $user->mnethostid = $mnethosts[$user->mnethosturl]->id; 01136 } else { 01137 $user->mnethostid = $CFG->mnet_localhost_id; 01138 } 01139 } 01140 01141 // Now, precheck that user and, based on returned results, annotate action/problem 01142 $usercheck = self::precheck_user($user, $samesite); 01143 01144 if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to 01145 // Annotate it, for later process. Set newitemid to mapping user->id 01146 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $usercheck->id); 01147 01148 } else if ($usercheck === false) { // Found conflict, report it as problem 01149 $problems[] = get_string('restoreuserconflict', '', $user->username); 01150 01151 } else if ($usercheck === true) { // User needs to be created, check if we are able 01152 if ($cancreateuser) { // Can create user, set newitemid to 0 so will be created later 01153 self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user); 01154 01155 } else { // Cannot create user, report it as problem 01156 $problems[] = get_string('restorecannotcreateuser', '', $user->username); 01157 } 01158 01159 } else { // Shouldn't arrive here ever, something is for sure wrong. Exception 01160 throw new restore_dbops_exception('restore_error_processing_user', $user->username); 01161 } 01162 } 01163 $rs->close(); 01164 return $problems; 01165 } 01166 01174 public static function process_included_users($restoreid, $courseid, $userid, $samesite) { 01175 global $DB; 01176 01177 // Just let precheck_included_users() to do all the hard work 01178 $problems = self::precheck_included_users($restoreid, $courseid, $userid, $samesite); 01179 01180 // With problems, throw exception, shouldn't happen if prechecks were originally 01181 // executed, so be radical here. 01182 if (!empty($problems)) { 01183 throw new restore_dbops_exception('restore_problems_processing_users', null, implode(', ', $problems)); 01184 } 01185 } 01186 01195 public static function process_categories_and_questions($restoreid, $courseid, $userid, $samesite) { 01196 global $DB; 01197 01198 // Just let precheck_included_users() to do all the hard work 01199 $problems = self::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite); 01200 01201 // With problems of type error, throw exception, shouldn't happen if prechecks were originally 01202 // executed, so be radical here. 01203 if (array_key_exists('errors', $problems)) { 01204 throw new restore_dbops_exception('restore_problems_processing_questions', null, implode(', ', $problems['errors'])); 01205 } 01206 } 01207 01208 public static function set_backup_files_record($restoreid, $filerec) { 01209 global $DB; 01210 01211 $filerec->info = base64_encode(serialize($filerec)); // Serialize the whole rec in info 01212 $filerec->backupid = $restoreid; 01213 $DB->insert_record('backup_files_temp', $filerec); 01214 } 01215 01216 01217 public static function set_backup_ids_record($restoreid, $itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) { 01218 global $DB; 01219 01220 // Build the basic (mandatory) record info 01221 $record = array( 01222 'backupid' => $restoreid, 01223 'itemname' => $itemname, 01224 'itemid' => $itemid 01225 ); 01226 // Build conditionally the extra record info 01227 $extrarecord = array(); 01228 if ($newitemid != 0) { 01229 $extrarecord['newitemid'] = $newitemid; 01230 } 01231 if ($parentitemid != null) { 01232 $extrarecord['parentitemid'] = $parentitemid; 01233 } 01234 if ($info != null) { 01235 $extrarecord['info'] = base64_encode(serialize($info)); 01236 } 01237 01238 // TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of get_record() calls 01239 // Note: Sure it will! And also will improve getter 01240 if (!$dbrec = $DB->get_record('backup_ids_temp', $record)) { // Need to insert the complete record 01241 $DB->insert_record('backup_ids_temp', array_merge($record, $extrarecord)); 01242 01243 } else { // Need to update the extra record info if there is something to 01244 if (!empty($extrarecord)) { 01245 $extrarecord['id'] = $dbrec->id; 01246 $DB->update_record('backup_ids_temp', $extrarecord); 01247 } 01248 } 01249 } 01250 01251 public static function get_backup_ids_record($restoreid, $itemname, $itemid) { 01252 global $DB; 01253 01254 // Build the basic (mandatory) record info to look for 01255 $record = array( 01256 'backupid' => $restoreid, 01257 'itemname' => $itemname, 01258 'itemid' => $itemid 01259 ); 01260 // TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of get_record() calls 01261 if ($dbrec = $DB->get_record('backup_ids_temp', $record)) { 01262 if ($dbrec->info != null) { 01263 $dbrec->info = unserialize(base64_decode($dbrec->info)); 01264 } 01265 } 01266 return $dbrec; 01267 } 01268 01272 public static function calculate_course_names($courseid, $fullname, $shortname) { 01273 global $CFG, $DB; 01274 01275 $currentfullname = ''; 01276 $currentshortname = ''; 01277 $counter = 0; 01278 // Iteratere while the name exists 01279 do { 01280 if ($counter) { 01281 $suffixfull = ' ' . get_string('copyasnoun') . ' ' . $counter; 01282 $suffixshort = '_' . $counter; 01283 } else { 01284 $suffixfull = ''; 01285 $suffixshort = ''; 01286 } 01287 $currentfullname = $fullname.$suffixfull; 01288 $currentshortname = substr($shortname, 0, 100 - strlen($suffixshort)).$suffixshort; // < 100cc 01289 $coursefull = $DB->get_record_select('course', 'fullname = ? AND id != ?', array($currentfullname, $courseid)); 01290 $courseshort = $DB->get_record_select('course', 'shortname = ? AND id != ?', array($currentshortname, $courseid)); 01291 $counter++; 01292 } while ($coursefull || $courseshort); 01293 01294 // Return results 01295 return array($currentfullname, $currentshortname); 01296 } 01297 01301 public static function set_course_role_names($restoreid, $courseid) { 01302 global $DB; 01303 01304 // Get the course context 01305 $coursectx = get_context_instance(CONTEXT_COURSE, $courseid); 01306 // Get all the mapped roles we have 01307 $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid'); 01308 foreach ($rs as $recrole) { 01309 // Get the complete temp_ids record 01310 $role = (object)self::get_backup_ids_record($restoreid, 'role', $recrole->itemid); 01311 // If it's one mapped role and we have one name for it 01312 if (!empty($role->newitemid) && !empty($role->info['nameincourse'])) { 01313 // If role name doesn't exist, add it 01314 $rolename = new stdclass(); 01315 $rolename->roleid = $role->newitemid; 01316 $rolename->contextid = $coursectx->id; 01317 if (!$DB->record_exists('role_names', (array)$rolename)) { 01318 $rolename->name = $role->info['nameincourse']; 01319 $DB->insert_record('role_names', $rolename); 01320 } 01321 } 01322 } 01323 $rs->close(); 01324 } 01325 01336 public static function create_new_course($fullname, $shortname, $categoryid) { 01337 global $DB; 01338 $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST); 01339 01340 $course = new stdClass; 01341 $course->fullname = $fullname; 01342 $course->shortname = $shortname; 01343 $course->category = $category->id; 01344 $course->sortorder = 0; 01345 $course->timecreated = time(); 01346 $course->timemodified = $course->timecreated; 01347 // forcing skeleton courses to be hidden instead of going by $category->visible , until MDL-27790 is resolved. 01348 $course->visible = 0; 01349 01350 $courseid = $DB->insert_record('course', $course); 01351 01352 $category->coursecount++; 01353 $DB->update_record('course_categories', $category); 01354 01355 return $courseid; 01356 } 01357 01364 public static function delete_course_content($courseid, array $options = null) { 01365 return remove_course_contents($courseid, false, $options); 01366 } 01367 } 01368 01369 /* 01370 * Exception class used by all the @dbops stuff 01371 */ 01372 class restore_dbops_exception extends backup_exception { 01373 01374 public function __construct($errorcode, $a=NULL, $debuginfo=null) { 01375 parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); 01376 } 01377 }