See Release Notes
Long Term Support Release
<?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> * Backup controller and related exception classes. > * > * @package core_backup* @subpackage backup-controller * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Class implementing the controller of any backup process * * This final class is in charge of controlling all the backup architecture, for any * type of backup. Based in type, format, interactivity and target, it stores the * whole execution plan and settings that will be used later by the @backup_worker, * applies all the defaults, performs all the security contraints and is in charge * of handling the ui if necessary. Also logging strategy is defined here. * * Note the class is 100% neutral and usable for *any* backup. It just stores/requests * all the needed information from other backup classes in order to have everything well * structured in order to allow the @backup_worker classes to do their job. * * In other words, a mammoth class, but don't worry, practically everything is delegated/ * aggregated!)< * < * TODO: Finish phpdocs*/ class backup_controller extends base_controller {> /** @var string Unique identifier for this backup */ > protected $backupid; protected $backupid; // Unique identificator for this backup > > /** protected $type; // Type of backup (activity, section, course) > * Type of item that is being stored in the backup. protected $id; // Course/section/course_module id to backup > * protected $courseid; // courseid where the id belongs to > * Should be selected from one of the backup::TYPE_ constants protected $format; // Format of backup (moodle, imscc) > * for example backup::TYPE_1ACTIVITY protected $interactive; // yes/no > * protected $mode; // Purpose of the backup (default settings) > * @var string protected $userid; // user id executing the backup > */ protected $operation; // Type of operation (backup/restore) > protected $type;< protected $backupid; // Unique identificator for this backup> /** @var int Course/section/course_module id to backup */ > protected $id;< protected $type; // Type of backup (activity, section, course) < protected $id; // Course/section/course_module id to backup < protected $courseid; // courseid where the id belongs to < protected $format; // Format of backup (moodle, imscc) < protected $interactive; // yes/no < protected $mode; // Purpose of the backup (default settings) < protected $userid; // user id executing the backup < protected $operation; // Type of operation (backup/restore) < < protected $status; // Current status of the controller (created, planned, configured...) < < /** @var backup_plan */ < protected $plan; // Backup execution plan < protected $includefiles; // Whether this backup includes files or not.> /** @var false|int The id of the course the backup belongs to, or false if no course. */ > protected $courseid; > > /** > * Format of backup (moodle, imscc). > * > * Should be one of the backup::FORMAT_ constants. > * for example backup::FORMAT_MOODLE > * > * @var string > */ > protected $format; > > /** > * Whether this backup will require user interaction. > * > * Should be one of backup::INTERACTIVE_YES or INTERACTIVE_NO > * > * @var bool > */ > protected $interactive; > > /** > * Purpose of the backup (default settings) > * > * Should be one of the the backup::MODE_ constants, > * for example backup::MODE_GENERAL > * > * @var int > */ > protected $mode; > > /** @var int The id of the user executing the backup. */ > protected $userid; > > /** > * Type of operation (backup/restore) > * > * Should be selected from: backup::OPERATION_BACKUP or OPERATION_RESTORE > * > * @var string > */ > protected $operation; > > /** > * Current status of the controller (created, planned, configured...) > * > * It should be one of the backup::STATUS_ constants, > * for example backup::STATUS_AWAITING. > * > * @var int > */ > protected $status; > > /** @var backup_plan Backup execution plan. */ > protected $plan; > > /** @var int Whether this backup includes files (1) or not (0). */ > protected $includefiles;< * @var integer> * @var int< protected $executiontime; // epoch time when we want the backup to be executed (requires cron to run)< protected $destination; // Destination chain object (fs_moodle, fs_os, db, email...)> /** @var int Epoch time when we want the backup to be executed (requires cron to run). */ > protected $executiontime; > > /** @var null Destination chain object (fs_moodle, fs_os, db, email...). */ > protected $destination;< protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses> /** @var string Cache {@see \checksumable} results for lighter {@see \backup_controller::is_checksum_correct()} uses. */ > protected $checksum;* The role ids to keep in a copy operation. * @var array */ protected $keptroles = array(); /** * Constructor for the backup controller class. *< * @param int $type Type of the backup; One of backup::TYPE_1COURSE, TYPE_1SECTION, TYPE_1ACTIVITY> * @param string $type Type of the backup; One of backup::TYPE_1COURSE, TYPE_1SECTION, TYPE_1ACTIVITY* @param int $id The ID of the item to backup; e.g the course id< * @param int $format The backup format to use; Most likely backup::FORMAT_MOODLE> * @param string $format The backup format to use; Most likely backup::FORMAT_MOODLE* @param bool $interactive Whether this backup will require user interaction; backup::INTERACTIVE_YES or INTERACTIVE_NO * @param int $mode One of backup::MODE_GENERAL, MODE_IMPORT, MODE_SAMESITE, MODE_HUB, MODE_AUTOMATED * @param int $userid The id of the user making the backup * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO */ public function __construct($type, $id, $format, $interactive, $mode, $userid, $releasesession = backup::RELEASESESSION_NO) { $this->type = $type; $this->id = $id; $this->courseid = backup_controller_dbops::get_courseid_from_type_id($this->type, $this->id); $this->format = $format; $this->interactive = $interactive; $this->mode = $mode; $this->userid = $userid; $this->releasesession = $releasesession; // Apply some defaults $this->operation = backup::OPERATION_BACKUP; $this->executiontime = 0; $this->checksum = ''; // Set execution based on backup mode. if ($mode == backup::MODE_ASYNC || $mode == backup::MODE_COPY) { $this->execution = backup::EXECUTION_DELAYED; } else { $this->execution = backup::EXECUTION_INMEDIATE; } // Apply current backup version and release if necessary backup_controller_dbops::apply_version_and_release(); // Check format and type are correct backup_check::check_format_and_type($this->format, $this->type); // Check id is correct backup_check::check_id($this->type, $this->id); // Check user is correct backup_check::check_user($this->userid); // Calculate unique $backupid $this->calculate_backupid(); // Default logger chain (based on interactive/execution) $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid); // By default there is no progress reporter. Interfaces that wish to // display progress must set it. $this->progress = new \core\progress\none(); // Instantiate the output_controller singleton and active it if interactive and immediate. $oc = output_controller::get_instance(); if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) { $oc->set_active(true); } $this->log('instantiating backup controller', backup::LOG_INFO, $this->backupid); // Default destination chain (based on type/mode/execution) $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution); // Set initial status $this->set_status(backup::STATUS_CREATED); // Load plan (based on type/format) $this->load_plan(); // Apply all default settings (based on type/format/mode) $this->apply_defaults(); // Perform all initial security checks and apply (2nd param) them to settings automatically backup_check::check_security($this, true); // Set status based on interactivity if ($this->interactive == backup::INTERACTIVE_YES) { $this->set_status(backup::STATUS_SETTING_UI); } else { $this->set_status(backup::STATUS_AWAITING); } } /** * Clean structures used by the backup_controller * * This method clean various structures used by the backup_controller, * destroying them in an ordered way, so their memory will be gc properly * by PHP (mainly circular references). * * Note that, while it's not mandatory to execute this method, it's highly * recommended to do so, specially in scripts performing multiple operations * (like the automated backups) or the system will run out of memory after * a few dozens of backups) */ public function destroy() { // Only need to destroy circulars under the plan. Delegate to it. $this->plan->destroy(); // Loggers may have also chained references, destroy them. Also closing resources when needed. $this->logger->destroy(); }> /** public function finish_ui() { > * Declare that all user interaction with the backup controller is complete. if ($this->status != backup::STATUS_SETTING_UI) { > * throw new backup_controller_exception('cannot_finish_ui_if_not_setting_ui'); > * After this the backup controller is waiting for processing. } > */$this->set_status(backup::STATUS_AWAITING); }> /** public function process_ui_event() { > * Validates the backup is valid after any user changes. > * // Perform security checks throwing exceptions (2nd param) if something is wrong > * A backup_controller_exception will be thrown if there is an issue. backup_check::check_security($this, false); > */}> /** public function set_status($status) { > * Sets the new status of the backup. // Note: never save_controller() with the object info after STATUS_EXECUTING or the whole controller, > * // containing all the steps will be sent to DB. 100% (monster) useless. > * @param int $status $this->log('setting controller status to', backup::LOG_DEBUG, $status); > */// TODO: Check it's a correct status. $this->status = $status; // Ensure that, once set to backup::STATUS_AWAITING, controller is stored in DB. // Also save if executing so we can better track progress. if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_EXECUTING) { $this->save_controller(); $tbc = self::load_controller($this->backupid); $this->logger = $tbc->logger; // wakeup loggers $tbc->plan->destroy(); // Clean plan controller structures, keeping logger alive. } else if ($status == backup::STATUS_FINISHED_OK) { // If the operation has ended without error (backup::STATUS_FINISHED_OK) // proceed by cleaning the object from database. MDL-29262. $this->save_controller(false, true); } else if ($status == backup::STATUS_FINISHED_ERR) { // If the operation has ended with an error save the controller // preserving the object in the database. We may want it for debugging. $this->save_controller(); } }> /** public function set_execution($execution, $executiontime = 0) { > * Sets if the backup will be processed immediately, or later. $this->log('setting controller execution', backup::LOG_DEBUG); > * // TODO: Check valid execution mode. > * @param int $execution Use backup::EXECUTION_INMEDIATE or backup::EXECUTION_DELAYED // TODO: Check time in future. > * @param int $executiontime The timestamp in the future when the task should be executed, or 0 for immediately. // TODO: Check time = 0 if immediate. > */$this->execution = $execution; $this->executiontime = $executiontime; // Default destination chain (based on type/mode/execution) $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution); // Default logger chain (based on interactive/execution) $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid); } // checksumable interface methods public function calculate_checksum() { // Reset current checksum to take it out from calculations! $this->checksum = ''; // Init checksum $tempchecksum = md5('backupid-' . $this->backupid . 'type-' . $this->type . 'id-' . $this->id . 'format-' . $this->format . 'interactive-'. $this->interactive . 'mode-' . $this->mode . 'userid-' . $this->userid . 'operation-' . $this->operation . 'status-' . $this->status . 'execution-' . $this->execution . 'plan-' . backup_general_helper::array_checksum_recursive(array($this->plan)) . 'destination-'. backup_general_helper::array_checksum_recursive(array($this->destination)) . 'logger-' . backup_general_helper::array_checksum_recursive(array($this->logger))); $this->log('calculating controller checksum', backup::LOG_DEBUG, $tempchecksum); return $tempchecksum; } public function is_checksum_correct($checksum) { return $this->checksum === $checksum; }> /** public function get_backupid() { > * Gets the unique identifier for this backup controller. return $this->backupid; > * } > * @return string > */public function get_type() {> /** return $this->type; > * Gets the type of backup to be performed. } > * > * Use {@see \backup_controller::get_id()} to find the instance being backed up. /** > * * Returns the current value of the include_files setting. > * @return string * This setting is intended to ensure that files are not included in > */* generated backups. * * @return int Indicates whether files should be included in backups. */ public function get_include_files() { return $this->includefiles; } /** * Returns the default value for $this->includefiles before we consider any settings. * * @return bool * @throws dml_exception */ protected function get_include_files_default() : bool { // We normally include files. $includefiles = true; // In an import, we don't need to include files. if ($this->get_mode() === backup::MODE_IMPORT) { $includefiles = false; } // When a backup is intended for the same site, we don't need to include the files. // Note, this setting is only used for duplication of an entire course. if ($this->get_mode() === backup::MODE_SAMESITE || $this->get_mode() === backup::MODE_COPY) { $includefiles = false; } // If backup is automated and we have set auto backup config to exclude // files then set them to be excluded here. $backupautofiles = (bool) get_config('backup', 'backup_auto_files'); if ($this->get_mode() === backup::MODE_AUTOMATED && !$backupautofiles) { $includefiles = false; } return $includefiles; }> /** public function get_operation() { > * Gets if this is a backup or restore. return $this->operation; > * } > * @return string > */public function get_id() {> /** return $this->id; > * Gets the instance id of the item being backed up. } > * > * It's meaning is related to the type of backup {@see \backup_controller::get_type()}. public function get_courseid() { > * return $this->courseid; > * @return int } > */> /** public function get_format() { > * Gets the course that the item being backed up is in. return $this->format; > * } > * @return false|int > */public function get_interactive() {> /** return $this->interactive; > * Gets the format the backup is stored in. } > * > * @return string public function get_mode() { > */return $this->mode;> /** } > * Gets if user interaction is expected during the backup. > * public function get_userid() { > * @return bool return $this->userid; > */}> /** > * Gets the mode that the backup will be performed in. public function get_status() { > * return $this->status; > * @return int } > */> /** public function get_execution() { > * Get the id of the user who started the backup. return $this->execution; > * } > * @return int > */public function get_executiontime() {> /** return $this->executiontime; > * Get the current status of the backup. } > * > * @return int /** > */* @return backup_plan> /** */ > * Get if the backup will be executed immediately, or later. public function get_plan() { > * return $this->plan; > * @return int } > */> /** /** > * Get when the backup will be executed. * For debug only. Get a simple test display of all the settings. > * * > * @return int 0 means now, otherwise a Unix timestamp * @return string > */*/> * Gets the plan that will be run during the backup. public function debug_display_all_settings_values(): string { > *return $this->get_plan()->debug_display_all_settings_values(); } /** * Sets the user roles that should be kept in the destination course * for a course copy operation. * * @param array $roleids * @throws backup_controller_exception */ public function set_kept_roles(array $roleids): void { // Only allow of keeping user roles when controller is in copy mode. if ($this->mode != backup::MODE_COPY) { throw new backup_controller_exception('cannot_set_keep_roles_wrong_mode'); } $this->keptroles = $roleids; } /** * Executes the backup * @return void Throws and exception of completes */ public function execute_plan() { // Basic/initial prevention against time/memory limits core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted raise_memory_limit(MEMORY_EXTRA); // Release the session so other tabs in the same session are not blocked. if ($this->get_releasesession() === backup::RELEASESESSION_YES) { \core\session\manager::write_close(); } // If the controller has decided that we can include files, then check the setting, otherwise do not include files. if ($this->get_include_files()) { $this->set_include_files((bool) $this->get_plan()->get_setting('files')->get_value()); } // If this is not a course backup, or single activity backup (e.g. duplicate) inform the plan we are not // including all the activities for sure. This will affect any // task/step executed conditionally to stop including information // for section and activity backup. MDL-28180. if ($this->get_type() !== backup::TYPE_1COURSE && $this->get_type() !== backup::TYPE_1ACTIVITY) { $this->log('notifying plan about excluded activities by type', backup::LOG_DEBUG); $this->plan->set_excluding_activities(); } // Handle copy operation specific settings. if ($this->mode == backup::MODE_COPY) { $this->plan->set_kept_roles($this->keptroles); } return $this->plan->execute(); }> /** public function get_results() { > * Gets the results of the plan execution for this backup. return $this->plan->get_results(); > * } > * @return array > *//** * Save controller information * * @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) */ public function save_controller($includeobj = true, $cleanobj = false) { // Going to save controller to persistent storage, calculate checksum for later checks and save it. // TODO: flag the controller as NA. Any operation on it should be forbidden until loaded back. $this->log('saving controller to db', backup::LOG_DEBUG); if ($includeobj ) { // Only calculate checksum if we are going to include the object. $this->checksum = $this->calculate_checksum(); } backup_controller_dbops::save_controller($this, $this->checksum, $includeobj, $cleanobj); }> /** public static function load_controller($backupid) { > * Loads a backup controller from the database. // Load controller from persistent storage > * // TODO: flag the controller as available. Operations on it can continue > * @param string $backupid The id of the backup controller. $controller = backup_controller_dbops::load_controller($backupid); > * @return \backup_controller $controller->log('loading controller from db', backup::LOG_DEBUG); > */return $controller; } // Protected API starts here> /** protected function calculate_backupid() { > * Creates a unique id for this backup. // Current epoch time + type + id + format + interactive + mode + userid + operation > */// should be unique enough. Add one random part at the end $this->backupid = md5(time() . '-' . $this->type . '-' . $this->id . '-' . $this->format . '-' . $this->interactive . '-' . $this->mode . '-' . $this->userid . '-' . $this->operation . '-' . random_string(20)); }> /** protected function load_plan() { > * Builds the plan for this backup job so that it may be executed. $this->log('loading controller plan', backup::LOG_DEBUG); > */$this->plan = new backup_plan($this); $this->plan->build(); // Build plan for this controller $this->set_status(backup::STATUS_PLANNED); }> /** protected function apply_defaults() { > * Sets default values for the backup controller. $this->log('applying plan defaults', backup::LOG_DEBUG); > */backup_controller_dbops::apply_config_defaults($this); $this->set_status(backup::STATUS_CONFIGURED); $this->set_include_files($this->get_include_files_default()); } /** * Set the initial value for the include_files setting. * * @param bool $includefiles * @see backup_controller::get_include_files for further information on the purpose of this setting. */ protected function set_include_files(bool $includefiles) { $this->log("setting file inclusion to {$this->includefiles}", backup::LOG_DEBUG); $this->includefiles = (int) $includefiles; } }< /*> /*** Exception class used by all the @backup_controller stuff */ class backup_controller_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } }