first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,716 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Non instantiable helper class providing DB support to the @backup_controller
*
* This class contains various static methods available for all the DB operations
* performed by the backup_controller class
*
* TODO: Finish phpdocs
*/
abstract class backup_controller_dbops extends backup_dbops {
/**
* @var string Backup id for cached backup_includes_files result.
*/
protected static $includesfilescachebackupid;
/**
* @var int Cached backup_includes_files result
*/
protected static $includesfilescache;
/**
* Send one backup controller to DB
*
* @param backup_controller $controller controller to send to DB
* @param string $checksum hash of the controller to be checked
* @param bool $includeobj to decide if the object itself must be updated (true) or no (false)
* @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false)
* @return int id of the controller record in the DB
* @throws backup_controller_exception|backup_dbops_exception
*/
public static function save_controller($controller, $checksum, $includeobj = true, $cleanobj = false) {
global $DB;
// Check we are going to save one backup_controller
if (! $controller instanceof backup_controller) {
throw new backup_controller_exception('backup_controller_expected');
}
// Check checksum is ok. Only if we are including object info. Sounds silly but it isn't ;-).
if ($includeobj and !$controller->is_checksum_correct($checksum)) {
throw new backup_dbops_exception('backup_controller_dbops_saving_checksum_mismatch');
}
// Cannot request to $includeobj and $cleanobj at the same time.
if ($includeobj and $cleanobj) {
throw new backup_dbops_exception('backup_controller_dbops_saving_cannot_include_and_delete');
}
// Get all the columns
$rec = new stdclass();
$rec->backupid = $controller->get_backupid();
$rec->operation = $controller->get_operation();
$rec->type = $controller->get_type();
$rec->itemid = $controller->get_id();
$rec->format = $controller->get_format();
$rec->interactive = $controller->get_interactive();
$rec->purpose = $controller->get_mode();
$rec->userid = $controller->get_userid();
$rec->status = $controller->get_status();
$rec->execution = $controller->get_execution();
$rec->executiontime= $controller->get_executiontime();
$rec->checksum = $checksum;
// Serialize information
if ($includeobj) {
$rec->controller = base64_encode(serialize($controller));
} else if ($cleanobj) {
$rec->controller = '';
}
// Send it to DB
if ($recexists = $DB->get_record('backup_controllers', array('backupid' => $rec->backupid))) {
$rec->id = $recexists->id;
$rec->timemodified = time();
$DB->update_record('backup_controllers', $rec);
} else {
$rec->timecreated = time();
$rec->timemodified = 0;
$rec->id = $DB->insert_record('backup_controllers', $rec);
}
return $rec->id;
}
public static function load_controller($backupid) {
global $DB;
if (! $controllerrec = $DB->get_record('backup_controllers', array('backupid' => $backupid))) {
throw new backup_dbops_exception('backup_controller_dbops_nonexisting');
}
$controller = unserialize(base64_decode($controllerrec->controller));
if (!is_object($controller)) {
// The controller field of the table did not contain a serialized object.
// It is made empty after it has been used successfully, it is likely that
// the user has pressed the browser back button at some point.
throw new backup_dbops_exception('backup_controller_dbops_loading_invalid_controller');
}
// Check checksum is ok. Sounds silly but it isn't ;-)
if (!$controller->is_checksum_correct($controllerrec->checksum)) {
throw new backup_dbops_exception('backup_controller_dbops_loading_checksum_mismatch');
}
return $controller;
}
public static function create_backup_ids_temp_table($backupid) {
global $CFG, $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services
$xmldb_table = new xmldb_table('backup_ids_temp');
$xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
// Set default backupid (not needed but this enforce any missing backupid). That's hackery in action!
$xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid);
$xmldb_table->add_field('itemname', XMLDB_TYPE_CHAR, 160, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
$xmldb_table->add_field('parentitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
$xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null);
$xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$xmldb_table->add_key('backupid_itemname_itemid_uk', XMLDB_KEY_UNIQUE, array('backupid','itemname','itemid'));
$xmldb_table->add_index('backupid_parentitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','parentitemid'));
$xmldb_table->add_index('backupid_itemname_newitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','newitemid'));
$dbman->create_temp_table($xmldb_table); // And create it
}
public static function create_backup_files_temp_table($backupid) {
global $CFG, $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services
$xmldb_table = new xmldb_table('backup_files_temp');
$xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
// Set default backupid (not needed but this enforce any missing backupid). That's hackery in action!
$xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid);
$xmldb_table->add_field('contextid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('component', XMLDB_TYPE_CHAR, 100, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('filearea', XMLDB_TYPE_CHAR, 50, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
$xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null);
$xmldb_table->add_field('newcontextid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
$xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
$xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$xmldb_table->add_index('backupid_contextid_component_filearea_itemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','contextid','component','filearea','itemid'));
$dbman->create_temp_table($xmldb_table); // And create it
}
public static function drop_backup_ids_temp_table($backupid) {
global $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services
$targettablename = 'backup_ids_temp';
if ($dbman->table_exists($targettablename)) {
$table = new xmldb_table($targettablename);
$dbman->drop_table($table); // And drop it
}
}
/**
* Decode the info field from backup_ids_temp or backup_files_temp.
*
* @param mixed $info The info field data to decode, may be an object or a simple integer.
* @return mixed The decoded information. For simple types it returns, for complex ones we decode.
*/
public static function decode_backup_temp_info($info) {
// We encode all data except null.
if ($info != null) {
return unserialize(gzuncompress(base64_decode($info)));
}
return $info;
}
/**
* Encode the info field for backup_ids_temp or backup_files_temp.
*
* @param mixed $info string The info field data to encode.
* @return string An encoded string of data or null if the input is null.
*/
public static function encode_backup_temp_info($info) {
// We encode if there is any information to keep the translations simpler.
if ($info != null) {
// We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
// Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
return base64_encode(gzcompress(serialize($info), 1));
}
return $info;
}
/**
* Given one type and id from controller, return the corresponding courseid
*/
public static function get_courseid_from_type_id($type, $id) {
global $DB;
if ($type == backup::TYPE_1COURSE) {
return $id; // id is the course id
} else if ($type == backup::TYPE_1SECTION) {
if (! $courseid = $DB->get_field('course_sections', 'course', array('id' => $id))) {
throw new backup_dbops_exception('course_not_found_for_section', $id);
}
return $courseid;
} else if ($type == backup::TYPE_1ACTIVITY) {
if (! $courseid = $DB->get_field('course_modules', 'course', array('id' => $id))) {
throw new backup_dbops_exception('course_not_found_for_moduleid', $id);
}
return $courseid;
}
}
/**
* Given one activity task, return the activity information and related settings
* Used by get_moodle_backup_information()
*/
private static function get_activity_backup_information($task) {
$contentinfo = array(
'moduleid' => $task->get_moduleid(),
'sectionid' => $task->get_sectionid(),
'modulename' => $task->get_modulename(),
'title' => $task->get_name(),
'directory' => 'activities/' . $task->get_modulename() . '_' . $task->get_moduleid());
// Now get activity settings
// Calculate prefix to find valid settings
$prefix = basename($contentinfo['directory']);
$settingsinfo = array();
foreach ($task->get_settings() as $setting) {
// Discard ones without valid prefix
if (strpos($setting->get_name(), $prefix) !== 0) {
continue;
}
// Validate level is correct (activity)
if ($setting->get_level() != backup_setting::ACTIVITY_LEVEL) {
throw new backup_controller_exception('setting_not_activity_level', $setting);
}
$settinginfo = array(
'level' => 'activity',
'activity' => $prefix,
'name' => $setting->get_name(),
'value' => $setting->get_value());
$settingsinfo[$setting->get_name()] = (object)$settinginfo;
}
return array($contentinfo, $settingsinfo);
}
/**
* Given one section task, return the section information and related settings
* Used by get_moodle_backup_information()
*/
private static function get_section_backup_information($task) {
$contentinfo = array(
'sectionid' => $task->get_sectionid(),
'title' => $task->get_name(),
'directory' => 'sections/' . 'section_' . $task->get_sectionid());
// Now get section settings
// Calculate prefix to find valid settings
$prefix = basename($contentinfo['directory']);
$settingsinfo = array();
foreach ($task->get_settings() as $setting) {
// Discard ones without valid prefix
if (strpos($setting->get_name(), $prefix) !== 0) {
continue;
}
// Validate level is correct (section)
if ($setting->get_level() != backup_setting::SECTION_LEVEL) {
throw new backup_controller_exception('setting_not_section_level', $setting);
}
$settinginfo = array(
'level' => 'section',
'section' => $prefix,
'name' => $setting->get_name(),
'value' => $setting->get_value());
$settingsinfo[$setting->get_name()] = (object)$settinginfo;
}
return array($contentinfo, $settingsinfo);
}
/**
* Given one course task, return the course information and related settings
* Used by get_moodle_backup_information()
*/
private static function get_course_backup_information($task) {
$contentinfo = array(
'courseid' => $task->get_courseid(),
'title' => $task->get_name(),
'directory' => 'course');
// Now get course settings
// Calculate prefix to find valid settings
$prefix = basename($contentinfo['directory']);
$settingsinfo = array();
foreach ($task->get_settings() as $setting) {
// Discard ones without valid prefix
if (strpos($setting->get_name(), $prefix) !== 0) {
continue;
}
// Validate level is correct (course)
if ($setting->get_level() != backup_setting::COURSE_LEVEL) {
throw new backup_controller_exception('setting_not_course_level', $setting);
}
$settinginfo = array(
'level' => 'course',
'name' => $setting->get_name(),
'value' => $setting->get_value());
$settingsinfo[$setting->get_name()] = (object)$settinginfo;
}
return array($contentinfo, $settingsinfo);
}
/**
* Given one root task, return the course information and related settings
* Used by get_moodle_backup_information()
*/
private static function get_root_backup_information($task) {
// Now get root settings
$settingsinfo = array();
foreach ($task->get_settings() as $setting) {
// Validate level is correct (root)
if ($setting->get_level() != backup_setting::ROOT_LEVEL) {
throw new backup_controller_exception('setting_not_root_level', $setting);
}
$settinginfo = array(
'level' => 'root',
'name' => $setting->get_name(),
'value' => $setting->get_value());
$settingsinfo[$setting->get_name()] = (object)$settinginfo;
}
return array(null, $settingsinfo);
}
/**
* Get details information for main moodle_backup.xml file, extracting it from
* the specified controller.
*
* If you specify the progress monitor, this will start a new progress section
* to track progress in processing (in case this task takes a long time).
*
* @param string $backupid Backup ID
* @param \core\progress\base $progress Optional progress monitor
*/
public static function get_moodle_backup_information($backupid,
\core\progress\base $progress = null) {
// Start tracking progress if required (for load_controller).
if ($progress) {
$progress->start_progress('get_moodle_backup_information', 2);
}
$detailsinfo = array(); // Information details
$contentsinfo= array(); // Information about backup contents
$settingsinfo= array(); // Information about backup settings
$bc = self::load_controller($backupid); // Load controller
// Note that we have loaded controller.
if ($progress) {
$progress->progress(1);
}
// Details info
$detailsinfo['id'] = $bc->get_id();
$detailsinfo['backup_id'] = $bc->get_backupid();
$detailsinfo['type'] = $bc->get_type();
$detailsinfo['format'] = $bc->get_format();
$detailsinfo['interactive'] = $bc->get_interactive();
$detailsinfo['mode'] = $bc->get_mode();
$detailsinfo['execution'] = $bc->get_execution();
$detailsinfo['executiontime'] = $bc->get_executiontime();
$detailsinfo['userid'] = $bc->get_userid();
$detailsinfo['courseid'] = $bc->get_courseid();
// Init content placeholders
$contentsinfo['activities'] = array();
$contentsinfo['sections'] = array();
$contentsinfo['course'] = array();
// Get tasks and start nested progress.
$tasks = $bc->get_plan()->get_tasks();
if ($progress) {
$progress->start_progress('get_moodle_backup_information', count($tasks));
$done = 1;
}
// Contents info (extract information from tasks)
foreach ($tasks as $task) {
if ($task instanceof backup_activity_task) { // Activity task
if ($task->get_setting_value('included')) { // Only return info about included activities
list($contentinfo, $settings) = self::get_activity_backup_information($task);
$contentsinfo['activities'][] = $contentinfo;
$settingsinfo = array_merge($settingsinfo, $settings);
}
} else if ($task instanceof backup_section_task) { // Section task
if ($task->get_setting_value('included')) { // Only return info about included sections
list($contentinfo, $settings) = self::get_section_backup_information($task);
$contentsinfo['sections'][] = $contentinfo;
$settingsinfo = array_merge($settingsinfo, $settings);
}
} else if ($task instanceof backup_course_task) { // Course task
list($contentinfo, $settings) = self::get_course_backup_information($task);
$contentsinfo['course'][] = $contentinfo;
$settingsinfo = array_merge($settingsinfo, $settings);
} else if ($task instanceof backup_root_task) { // Root task
list($contentinfo, $settings) = self::get_root_backup_information($task);
$settingsinfo = array_merge($settingsinfo, $settings);
}
// Report task handled.
if ($progress) {
$progress->progress($done++);
}
}
$bc->destroy(); // Always need to destroy controller to handle circular references
// Finish progress reporting.
if ($progress) {
$progress->end_progress();
$progress->end_progress();
}
return array(array((object)$detailsinfo), $contentsinfo, $settingsinfo);
}
/**
* Update CFG->backup_version and CFG->backup_release if change in
* version is detected.
*/
public static function apply_version_and_release() {
global $CFG;
if ($CFG->backup_version < backup::VERSION) {
set_config('backup_version', backup::VERSION);
set_config('backup_release', backup::RELEASE);
}
}
/**
* Given the backupid, detect if the backup includes "mnet" remote users or no
*/
public static function backup_includes_mnet_remote_users($backupid) {
global $CFG, $DB;
$sql = "SELECT COUNT(*)
FROM {backup_ids_temp} b
JOIN {user} u ON u.id = b.itemid
WHERE b.backupid = ?
AND b.itemname = 'userfinal'
AND u.mnethostid != ?";
$count = $DB->count_records_sql($sql, array($backupid, $CFG->mnet_localhost_id));
return (int)(bool)$count;
}
/**
* Given the backupid, determine whether this backup should include
* files from the moodle file storage system.
*
* @param string $backupid The ID of the backup.
* @return int Indicates whether files should be included in backups.
*/
public static function backup_includes_files($backupid) {
// This function is called repeatedly in a backup with many files.
// Loading the controller is a nontrivial operation (in a large test
// backup it took 0.3 seconds), so we do a temporary cache of it within
// this request.
if (self::$includesfilescachebackupid === $backupid) {
return self::$includesfilescache;
}
// Load controller, get value, then destroy controller and return result.
self::$includesfilescachebackupid = $backupid;
$bc = self::load_controller($backupid);
self::$includesfilescache = $bc->get_include_files();
$bc->destroy();
return self::$includesfilescache;
}
/**
* Given the backupid, detect if the backup contains references to external contents
*
* @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
* @return int
*/
public static function backup_includes_file_references($backupid) {
global $CFG, $DB;
$sql = "SELECT count(r.repositoryid)
FROM {files} f
LEFT JOIN {files_reference} r
ON r.id = f.referencefileid
JOIN {backup_ids_temp} bi
ON f.id = bi.itemid
WHERE bi.backupid = ?
AND bi.itemname = 'filefinal'";
$count = $DB->count_records_sql($sql, array($backupid));
return (int)(bool)$count;
}
/**
* Given the courseid, return some course related information we want to transport
*
* @param int $course the id of the course this backup belongs to
*/
public static function backup_get_original_course_info($courseid) {
global $DB;
return $DB->get_record('course', array('id' => $courseid), 'fullname, shortname, startdate, enddate, format');
}
/**
* Sets the default values for the settings in a backup operation
*
* Based on the mode of the backup it will load proper defaults
* using {@link apply_admin_config_defaults}.
*
* @param backup_controller $controller
*/
public static function apply_config_defaults(backup_controller $controller) {
// Based on the mode of the backup (general, automated, import, hub...)
// decide the action to perform to get defaults loaded
$mode = $controller->get_mode();
switch ($mode) {
case backup::MODE_GENERAL:
case backup::MODE_ASYNC:
// Load the general defaults
$settings = array(
'backup_general_users' => 'users',
'backup_general_anonymize' => 'anonymize',
'backup_general_role_assignments' => 'role_assignments',
'backup_general_activities' => 'activities',
'backup_general_blocks' => 'blocks',
'backup_general_filters' => 'filters',
'backup_general_comments' => 'comments',
'backup_general_badges' => 'badges',
'backup_general_calendarevents' => 'calendarevents',
'backup_general_userscompletion' => 'userscompletion',
'backup_general_logs' => 'logs',
'backup_general_histories' => 'grade_histories',
'backup_general_questionbank' => 'questionbank',
'backup_general_groups' => 'groups',
'backup_general_competencies' => 'competencies',
'backup_general_contentbankcontent' => 'contentbankcontent',
'backup_general_xapistate' => 'xapistate',
'backup_general_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, true);
break;
case backup::MODE_IMPORT:
// Load the import defaults.
$settings = array(
'backup_import_activities' => 'activities',
'backup_import_blocks' => 'blocks',
'backup_import_filters' => 'filters',
'backup_import_calendarevents' => 'calendarevents',
'backup_import_permissions' => 'permissions',
'backup_import_questionbank' => 'questionbank',
'backup_import_groups' => 'groups',
'backup_import_competencies' => 'competencies',
'backup_import_contentbankcontent' => 'contentbankcontent',
'backup_import_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, true);
if ((!$controller->get_interactive()) &&
$controller->get_type() == backup::TYPE_1ACTIVITY) {
// This is duplicate - there is no concept of defaults - these settings must be on.
$settings = array(
'activities',
'blocks',
'filters',
'questionbank'
);
self::force_enable_settings($controller, $settings);
}
break;
case backup::MODE_AUTOMATED:
// Load the automated defaults.
$settings = array(
'backup_auto_users' => 'users',
'backup_auto_role_assignments' => 'role_assignments',
'backup_auto_activities' => 'activities',
'backup_auto_blocks' => 'blocks',
'backup_auto_filters' => 'filters',
'backup_auto_comments' => 'comments',
'backup_auto_badges' => 'badges',
'backup_auto_calendarevents' => 'calendarevents',
'backup_auto_userscompletion' => 'userscompletion',
'backup_auto_logs' => 'logs',
'backup_auto_histories' => 'grade_histories',
'backup_auto_questionbank' => 'questionbank',
'backup_auto_groups' => 'groups',
'backup_auto_competencies' => 'competencies',
'backup_auto_contentbankcontent' => 'contentbankcontent',
'backup_auto_xapistate' => 'xapistate',
'backup_auto_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, false);
break;
default:
// Nothing to do for other modes (HUB...). Some day we
// can define defaults (admin UI...) for them if we want to
}
}
/**
* Turn these settings on. No defaults from admin settings.
*
* @param backup_controller $controller
* @param array $settings a map from admin config names to setting names (Config name => Setting name)
*/
private static function force_enable_settings(backup_controller $controller, array $settings) {
$plan = $controller->get_plan();
foreach ($settings as $config => $settingname) {
$value = true;
if ($plan->setting_exists($settingname)) {
$setting = $plan->get_setting($settingname);
// We do not allow this setting to be locked for a duplicate function.
if ($setting->get_status() !== base_setting::NOT_LOCKED) {
$setting->set_status(base_setting::NOT_LOCKED);
}
$setting->set_value($value);
$setting->set_status(base_setting::LOCKED_BY_CONFIG);
} else {
$controller->log('Unknown setting: ' . $setting, BACKUP::LOG_DEBUG);
}
}
}
/**
* Sets the controller settings default values from the admin config.
*
* @param backup_controller $controller
* @param array $settings a map from admin config names to setting names (Config name => Setting name)
* @param boolean $uselocks whether "locked" admin settings should be honoured
*/
private static function apply_admin_config_defaults(backup_controller $controller, array $settings, $uselocks) {
$plan = $controller->get_plan();
foreach ($settings as $config=>$settingname) {
$value = get_config('backup', $config);
if ($value === false) {
// Ignore this because the config has not been set. get_config
// returns false if a setting doesn't exist, '0' is returned when
// the configuration is set to false.
$controller->log('Could not find a value for the config ' . $config, BACKUP::LOG_DEBUG);
continue;
}
$locked = $uselocks && (get_config('backup', $config.'_locked') == true);
if ($plan->setting_exists($settingname)) {
$setting = $plan->get_setting($settingname);
// We can only update the setting if it isn't already locked by config or permission.
if ($setting->get_status() !== base_setting::LOCKED_BY_CONFIG
&& $setting->get_status() !== base_setting::LOCKED_BY_PERMISSION) {
$setting->set_value($value);
if ($locked) {
$setting->set_status(base_setting::LOCKED_BY_CONFIG);
}
}
} else {
$controller->log('Unknown setting: ' . $setting, BACKUP::LOG_DEBUG);
}
}
}
/**
* Get the progress details of a backup operation.
* Get backup records directly from database, if the backup has successfully completed
* there will be no controller object to load.
*
* @param string $backupid The backup id to query.
* @return array $progress The backup progress details.
*/
public static function get_progress($backupid) {
global $DB;
$progress = array();
$backuprecord = $DB->get_record(
'backup_controllers',
array('backupid' => $backupid),
'status, progress, operation',
MUST_EXIST);
$status = $backuprecord->status;
$progress = $backuprecord->progress;
$operation = $backuprecord->operation;
$progress = array('status' => $status, 'progress' => $progress, 'backupid' => $backupid, 'operation' => $operation);
return $progress;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Base abstract class for all the helper classes providing DB operations
*
* TODO: Finish phpdocs
*/
abstract class backup_dbops { }
/*
* Exception class used by all the @dbops stuff
*/
class backup_dbops_exception extends backup_exception {
public function __construct($errorcode, $a=NULL, $debuginfo=null) {
parent::__construct($errorcode, 'error', '', $a, null, $debuginfo);
}
}
@@ -0,0 +1,287 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Non instantiable helper class providing DB support to the @backup_plan class
*
* This class contains various static methods available for all the DB operations
* performed by the @backup_plan (and builder) classes
*
* TODO: Finish phpdocs
*/
abstract class backup_plan_dbops extends backup_dbops {
/**
* Given one course module id, return one array with all the block intances that belong to it
*/
public static function get_blockids_from_moduleid($moduleid) {
global $DB;
// Get the context of the module
$contextid = context_module::instance($moduleid)->id;
// Get all the block instances which parentcontextid is the module contextid
$blockids = array();
$instances = $DB->get_records('block_instances', array('parentcontextid' => $contextid), '', 'id');
foreach ($instances as $instance) {
$blockids[] = $instance->id;
}
return $blockids;
}
/**
* Given one course id, return one array with all the block intances that belong to it
*/
public static function get_blockids_from_courseid($courseid) {
global $DB;
// Get the context of the course
$contextid = context_course::instance($courseid)->id;
// Get all the block instances which parentcontextid is the course contextid
$blockids = array();
$instances = $DB->get_records('block_instances', array('parentcontextid' => $contextid), '', 'id');
foreach ($instances as $instance) {
$blockids[] = $instance->id;
}
return $blockids;
}
/**
* Given one section id, return one array with all the course modules that belong to it
*/
public static function get_modules_from_sectionid($sectionid) {
global $DB;
// Get the course and sequence of the section
$secrec = $DB->get_record('course_sections', array('id' => $sectionid), 'course, sequence');
$courseid = $secrec->course;
// Get the section->sequence contents (it roots the activities order)
// Get all course modules belonging to requested section
$modulesarr = array();
$modules = $DB->get_records_sql("
SELECT cm.id, m.name AS modname
FROM {course_modules} cm
JOIN {modules} m ON m.id = cm.module
WHERE cm.course = ?
AND cm.section = ?
AND cm.deletioninprogress <> 1", array($courseid, $sectionid));
foreach (explode(',', (string) $secrec->sequence) as $moduleid) {
if (isset($modules[$moduleid])) {
$module = array('id' => $modules[$moduleid]->id, 'modname' => $modules[$moduleid]->modname);
$modulesarr[] = (object)$module;
unset($modules[$moduleid]);
}
}
if (!empty($modules)) { // This shouldn't happen, but one borked sequence can lead to it. Add the rest
foreach ($modules as $module) {
$module = array('id' => $module->id, 'modname' => $module->modname);
$modulesarr[] = (object)$module;
}
}
return $modulesarr;
}
/**
* Given one course id, return one array with all the course_sections belonging to it
*/
public static function get_sections_from_courseid($courseid) {
global $DB;
// Get all sections belonging to requested course
$sectionsarr = array();
$sections = $DB->get_records('course_sections', array('course' => $courseid), 'section');
foreach ($sections as $section) {
$sectionsarr[] = $section->id;
}
return $sectionsarr;
}
/**
* Given one course id, return its format in DB
*/
public static function get_courseformat_from_courseid($courseid) {
global $DB;
return $DB->get_field('course', 'format', array('id' => $courseid));
}
/**
* Given a course id, returns its theme. This can either be the course
* theme or (if not specified in course) the category, site theme.
*
* User, session, and inherited-from-mnet themes cannot have backed-up
* per course data. This is course-related data so it must be in a course
* theme specified as part of the course structure
* @param int $courseid
* @return string Name of course theme
* @see moodle_page#resolve_theme()
*/
public static function get_theme_from_courseid($courseid) {
global $DB, $CFG;
// Course theme first
if (!empty($CFG->allowcoursethemes)) {
$theme = $DB->get_field('course', 'theme', array('id' => $courseid));
if ($theme) {
return $theme;
}
}
// Category themes in reverse order
if (!empty($CFG->allowcategorythemes)) {
$catid = $DB->get_field('course', 'category', array('id' => $courseid));
while($catid) {
$category = $DB->get_record('course_categories', array('id'=>$catid),
'theme,parent', MUST_EXIST);
if ($category->theme) {
return $category->theme;
}
$catid = $category->parent;
}
}
// Finally use site theme
return $CFG->theme;
}
/**
* Return the wwwroot of the $CFG->mnet_localhost_id host
* caching it along the request
*/
public static function get_mnet_localhost_wwwroot() {
global $CFG, $DB;
static $wwwroot = null;
if (is_null($wwwroot)) {
$wwwroot = $DB->get_field('mnet_host', 'wwwroot', array('id' => $CFG->mnet_localhost_id));
}
return $wwwroot;
}
/**
* Returns the default backup filename, based in passed params.
*
* Default format is (see MDL-22145)
* backup word - format - type - name - date - info . mbz
* where name is variable (course shortname, section name/id, activity modulename + cmid)
* and info can be (nu = no user info, an = anonymized). The last param $useidasname,
* defaulting to false, allows to replace the course shortname by the course id (used
* by automated backups, to avoid non-ascii chars in OS filesystem)
*
* @param string $format One of backup::FORMAT_
* @param string $type One of backup::TYPE_
* @param int $courseid/$sectionid/$cmid
* @param bool $users Should be true is users were included in the backup
* @param bool $anonymised Should be true is user information was anonymized.
* @param bool $useidonly only use the ID in the file name
* @return string The filename to use
*/
public static function get_default_backup_filename($format, $type, $id, $users, $anonymised,
$useidonly = false, $files = true) {
global $DB;
// Calculate backup word
$backupword = str_replace(' ', '_', core_text::strtolower(get_string('backupfilename')));
$backupword = trim(clean_filename($backupword), '_');
// Not $useidonly, lets fetch the name
$shortname = '';
if (!$useidonly) {
// Calculate proper name element (based on type)
switch ($type) {
case backup::TYPE_1COURSE:
$shortname = $DB->get_field('course', 'shortname', array('id' => $id));
$context = context_course::instance($id);
$shortname = format_string($shortname, true, array('context' => $context));
break;
case backup::TYPE_1SECTION:
if (!$shortname = $DB->get_field('course_sections', 'name', array('id' => $id))) {
$shortname = $DB->get_field('course_sections', 'section', array('id' => $id));
}
break;
case backup::TYPE_1ACTIVITY:
$cm = get_coursemodule_from_id(null, $id);
$shortname = $cm->modname . $id;
break;
}
$shortname = str_replace(' ', '_', $shortname);
$shortname = core_text::strtolower(trim(clean_filename($shortname), '_'));
}
// The name will always contain the ID, but we append the course short name if requested.
$name = $id;
if (!$useidonly && $shortname != '') {
$name .= '-' . $shortname;
}
// Calculate date
$backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig'));
$date = userdate(time(), $backupdateformat, 99, false);
$date = core_text::strtolower(trim(clean_filename($date), '_'));
// Calculate info
$info = '';
if (!$users) {
$info = '-nu';
} else if ($anonymised) {
$info = '-an';
}
// Indicate if backup doesn't contain files.
if (!$files) {
$info .= '-nf';
}
return $backupword . '-' . $format . '-' . $type . '-' .
$name . '-' . $date . $info . '.mbz';
}
/**
* Returns a flag indicating the need to backup gradebook elements like calculated grade items and category visibility
* If all activity related grade items are being backed up we can also backup calculated grade items and categories
*/
public static function require_gradebook_backup($courseid, $backupid) {
global $DB;
$sql = "SELECT count(id)
FROM {grade_items}
WHERE courseid=:courseid
AND itemtype = 'mod'
AND id NOT IN (
SELECT bi.itemid
FROM {backup_ids_temp} bi
WHERE bi.itemname = 'grade_itemfinal'
AND bi.backupid = :backupid)";
$params = array('courseid'=>$courseid, 'backupid'=>$backupid);
$count = $DB->count_records_sql($sql, $params);
//if there are 0 activity grade items not already included in the backup
return $count == 0;
}
}
@@ -0,0 +1,107 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Non instantiable helper class providing DB support to the questions backup stuff
*
* This class contains various static methods available for all the DB operations
* performed by questions stuff
*
* TODO: Finish phpdocs
*/
abstract class backup_question_dbops extends backup_dbops {
/**
* Calculates all the question_categories to be included
* in backup, based in a given context (course/module) and
* the already annotated questions present in backup_ids_temp
*/
public static function calculate_question_categories($backupid, $contextid) {
global $DB;
// First step, annotate all the categories for the given context (course/module)
// i.e. the whole context questions bank
$DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid)
SELECT ?, 'question_category', id
FROM {question_categories}
WHERE contextid = ?", array($backupid, $contextid));
// Now, based in the annotated questions, annotate all the categories they
// belong to (whole context question banks too)
// First, get all the contexts we are going to save their question bank (no matter
// where they are in the contexts hierarchy, transversals... whatever)
$contexts = $DB->get_fieldset_sql("SELECT DISTINCT qc2.contextid
FROM {question_categories} qc2
JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc2.id
JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
JOIN {question} q ON q.id = qv.questionid
JOIN {backup_ids_temp} bi ON bi.itemid = q.id
WHERE bi.backupid = ?
AND bi.itemname = 'question'
AND qc2.contextid != ?", array($backupid, $contextid));
// Calculate and get the set reference records.
$setreferencecontexts = $DB->get_fieldset_sql("
SELECT DISTINCT qc.contextid
FROM {question_categories} qc
JOIN {question_set_references} qsr ON qsr.questionscontextid = qc.contextid
WHERE qsr.usingcontextid = ?", [$contextid]);
foreach ($setreferencecontexts as $setreferencecontext) {
if (!in_array($setreferencecontext, $contexts) && (int)$setreferencecontext !== $contextid) {
$contexts [] = $setreferencecontext;
}
}
// Calculate the get the reference records.
$referencecontexts = $DB->get_fieldset_sql("
SELECT DISTINCT qc.contextid
FROM {question_categories} qc
JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id
JOIN {question_references} qr ON qr.questionbankentryid = qbe.id
WHERE qr.usingcontextid =?", [$contextid]);
foreach ($referencecontexts as $referencecontext) {
if (!in_array($referencecontext, $contexts) && (int)$referencecontext !== $contextid) {
$contexts [] = $referencecontext;
}
}
// And now, simply insert all the question categories (complete question bank)
// for those contexts if we have found any
if ($contexts) {
list($contextssql, $contextparams) = $DB->get_in_or_equal($contexts);
$params = array_merge(array($backupid), $contextparams);
$DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid)
SELECT ?, 'question_category', id
FROM {question_categories}
WHERE contextid $contextssql", $params);
}
}
/**
* Delete all the annotated questions present in backup_ids_temp
*/
public static function delete_temp_questions($backupid) {
global $DB;
$DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'question'));
}
}
@@ -0,0 +1,187 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Non instantiable helper class providing DB support to the backup_structure stuff
*
* This class contains various static methods available for all the DB operations
* performed by the backup_structure stuff (mainly @backup_nested_element class)
*
* TODO: Finish phpdocs
*/
abstract class backup_structure_dbops extends backup_dbops {
public static function get_iterator($element, $params, $processor) {
global $DB;
// Check we are going to get_iterator for one backup_nested_element
if (! $element instanceof backup_nested_element) {
throw new base_element_struct_exception('backup_nested_element_expected');
}
// If var_array, table and sql are null, and element has no final elements it is one nested element without source
// Just return one 1 element iterator without information
if ($element->get_source_array() === null && $element->get_source_table() === null &&
$element->get_source_sql() === null && count($element->get_final_elements()) == 0) {
return new backup_array_iterator(array(0 => null));
} else if ($element->get_source_array() !== null) { // It's one array, return array_iterator
return new backup_array_iterator($element->get_source_array());
} else if ($element->get_source_table() !== null) { // It's one table, return recordset iterator
return $DB->get_recordset($element->get_source_table(), self::convert_params_to_values($params, $processor), $element->get_source_table_sortby());
} else if ($element->get_source_sql() !== null) { // It's one sql, return recordset iterator
return $DB->get_recordset_sql($element->get_source_sql(), self::convert_params_to_values($params, $processor));
} else { // No sources, supress completely, using null iterator
return new backup_null_iterator();
}
}
public static function convert_params_to_values($params, $processor) {
$newparams = array();
foreach ($params as $key => $param) {
$newvalue = null;
// If we have a base element, get its current value, exception if not set
if ($param instanceof base_atom) {
if ($param->is_set()) {
$newvalue = $param->get_value();
} else {
throw new base_element_struct_exception('valueofparamelementnotset', $param->get_name());
}
} else if (is_int($param) && $param < 0) { // Possibly one processor variable, let's process it
// See @backup class for all the VAR_XXX variables available.
// Note1: backup::VAR_PARENTID is handled by nested elements themselves
// Note2: trying to use one non-existing var will throw exception
$newvalue = $processor->get_var($param);
// Else we have one raw param value, use it
} else {
$newvalue = $param;
}
$newparams[$key] = $newvalue;
}
return $newparams;
}
public static function insert_backup_ids_record($backupid, $itemname, $itemid) {
global $DB;
// We need to do some magic with scales (that are stored in negative way)
if ($itemname == 'scale') {
$itemid = -($itemid);
}
// Now, we skip any annotation with negatives/zero/nulls, ids table only stores true id (always > 0)
if ($itemid <= 0 || is_null($itemid)) {
return;
}
// TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of record_exists() calls
// Note: Sure it will!
if (!$DB->record_exists('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname, 'itemid' => $itemid))) {
$DB->insert_record('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname, 'itemid' => $itemid));
}
}
/**
* Adds backup id database record for all files in the given file area.
*
* @param string $backupid Backup ID
* @param int $contextid Context id
* @param string $component Component
* @param string $filearea File area
* @param int $itemid Item id
* @param \core\progress\base $progress
*/
public static function annotate_files($backupid, $contextid, $component, $filearea, $itemid,
\core\progress\base $progress = null) {
global $DB;
$sql = 'SELECT id
FROM {files}
WHERE contextid = ?
AND component = ?';
$params = array($contextid, $component);
if (!is_null($filearea)) { // Add filearea to query and params if necessary
$sql .= ' AND filearea = ?';
$params[] = $filearea;
}
if (!is_null($itemid)) { // Add itemid to query and params if necessary
$sql .= ' AND itemid = ?';
$params[] = $itemid;
}
if ($progress) {
$progress->start_progress('');
}
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $record) {
if ($progress) {
$progress->progress();
}
self::insert_backup_ids_record($backupid, 'file', $record->id);
}
if ($progress) {
$progress->end_progress();
}
$rs->close();
}
/**
* Moves all the existing 'item' annotations to their final 'itemfinal' ones
* for a given backup.
*
* @param string $backupid Backup ID
* @param string $itemname Item name
* @param \core\progress\base $progress Progress tracker
*/
public static function move_annotations_to_final($backupid, $itemname, \core\progress\base $progress) {
global $DB;
$progress->start_progress('move_annotations_to_final');
$rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
$progress->progress();
foreach($rs as $annotation) {
// If corresponding 'itemfinal' annotation does not exist, update 'item' to 'itemfinal'
if (! $DB->record_exists('backup_ids_temp', array('backupid' => $backupid,
'itemname' => $itemname . 'final',
'itemid' => $annotation->itemid))) {
$DB->set_field('backup_ids_temp', 'itemname', $itemname . 'final', array('id' => $annotation->id));
}
$progress->progress();
}
$rs->close();
// All the remaining $itemname annotations can be safely deleted
$DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
$progress->end_progress();
}
/**
* Returns true/false if there are annotations for a given item
*/
public static function annotations_exist($backupid, $itemname) {
global $DB;
return (bool)$DB->count_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
}
}
@@ -0,0 +1,312 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-dbops
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Non instantiable helper class providing DB support to the @restore_controller
*
* This class contains various static methods available for all the DB operations
* performed by the restore_controller class
*
* TODO: Finish phpdocs
*/
abstract class restore_controller_dbops extends restore_dbops {
/**
* Send one restore controller to DB
*
* @param restore_controller $controller controller to send to DB
* @param string $checksum hash of the controller to be checked
* @param bool $includeobj to decide if the object itself must be updated (true) or no (false)
* @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false)
* @return int id of the controller record in the DB
* @throws backup_controller_exception|restore_dbops_exception
*/
public static function save_controller($controller, $checksum, $includeobj = true, $cleanobj = false) {
global $DB;
// Check we are going to save one backup_controller
if (! $controller instanceof restore_controller) {
throw new backup_controller_exception('restore_controller_expected');
}
// Check checksum is ok. Only if we are including object info. Sounds silly but it isn't ;-).
if ($includeobj and !$controller->is_checksum_correct($checksum)) {
throw new restore_dbops_exception('restore_controller_dbops_saving_checksum_mismatch');
}
// Cannot request to $includeobj and $cleanobj at the same time.
if ($includeobj and $cleanobj) {
throw new restore_dbops_exception('restore_controller_dbops_saving_cannot_include_and_delete');
}
// Get all the columns
$rec = new stdclass();
$rec->backupid = $controller->get_restoreid();
$rec->operation = $controller->get_operation();
$rec->type = $controller->get_type();
$rec->itemid = $controller->get_courseid();
$rec->format = $controller->get_format();
$rec->interactive = $controller->get_interactive();
$rec->purpose = $controller->get_mode();
$rec->userid = $controller->get_userid();
$rec->status = $controller->get_status();
$rec->execution = $controller->get_execution();
$rec->executiontime= $controller->get_executiontime();
$rec->checksum = $checksum;
// Serialize information
if ($includeobj) {
$rec->controller = base64_encode(serialize($controller));
} else if ($cleanobj) {
$rec->controller = '';
}
// Send it to DB
if ($recexists = $DB->get_record('backup_controllers', array('backupid' => $rec->backupid))) {
$rec->id = $recexists->id;
$rec->timemodified = time();
$DB->update_record('backup_controllers', $rec);
} else {
$rec->timecreated = time();
$rec->timemodified = 0;
$rec->id = $DB->insert_record('backup_controllers', $rec);
}
return $rec->id;
}
public static function load_controller($restoreid) {
global $DB;
if (! $controllerrec = $DB->get_record('backup_controllers', array('backupid' => $restoreid))) {
throw new backup_dbops_exception('restore_controller_dbops_nonexisting');
}
$controller = unserialize(base64_decode($controllerrec->controller));
if (!is_object($controller)) {
// The controller field of the table did not contain a serialized object.
// It is made empty after it has been used successfully, it is likely that
// the user has pressed the browser back button at some point.
throw new backup_dbops_exception('restore_controller_dbops_loading_invalid_controller');
}
// Check checksum is ok. Sounds silly but it isn't ;-)
if (!$controller->is_checksum_correct($controllerrec->checksum)) {
throw new backup_dbops_exception('restore_controller_dbops_loading_checksum_mismatch');
}
return $controller;
}
public static function create_restore_temp_tables($restoreid) {
global $CFG, $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services
if ($dbman->table_exists('backup_ids_temp')) { // Table exists, from restore prechecks
// TODO: Improve this by inserting/selecting some record to see there is restoreid match
// TODO: If not match, exception, table corresponds to another backup/restore operation
return true;
}
backup_controller_dbops::create_backup_ids_temp_table($restoreid);
backup_controller_dbops::create_backup_files_temp_table($restoreid);
return false;
}
public static function drop_restore_temp_tables($backupid) {
global $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services
$targettablenames = array('backup_ids_temp', 'backup_files_temp');
foreach ($targettablenames as $targettablename) {
$table = new xmldb_table($targettablename);
$dbman->drop_table($table); // And drop it
}
// Invalidate the backup_ids caches.
restore_dbops::reset_backup_ids_cached();
}
/**
* Sets the default values for the settings in a restore operation
*
* @param restore_controller $controller
*/
public static function apply_config_defaults(restore_controller $controller) {
$settings = array(
'restore_general_users' => 'users',
'restore_general_enrolments' => 'enrolments',
'restore_general_role_assignments' => 'role_assignments',
'restore_general_permissions' => 'permissions',
'restore_general_activities' => 'activities',
'restore_general_blocks' => 'blocks',
'restore_general_filters' => 'filters',
'restore_general_comments' => 'comments',
'restore_general_badges' => 'badges',
'restore_general_calendarevents' => 'calendarevents',
'restore_general_userscompletion' => 'userscompletion',
'restore_general_logs' => 'logs',
'restore_general_histories' => 'grade_histories',
'restore_general_questionbank' => 'questionbank',
'restore_general_groups' => 'groups',
'restore_general_competencies' => 'competencies',
'restore_general_contentbankcontent' => 'contentbankcontent',
'restore_general_xapistate' => 'xapistate',
'restore_general_legacyfiles' => 'legacyfiles'
);
self::apply_admin_config_defaults($controller, $settings, true);
$target = $controller->get_target();
if ($target == backup::TARGET_EXISTING_ADDING || $target == backup::TARGET_CURRENT_ADDING) {
$settings = array(
'restore_merge_overwrite_conf' => 'overwrite_conf',
'restore_merge_course_fullname' => 'course_fullname',
'restore_merge_course_shortname' => 'course_shortname',
'restore_merge_course_startdate' => 'course_startdate',
);
self::apply_admin_config_defaults($controller, $settings, true);
}
if ($target == backup::TARGET_EXISTING_DELETING || $target == backup::TARGET_CURRENT_DELETING) {
$settings = array(
'restore_replace_overwrite_conf' => 'overwrite_conf',
'restore_replace_course_fullname' => 'course_fullname',
'restore_replace_course_shortname' => 'course_shortname',
'restore_replace_course_startdate' => 'course_startdate',
'restore_replace_keep_roles_and_enrolments' => 'keep_roles_and_enrolments',
'restore_replace_keep_groups_and_groupings' => 'keep_groups_and_groupings',
);
self::apply_admin_config_defaults($controller, $settings, true);
}
if ($controller->get_mode() == backup::MODE_IMPORT &&
(!$controller->get_interactive()) &&
$controller->get_type() == backup::TYPE_1ACTIVITY) {
// This is duplicate - there is no concept of defaults - these settings must be on.
$settings = array(
'activities',
'blocks',
'filters',
'questionbank'
);
self::force_enable_settings($controller, $settings);
};
// Add some dependencies.
$plan = $controller->get_plan();
if ($plan->setting_exists('overwrite_conf')) {
/** @var restore_course_overwrite_conf_setting $overwriteconf */
$overwriteconf = $plan->get_setting('overwrite_conf');
if ($overwriteconf->get_visibility()) {
foreach (['course_fullname', 'course_shortname', 'course_startdate'] as $settingname) {
if ($plan->setting_exists($settingname)) {
$setting = $plan->get_setting($settingname);
$overwriteconf->add_dependency($setting, setting_dependency::DISABLED_FALSE,
array('defaultvalue' => $setting->get_value()));
}
}
}
}
}
/**
* Returns the default value to be used for a setting from the admin restore config
*
* @param string $config
* @param backup_setting $setting
* @return mixed
*/
private static function get_setting_default($config, $setting) {
$value = get_config('restore', $config);
if (in_array($setting->get_name(), ['course_fullname', 'course_shortname', 'course_startdate']) &&
$setting->get_ui() instanceof backup_setting_ui_defaultcustom) {
// Special case - admin config settings course_fullname, etc. are boolean and the restore settings are strings.
$value = (bool)$value;
if ($value) {
$attributes = $setting->get_ui()->get_attributes();
$value = $attributes['customvalue'];
}
}
if ($setting->get_ui() instanceof backup_setting_ui_select) {
// Make sure the value is a valid option in the select element, otherwise just pick the first from the options list.
// Example: enrolments dropdown may not have the "enrol_withusers" option because users info can not be restored.
$options = array_keys($setting->get_ui()->get_values());
if (!in_array($value, $options)) {
$value = reset($options);
}
}
return $value;
}
/**
* Turn these settings on. No defaults from admin settings.
*
* @param restore_controller $controller
* @param array $settings a map from admin config names to setting names (Config name => Setting name)
*/
private static function force_enable_settings(restore_controller $controller, array $settings) {
$plan = $controller->get_plan();
foreach ($settings as $config => $settingname) {
$value = true;
if ($plan->setting_exists($settingname)) {
$setting = $plan->get_setting($settingname);
// We do not allow this setting to be locked for a duplicate function.
if ($setting->get_status() !== base_setting::NOT_LOCKED) {
$setting->set_status(base_setting::NOT_LOCKED);
}
$setting->set_value($value);
$setting->set_status(base_setting::LOCKED_BY_CONFIG);
} else {
$controller->log('Unknown setting: ' . $settingname, BACKUP::LOG_DEBUG);
}
}
}
/**
* Sets the controller settings default values from the admin config.
*
* @param restore_controller $controller
* @param array $settings a map from admin config names to setting names (Config name => Setting name)
* @param boolean $uselocks whether "locked" admin settings should be honoured
*/
private static function apply_admin_config_defaults(restore_controller $controller, array $settings, $uselocks) {
$plan = $controller->get_plan();
foreach ($settings as $config => $settingname) {
if ($plan->setting_exists($settingname)) {
$setting = $plan->get_setting($settingname);
$value = self::get_setting_default($config, $setting);
$locked = (get_config('restore',$config . '_locked') == true);
// Use the original value when this is an import and the setting is unlocked.
if ($controller->get_mode() == backup::MODE_IMPORT && $controller->get_interactive()) {
if (!$uselocks || !$locked) {
$value = $setting->get_value();
}
}
// We can only update the setting if it isn't already locked by config or permission.
if ($setting->get_status() != base_setting::LOCKED_BY_CONFIG
&& $setting->get_status() != base_setting::LOCKED_BY_PERMISSION
&& $setting->get_ui()->is_changeable()) {
$setting->set_value($value);
if ($uselocks && $locked) {
$setting->set_status(base_setting::LOCKED_BY_CONFIG);
}
}
} else {
$controller->log('Unknown setting: ' . $settingname, BACKUP::LOG_DEBUG);
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package core_backup
* @category test
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_backup;
use backup;
use backup_controller;
use backup_controller_dbops;
use backup_controller_exception;
use backup_dbops_exception;
defined('MOODLE_INTERNAL') || die();
// Include all the needed stuff
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* @package core_backup
* @category test
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_dbops_test extends \advanced_testcase {
protected $moduleid; // course_modules id used for testing
protected $sectionid; // course_sections id used for testing
protected $courseid; // course id used for testing
protected $userid; // user record used for testing
protected function setUp(): void {
global $DB, $CFG;
parent::setUp();
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3));
$coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid));
$this->moduleid = $page->cmid;
$this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id));
$this->courseid = $coursemodule->course;
$this->userid = 2; // admin
$CFG->backup_error_log_logger_level = backup::LOG_NONE;
$CFG->backup_output_indented_logger_level = backup::LOG_NONE;
$CFG->backup_file_logger_level = backup::LOG_NONE;
$CFG->backup_database_logger_level = backup::LOG_NONE;
unset($CFG->backup_file_logger_extra);
$CFG->backup_file_logger_level_extra = backup::LOG_NONE;
}
/*
* test backup_ops class
*/
function test_backup_dbops(): void {
// Nothing to do here, abstract class + exception, will be tested by the rest
}
/*
* test backup_controller_dbops class
*/
function test_backup_controller_dbops(): void {
global $DB;
$dbman = $DB->get_manager(); // Going to use some database_manager services for testing
// Instantiate non interactive backup_controller
$bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
$this->assertTrue($bc instanceof backup_controller);
// Calculate checksum
$checksum = $bc->calculate_checksum();
$this->assertEquals(strlen($checksum), 32); // is one md5
// save controller
$recid = backup_controller_dbops::save_controller($bc, $checksum);
$this->assertNotEmpty($recid);
// save it again (should cause update to happen)
$recid2 = backup_controller_dbops::save_controller($bc, $checksum);
$this->assertNotEmpty($recid2);
$this->assertEquals($recid, $recid2); // Same record in both save operations
// Try incorrect checksum
$bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
$checksum = $bc->calculate_checksum();
try {
$recid = backup_controller_dbops::save_controller($bc, 'lalala');
$this->assertTrue(false, 'backup_dbops_exception expected');
} catch (\Exception $e) {
$this->assertTrue($e instanceof backup_dbops_exception);
$this->assertEquals($e->errorcode, 'backup_controller_dbops_saving_checksum_mismatch');
}
// Try to save non backup_controller object
$bc = new \stdClass();
try {
$recid = backup_controller_dbops::save_controller($bc, 'lalala');
$this->assertTrue(false, 'backup_controller_exception expected');
} catch (\Exception $e) {
$this->assertTrue($e instanceof backup_controller_exception);
$this->assertEquals($e->errorcode, 'backup_controller_expected');
}
// save and load controller (by backupid). Then compare
$bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
$checksum = $bc->calculate_checksum(); // Calculate checksum
$backupid = $bc->get_backupid();
$this->assertEquals(strlen($backupid), 32); // is one md5
$recid = backup_controller_dbops::save_controller($bc, $checksum); // save controller
$newbc = backup_controller_dbops::load_controller($backupid); // load controller
$this->assertTrue($newbc instanceof backup_controller);
$newchecksum = $newbc->calculate_checksum();
$this->assertEquals($newchecksum, $checksum);
// try to load non-existing controller
try {
$bc = backup_controller_dbops::load_controller('1234567890');
$this->assertTrue(false, 'backup_dbops_exception expected');
} catch (\Exception $e) {
$this->assertTrue($e instanceof backup_dbops_exception);
$this->assertEquals($e->errorcode, 'backup_controller_dbops_nonexisting');
}
// backup_ids_temp table tests
// If, for any reason table exists, drop it
if ($dbman->table_exists('backup_ids_temp')) {
$dbman->drop_table(new xmldb_table('backup_ids_temp'));
}
// Check backup_ids_temp table doesn't exist
$this->assertFalse($dbman->table_exists('backup_ids_temp'));
// Create and check it exists
backup_controller_dbops::create_backup_ids_temp_table('testingid');
$this->assertTrue($dbman->table_exists('backup_ids_temp'));
// Drop and check it doesn't exists anymore
backup_controller_dbops::drop_backup_ids_temp_table('testingid');
$this->assertFalse($dbman->table_exists('backup_ids_temp'));
// Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions.
// We need to handle both objects and data elements.
$object = new \stdClass();
$object->item1 = 10;
$object->item2 = 'a String';
$testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1));
foreach ($testarray as $item) {
$encoded = backup_controller_dbops::encode_backup_temp_info($item);
$decoded = backup_controller_dbops::decode_backup_temp_info($encoded);
$this->assertEquals($item, $decoded);
}
}
/**
* Check backup_includes_files
*/
function test_backup_controller_dbops_includes_files(): void {
global $DB;
$dbman = $DB->get_manager(); // Going to use some database_manager services for testing
// A MODE_GENERAL controller - this should include files
$bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid);
$this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 1);
// A MODE_IMPORT controller - should not include files
$bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $this->userid);
$this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
// A MODE_SAMESITE controller - should not include files
$bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $this->userid);
$this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
}
}
class mock_backup_controller4dbops extends backup_controller {
/**
* Change standard behavior so the checksum is also stored and not onlt calculated
*/
public function calculate_checksum() {
$this->checksum = parent::calculate_checksum();
return $this->checksum;
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_backup;
use backup_structure_dbops;
/**
* Tests for backup_structure_dbops
*
* @package core_backup
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \backup_structure_dbops
*/
class backup_structure_dbops_test extends \advanced_testcase {
public static function setUpBeforeClass(): void {
global $CFG;
parent::setUpBeforeClass();
require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php");
}
/**
* Tests for convert_params_to_values.
*
* @dataProvider convert_params_to_values_provider
* @param array $params
* @param mixed $processor
* @param array $expected
*/
public function test_convert_params_to_values(
array $params,
$processor,
array $expected,
): void {
if (is_callable($processor)) {
$newprocessor = $this->createMock(\backup_structure_processor::class);
$newprocessor->method('get_var')->willReturnCallback($processor);
$processor = $newprocessor;
}
$result = backup_structure_dbops::convert_params_to_values($params, $processor);
$this->assertEqualsCanonicalizing($expected, $result);
}
/**
* Data provider for convert_params_to_values_provider.
*/
public static function convert_params_to_values_provider(): array {
return [
'String value is not processed' => [
['/0/1/2/345'],
null,
['/0/1/2/345'],
],
'Positive integer' => [
[123, 456],
null,
[123, 456],
],
'Negative integer' => [
[-42],
function () {
return 'Life, the Universe, and Everything';
},
['Life, the Universe, and Everything'],
],
'Mix of strings, and ints with a processor' => [
['foo', 123, 'bar', -42],
function () {
return 'Life, the Universe, and Everything';
},
['foo', 123, 'bar', 'Life, the Universe, and Everything'],
],
];
}
/**
* Tests for convert_params_to_values with an atom.
*/
public function test_convert_params_to_values_with_atom(): void {
$atom = $this->createMock(\base_atom::class);
$atom->method('is_set')->willReturn(true);
$atom->method('get_value')->willReturn('Some atomised value');
$result = backup_structure_dbops::convert_params_to_values([$atom], null);
$this->assertEqualsCanonicalizing(['Some atomised value'], $result);
}
/**
* Tests for convert_params_to_values with an atom without any value.
*/
public function test_convert_params_to_values_with_atom_no_value(): void {
$atom = $this->createMock(\base_atom::class);
$atom->method('is_set')->willReturn(false);
$atom->method('get_name')->willReturn('Atomisd name');
$this->expectException(\base_element_struct_exception::class);
backup_structure_dbops::convert_params_to_values([$atom], null);
}
}
@@ -0,0 +1,364 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_backup;
use restore_controller_dbops;
use restore_dbops;
defined('MOODLE_INTERNAL') || die();
// Include all the needed stuff
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
/**
* @package core_backup
* @category test
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_dbops_test extends \advanced_testcase {
/**
* Verify the xxx_ids_cached (in-memory backup_ids cache) stuff works as expected.
*
* Note that those private implementations are tested here by using the public
* backup_ids API and later performing low-level tests.
*/
public function test_backup_ids_cached(): void {
global $DB;
$dbman = $DB->get_manager(); // We are going to use database_manager services.
$this->resetAfterTest(true); // Playing with temp tables, better reset once finished.
// Some variables and objects for testing.
$restoreid = 'testrestoreid';
$mapping = new \stdClass();
$mapping->itemname = 'user';
$mapping->itemid = 1;
$mapping->newitemid = 2;
$mapping->parentitemid = 3;
$mapping->info = 'info';
// Create the backup_ids temp tables used by restore.
restore_controller_dbops::create_restore_temp_tables($restoreid);
// Send one mapping using the public api with defaults.
restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
// Get that mapping and verify everything is returned as expected.
$result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
$this->assertSame($mapping->itemname, $result->itemname);
$this->assertSame($mapping->itemid, $result->itemid);
$this->assertSame(0, $result->newitemid);
$this->assertSame(null, $result->parentitemid);
$this->assertSame(null, $result->info);
// Drop the backup_xxx_temp temptables manually, so memory cache won't be invalidated.
$dbman->drop_table(new \xmldb_table('backup_ids_temp'));
$dbman->drop_table(new \xmldb_table('backup_files_temp'));
// Verify the mapping continues returning the same info,
// now from cache (the table does not exist).
$result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
$this->assertSame($mapping->itemname, $result->itemname);
$this->assertSame($mapping->itemid, $result->itemid);
$this->assertSame(0, $result->newitemid);
$this->assertSame(null, $result->parentitemid);
$this->assertSame(null, $result->info);
// Recreate the temp table, just to drop it using the restore API in
// order to check that, then, the cache becomes invalid for the same request.
restore_controller_dbops::create_restore_temp_tables($restoreid);
restore_controller_dbops::drop_restore_temp_tables($restoreid);
// No cached info anymore, so the mapping request will arrive to
// DB leading to error (temp table does not exist).
try {
$result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
$this->fail('Expecting an exception, none occurred');
} catch (\Exception $e) {
$this->assertTrue($e instanceof \dml_exception);
$this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
}
// Create the backup_ids temp tables once more.
restore_controller_dbops::create_restore_temp_tables($restoreid);
// Send one mapping using the public api with complete values.
restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid,
$mapping->newitemid, $mapping->parentitemid, $mapping->info);
// Get that mapping and verify everything is returned as expected.
$result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
$this->assertSame($mapping->itemname, $result->itemname);
$this->assertSame($mapping->itemid, $result->itemid);
$this->assertSame($mapping->newitemid, $result->newitemid);
$this->assertSame($mapping->parentitemid, $result->parentitemid);
$this->assertSame($mapping->info, $result->info);
// Finally, drop the temp tables properly and get the DB error again (memory caches empty).
restore_controller_dbops::drop_restore_temp_tables($restoreid);
try {
$result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
$this->fail('Expecting an exception, none occurred');
} catch (\Exception $e) {
$this->assertTrue($e instanceof \dml_exception);
$this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
}
}
/**
* Data provider for {@link test_precheck_user()}
*/
public function precheck_user_provider() {
$emailmultiplier = [
'shortmail' => 'normalusername@example.com',
'longmail' => str_repeat('a', 100) // It's not validated, hence any string is ok.
];
$providercases = [];
foreach ($emailmultiplier as $emailk => $email) {
// Get the related cases.
$cases = $this->precheck_user_cases($email);
// Rename them (keys).
foreach ($cases as $key => $case) {
$providercases[$key . ' - ' . $emailk] = $case;
}
}
return $providercases;
}
/**
* Get all the cases implemented in {@link restore_dbops::precheck_users()}
*
* @param string $email
*/
private function precheck_user_cases($email) {
global $CFG;
$baseuserarr = [
'username' => 'normalusername',
'email' => $email,
'mnethostid' => $CFG->mnet_localhost_id,
'firstaccess' => 123456789,
'deleted' => 0,
'forceemailcleanup' => false, // Hack to force the DB record to have empty mail.
'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed.
return [
// Cases with samesite = true.
'samesite match existing (1A)' => [
'dbuser' => $baseuserarr,
'backupuser' => $baseuserarr,
'samesite' => true,
'outcome' => 'match'
],
'samesite match existing anon (1B)' => [
'dbuser' => array_merge($baseuserarr, [
'username' => 'anon01']),
'backupuser' => array_merge($baseuserarr, [
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
'samesite' => true,
'outcome' => 'match'
],
'samesite match existing deleted in db, alive in backup, by db username (1C)' => [
'dbuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'backupuser' => array_merge($baseuserarr, [
'username' => 'this_wont_match']),
'samesite' => true,
'outcome' => 'match'
],
'samesite match existing deleted in db, alive in backup, by db email (1C)' => [
'dbuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'backupuser' => array_merge($baseuserarr, [
'email' => 'this_wont_match']),
'samesite' => true,
'outcome' => 'match'
],
'samesite match existing alive in db, deleted in backup (1D)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'samesite' => true,
'outcome' => 'match'
],
'samesite conflict (1E)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, ['id' => -1]),
'samesite' => true,
'outcome' => false
],
'samesite create user (1F)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'username' => 'newusername']),
'samesite' => false,
'outcome' => true
],
// Cases with samesite = false.
'no samesite match existing, by db email (2A1)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'firstaccess' => 0]),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing, by db firstaccess (2A1)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'email' => 'this_wont_match@example.con']),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing anon (2A1 too)' => [
'dbuser' => array_merge($baseuserarr, [
'username' => 'anon01']),
'backupuser' => array_merge($baseuserarr, [
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match dupe admin (2A2)' => [
'dbuser' => array_merge($baseuserarr, [
'username' => 'admin_old_site_id',
'forceduplicateadminallowed' => true]),
'backupuser' => array_merge($baseuserarr, [
'username' => 'admin']),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [
'dbuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'backupuser' => array_merge($baseuserarr, [
'firstaccess' => 0]),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [
'dbuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'backupuser' => array_merge($baseuserarr, [
'mail' => 'this_wont_match']),
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing deleted in db, alive in backup (2B2)' => [
'dbuser' => array_merge($baseuserarr, [
'deleted' => 1,
'forceemailcleanup' => true]),
'backupuser' => $baseuserarr,
'samesite' => false,
'outcome' => 'match'
],
'no samesite match existing alive in db, deleted in backup (2C)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'deleted' => 1]),
'samesite' => false,
'outcome' => 'match'
],
'no samesite conflict (2D)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'email' => 'anotheruser@example.com', 'firstaccess' => 0]),
'samesite' => false,
'outcome' => false
],
'no samesite create user (2E)' => [
'dbuser' => $baseuserarr,
'backupuser' => array_merge($baseuserarr, [
'username' => 'newusername']),
'samesite' => false,
'outcome' => true
],
];
}
/**
* Test restore precheck_user method
*
* @dataProvider precheck_user_provider
* @covers \restore_dbops::precheck_user()
*
* @param array $dbuser
* @param array $backupuser
* @param bool $samesite
* @param mixed $outcome
**/
public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome): void {
global $DB;
$this->resetAfterTest();
$dbuser = (object)$dbuser;
$backupuser = (object)$backupuser;
$siteid = null;
// If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back).
if ($backupuser->deleted) {
$backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0]));
delete_user($backupuser);
$backupuser = $DB->get_record('user', ['id' => $backupuser->id]);
$DB->delete_records('user', ['id' => $backupuser->id]);
unset($backupuser->id);
}
// Create the db user, normally.
$dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0]));
$backupuser->id = $backupuser->id ?? $dbuser->id;
// We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records.
if ($dbuser->forceduplicateadminallowed) {
set_config('import_general_duplicate_admin_allowed', true, 'backup');
$siteid = 'old_site_id';
}
// If the DB user must be deleted, do it and fetch it back.
if ($dbuser->deleted) {
delete_user($dbuser);
// We may want to clean the mail field (old behavior, not containing the current md5(username).
if ($dbuser->forceemailcleanup) {
$DB->set_field('user', 'email', '', ['id' => $dbuser->id]);
}
}
// Get the dbuser record, because we may have changed it above.
$dbuser = $DB->get_record('user', ['id' => $dbuser->id]);
$method = (new \ReflectionClass('restore_dbops'))->getMethod('precheck_user');
$result = $method->invoke(null, $backupuser, $samesite, $siteid);
if (is_bool($result)) {
$this->assertSame($outcome, $result);
} else {
$outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser,
// Just check ids, it means the expected match has been found in database.
$this->assertSame($outcome->id, $result->id);
}
}
}