���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/helper.tar
���ѧ٧ѧ�
backup_array_iterator.class.php 0000644 00000003660 15152170610 0012740 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementation of iterator interface to work with common arrays * * This class implements the iterator interface in order to provide one * common API to be used in backup and restore when, within the same code, * both database recordsets (already iteratorors) and arrays of information * are used. * * TODO: Finish phpdocs */ class backup_array_iterator implements iterator { private $arr; public function __construct(array $arr) { $this->arr = $arr; } public function rewind(): void { reset($this->arr); } #[\ReturnTypeWillChange] public function current() { return current($this->arr); } #[\ReturnTypeWillChange] public function key() { return key($this->arr); } public function next(): void { next($this->arr); } public function valid(): bool { return key($this->arr) !== null; } public function close() { // Added to provide compatibility with recordset iterators reset($this->arr); // Just reset the array } } restore_logs_processor.class.php 0000644 00000013364 15152170610 0013174 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * TODO: Finish phpdocs */ /** * This class is one varying singleton that, for all the logs corresponding to * one task, is in charge of storing all its {@link restore_log_rule} rules, * dispatching to the correct one and insert/log the resulting information. * * Each time the task getting the instance changes, the rules are completely * reloaded with the ones in the new task. And all rules are informed with * new fixed values if explicity set. * * This class adopts the singleton pattern to be able to provide some persistency * of rules along the restore of all the logs corresponding to one restore_task */ class restore_logs_processor { private static $instance; // The current instance of restore_logs_processor private static $task; // The current restore_task instance this processor belongs to private $rules; // Array of restore_log_rule rules (module-action being keys), supports multiple per key private function __construct($values) { // Private constructor // Constructor has been called, so we need to reload everything // Process rules $this->rules = array(); $rules = call_user_func(array(self::$task, 'define_restore_log_rules')); foreach ($rules as $rule) { // TODO: Check it is one restore_log_rule // Set rule restoreid $rule->set_restoreid(self::$task->get_restoreid()); // Set rule fixed values if needed if (is_array($values) and !empty($values)) { $rule->set_fixed_values($values); } // Add the rule to the associative array if (array_key_exists($rule->get_key_name(), $this->rules)) { $this->rules[$rule->get_key_name()][] = $rule; } else { $this->rules[$rule->get_key_name()] = array($rule); } } } public static function get_instance($task, $values) { // If the singleton isn't set or if the task is another one, create new instance if (!isset(self::$instance) || self::$task !== $task) { self::$task = $task; self::$instance = new restore_logs_processor($values); } return self::$instance; } public function process_log_record($log) { // Check we have one restore_log_rule for this log record $keyname = $log->module . '-' . $log->action; if (array_key_exists($keyname, $this->rules)) { // Try it for each rule available foreach ($this->rules[$keyname] as $rule) { $newlog = $rule->process($log); // Some rule has been able to perform the conversion, exit from loop if (!empty($newlog)) { break; } } // Arrived here log is empty, no rule was able to perform the conversion, log the problem if (empty($newlog)) { self::$task->log('Log module-action "' . $keyname . '" process problem. Not restored. ' . json_encode($log), backup::LOG_DEBUG); } } else { // Action not found log the problem self::$task->log('Log module-action "' . $keyname . '" unknown. Not restored. '.json_encode($log), backup::LOG_DEBUG); $newlog = false; } return $newlog; } /** * Adds all the activity {@link restore_log_rule} rules * defined in activity task but corresponding to log * records at course level (cmid = 0). */ public static function register_log_rules_for_course() { $tasks = array(); // To get the list of tasks having log rules for course $rules = array(); // To accumulate rules for course // Add the module tasks $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { if (class_exists('restore_' . $mod . '_activity_task')) { $tasks[$mod] = 'restore_' . $mod . '_activity_task'; } } foreach ($tasks as $mod => $classname) { if (!method_exists($classname, 'define_restore_log_rules_for_course')) { continue; // This method is optional } // Get restore_log_rule array and accumulate $taskrules = call_user_func(array($classname, 'define_restore_log_rules_for_course')); if (!is_array($taskrules)) { throw new restore_logs_processor_exception('define_restore_log_rules_for_course_not_array', $classname); } $rules = array_merge($rules, $taskrules); } return $rules; } } /* * Exception class used by all the @restore_logs_processor stuff */ class restore_logs_processor_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } restore_structure_parser_processor.class.php 0000644 00000011540 15152170610 0015636 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * support the process of all the moodle2 backup files, with * complete specs about what to load (grouped or no), dispatching * to corresponding methods and basic decoding of contents * (NULLs and legacy file.php uses) * * TODO: Complete phpdocs */ class restore_structure_parser_processor extends grouped_parser_processor { protected $courseid; // Course->id we are restoring to protected $step; // @restore_structure_step using this processor public function __construct($courseid, $step) { $this->courseid = $courseid; $this->step = $step; parent::__construct(); } /** * Provide NULL and legacy file.php uses decoding */ public function process_cdata($cdata) { global $CFG; if ($cdata === '$@NULL@$') { // Some cases we know we can skip complete processing return null; } else if ($cdata === '') { return ''; } else if (is_numeric($cdata)) { return $cdata; } else if (strlen($cdata ?? '') < 32) { // Impossible to have one link in 32cc. // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=). return $cdata; } if (strpos($cdata, '$@FILEPHP@$') !== false) { // We need to convert $@FILEPHP@$. if ($CFG->slasharguments) { $slash = '/'; $forcedownload = '?forcedownload=1'; } else { $slash = '%2F'; $forcedownload = '&forcedownload=1'; } // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash. $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash); // Decode file.php calls. $search = array ("$@FILEPHP@$"); $replace = array($basefileurl); $result = str_replace($search, $replace, $cdata); // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799. $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$'); $replace = array($slash, $forcedownload); $cdata = str_replace($search, $replace, $result); } if (strpos($cdata, '$@H5PEMBED@$') !== false) { // We need to convert $@H5PEMBED@$. // Decode embed.php calls. $cdata = str_replace('$@H5PEMBED@$', $CFG->wwwroot.'/h5p/embed.php', $cdata); } return $cdata; } /** * Override this method so we'll be able to skip * dispatching some well-known chunks, like the * ones being 100% part of subplugins stuff. Useful * for allowing development without having all the * possible restore subplugins defined */ protected function postprocess_chunk($data) { // Iterate over all the data tags, if any of them is // not 'subplugin_XXXX' or has value, then it's a valid chunk, // pass it to standard (parent) processing of chunks. foreach ($data['tags'] as $key => $value) { if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) { parent::postprocess_chunk($data); return; } } // Arrived here, all the tags correspond to sublplugins and are empty, // skip the chunk, and debug_developer notice $this->chunks--; // not counted debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) . ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER); } protected function dispatch_chunk($data) { $this->step->process($data); } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } } async_helper.class.php 0000644 00000033056 15152170610 0011042 0 ustar 00 <?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/>. /** * Helper functions for asynchronous backups and restores. * * @package core * @copyright 2019 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/user/lib.php'); /** * Helper functions for asynchronous backups and restores. * * @package core * @copyright 2019 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class async_helper { /** * @var string $type The type of async operation. */ protected $type = 'backup'; /** * @var string $backupid The id of the backup or restore. */ protected $backupid; /** * @var object $user The user who created the backup record. */ protected $user; /** * @var object $backuprec The backup controller record from the database. */ protected $backuprec; /** * Class constructor. * * @param string $type The type of async operation. * @param string $id The id of the backup or restore. */ public function __construct($type, $id) { $this->type = $type; $this->backupid = $id; $this->backuprec = self::get_backup_record($id); $this->user = $this->get_user(); } /** * Given a backup id return a the record from the database. * We use this method rather than 'load_controller' as the controller may * not exist if this backup/restore has completed. * * @param int $id The backup id to get. * @return object $backuprec The backup controller record. */ static public function get_backup_record($id) { global $DB; $backuprec = $DB->get_record('backup_controllers', array('backupid' => $id), '*', MUST_EXIST); return $backuprec; } /** * Given a user id return a user object. * * @return object $user The limited user record. */ private function get_user() { $userid = $this->backuprec->userid; $user = core_user::get_user($userid, '*', MUST_EXIST); return $user; } /** * Return appropriate description for current async operation {@see async_helper::type} * * @return string */ private function get_operation_description(): string { $operations = [ 'backup' => new lang_string('backup'), 'copy' => new lang_string('copycourse'), 'restore' => new lang_string('restore'), ]; return (string) ($operations[$this->type] ?? $this->type); } /** * Callback for preg_replace_callback. * Replaces message placeholders with real values. * * @param array $matches The match array from from preg_replace_callback. * @return string $match The replaced string. */ private function lookup_message_variables($matches) { $options = array( 'operation' => $this->get_operation_description(), 'backupid' => $this->backupid, 'user_username' => $this->user->username, 'user_email' => $this->user->email, 'user_firstname' => $this->user->firstname, 'user_lastname' => $this->user->lastname, 'link' => $this->get_resource_link(), ); $match = $options[$matches[1]] ?? $matches[1]; return $match; } /** * Get the link to the resource that is being backuped or restored. * * @return moodle_url $url The link to the resource. */ private function get_resource_link() { // Get activity context only for backups. if ($this->backuprec->type == 'activity' && $this->type == 'backup') { $context = context_module::instance($this->backuprec->itemid); } else { // Course or Section which have the same context getter. $context = context_course::instance($this->backuprec->itemid); } // Generate link based on operation type. if ($this->type == 'backup') { // For backups simply generate link to restore file area UI. $url = new moodle_url('/backup/restorefile.php', array('contextid' => $context->id)); } else { // For restore generate link to the item itself. $url = $context->get_url(); } return $url; } /** * Sends a confirmation message for an aynchronous process. * * @return int $messageid The id of the sent message. */ public function send_message() { global $USER; $subjectraw = get_config('backup', 'backup_async_message_subject'); $subjecttext = preg_replace_callback( '/\{([-_A-Za-z0-9]+)\}/u', array('async_helper', 'lookup_message_variables'), $subjectraw); $messageraw = get_config('backup', 'backup_async_message'); $messagehtml = preg_replace_callback( '/\{([-_A-Za-z0-9]+)\}/u', array('async_helper', 'lookup_message_variables'), $messageraw); $messagetext = html_to_text($messagehtml); $message = new \core\message\message(); $message->component = 'moodle'; $message->name = 'asyncbackupnotification'; $message->userfrom = $USER; $message->userto = $this->user; $message->subject = $subjecttext; $message->fullmessage = $messagetext; $message->fullmessageformat = FORMAT_HTML; $message->fullmessagehtml = $messagehtml; $message->smallmessage = ''; $message->notification = '1'; $messageid = message_send($message); return $messageid; } /** * Check if asynchronous backup and restore mode is * enabled at system level. * * @return bool $async True if async mode enabled false otherwise. */ static public function is_async_enabled() { global $CFG; $async = false; if (!empty($CFG->enableasyncbackup)) { $async = true; } return $async; } /** * Check if there is a pending async operation for given details. * * @param int $id The item id to check in the backup record. * @param string $type The type of operation: course, activity or section. * @param string $operation Operation backup or restore. * @return boolean $asyncpedning Is there a pending async operation. */ public static function is_async_pending($id, $type, $operation) { global $DB, $USER, $CFG; $asyncpending = false; // Only check for pending async operations if async mode is enabled. require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); $select = 'userid = ? AND itemid = ? AND type = ? AND operation = ? AND execution = ? AND status < ? AND status > ?'; $params = array( $USER->id, $id, $type, $operation, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK ); $asyncrecord= $DB->get_record_select('backup_controllers', $select, $params); if ((self::is_async_enabled() && $asyncrecord) || ($asyncrecord && $asyncrecord->purpose == backup::MODE_COPY)) { $asyncpending = true; } return $asyncpending; } /** * Get the size, url and restore url for a backup file. * * @param string $filename The name of the file to get info for. * @param string $filearea The file area for the file. * @param int $contextid The context ID of the file. * @return array $results The result array containing the size, url and restore url of the file. */ public static function get_backup_file_info($filename, $filearea, $contextid) { $fs = get_file_storage(); $file = $fs->get_file($contextid, 'backup', $filearea, 0, '/', $filename); $filesize = display_size ($file->get_filesize()); $fileurl = moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), null, $file->get_filepath(), $file->get_filename(), true ); $params = array(); $params['action'] = 'choosebackupfile'; $params['filename'] = $file->get_filename(); $params['filepath'] = $file->get_filepath(); $params['component'] = $file->get_component(); $params['filearea'] = $file->get_filearea(); $params['filecontextid'] = $file->get_contextid(); $params['contextid'] = $contextid; $params['itemid'] = $file->get_itemid(); $restoreurl = new moodle_url('/backup/restorefile.php', $params); $filesize = display_size ($file->get_filesize()); $results = array( 'filesize' => $filesize, 'fileurl' => $fileurl->out(false), 'restoreurl' => $restoreurl->out(false)); return $results; } /** * Get the url of a restored backup item based on the backup ID. * * @param string $backupid The backup ID to get the restore location url. * @return array $urlarray The restored item URL as an array. */ public static function get_restore_url($backupid) { global $DB; $backupitemid = $DB->get_field('backup_controllers', 'itemid', array('backupid' => $backupid), MUST_EXIST); $newcontext = context_course::instance($backupitemid); $restoreurl = $newcontext->get_url()->out(); $urlarray = array('restoreurl' => $restoreurl); return $urlarray; } /** * Get markup for in progress async backups, * to use in backup table UI. * * @param \core_backup_renderer $renderer The backup renderer object. * @param integer $instanceid The context id to get backup data for. * @return array $tabledata the rows of table data. */ public static function get_async_backups($renderer, $instanceid) { global $DB; $tabledata = array(); // Get relevant backup ids based on context instance id. $select = 'itemid = :itemid AND execution = :execution AND status < :status1 AND status > :status2 ' . 'AND operation = :operation'; $params = [ 'itemid' => $instanceid, 'execution' => backup::EXECUTION_DELAYED, 'status1' => backup::STATUS_FINISHED_ERR, 'status2' => backup::STATUS_NEED_PRECHECK, 'operation' => 'backup', ]; $backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated'); foreach ($backups as $backup) { $bc = \backup_controller::load_controller($backup->backupid); // Get the backup controller. $filename = $bc->get_plan()->get_setting('filename')->get_value(); $timecreated = $backup->timecreated; $status = $renderer->get_status_display($bc->get_status(), $bc->get_backupid()); $bc->destroy(); $tablerow = array($filename, userdate($timecreated), '-', '-', '-', $status); $tabledata[] = $tablerow; } return $tabledata; } /** * Get the course name of the resource being restored. * * @param \context $context The Moodle context for the restores. * @return string $coursename The full name of the course. */ public static function get_restore_name(\context $context) { global $DB; $instanceid = $context->instanceid; if ($context->contextlevel == CONTEXT_MODULE) { // For modules get the course name and module name. $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); $coursename = $DB->get_field('course', 'fullname', array('id' => $cm->course)); $itemname = $coursename . ' - ' . $cm->name; } else { $itemname = $DB->get_field('course', 'fullname', array('id' => $context->instanceid)); } return $itemname; } /** * Get all the current in progress async restores for a user. * * @param int $userid Moodle user id. * @return array $restores List of current restores in progress. */ public static function get_async_restores($userid) { global $DB; $select = 'userid = ? AND execution = ? AND status < ? AND status > ? AND operation = ?'; $params = array($userid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK, 'restore'); $restores = $DB->get_records_select( 'backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, status, itemid, timecreated'); return $restores; } } restore_inforef_parser_processor.class.php 0000644 00000004343 15152170610 0015231 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one inforef.xml file to the backup_ids table * * TODO: Complete phpdocs */ class restore_inforef_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Get itemnames handled by inforef files $items = backup_helper::get_inforef_itemnames(); // Let's add all them as target paths for the processor foreach($items as $itemname) { $pathvalue = '/inforef/' . $itemname . 'ref/' . $itemname; $this->add_path($pathvalue); } } protected function dispatch_chunk($data) { // Received one inforef chunck, we are going to store it into backup_ids // table, with name = itemname + "ref" for later use $itemname = basename($data['path']). 'ref'; $itemid = $data['tags']['id']; restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid); } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } } restore_users_parser_processor.class.php 0000644 00000006364 15152170610 0014747 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one users.xml file to the backup_ids table * storing the whole structure there for later processing. * Note: only "needed" users are loaded (must have userref record in backup_ids) * Note: parentitemid will contain the user->contextid * Note: althought included in backup, we don't restore user context ras/caps * in same site they will be already there and it doesn't seem a good idea * to make them "transportable" arround sites. * * TODO: Complete phpdocs */ class restore_users_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Set the paths we are interested on, returning all them grouped under user $this->add_path('/users/user', true); $this->add_path('/users/user/custom_fields/custom_field'); $this->add_path('/users/user/tags/tag'); $this->add_path('/users/user/preferences/preference'); // As noted above, we skip user context ras and caps // $this->add_path('/users/user/roles/role_overrides/override'); // $this->add_path('/users/user/roles/role_assignments/assignment'); } protected function dispatch_chunk($data) { // Received one user chunck, we are going to store it into backup_ids // table, with name = user and parentid = contextid for later use $itemname = 'user'; $itemid = $data['tags']['id']; $parentitemid = $data['tags']['contextid']; $info = $data['tags']; // Only load it if needed (exist same userref itemid in table) if (restore_dbops::get_backup_ids_record($this->restoreid, 'userref', $itemid)) { restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, $parentitemid, $info); } } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } /** * Provide NULL decoding */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } } backup_null_iterator.class.php 0000644 00000003247 15152170610 0012575 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementation of iterator interface to work without information * * This class implementes the iterator but does nothing (as far as it * doesn't handle real data at all). It's here to provide one common * API when we want to skip some elements from structure, while also * working with array/db iterators at the same time. * * TODO: Finish phpdocs */ class backup_null_iterator implements iterator { public function rewind(): void { } #[\ReturnTypeWillChange] public function current() { } #[\ReturnTypeWillChange] public function key() { } public function next(): void { } public function valid(): bool { return false; } public function close() { // Added to provide compatibility with recordset iterators } } backup_helper.class.php 0000644 00000037512 15152170610 0011173 0 ustar 00 <?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-helper * @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 various operations * * TODO: Finish phpdocs */ abstract class backup_helper { /** * Given one backupid, create all the needed dirs to have one backup temp dir available */ static public function check_and_create_backup_dir($backupid) { $backupiddir = make_backup_temp_directory($backupid, false); if (empty($backupiddir)) { throw new backup_helper_exception('cannot_create_backup_temp_dir'); } } /** * Given one backupid, ensure its temp dir is completely empty * * If supplied, progress object should be ready to receive indeterminate * progress reports. * * @param string $backupid Backup id * @param \core\progress\base $progress Optional progress reporting object */ static public function clear_backup_dir($backupid, \core\progress\base $progress = null) { $backupiddir = make_backup_temp_directory($backupid, false); if (!self::delete_dir_contents($backupiddir, '', $progress)) { throw new backup_helper_exception('cannot_empty_backup_temp_dir'); } return true; } /** * Given one backupid, delete completely its temp dir * * If supplied, progress object should be ready to receive indeterminate * progress reports. * * @param string $backupid Backup id * @param \core\progress\base $progress Optional progress reporting object */ static public function delete_backup_dir($backupid, \core\progress\base $progress = null) { $backupiddir = make_backup_temp_directory($backupid, false); self::clear_backup_dir($backupid, $progress); return rmdir($backupiddir); } /** * Given one fullpath to directory, delete its contents recursively * Copied originally from somewhere in the net. * TODO: Modernise this * * If supplied, progress object should be ready to receive indeterminate * progress reports. * * @param string $dir Directory to delete * @param string $excludedir Exclude this directory * @param \core\progress\base $progress Optional progress reporting object */ static public function delete_dir_contents($dir, $excludeddir='', \core\progress\base $progress = null) { global $CFG; if ($progress) { $progress->progress(); } if (!is_dir($dir)) { // if we've been given a directory that doesn't exist yet, return true. // this happens when we're trying to clear out a course that has only just // been created. return true; } $slash = "/"; // Create arrays to store files and directories $dir_files = array(); $dir_subdirs = array(); // Make sure we can delete it chmod($dir, $CFG->directorypermissions); if ((($handle = opendir($dir))) == false) { // The directory could not be opened return false; } // Loop through all directory entries, and construct two temporary arrays containing files and sub directories while (false !== ($entry = readdir($handle))) { if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != "." && $entry != $excludeddir) { $dir_subdirs[] = $dir. $slash .$entry; } else if ($entry != ".." && $entry != "." && $entry != $excludeddir) { $dir_files[] = $dir. $slash .$entry; } } // Delete all files in the curent directory return false and halt if a file cannot be removed for ($i=0; $i<count($dir_files); $i++) { chmod($dir_files[$i], $CFG->directorypermissions); if (((unlink($dir_files[$i]))) == false) { return false; } } // Empty sub directories and then remove the directory for ($i=0; $i<count($dir_subdirs); $i++) { chmod($dir_subdirs[$i], $CFG->directorypermissions); if (self::delete_dir_contents($dir_subdirs[$i], '', $progress) == false) { return false; } else { if (remove_dir($dir_subdirs[$i]) == false) { return false; } } } // Close directory closedir($handle); // Success, every thing is gone return true return true; } /** * Delete all the temp dirs older than the time specified. * * If supplied, progress object should be ready to receive indeterminate * progress reports. * * @param int $deletebefore Delete files and directories older than this time * @param \core\progress\base $progress Optional progress reporting object */ static public function delete_old_backup_dirs($deletebefore, \core\progress\base $progress = null) { $status = true; // Get files and directories in the backup temp dir. $backuptempdir = make_backup_temp_directory(''); $items = new DirectoryIterator($backuptempdir); foreach ($items as $item) { if ($item->isDot()) { continue; } if ($item->getMTime() < $deletebefore) { if ($item->isDir()) { // The item is a directory for some backup. if (!self::delete_backup_dir($item->getFilename(), $progress)) { // Something went wrong. Finish the list of items and then throw an exception. $status = false; } } else if ($item->isFile()) { unlink($item->getPathname()); } } } if (!$status) { throw new backup_helper_exception('problem_deleting_old_backup_temp_dirs'); } } /** * This function will be invoked by any log() method in backup/restore, acting * as a simple forwarder to the standard loggers but also, if the $display * parameter is true, supporting translation via get_string() and sending to * standard output. */ static public function log($message, $level, $a, $depth, $display, $logger) { // Send to standard loggers $logmessage = $message; $options = empty($depth) ? array() : array('depth' => $depth); if (!empty($a)) { $logmessage = $logmessage . ' ' . implode(', ', (array)$a); } $logger->process($logmessage, $level, $options); // If $display specified, send translated string to output_controller if ($display) { output_controller::get_instance()->output($message, 'backup', $a, $depth); } } /** * Given one backupid and the (FS) final generated file, perform its final storage * into Moodle file storage. For stored files it returns the complete file_info object * * Note: the $filepath is deleted if the backup file is created successfully * * 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 int $backupid * @param string $filepath zip file containing the backup * @param \core\progress\base $progress Optional progress monitor * @return stored_file if created, null otherwise * * @throws moodle_exception in case of any problems */ static public function store_backup_file($backupid, $filepath, \core\progress\base $progress = null) { global $CFG; // First of all, get some information from the backup_controller to help us decide list($dinfo, $cinfo, $sinfo) = backup_controller_dbops::get_moodle_backup_information( $backupid, $progress); // Extract useful information to decide $hasusers = (bool)$sinfo['users']->value; // Backup has users $isannon = (bool)$sinfo['anonymize']->value; // Backup is anonymised $filename = $sinfo['filename']->value; // Backup filename $backupmode= $dinfo[0]->mode; // Backup mode backup::MODE_GENERAL/IMPORT/HUB $backuptype= $dinfo[0]->type; // Backup type backup::TYPE_1ACTIVITY/SECTION/COURSE $userid = $dinfo[0]->userid; // User->id executing the backup $id = $dinfo[0]->id; // Id of activity/section/course (depends of type) $courseid = $dinfo[0]->courseid; // Id of the course $format = $dinfo[0]->format; // Type of backup file // Quick hack. If for any reason, filename is blank, fix it here. // TODO: This hack will be out once MDL-22142 - P26 gets fixed if (empty($filename)) { $filename = backup_plan_dbops::get_default_backup_filename('moodle2', $backuptype, $id, $hasusers, $isannon); } // Backups of type IMPORT aren't stored ever if ($backupmode == backup::MODE_IMPORT) { return null; } if (!is_readable($filepath)) { // we have a problem if zip file does not exist throw new coding_exception('backup_helper::store_backup_file() expects valid $filepath parameter'); } // Calculate file storage options of id being backup $ctxid = 0; $filearea = ''; $component = ''; $itemid = 0; switch ($backuptype) { case backup::TYPE_1ACTIVITY: $ctxid = context_module::instance($id)->id; $component = 'backup'; $filearea = 'activity'; $itemid = 0; break; case backup::TYPE_1SECTION: $ctxid = context_course::instance($courseid)->id; $component = 'backup'; $filearea = 'section'; $itemid = $id; break; case backup::TYPE_1COURSE: $ctxid = context_course::instance($courseid)->id; $component = 'backup'; $filearea = 'course'; $itemid = 0; break; } if ($backupmode == backup::MODE_AUTOMATED) { // Automated backups have there own special area! $filearea = 'automated'; // If we're keeping the backup only in a chosen path, just move it there now // this saves copying from filepool to here later and filling trashdir. $config = get_config('backup'); $dir = $config->backup_auto_destination; if ($config->backup_auto_storage == 1 and $dir and is_dir($dir) and is_writable($dir)) { $filedest = $dir.'/' .backup_plan_dbops::get_default_backup_filename( $format, $backuptype, $courseid, $hasusers, $isannon, !$config->backup_shortname, (bool)$config->backup_auto_files); // first try to move the file, if it is not possible copy and delete instead if (@rename($filepath, $filedest)) { return null; } umask($CFG->umaskpermissions); if (copy($filepath, $filedest)) { @chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot unlink($filepath); return null; } else { $bc = backup_controller::load_controller($backupid); $bc->log('Attempt to copy backup file to the specified directory using filesystem failed - ', backup::LOG_WARNING, $dir); $bc->destroy(); } // bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system } } // Backups of type HUB (by definition never have user info) // are sent to user's "user_tohub" file area. The upload process // will be responsible for cleaning that filearea once finished if ($backupmode == backup::MODE_HUB) { $ctxid = context_user::instance($userid)->id; $component = 'user'; $filearea = 'tohub'; $itemid = 0; } // Backups without user info or with the anonymise functionality // enabled are sent to user's "user_backup" // file area. Maintenance of such area is responsibility of // the user via corresponding file manager frontend if ($backupmode == backup::MODE_GENERAL && (!$hasusers || $isannon)) { $ctxid = context_user::instance($userid)->id; $component = 'user'; $filearea = 'backup'; $itemid = 0; } // Let's send the file to file storage, everything already defined $fs = get_file_storage(); $fr = array( 'contextid' => $ctxid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, 'filepath' => '/', 'filename' => $filename, 'userid' => $userid, 'timecreated' => time(), 'timemodified'=> time()); // If file already exists, delete if before // creating it again. This is BC behaviour - copy() // overwrites by default if ($fs->file_exists($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename'])) { $pathnamehash = $fs->get_pathname_hash($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename']); $sf = $fs->get_file_by_hash($pathnamehash); $sf->delete(); } $file = $fs->create_file_from_pathname($fr, $filepath); unlink($filepath); return $file; } /** * This function simply marks one param to be considered as straight sql * param, so it won't be searched in the structure tree nor converted at * all. Useful for better integration of definition of sources in structure * and DB stuff */ public static function is_sqlparam($value) { return array('sqlparam' => $value); } /** * This function returns one array of itemnames that are being handled by * inforef.xml files. Used both by backup and restore */ public static function get_inforef_itemnames() { return array('user', 'grouping', 'group', 'role', 'file', 'scale', 'outcome', 'grade_item', 'question_category'); } } /* * Exception class used by all the @helper stuff */ class backup_helper_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } backup_file_manager.class.php 0000644 00000006705 15152170610 0012325 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Collection of helper functions to handle files * * This class implements various functions related with moodle storage * handling (get file from storage, put it there...) and some others * to use the zip/unzip facilities. * * Note: It's supposed that, some day, files implementation will offer * those functions without needeing to know storage internals at all. * That day, we'll move related functions here to proper file api ones. * * TODO: Finish phpdocs */ class backup_file_manager { /** * Returns the full path to backup storage base dir */ public static function get_backup_storage_base_dir($backupid) { global $CFG; $backupiddir = make_backup_temp_directory($backupid); return $backupiddir . '/files'; } /** * Given one file content hash, returns the path (relative to filedir) * to the file. */ public static function get_backup_content_file_location($contenthash) { $l1 = $contenthash[0].$contenthash[1]; return "$l1/$contenthash"; } /** * Copy one file from moodle storage to backup storage */ public static function copy_file_moodle2backup($backupid, $filerecorid) { global $DB; if (!backup_controller_dbops::backup_includes_files($backupid)) { // Only include the files if required by the controller. return; } // Normalise param if (!is_object($filerecorid)) { $filerecorid = $DB->get_record('files', array('id' => $filerecorid)); } // Directory, nothing to do if ($filerecorid->filename === '.') { return; } $fs = get_file_storage(); $file = $fs->get_file_instance($filerecorid); // If the file is external file, skip copying. if ($file->is_external_file()) { return; } // Calculate source and target paths (use same subdirs strategy for both) $targetfilepath = self::get_backup_storage_base_dir($backupid) . '/' . self::get_backup_content_file_location($filerecorid->contenthash); // Create target dir if necessary if (!file_exists(dirname($targetfilepath))) { if (!check_dir_exists(dirname($targetfilepath), true, true)) { throw new backup_helper_exception('cannot_create_directory', dirname($targetfilepath)); } } // And copy the file (if doesn't exist already) if (!file_exists($targetfilepath)) { $file->copy_content_to($targetfilepath); } } } backup_cron_helper.class.php 0000644 00000077464 15152170610 0012226 0 ustar 00 <?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/>. /** * Utility helper for automated backups run through cron. * * @package core * @subpackage backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * This class is an abstract class with methods that can be called to aid the * running of automated backups over cron. */ abstract class backup_cron_automated_helper { /** Automated backups are active and ready to run */ const STATE_OK = 0; /** Automated backups are disabled and will not be run */ const STATE_DISABLED = 1; /** Automated backups are all ready running! */ const STATE_RUNNING = 2; /** Course automated backup completed successfully */ const BACKUP_STATUS_OK = 1; /** Course automated backup errored */ const BACKUP_STATUS_ERROR = 0; /** Course automated backup never finished */ const BACKUP_STATUS_UNFINISHED = 2; /** Course automated backup was skipped */ const BACKUP_STATUS_SKIPPED = 3; /** Course automated backup had warnings */ const BACKUP_STATUS_WARNING = 4; /** Course automated backup has yet to be run */ const BACKUP_STATUS_NOTYETRUN = 5; /** Course automated backup has been added to adhoc task queue */ const BACKUP_STATUS_QUEUED = 6; /** Run if required by the schedule set in config. Default. **/ const RUN_ON_SCHEDULE = 0; /** Run immediately. **/ const RUN_IMMEDIATELY = 1; const AUTO_BACKUP_DISABLED = 0; const AUTO_BACKUP_ENABLED = 1; const AUTO_BACKUP_MANUAL = 2; /** Automated backup storage in course backup filearea */ const STORAGE_COURSE = 0; /** Automated backup storage in specified directory */ const STORAGE_DIRECTORY = 1; /** Automated backup storage in course backup filearea and specified directory */ const STORAGE_COURSE_AND_DIRECTORY = 2; /** * Get the courses to backup. * * When there are multiple courses to backup enforce some order to the record set. * The following is the preference order. * First backup courses that do not have an entry in backup_courses first, * as they are likely new and never been backed up. Do the oldest modified courses first. * Then backup courses that have previously been backed up starting with the oldest next start time. * Finally, all else being equal, defer to the sortorder of the courses. * * @param null|int $now timestamp to use in course selection. * @return moodle_recordset The recordset of matching courses. */ protected static function get_courses($now = null) { global $DB; if ($now == null) { $now = time(); } $sql = 'SELECT c.*, COALESCE(bc.nextstarttime, 1) nextstarttime FROM {course} c LEFT JOIN {backup_courses} bc ON bc.courseid = c.id WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ? ORDER BY nextstarttime ASC, c.timemodified DESC, c.sortorder'; $params = array( $now, // Only get courses where the backup start time is in the past. ); $rs = $DB->get_recordset_sql($sql, $params); return $rs; } /** * Runs the automated backups if required * * @param bool $rundirective */ public static function run_automated_backup($rundirective = self::RUN_ON_SCHEDULE) { $now = time(); $lock = self::get_automated_backup_lock($rundirective); if (!$lock) { return; } try { mtrace("Checking courses"); mtrace("Skipping deleted courses", '...'); mtrace(sprintf("%d courses", self::remove_deleted_courses_from_schedule())); mtrace('Running required automated backups...'); cron_trace_time_and_memory(); mtrace("Getting admin info"); $admin = get_admin(); if (!$admin) { mtrace("Error: No admin account was found"); return; } $rs = self::get_courses($now); // Get courses to backup. $emailpending = self::check_and_push_automated_backups($rs, $admin); $rs->close(); // Send email to admin if necessary. if ($emailpending) { self::send_backup_status_to_admin($admin); } } finally { // Everything is finished release lock. $lock->release(); mtrace('Automated backups complete.'); } } /** * Gets the results from the last automated backup that was run based upon * the statuses of the courses that were looked at. * * @return array */ public static function get_backup_status_array() { global $DB; $result = array( self::BACKUP_STATUS_ERROR => 0, self::BACKUP_STATUS_OK => 0, self::BACKUP_STATUS_UNFINISHED => 0, self::BACKUP_STATUS_SKIPPED => 0, self::BACKUP_STATUS_WARNING => 0, self::BACKUP_STATUS_NOTYETRUN => 0, self::BACKUP_STATUS_QUEUED => 0, ); $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus'); foreach ($statuses as $status) { if (empty($status->statuscount)) { $status->statuscount = 0; } $result[(int)$status->laststatus] += $status->statuscount; } return $result; } /** * Collect details for all statuses of the courses * and send report to admin. * * @param stdClass $admin * @return array */ private static function send_backup_status_to_admin($admin) { global $DB, $CFG; mtrace("Sending email to admin"); $message = ""; $count = self::get_backup_status_array(); $haserrors = ($count[self::BACKUP_STATUS_ERROR] != 0 || $count[self::BACKUP_STATUS_UNFINISHED] != 0); // Build the message text. // Summary. $message .= get_string('summary') . "\n"; $message .= "==================================================\n"; $message .= ' ' . get_string('courses') . ': ' . array_sum($count) . "\n"; $message .= ' ' . get_string('statusok') . ': ' . $count[self::BACKUP_STATUS_OK] . "\n"; $message .= ' ' . get_string('skipped') . ': ' . $count[self::BACKUP_STATUS_SKIPPED] . "\n"; $message .= ' ' . get_string('error') . ': ' . $count[self::BACKUP_STATUS_ERROR] . "\n"; $message .= ' ' . get_string('unfinished') . ': ' . $count[self::BACKUP_STATUS_UNFINISHED] . "\n"; $message .= ' ' . get_string('backupadhocpending') . ': ' . $count[self::BACKUP_STATUS_QUEUED] . "\n"; $message .= ' ' . get_string('warning') . ': ' . $count[self::BACKUP_STATUS_WARNING] . "\n"; $message .= ' ' . get_string('backupnotyetrun') . ': ' . $count[self::BACKUP_STATUS_NOTYETRUN]."\n\n"; // Reference. if ($haserrors) { $message .= " ".get_string('backupfailed')."\n\n"; $desturl = "$CFG->wwwroot/report/backups/index.php"; $message .= " ".get_string('backuptakealook', '', $desturl)."\n\n"; // Set message priority. $admin->priority = 1; // Reset error and unfinished statuses to ok if longer than 24 hours. $sql = "laststatus IN (:statuserror,:statusunfinished) AND laststarttime < :yesterday"; $params = [ 'statuserror' => self::BACKUP_STATUS_ERROR, 'statusunfinished' => self::BACKUP_STATUS_UNFINISHED, 'yesterday' => time() - 86400, ]; $DB->set_field_select('backup_courses', 'laststatus', self::BACKUP_STATUS_OK, $sql, $params); } else { $message .= " ".get_string('backupfinished')."\n"; } // Build the message subject. $site = get_site(); $prefix = format_string($site->shortname, true, array('context' => context_course::instance(SITEID))).": "; if ($haserrors) { $prefix .= "[".strtoupper(get_string('error'))."] "; } $subject = $prefix.get_string('automatedbackupstatus', 'backup'); // Send the message. $eventdata = new \core\message\message(); $eventdata->courseid = SITEID; $eventdata->modulename = 'moodle'; $eventdata->userfrom = $admin; $eventdata->userto = $admin; $eventdata->subject = $subject; $eventdata->fullmessage = $message; $eventdata->fullmessageformat = FORMAT_PLAIN; $eventdata->fullmessagehtml = ''; $eventdata->smallmessage = ''; $eventdata->component = 'moodle'; $eventdata->name = 'backup'; return message_send($eventdata); } /** * Loop through courses and push to course ad-hoc task if required * * @param \record_set $courses * @param stdClass $admin * @return boolean */ private static function check_and_push_automated_backups($courses, $admin) { global $DB; $now = time(); $emailpending = false; $nextstarttime = self::calculate_next_automated_backup(null, $now); $showtime = "undefined"; if ($nextstarttime > 0) { $showtime = date('r', $nextstarttime); } foreach ($courses as $course) { $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id)); if (!$backupcourse) { $backupcourse = new stdClass; $backupcourse->courseid = $course->id; $backupcourse->laststatus = self::BACKUP_STATUS_NOTYETRUN; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id)); } // Check if we are going to be running the backup now. $shouldrunnow = ($backupcourse->nextstarttime > 0 && $backupcourse->nextstarttime < $now); // Check if the course is not scheduled to run right now, or it has been put in queue. if (!$shouldrunnow || $backupcourse->laststatus == self::BACKUP_STATUS_QUEUED) { $backupcourse->nextstarttime = $nextstarttime; $DB->update_record('backup_courses', $backupcourse); mtrace('Skipping course id ' . $course->id . ': Not scheduled for backup until ' . $showtime); } else { $skipped = self::should_skip_course_backup($backupcourse, $course, $nextstarttime); if (!$skipped) { // If it should not be skipped. // Only make the backup if laststatus isn't 2-UNFINISHED (uncontrolled error or being backed up). if ($backupcourse->laststatus != self::BACKUP_STATUS_UNFINISHED) { // Add every non-skipped courses to backup adhoc task queue. mtrace('Putting backup of course id ' . $course->id . ' in adhoc task queue'); // We have to send an email because we have included at least one backup. $emailpending = true; // Create adhoc task for backup. self::push_course_backup_adhoc_task($backupcourse, $admin); } } } } return $emailpending; } /** * Check if we can skip this course backup. * * @param stdClass $backupcourse * @param stdClass $course * @param int $nextstarttime * @return boolean */ private static function should_skip_course_backup($backupcourse, $course, $nextstarttime) { global $DB; $config = get_config('backup'); $now = time(); // Assume that we are not skipping anything. $skipped = false; $skippedmessage = ''; // The last backup is considered as successful when OK or SKIPPED. $lastbackupwassuccessful = ($backupcourse->laststatus == self::BACKUP_STATUS_SKIPPED || $backupcourse->laststatus == self::BACKUP_STATUS_OK) && ( $backupcourse->laststarttime > 0 && $backupcourse->lastendtime > 0); // If config backup_auto_skip_hidden is set to true, skip courses that are not visible. if ($config->backup_auto_skip_hidden) { $skipped = ($config->backup_auto_skip_hidden && !$course->visible); $skippedmessage = 'Not visible'; } // If config backup_auto_skip_modif_days is set to true, skip courses // that have not been modified since the number of days defined. if (!$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_days) { $timenotmodifsincedays = $now - ($config->backup_auto_skip_modif_days * DAYSECS); // Check log if there were any modifications to the course content. $logexists = self::is_course_modified($course->id, $timenotmodifsincedays); $skipped = ($course->timemodified <= $timenotmodifsincedays && !$logexists); $skippedmessage = 'Not modified in the past '.$config->backup_auto_skip_modif_days.' days'; } // If config backup_auto_skip_modif_prev is set to true, skip courses // that have not been modified since previous backup. if (!$skipped && $lastbackupwassuccessful && $config->backup_auto_skip_modif_prev) { // Check log if there were any modifications to the course content. $logexists = self::is_course_modified($course->id, $backupcourse->laststarttime); $skipped = ($course->timemodified <= $backupcourse->laststarttime && !$logexists); $skippedmessage = 'Not modified since previous backup'; } if ($skipped) { // Must have been skipped for a reason. $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED; $backupcourse->nextstarttime = $nextstarttime; $DB->update_record('backup_courses', $backupcourse); mtrace('Skipping course id ' . $course->id . ': ' . $skippedmessage); } return $skipped; } /** * Create course backup adhoc task * * @param stdClass $backupcourse * @param stdClass $admin * @return void */ private static function push_course_backup_adhoc_task($backupcourse, $admin) { global $DB; $asynctask = new \core\task\course_backup_task(); $asynctask->set_blocking(false); $asynctask->set_custom_data(array( 'courseid' => $backupcourse->courseid, 'adminid' => $admin->id )); \core\task\manager::queue_adhoc_task($asynctask); $backupcourse->laststatus = self::BACKUP_STATUS_QUEUED; $DB->update_record('backup_courses', $backupcourse); } /** * Works out the next time the automated backup should be run. * * @param mixed $ignoredtimezone all settings are in server timezone! * @param int $now timestamp, should not be in the past, most likely time() * @return int timestamp of the next execution at server time */ public static function calculate_next_automated_backup($ignoredtimezone, $now) { $config = get_config('backup'); $backuptime = new DateTime('@' . $now); $backuptime->setTimezone(core_date::get_server_timezone_object()); $backuptime->setTime($config->backup_auto_hour, $config->backup_auto_minute); while ($backuptime->getTimestamp() < $now) { $backuptime->add(new DateInterval('P1D')); } // Get number of days from backup date to execute backups. $automateddays = substr($config->backup_auto_weekdays, $backuptime->format('w')) . $config->backup_auto_weekdays; $daysfromnow = strpos($automateddays, "1"); // Error, there are no days to schedule the backup for. if ($daysfromnow === false) { return 0; } if ($daysfromnow > 0) { $backuptime->add(new DateInterval('P' . $daysfromnow . 'D')); } return $backuptime->getTimestamp(); } /** * Launches a automated backup routine for the given course * * @param stdClass $course * @param int $starttime * @param int $userid * @return bool */ public static function launch_automated_backup($course, $starttime, $userid) { $outcome = self::BACKUP_STATUS_OK; $config = get_config('backup'); $dir = $config->backup_auto_destination; $storage = (int)$config->backup_auto_storage; $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid); try { // Set the default filename. $format = $bc->get_format(); $type = $bc->get_type(); $id = $bc->get_id(); $users = $bc->get_plan()->get_setting('users')->get_value(); $anonymised = $bc->get_plan()->get_setting('anonymize')->get_value(); $incfiles = (bool)$config->backup_auto_files; $bc->get_plan()->get_setting('filename')->set_value(backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised, false, $incfiles)); $bc->set_status(backup::STATUS_AWAITING); $bc->execute_plan(); $results = $bc->get_results(); $outcome = self::outcome_from_results($results); $file = $results['backup_destination']; // May be empty if file already moved to target location. // If we need to copy the backup file to an external dir and it is not writable, change status to error. // This is a feature to prevent moodledata to be filled up and break a site when the admin misconfigured // the automated backups storage type and destination directory. if ($storage !== 0 && (empty($dir) || !file_exists($dir) || !is_dir($dir) || !is_writable($dir))) { $bc->log('Specified backup directory is not writable - ', backup::LOG_ERROR, $dir); $dir = null; $outcome = self::BACKUP_STATUS_ERROR; } // Copy file only if there was no error. if ($file && !empty($dir) && $storage !== 0 && $outcome != self::BACKUP_STATUS_ERROR) { $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, !$config->backup_shortname); if (!$file->copy_content_to($dir.'/'.$filename)) { $bc->log('Attempt to copy backup file to the specified directory failed - ', backup::LOG_ERROR, $dir); $outcome = self::BACKUP_STATUS_ERROR; } if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) { if (!$file->delete()) { $outcome = self::BACKUP_STATUS_WARNING; $bc->log('Attempt to delete the backup file from course automated backup area failed - ', backup::LOG_WARNING, $file->get_filename()); } } } } catch (moodle_exception $e) { $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header. $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem. $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information. $outcome = self::BACKUP_STATUS_ERROR; } // Delete the backup file immediately if something went wrong. if ($outcome === self::BACKUP_STATUS_ERROR) { // Delete the file from file area if exists. if (!empty($file)) { $file->delete(); } // Delete file from external storage if exists. if ($storage !== 0 && !empty($filename) && file_exists($dir.'/'.$filename)) { @unlink($dir.'/'.$filename); } } $bc->destroy(); unset($bc); return $outcome; } /** * Returns the backup outcome by analysing its results. * * @param array $results returned by a backup * @return int {@link self::BACKUP_STATUS_OK} and other constants */ public static function outcome_from_results($results) { $outcome = self::BACKUP_STATUS_OK; foreach ($results as $code => $value) { // Each possible error and warning code has to be specified in this switch // which basically analyses the results to return the correct backup status. switch ($code) { case 'missing_files_in_pool': $outcome = self::BACKUP_STATUS_WARNING; break; } // If we found the highest error level, we exit the loop. if ($outcome == self::BACKUP_STATUS_ERROR) { break; } } return $outcome; } /** * Removes deleted courses fromn the backup_courses table so that we don't * waste time backing them up. * * @return int */ public static function remove_deleted_courses_from_schedule() { global $DB; $skipped = 0; $sql = "SELECT bc.courseid FROM {backup_courses} bc WHERE bc.courseid NOT IN (SELECT c.id FROM {course} c)"; $rs = $DB->get_recordset_sql($sql); foreach ($rs as $deletedcourse) { // Doesn't exist, so delete from backup tables. $DB->delete_records('backup_courses', array('courseid' => $deletedcourse->courseid)); $skipped++; } $rs->close(); return $skipped; } /** * Try to get lock for automated backup. * @param int $rundirective * * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false. */ public static function get_automated_backup_lock($rundirective = self::RUN_ON_SCHEDULE) { $config = get_config('backup'); $active = (int)$config->backup_auto_active; $weekdays = (string)$config->backup_auto_weekdays; mtrace("Checking automated backup status", '...'); $locktype = 'automated_backup'; $resource = 'queue_backup_jobs_running'; $lockfactory = \core\lock\lock_config::get_lock_factory($locktype); // In case of automated backup also check that it is scheduled for at least one weekday. if ($active === self::AUTO_BACKUP_DISABLED || ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL) || ($rundirective == self::RUN_ON_SCHEDULE && strpos($weekdays, '1') === false)) { mtrace('INACTIVE'); return false; } if (!$lock = $lockfactory->get_lock($resource, 10)) { return false; } mtrace('OK'); return $lock; } /** * Removes excess backups from a specified course. * * @param stdClass $course Course object * @param int $now Starting time of the process * @return bool Whether or not backups is being removed */ public static function remove_excess_backups($course, $now = null) { $config = get_config('backup'); $maxkept = (int)$config->backup_auto_max_kept; $storage = $config->backup_auto_storage; $deletedays = (int)$config->backup_auto_delete_days; if ($maxkept == 0 && $deletedays == 0) { // Means keep all backup files and never delete backup after x days. return true; } if (!isset($now)) { $now = time(); } // Clean up excess backups in the course backup filearea. $deletedcoursebackups = false; if ($storage == self::STORAGE_COURSE || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { $deletedcoursebackups = self::remove_excess_backups_from_course($course, $now); } // Clean up excess backups in the specified external directory. $deleteddirectorybackups = false; if ($storage == self::STORAGE_DIRECTORY || $storage == self::STORAGE_COURSE_AND_DIRECTORY) { $deleteddirectorybackups = self::remove_excess_backups_from_directory($course, $now); } if ($deletedcoursebackups || $deleteddirectorybackups) { return true; } else { return false; } } /** * Removes excess backups in the course backup filearea from a specified course. * * @param stdClass $course Course object * @param int $now Starting time of the process * @return bool Whether or not backups are being removed */ protected static function remove_excess_backups_from_course($course, $now) { $fs = get_file_storage(); $context = context_course::instance($course->id); $component = 'backup'; $filearea = 'automated'; $itemid = 0; $backupfiles = array(); $backupfilesarea = $fs->get_area_files($context->id, $component, $filearea, $itemid, 'timemodified DESC', false); // Store all the matching files into timemodified => stored_file array. foreach ($backupfilesarea as $backupfile) { $backupfiles[$backupfile->get_timemodified()] = $backupfile; } $backupstodelete = self::get_backups_to_delete($backupfiles, $now); if ($backupstodelete) { foreach ($backupstodelete as $backuptodelete) { $backuptodelete->delete(); } mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from the automated filearea'); return true; } else { return false; } } /** * Removes excess backups in the specified external directory from a specified course. * * @param stdClass $course Course object * @param int $now Starting time of the process * @return bool Whether or not backups are being removed */ protected static function remove_excess_backups_from_directory($course, $now) { $config = get_config('backup'); $dir = $config->backup_auto_destination; $isnotvaliddir = !file_exists($dir) || !is_dir($dir) || !is_writable($dir); if ($isnotvaliddir) { mtrace('Error: ' . $dir . ' does not appear to be a valid directory'); return false; } // Calculate backup filename regex, ignoring the date/time/info parts that can be // variable, depending of languages, formats and automated backup settings. $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-'; $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#'; // Store all the matching files into filename => timemodified array. $backupfiles = array(); foreach (scandir($dir) as $backupfile) { // Skip files not matching the naming convention. if (!preg_match($regex, $backupfile)) { continue; } // Read the information contained in the backup itself. try { $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $backupfile); } catch (backup_helper_exception $e) { mtrace('Error: ' . $backupfile . ' does not appear to be a valid backup (' . $e->errorcode . ')'); continue; } // Make sure this backup concerns the course and site we are looking for. if ($bcinfo->format === backup::FORMAT_MOODLE && $bcinfo->type === backup::TYPE_1COURSE && $bcinfo->original_course_id == $course->id && backup_general_helper::backup_is_samesite($bcinfo)) { $backupfiles[$bcinfo->backup_date] = $backupfile; } } $backupstodelete = self::get_backups_to_delete($backupfiles, $now); if ($backupstodelete) { foreach ($backupstodelete as $backuptodelete) { unlink($dir . '/' . $backuptodelete); } mtrace('Deleted ' . count($backupstodelete) . ' old backup file(s) from external directory'); return true; } else { return false; } } /** * Get the list of backup files to delete depending on the automated backup settings. * * @param array $backupfiles Existing backup files * @param int $now Starting time of the process * @return array Backup files to delete */ protected static function get_backups_to_delete($backupfiles, $now) { $config = get_config('backup'); $maxkept = (int)$config->backup_auto_max_kept; $deletedays = (int)$config->backup_auto_delete_days; $minkept = (int)$config->backup_auto_min_kept; // Sort by keys descending (newer to older filemodified). krsort($backupfiles); $tokeep = $maxkept; if ($deletedays > 0) { $deletedayssecs = $deletedays * DAYSECS; $tokeep = 0; $backupfileskeys = array_keys($backupfiles); foreach ($backupfileskeys as $timemodified) { $mustdeletebackup = $timemodified < ($now - $deletedayssecs); if ($mustdeletebackup || $tokeep >= $maxkept) { break; } $tokeep++; } if ($tokeep < $minkept) { $tokeep = $minkept; } } if (count($backupfiles) <= $tokeep) { // There are less or equal matching files than the desired number to keep, there is nothing to clean up. return false; } else { $backupstodelete = array_splice($backupfiles, $tokeep); return $backupstodelete; } } /** * Check logs to find out if a course was modified since the given time. * * @param int $courseid course id to check * @param int $since timestamp, from which to check * * @return bool true if the course was modified, false otherwise. This also returns false if no readers are enabled. This is * intentional, since we cannot reliably determine if any modification was made or not. */ protected static function is_course_modified($courseid, $since) { $logmang = get_log_manager(); $readers = $logmang->get_readers('core\log\sql_reader'); $params = array('courseid' => $courseid, 'since' => $since); foreach ($readers as $readerpluginname => $reader) { $where = "courseid = :courseid and timecreated > :since and crud <> 'r'"; // Prevent logs of prevous backups causing a false positive. if ($readerpluginname != 'logstore_legacy') { $where .= " and target <> 'course_backup'"; } if ($reader->get_events_select_exists($where, $params)) { return true; } } return false; } } restore_decode_content.class.php 0000644 00000011755 15152170610 0013110 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class in charge of providing the contents to be processed by restore_decode_rules * * This class is in charge of looking (in DB) for the contents needing to be * processed by the declared restore_decode_rules. Basically it iterates over * one recordset (optimised by joining them with backup_ids records), retrieving * them from DB, delegating process to the restore_plan and storing results back * to DB. * * Implements one visitor-like pattern so the decode_processor will visit it * to get all the contents processed by its defined rules * * TODO: Complete phpdocs */ class restore_decode_content implements processable { protected $tablename; // Name, without prefix, of the table we are going to retrieve contents protected $fields; // Array of fields we are going to decode in that table (usually 1) protected $mapping; // Mapping (itemname) in backup_ids used to determine target ids (defaults to $tablename) protected $restoreid; // Unique id of the restore operation we are running protected $iterator; // The iterator for this content (usually one recordset) public function __construct($tablename, $fields, $mapping = null) { // TODO: check table exists // TODO: check fields exist $this->tablename = $tablename; $this->fields = !is_array($fields) ? array($fields) : $fields; // Accept string/array $this->mapping = is_null($mapping) ? $tablename : $mapping; // Default to tableanme $this->restoreid = 0; } public function set_restoreid($restoreid) { $this->restoreid = $restoreid; } public function process($processor) { if (!$processor instanceof restore_decode_processor) { // No correct processor, throw exception throw new restore_decode_content_exception('incorrect_restore_decode_processor', get_class($processor)); } if (!$this->restoreid) { // Check restoreid is set throw new restore_decode_rule_exception('decode_content_restoreid_not_set'); } // Get the iterator of contents $it = $this->get_iterator(); foreach ($it as $itrow) { // Iterate over rows $itrowarr = (array)$itrow; // Array-ize for clean access $rowchanged = false; // To track changes in the row foreach ($this->fields as $field) { // Iterate for each field $content = $this->preprocess_field($itrowarr[$field]); // Apply potential pre-transformations if ($result = $processor->decode_content($content)) { $itrowarr[$field] = $this->postprocess_field($result); // Apply potential post-transformations $rowchanged = true; } } if ($rowchanged) { // Change detected, perform update in the row $this->update_iterator_row($itrowarr); } } $it->close(); // Always close the iterator at the end } // Protected API starts here protected function get_iterator() { global $DB; // Build the SQL dynamically here $fieldslist = 't.' . implode(', t.', $this->fields); $sql = "SELECT t.id, $fieldslist FROM {" . $this->tablename . "} t JOIN {backup_ids_temp} b ON b.newitemid = t.id WHERE b.backupid = ? AND b.itemname = ?"; $params = array($this->restoreid, $this->mapping); return ($DB->get_recordset_sql($sql, $params)); } protected function update_iterator_row($row) { global $DB; $DB->update_record($this->tablename, $row); } protected function preprocess_field($field) { return $field; } protected function postprocess_field($field) { return $field; } } /* * Exception class used by all the @restore_decode_content stuff */ class restore_decode_content_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } restore_log_rule.class.php 0000644 00000023511 15152170610 0011734 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class used to restore logs, converting all the information as needed * * This class allows each restore task to specify which logs (by action) will * be handled on restore and which transformations will be performed in order * to accomodate them into their new destination * * TODO: Complete phpdocs */ class restore_log_rule implements processable { protected $module; // module of the log record protected $action; // action of the log record protected $urlread; // url format of the log record in backup file protected $inforead; // info format of the log record in backup file protected $modulewrite;// module of the log record to be written (defaults to $module if not specified) protected $actionwrite;// action of the log record to be written (defaults to $action if not specified) protected $urlwrite; // url format of the log record to be written (defaults to $urlread if not specified) protected $infowrite;// info format of the log record to be written (defaults to $inforead if not specified) protected $urlreadregexp; // Regexps for extracting information from url and info protected $inforeadregexp; protected $allpairs; // to acummulate all tokens and values pairs on each log record restored protected $urltokens; // tokens present int the $urlread attribute protected $infotokens;// tokens present in the $inforead attribute protected $fixedvalues; // Some values that will have precedence over mappings to save tons of DB mappings protected $restoreid; public function __construct($module, $action, $urlread, $inforead, $modulewrite = null, $actionwrite = null, $urlwrite = null, $infowrite = null) { $this->module = $module; $this->action = $action; $this->urlread = $urlread; $this->inforead = $inforead; $this->modulewrite = is_null($modulewrite) ? $module : $modulewrite; $this->actionwrite= is_null($actionwrite) ? $action : $actionwrite; $this->urlwrite = is_null($urlwrite) ? $urlread : $urlwrite; $this->infowrite= is_null($infowrite) ? $inforead : $infowrite; $this->allpairs = array(); $this->urltokens = array(); $this->infotokens= array(); $this->urlreadregexp = null; $this->inforeadregexp = null; $this->fixedvalues = array(); $this->restoreid = null; // TODO: validate module, action are valid => exception // Calculate regexps and tokens, both for urlread and inforead $this->calculate_url_regexp($this->urlread); $this->calculate_info_regexp($this->inforead); } public function set_restoreid($restoreid) { $this->restoreid = $restoreid; } public function set_fixed_values($values) { //TODO: check $values is array => exception $this->fixedvalues = $values; } public function get_key_name() { return $this->module . '-' . $this->action; } public function process($inputlog) { // There might be multiple rules that process this log, we can't alter it in the process of checking it. $log = clone($inputlog); // Reset the allpairs array $this->allpairs = array(); $urlmatches = array(); $infomatches = array(); // Apply urlreadregexp to the $log->url if necessary if ($this->urlreadregexp) { preg_match($this->urlreadregexp, $log->url, $urlmatches); if (empty($urlmatches)) { return false; } } else { if (!is_null($this->urlread)) { // If not null, use it (null means unmodified) $log->url = $this->urlread; } } // Apply inforeadregexp to the $log->info if necessary if ($this->inforeadregexp) { preg_match($this->inforeadregexp, $log->info, $infomatches); if (empty($infomatches)) { return false; } } else { if (!is_null($this->inforead)) { // If not null, use it (null means unmodified) $log->info = $this->inforead; } } // If there are $urlmatches, let's process them if (!empty($urlmatches)) { array_shift($urlmatches); // Take out first element if (count($urlmatches) !== count($this->urltokens)) { // Number of matches must be number of tokens return false; } // Let's process all the tokens and matches, using them to parse the urlwrite $log->url = $this->parse_tokens_and_matches($this->urltokens, $urlmatches, $this->urlwrite); } // If there are $infomatches, let's process them if (!empty($infomatches)) { array_shift($infomatches); // Take out first element if (count($infomatches) !== count($this->infotokens)) { // Number of matches must be number of tokens return false; } // Let's process all the tokens and matches, using them to parse the infowrite $log->info = $this->parse_tokens_and_matches($this->infotokens, $infomatches, $this->infowrite); } // Arrived here, if there is any pending token in $log->url or $log->info, stop if ($this->extract_tokens($log->url) || $this->extract_tokens($log->info)) { return false; } // Finally, set module and action $log->module = $this->modulewrite; $log->action = $this->actionwrite; return $log; } // Protected API starts here protected function parse_tokens_and_matches($tokens, $values, $content) { $pairs = array_combine($tokens, $values); ksort($pairs); // First literals, then mappings foreach ($pairs as $token => $value) { // If one token has already been processed, continue if (array_key_exists($token, $this->allpairs)) { continue; } // If the pair is one literal token, just keep it unmodified if (substr($token, 0, 1) == '[') { $this->allpairs[$token] = $value; // If the pair is one mapping token, let's process it } else if (substr($token, 0, 1) == '{') { $ctoken = $token; // First, resolve mappings to literals if necessary if (substr($token, 1, 1) == '[') { $literaltoken = trim($token, '{}'); if (array_key_exists($literaltoken, $this->allpairs)) { $ctoken = '{' . $this->allpairs[$literaltoken] . '}'; } } // Look for mapping in fixedvalues before going to DB $plaintoken = trim($ctoken, '{}'); if (array_key_exists($plaintoken, $this->fixedvalues)) { $this->allpairs[$token] = $this->fixedvalues[$plaintoken]; // Last chance, fetch value from backup_ids_temp, via mapping } else { if ($mapping = restore_dbops::get_backup_ids_record($this->restoreid, $plaintoken, $value)) { $this->allpairs[$token] = $mapping->newitemid; } } } } // Apply all the conversions array (allpairs) to content krsort($this->allpairs); // First mappings, then literals $content = str_replace(array_keys($this->allpairs), $this->allpairs, $content); return $content; } protected function calculate_url_regexp($urlexpression) { // Detect all the tokens in the expression if ($tokens = $this->extract_tokens($urlexpression)) { $this->urltokens = $tokens; // Now, build the regexp $this->urlreadregexp = $this->build_regexp($urlexpression, $this->urltokens); } } protected function calculate_info_regexp($infoexpression) { // Detect all the tokens in the expression if ($tokens = $this->extract_tokens($infoexpression)) { $this->infotokens = $tokens; // Now, build the regexp $this->inforeadregexp = $this->build_regexp($infoexpression, $this->infotokens); } } protected function extract_tokens($expression) { // Extract all the tokens enclosed in square and curly brackets preg_match_all('~\[[^\]]+\]|\{[^\}]+\}~', $expression, $matches); return $matches[0]; } protected function build_regexp($expression, $tokens) { // Replace to temp (and preg_quote() safe) placeholders foreach ($tokens as $token) { $expression = preg_replace('~' . preg_quote($token, '~') . '~', '%@@%@@%', $expression, 1); } // quote the expression $expression = preg_quote($expression, '~'); // Replace all the placeholders $expression = preg_replace('~%@@%@@%~', '(.*)', $expression); return '~' . $expression . '~'; } } tests/decode_test.php 0000644 00000017604 15152170610 0010707 0 ustar 00 <?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 restore_decode_rule; use restore_decode_rule_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Restore_decode tests (both rule and content) * * @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 decode_test extends \basic_testcase { /** * test restore_decode_rule class */ function test_restore_decode_rule() { // Test various incorrect constructors try { $dr = new restore_decode_rule('28 HJH', '/index.php', array()); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_incorrect_name'); $this->assertEquals($e->a, '28 HJH'); } try { $dr = new restore_decode_rule('HJHJhH', '/index.php', array()); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_incorrect_name'); $this->assertEquals($e->a, 'HJHJhH'); } try { $dr = new restore_decode_rule('', '/index.php', array()); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_incorrect_name'); $this->assertEquals($e->a, ''); } try { $dr = new restore_decode_rule('TESTRULE', 'index.php', array()); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_incorrect_urltemplate'); $this->assertEquals($e->a, 'index.php'); } try { $dr = new restore_decode_rule('TESTRULE', '', array()); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_incorrect_urltemplate'); $this->assertEquals($e->a, ''); } try { $dr = new restore_decode_rule('TESTRULE', '/course/view.php?id=$1&c=$2$3', array('test1', 'test2')); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_mappings_incorrect_count'); $this->assertEquals($e->a->placeholders, 3); $this->assertEquals($e->a->mappings, 2); } try { $dr = new restore_decode_rule('TESTRULE', '/course/view.php?id=$5&c=$4$1', array('test1', 'test2', 'test3')); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_nonconsecutive_placeholders'); $this->assertEquals($e->a, '1, 4, 5'); } try { $dr = new restore_decode_rule('TESTRULE', '/course/view.php?id=$0&c=$3$2', array('test1', 'test2', 'test3')); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_nonconsecutive_placeholders'); $this->assertEquals($e->a, '0, 2, 3'); } try { $dr = new restore_decode_rule('TESTRULE', '/course/view.php?id=$1&c=$3$3', array('test1', 'test2', 'test3')); $this->assertTrue(false, 'restore_decode_rule_exception exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_decode_rule_exception); $this->assertEquals($e->errorcode, 'decode_rule_duplicate_placeholders'); $this->assertEquals($e->a, '1, 3, 3'); } // Provide some example content and test the regexp is calculated ok $content = '$@TESTRULE*22*33*44@$'; $linkname = 'TESTRULE'; $urltemplate= '/course/view.php?id=$1&c=$3$2'; $mappings = array('test1', 'test2', 'test3'); $result = '1/course/view.php?id=44&c=8866'; $dr = new mock_restore_decode_rule($linkname, $urltemplate, $mappings); $this->assertEquals($dr->decode($content), $result); $content = '$@TESTRULE*22*33*44@$ñ$@TESTRULE*22*33*44@$'; $linkname = 'TESTRULE'; $urltemplate= '/course/view.php?id=$1&c=$3$2'; $mappings = array('test1', 'test2', 'test3'); $result = '1/course/view.php?id=44&c=8866ñ1/course/view.php?id=44&c=8866'; $dr = new mock_restore_decode_rule($linkname, $urltemplate, $mappings); $this->assertEquals($dr->decode($content), $result); $content = 'ñ$@TESTRULE*22*0*44@$ñ$@TESTRULE*22*33*44@$ñ'; $linkname = 'TESTRULE'; $urltemplate= '/course/view.php?id=$1&c=$3$2'; $mappings = array('test1', 'test2', 'test3'); $result = 'ñ0/course/view.php?id=22&c=440ñ1/course/view.php?id=44&c=8866ñ'; $dr = new mock_restore_decode_rule($linkname, $urltemplate, $mappings); $this->assertEquals($dr->decode($content), $result); } /** * test restore_decode_content class */ function test_restore_decode_content() { // TODO: restore_decode_content tests } /** * test restore_decode_processor class */ function test_restore_decode_processor() { // TODO: restore_decode_processor tests } } /** * Mockup restore_decode_rule for testing purposes */ class mock_restore_decode_rule extends restore_decode_rule { /** * Originally protected, make it public */ public function get_calculated_regexp() { return parent::get_calculated_regexp(); } /** * Simply map each itemid by its double */ protected function get_mapping($itemname, $itemid) { return $itemid * 2; } /** * Simply prefix with '0' non-mapped results and with '1' mapped ones */ protected function apply_modifications($toreplace, $mappingsok) { return ($mappingsok ? '1' : '0') . $toreplace; } } tests/copy_helper_test.php 0000644 00000101716 15152170610 0011773 0 ustar 00 <?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; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->libdir . '/completionlib.php'); /** * Course copy tests. * * @package core_backup * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> * @author Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \copy_helper */ class copy_helper_test extends \advanced_testcase { /** * * @var \stdClass Course used for testing. */ protected $course; /** * * @var int User used to perform backups. */ protected $userid; /** * * @var array Ids of users in test course. */ protected $courseusers; /** * * @var array Names of the created activities. */ protected $activitynames; /** * Set up tasks for all tests. */ protected function setUp(): void { global $DB, $CFG, $USER; $this->resetAfterTest(true); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Create a course with some availability data set. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $forum = $generator->create_module('forum', array( 'course' => $course->id)); $forum2 = $generator->create_module('forum', array( 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); // We need a grade, easiest is to add an assignment. $assignrow = $generator->create_module('assign', array( 'course' => $course->id)); $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); $item = $assign->get_grade_item(); // Make a test grouping as well. $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); // Create some users. $user1 = $generator->create_user(); $user2 = $generator->create_user(); $user3 = $generator->create_user(); $user4 = $generator->create_user(); $this->courseusers = array( $user1->id, $user2->id, $user3->id, $user4->id ); // Enrol users into the course. $generator->enrol_user($user1->id, $course->id, 'student'); $generator->enrol_user($user2->id, $course->id, 'editingteacher'); $generator->enrol_user($user3->id, $course->id, 'manager'); $generator->enrol_user($user4->id, $course->id, 'editingteacher'); $generator->enrol_user($user4->id, $course->id, 'manager'); $availability = '{"op":"|","show":false,"c":[' . '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . '{"type":"grouping","id":' . $grouping->id . '}' . ']}'; $DB->set_field('course_modules', 'availability', $availability, array( 'id' => $forum->cmid)); $DB->set_field('course_sections', 'availability', $availability, array( 'course' => $course->id, 'section' => 1)); // Add some user data to the course. $discussion = $generator->get_plugin_generator('mod_forum')->create_discussion(['course' => $course->id, 'forum' => $forum->id, 'userid' => $user1->id, 'timemodified' => time(), 'name' => 'Frog']); $generator->get_plugin_generator('mod_forum')->create_post(['discussion' => $discussion->id, 'userid' => $user1->id]); $this->course = $course; $this->userid = $USER->id; // Admin. $this->activitynames = array( $forum->name, $forum2->name, $assignrow->name ); // Set the user doing the backup to be a manager in the course. // By default Managers can restore courses AND users, teachers can only do users. $this->setUser($user3); // Disable all loggers. $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; $CFG->backup_file_logger_level_extra = backup::LOG_NONE; } /** * Test process form data with invalid data. * * @covers ::process_formdata */ public function test_process_formdata_missing_fields() { $this->expectException(\moodle_exception::class); \copy_helper::process_formdata(new \stdClass); } /** * Test processing form data. * * @covers ::process_formdata */ public function test_process_formdata() { $validformdata = [ 'courseid' => 1729, 'fullname' => 'Taxicab Numbers', 'shortname' => 'Taxi101', 'category' => 2, 'visible' => 1, 'startdate' => 87539319, 'enddate' => 6963472309248, 'idnumber' => 1730, 'userdata' => 1 ]; $roles = [ 'role_one' => 1, 'role_two' => 2, 'role_three' => 0 ]; $expected = (object)array_merge($validformdata, ['keptroles' => []]); $expected->keptroles = [1, 2]; $processed = \copy_helper::process_formdata( (object)array_merge( $validformdata, $roles, ['extra' => 'stuff', 'remove' => 'this']) ); $this->assertEquals($expected, $processed); } /** * Test orphaned controller cleanup. * * @covers ::cleanup_orphaned_copy_controllers */ public function test_cleanup_orphaned_copy_controllers() { global $DB; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'data1'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; $copies = []; for ($i = 0; $i < 5; $i++) { $formdata->shortname = 'data' . $i; $copies[] = \copy_helper::create_copy($formdata); } // Delete one of the restore controllers. Simulates a situation where copy creation // interrupted and the restore controller never gets created. $DB->delete_records('backup_controllers', ['backupid' => $copies[0]['restoreid']]); // Set a backup/restore controller pair to be in an intermediate state. \backup_controller::load_controller($copies[2]['backupid'])->set_status(backup::STATUS_FINISHED_OK); // Set a backup/restore controller pair to completed. \backup_controller::load_controller($copies[3]['backupid'])->set_status(backup::STATUS_FINISHED_OK); \restore_controller::load_controller($copies[3]['restoreid'])->set_status(backup::STATUS_FINISHED_OK); // Set a backup/restore controller pair to have a failed backup. \backup_controller::load_controller($copies[4]['backupid'])->set_status(backup::STATUS_FINISHED_ERR); // Create some backup/restore controllers that are unrelated to course copies. $bc = new \backup_controller(backup::TYPE_1COURSE, 1, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 2, backup::RELEASESESSION_YES); $rc = new \restore_controller('restore-test-1729', 1, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 1, 2); $rc->save_controller(); $unrelatedvanillacontrollers = ['backupid' => $bc->get_backupid(), 'restoreid' => $rc->get_restoreid()]; $bc = new \backup_controller(backup::TYPE_1COURSE, 1, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 2, backup::RELEASESESSION_YES); $rc = new \restore_controller('restore-test-1729', 1, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 1, 2); $bc->set_status(backup::STATUS_FINISHED_OK); $rc->set_status(backup::STATUS_FINISHED_OK); $unrelatedfinishedcontrollers = ['backupid' => $bc->get_backupid(), 'restoreid' => $rc->get_restoreid()]; $bc = new \backup_controller(backup::TYPE_1COURSE, 1, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 2, backup::RELEASESESSION_YES); $rc = new \restore_controller('restore-test-1729', 1, backup::INTERACTIVE_NO, backup::MODE_ASYNC, 1, 2); $bc->set_status(backup::STATUS_FINISHED_ERR); $rc->set_status(backup::STATUS_FINISHED_ERR); $unrelatedfailedcontrollers = ['backupid' => $bc->get_backupid(), 'restoreid' => $rc->get_restoreid()]; // Clean up the backup_controllers table. $records = $DB->get_records('backup_controllers', null, '', 'id, backupid, status, operation, purpose, timecreated'); \copy_helper::cleanup_orphaned_copy_controllers($records, 0); // Retrieve them again and check. $records = $DB->get_records('backup_controllers', null, '', 'backupid, status'); // Verify the backup associated with the deleted restore is marked as failed. $this->assertEquals(backup::STATUS_FINISHED_ERR, $records[$copies[0]['backupid']]->status); // Verify other controllers remain untouched. $this->assertEquals(backup::STATUS_AWAITING, $records[$copies[1]['backupid']]->status); $this->assertEquals(backup::STATUS_REQUIRE_CONV, $records[$copies[1]['restoreid']]->status); $this->assertEquals(backup::STATUS_FINISHED_OK, $records[$copies[2]['backupid']]->status); $this->assertEquals(backup::STATUS_REQUIRE_CONV, $records[$copies[2]['restoreid']]->status); $this->assertEquals(backup::STATUS_FINISHED_OK, $records[$copies[3]['restoreid']]->status); $this->assertEquals(backup::STATUS_FINISHED_OK, $records[$copies[3]['backupid']]->status); // Verify that the restore associated with the failed backup is also marked as failed. $this->assertEquals(backup::STATUS_FINISHED_ERR, $records[$copies[4]['restoreid']]->status); // Verify that the unrelated controllers remain unchanged. $this->assertEquals(backup::STATUS_AWAITING, $records[$unrelatedvanillacontrollers['backupid']]->status); $this->assertEquals(backup::STATUS_REQUIRE_CONV, $records[$unrelatedvanillacontrollers['restoreid']]->status); $this->assertEquals(backup::STATUS_FINISHED_OK, $records[$unrelatedfinishedcontrollers['backupid']]->status); $this->assertEquals(backup::STATUS_FINISHED_OK, $records[$unrelatedfinishedcontrollers['restoreid']]->status); $this->assertEquals(backup::STATUS_FINISHED_ERR, $records[$unrelatedfailedcontrollers['backupid']]->status); $this->assertEquals(backup::STATUS_FINISHED_ERR, $records[$unrelatedfailedcontrollers['restoreid']]->status); } /** * Test creating a course copy. * * @covers ::create_copy */ public function test_create_copy() { // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; $copydata = \copy_helper::process_formdata($formdata); $result = \copy_helper::create_copy($copydata); // Load the controllers, to extract the data we need. $bc = \backup_controller::load_controller($result['backupid']); $rc = \restore_controller::load_controller($result['restoreid']); // Check the backup controller. $this->assertEquals(backup::MODE_COPY, $bc->get_mode()); $this->assertEquals($this->course->id, $bc->get_courseid()); $this->assertEquals(backup::TYPE_1COURSE, $bc->get_type()); // Check the restore controller. $newcourseid = $rc->get_courseid(); $newcourse = get_course($newcourseid); $this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname); $this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname); $this->assertEquals(backup::MODE_COPY, $rc->get_mode()); $this->assertEquals($newcourseid, $rc->get_courseid()); // Check the created ad-hoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); $this->assertEquals($result, (array)$task->get_custom_data()); $this->assertFalse($task->is_blocking()); \core\task\manager::adhoc_task_complete($task); } /** * Test getting the current copies. * * @covers ::get_copies */ public function test_get_copies() { global $USER; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = ''; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; $formdata2 = clone($formdata); $formdata2->shortname = 'tree'; // Create some copies. $copydata = \copy_helper::process_formdata($formdata); $result = \copy_helper::create_copy($copydata); // Backup, awaiting. $copies = \copy_helper::get_copies($USER->id); $this->assertEquals($result['backupid'], $copies[0]->backupid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status); $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); $bc = \backup_controller::load_controller($result['backupid']); // Backup, in progress. $bc->set_status(\backup::STATUS_EXECUTING); $copies = \copy_helper::get_copies($USER->id); $this->assertEquals($result['backupid'], $copies[0]->backupid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status); $this->assertEquals(\backup::OPERATION_BACKUP, $copies[0]->operation); // Restore, ready to process. $bc->set_status(\backup::STATUS_FINISHED_OK); $copies = \copy_helper::get_copies($USER->id); $this->assertEquals(null, $copies[0]->backupid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status); $this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation); // No records. $bc->set_status(\backup::STATUS_FINISHED_ERR); $copies = \copy_helper::get_copies($USER->id); $this->assertEmpty($copies); $copydata2 = \copy_helper::process_formdata($formdata2); $result2 = \copy_helper::create_copy($copydata2); // Set the second copy to be complete. $bc = \backup_controller::load_controller($result2['backupid']); $bc->set_status(\backup::STATUS_FINISHED_OK); // Set the restore to be finished. $rc = \backup_controller::load_controller($result2['restoreid']); $rc->set_status(\backup::STATUS_FINISHED_OK); // No records. $copies = \copy_helper::get_copies($USER->id); $this->assertEmpty($copies); } /** * Test getting the current copies when they are in an invalid state. * * @covers ::get_copies */ public function test_get_copies_invalid_state() { global $DB, $USER; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = ''; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; $formdata2 = clone ($formdata); $formdata2->shortname = 'tree'; // Create some copies. $copydata = \copy_helper::process_formdata($formdata); $result = \copy_helper::create_copy($copydata); $copydata2 = \copy_helper::process_formdata($formdata2); $result2 = \copy_helper::create_copy($copydata2); $copies = \copy_helper::get_copies($USER->id); // Verify get_copies gives back both backup controllers. $this->assertEqualsCanonicalizing([$result['backupid'], $result2['backupid']], array_column($copies, 'backupid')); // Set one of the backup controllers to failed, this should cause it to not be present. \backup_controller::load_controller($result['backupid'])->set_status(backup::STATUS_FINISHED_ERR); $copies = \copy_helper::get_copies($USER->id); // Verify there is only one backup listed, and that it is not the failed one. $this->assertEqualsCanonicalizing([$result2['backupid']], array_column($copies, 'backupid')); // Set the controller back to awaiting. \backup_controller::load_controller($result['backupid'])->set_status(backup::STATUS_AWAITING); $copies = \copy_helper::get_copies($USER->id); // Verify both backup controllers are back. $this->assertEqualsCanonicalizing([$result['backupid'], $result2['backupid']], array_column($copies, 'backupid')); // Delete the restore controller for one of the copies, this should cause it to not be present. $DB->delete_records('backup_controllers', ['backupid' => $result['restoreid']]); $copies = \copy_helper::get_copies($USER->id); // Verify there is only one backup listed, and that it is not the failed one. $this->assertEqualsCanonicalizing([$result2['backupid']], array_column($copies, 'backupid')); } /** * Test getting the current copies for specific course. * * @covers ::get_copies */ public function test_get_copies_course() { global $USER; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = ''; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; // Create some copies. $copydata = \copy_helper::process_formdata($formdata); \copy_helper::create_copy($copydata); // No copies match this course id. $copies = \copy_helper::get_copies($USER->id, ($this->course->id + 1)); $this->assertEmpty($copies); } /** * Test getting the current copies if course has been deleted. * * @covers ::get_copies */ public function test_get_copies_course_deleted() { global $USER; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = 1582376400; $formdata->enddate = 0; $formdata->idnumber = ''; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; // Create some copies. $copydata = \copy_helper::process_formdata($formdata); \copy_helper::create_copy($copydata); delete_course($this->course->id, false); // No copies match this course id as it has been deleted. $copies = \copy_helper::get_copies($USER->id, ($this->course->id)); $this->assertEmpty($copies); } /** * Test course copy. */ public function test_course_copy() { global $DB; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'copy course'; $formdata->shortname = 'copy course short'; $formdata->category = 1; $formdata->visible = 0; $formdata->startdate = 1582376400; $formdata->enddate = 1582386400; $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; // Create the course copy records and associated ad-hoc task. $copydata = \copy_helper::process_formdata($formdata); $copyids = \copy_helper::create_copy($copydata); $courseid = $this->course->id; // We are expecting trace output during this test. $this->expectOutputRegex("/$courseid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postbackuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid'])); $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); // Check backup was completed successfully. $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); $this->assertEquals(1.0, $postbackuprec->progress); // Check restore was completed successfully. $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status); $this->assertEquals(1.0, $postrestorerec->progress); // Check the restored course itself. $coursecontext = \context_course::instance($postrestorerec->itemid); $users = get_enrolled_users($coursecontext); $modinfo = get_fast_modinfo($postrestorerec->itemid); $forums = $modinfo->get_instances_of('forum'); $forum = reset($forums); $discussions = forum_get_discussions($forum); $course = $modinfo->get_course(); $this->assertEquals($formdata->startdate, $course->startdate); $this->assertEquals($formdata->enddate, $course->enddate); $this->assertEquals('copy course', $course->fullname); $this->assertEquals('copy course short', $course->shortname); $this->assertEquals(0, $course->visible); $this->assertEquals(123, $course->idnumber); foreach ($modinfo->get_cms() as $cm) { $this->assertContains($cm->get_formatted_name(), $this->activitynames); } foreach ($this->courseusers as $user) { $this->assertEquals($user, $users[$user]->id); } $this->assertEquals(count($this->courseusers), count($users)); $this->assertEquals(2, count($discussions)); } /** * Test course copy, not including any users (or data). */ public function test_course_copy_no_users() { global $DB; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'copy course'; $formdata->shortname = 'copy course short'; $formdata->category = 1; $formdata->visible = 0; $formdata->startdate = 1582376400; $formdata->enddate = 1582386400; $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 0; $formdata->role_3 = 0; $formdata->role_5 = 0; // Create the course copy records and associated ad-hoc task. $copydata = \copy_helper::process_formdata($formdata); $copyids = \copy_helper::create_copy($copydata); $courseid = $this->course->id; // We are expecting trace output during this test. $this->expectOutputRegex("/$courseid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); // Check the restored course itself. $coursecontext = \context_course::instance($postrestorerec->itemid); $users = get_enrolled_users($coursecontext); $modinfo = get_fast_modinfo($postrestorerec->itemid); $forums = $modinfo->get_instances_of('forum'); $forum = reset($forums); $discussions = forum_get_discussions($forum); $course = $modinfo->get_course(); $this->assertEquals($formdata->startdate, $course->startdate); $this->assertEquals($formdata->enddate, $course->enddate); $this->assertEquals('copy course', $course->fullname); $this->assertEquals('copy course short', $course->shortname); $this->assertEquals(0, $course->visible); $this->assertEquals(123, $course->idnumber); foreach ($modinfo->get_cms() as $cm) { $this->assertContains($cm->get_formatted_name(), $this->activitynames); } // Should be no discussions as the user that made them wasn't included. $this->assertEquals(0, count($discussions)); // There should only be one user in the new course, and that's the user who did the copy. $this->assertEquals(1, count($users)); $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); } /** * Test course copy, including students and their data. */ public function test_course_copy_students_data() { global $DB; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'copy course'; $formdata->shortname = 'copy course short'; $formdata->category = 1; $formdata->visible = 0; $formdata->startdate = 1582376400; $formdata->enddate = 1582386400; $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 0; $formdata->role_3 = 0; $formdata->role_5 = 5; // Create the course copy records and associated ad-hoc task. $copydata = \copy_helper::process_formdata($formdata); $copyids = \copy_helper::create_copy($copydata); $courseid = $this->course->id; // We are expecting trace output during this test. $this->expectOutputRegex("/$courseid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); // Check the restored course itself. $coursecontext = \context_course::instance($postrestorerec->itemid); $users = get_enrolled_users($coursecontext); $modinfo = get_fast_modinfo($postrestorerec->itemid); $forums = $modinfo->get_instances_of('forum'); $forum = reset($forums); $discussions = forum_get_discussions($forum); $course = $modinfo->get_course(); $this->assertEquals($formdata->startdate, $course->startdate); $this->assertEquals($formdata->enddate, $course->enddate); $this->assertEquals('copy course', $course->fullname); $this->assertEquals('copy course short', $course->shortname); $this->assertEquals(0, $course->visible); $this->assertEquals(123, $course->idnumber); foreach ($modinfo->get_cms() as $cm) { $this->assertContains($cm->get_formatted_name(), $this->activitynames); } // Should be no discussions as the user that made them wasn't included. $this->assertEquals(2, count($discussions)); // There should only be two users in the new course. The copier and one student. $this->assertEquals(2, count($users)); $this->assertEquals($this->courseusers[2], $users[$this->courseusers[2]]->id); $this->assertEquals($this->courseusers[0], $users[$this->courseusers[0]]->id); } /** * Test course copy, not including any users (or data). */ public function test_course_copy_no_data() { global $DB; // Mock up the form data. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'copy course'; $formdata->shortname = 'copy course short'; $formdata->category = 1; $formdata->visible = 0; $formdata->startdate = 1582376400; $formdata->enddate = 1582386400; $formdata->idnumber = 123; $formdata->userdata = 0; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; // Create the course copy records and associated ad-hoc task. $copydata = \copy_helper::process_formdata($formdata); $copyids = \copy_helper::create_copy($copydata); $courseid = $this->course->id; // We are expecting trace output during this test. $this->expectOutputRegex("/$courseid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_copy_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); // Check the restored course itself. $coursecontext = \context_course::instance($postrestorerec->itemid); $users = get_enrolled_users($coursecontext); get_fast_modinfo($postrestorerec->itemid, 0, true); $modinfo = get_fast_modinfo($postrestorerec->itemid); $forums = $modinfo->get_instances_of('forum'); $forum = reset($forums); $discussions = forum_get_discussions($forum); $course = $modinfo->get_course(); $this->assertEquals($formdata->startdate, $course->startdate); $this->assertEquals($formdata->enddate, $course->enddate); $this->assertEquals('copy course', $course->fullname); $this->assertEquals('copy course short', $course->shortname); $this->assertEquals(0, $course->visible); $this->assertEquals(123, $course->idnumber); foreach ($modinfo->get_cms() as $cm) { $this->assertContains($cm->get_formatted_name(), $this->activitynames); } // Should be no discussions as the user data wasn't included. $this->assertEquals(0, count($discussions)); // There should only be all users in the new course. $this->assertEquals(count($this->courseusers), count($users)); } /** * Test instantiation with incomplete formdata. */ public function test_malformed_instantiation() { // Mock up the form data, missing things so we get an exception. $formdata = new \stdClass; $formdata->courseid = $this->course->id; $formdata->fullname = 'copy course'; $formdata->shortname = 'copy course short'; $formdata->category = 1; // Expect and exception as form data is incomplete. $this->expectException(\moodle_exception::class); $copydata = \copy_helper::process_formdata($formdata); \copy_helper::create_copy($copydata); } } tests/restore_log_rule_test.php 0000644 00000005051 15152170610 0013030 0 ustar 00 <?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_log_rule; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Test the backup and restore of logs using rules. * * @package core_backup * @category test * @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_log_rule_test extends \basic_testcase { function test_process_keeps_log_unmodified() { // Prepare a tiny log entry. $originallog = new \stdClass(); $originallog->url = 'original'; $originallog->info = 'original'; $log = clone($originallog); // Process it with a tiny log rule, only modifying url and info. $lr = new restore_log_rule('test', 'test', 'changed', 'changed'); $result = $lr->process($log); // The log has been processed. $this->assertEquals('changed', $result->url); $this->assertEquals('changed', $result->info); // But the original log has been kept unmodified by the process() call. $this->assertEquals($originallog, $log); } public function test_build_regexp() { $original = 'Any (string) with [placeholders] like {this} and {this}. [end].'; $expectation = '~Any \(string\) with (.*) like (.*) and (.*)\. (.*)\.~'; $lr = new restore_log_rule('this', 'doesnt', 'matter', 'here'); $class = new \ReflectionClass('restore_log_rule'); $method = $class->getMethod('extract_tokens'); $method->setAccessible(true); $tokens = $method->invoke($lr, $original); $method = $class->getMethod('build_regexp'); $method->setAccessible(true); $this->assertSame($expectation, $method->invoke($lr, $original, $tokens)); } } tests/cronhelper_test.php 0000644 00000055043 15152170610 0011624 0 ustar 00 <?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/>. /** * Unit tests for backups cron helper. * * @package core_backup * @category test * @copyright 2012 Frédéric Massart <fred@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup; use backup; use backup_cron_automated_helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once("$CFG->dirroot/backup/backup.class.php"); /** * Unit tests for backups cron helper. * * @package core_backup * @category test * @copyright 2012 Frédéric Massart <fred@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cronhelper_test extends \advanced_testcase { /** * Test {@link backup_cron_automated_helper::calculate_next_automated_backup}. */ public function test_next_automated_backup() { global $CFG; $this->resetAfterTest(); set_config('backup_auto_active', '1', 'backup'); $this->setTimezone('Australia/Perth'); // Notes // - backup_auto_weekdays starts on Sunday // - Tests cannot be done in the past // - Only the DST on the server side is handled. // Every Tue and Fri at 11pm. set_config('backup_auto_weekdays', '0010010', 'backup'); set_config('backup_auto_hour', '23', 'backup'); set_config('backup_auto_minute', '0', 'backup'); $timezone = 99; // Ignored, everything is calculated in server timezone!!! $now = strtotime('next Monday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-23:00', date('w-H:i', $next)); $now = strtotime('next Tuesday 18:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-23:00', date('w-H:i', $next)); $now = strtotime('next Wednesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('5-23:00', date('w-H:i', $next)); $now = strtotime('next Thursday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('5-23:00', date('w-H:i', $next)); $now = strtotime('next Friday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('5-23:00', date('w-H:i', $next)); $now = strtotime('next Saturday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-23:00', date('w-H:i', $next)); $now = strtotime('next Sunday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-23:00', date('w-H:i', $next)); // Every Sun and Sat at 12pm. set_config('backup_auto_weekdays', '1000001', 'backup'); set_config('backup_auto_hour', '0', 'backup'); set_config('backup_auto_minute', '0', 'backup'); $now = strtotime('next Monday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Tuesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Wednesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Thursday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Friday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Saturday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-00:00', date('w-H:i', $next)); $now = strtotime('next Sunday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); // Every Sun at 4am. set_config('backup_auto_weekdays', '1000000', 'backup'); set_config('backup_auto_hour', '4', 'backup'); set_config('backup_auto_minute', '0', 'backup'); $now = strtotime('next Monday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Tuesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Wednesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Thursday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Friday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Saturday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); $now = strtotime('next Sunday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-04:00', date('w-H:i', $next)); // Every day but Wed at 8:30pm. set_config('backup_auto_weekdays', '1110111', 'backup'); set_config('backup_auto_hour', '20', 'backup'); set_config('backup_auto_minute', '30', 'backup'); $now = strtotime('next Monday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('1-20:30', date('w-H:i', $next)); $now = strtotime('next Tuesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-20:30', date('w-H:i', $next)); $now = strtotime('next Wednesday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('4-20:30', date('w-H:i', $next)); $now = strtotime('next Thursday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('4-20:30', date('w-H:i', $next)); $now = strtotime('next Friday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('5-20:30', date('w-H:i', $next)); $now = strtotime('next Saturday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-20:30', date('w-H:i', $next)); $now = strtotime('next Sunday 17:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-20:30', date('w-H:i', $next)); // Sun, Tue, Thu, Sat at 12pm. set_config('backup_auto_weekdays', '1010101', 'backup'); set_config('backup_auto_hour', '0', 'backup'); set_config('backup_auto_minute', '0', 'backup'); $now = strtotime('next Monday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-00:00', date('w-H:i', $next)); $now = strtotime('next Tuesday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('4-00:00', date('w-H:i', $next)); $now = strtotime('next Wednesday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('4-00:00', date('w-H:i', $next)); $now = strtotime('next Thursday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Friday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('6-00:00', date('w-H:i', $next)); $now = strtotime('next Saturday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0-00:00', date('w-H:i', $next)); $now = strtotime('next Sunday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('2-00:00', date('w-H:i', $next)); // None. set_config('backup_auto_weekdays', '0000000', 'backup'); set_config('backup_auto_hour', '15', 'backup'); set_config('backup_auto_minute', '30', 'backup'); $now = strtotime('next Sunday 13:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals('0', $next); // Playing with timezones. set_config('backup_auto_weekdays', '1111111', 'backup'); set_config('backup_auto_hour', '20', 'backup'); set_config('backup_auto_minute', '00', 'backup'); $this->setTimezone('Australia/Perth'); $now = strtotime('18:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals(date('w-20:00'), date('w-H:i', $next)); $this->setTimezone('Europe/Brussels'); $now = strtotime('18:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals(date('w-20:00'), date('w-H:i', $next)); $this->setTimezone('America/New_York'); $now = strtotime('18:00:00'); $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now); $this->assertEquals(date('w-20:00'), date('w-H:i', $next)); } /** * Test {@link backup_cron_automated_helper::get_backups_to_delete}. */ public function test_get_backups_to_delete() { $this->resetAfterTest(); // Active only backup_auto_max_kept config to 2 days. set_config('backup_auto_max_kept', '2', 'backup'); set_config('backup_auto_delete_days', '0', 'backup'); set_config('backup_auto_min_kept', '0', 'backup'); // No backups to delete. $backupfiles = array( '1000000000' => 'file1.mbz', '1000432000' => 'file3.mbz' ); $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000); $this->assertFalse($deletedbackups); // Older backup to delete. $backupfiles['1000172800'] = 'file2.mbz'; $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000432000); $this->assertEquals(1, count($deletedbackups)); $this->assertArrayHasKey('1000000000', $backupfiles); $this->assertEquals('file1.mbz', $backupfiles['1000000000']); // Activate backup_auto_max_kept to 5 days and backup_auto_delete_days to 10 days. set_config('backup_auto_max_kept', '5', 'backup'); set_config('backup_auto_delete_days', '10', 'backup'); set_config('backup_auto_min_kept', '0', 'backup'); // No backups to delete. Timestamp is 1000000000 + 10 days. $backupfiles['1000432001'] = 'file4.mbz'; $backupfiles['1000864000'] = 'file5.mbz'; $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864000); $this->assertFalse($deletedbackups); // One old backup to delete. Timestamp is 1000000000 + 10 days + 1 second. $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1000864001); $this->assertEquals(1, count($deletedbackups)); $this->assertArrayHasKey('1000000000', $backupfiles); $this->assertEquals('file1.mbz', $backupfiles['1000000000']); // Two old backups to delete. Timestamp is 1000000000 + 12 days + 1 second. $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001036801); $this->assertEquals(2, count($deletedbackups)); $this->assertArrayHasKey('1000000000', $backupfiles); $this->assertEquals('file1.mbz', $backupfiles['1000000000']); $this->assertArrayHasKey('1000172800', $backupfiles); $this->assertEquals('file2.mbz', $backupfiles['1000172800']); // Activate backup_auto_max_kept to 5 days, backup_auto_delete_days to 10 days and backup_auto_min_kept to 2. set_config('backup_auto_max_kept', '5', 'backup'); set_config('backup_auto_delete_days', '10', 'backup'); set_config('backup_auto_min_kept', '2', 'backup'); // Three instead of four old backups are deleted. Timestamp is 1000000000 + 16 days. $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1001382400); $this->assertEquals(3, count($deletedbackups)); $this->assertArrayHasKey('1000000000', $backupfiles); $this->assertEquals('file1.mbz', $backupfiles['1000000000']); $this->assertArrayHasKey('1000172800', $backupfiles); $this->assertEquals('file2.mbz', $backupfiles['1000172800']); $this->assertArrayHasKey('1000432000', $backupfiles); $this->assertEquals('file3.mbz', $backupfiles['1000432000']); // Three instead of all five backups are deleted. Timestamp is 1000000000 + 60 days. $deletedbackups = testable_backup_cron_automated_helper::testable_get_backups_to_delete($backupfiles, 1005184000); $this->assertEquals(3, count($deletedbackups)); $this->assertArrayHasKey('1000000000', $backupfiles); $this->assertEquals('file1.mbz', $backupfiles['1000000000']); $this->assertArrayHasKey('1000172800', $backupfiles); $this->assertEquals('file2.mbz', $backupfiles['1000172800']); $this->assertArrayHasKey('1000432000', $backupfiles); $this->assertEquals('file3.mbz', $backupfiles['1000432000']); } /** * Test {@link backup_cron_automated_helper::is_course_modified}. */ public function test_is_course_modified() { $this->resetAfterTest(); $this->preventResetByRollback(); set_config('enabled_stores', 'logstore_standard', 'tool_log'); set_config('buffersize', 0, 'logstore_standard'); set_config('logguests', 1, 'logstore_standard'); $course = $this->getDataGenerator()->create_course(); // New courses should be backed up. $this->assertTrue(testable_backup_cron_automated_helper::testable_is_course_modified($course->id, 0)); $timepriortobackup = time(); $this->waitForSecond(); $otherarray = [ 'format' => backup::FORMAT_MOODLE, 'mode' => backup::MODE_GENERAL, 'interactive' => backup::INTERACTIVE_YES, 'type' => backup::TYPE_1COURSE, ]; $event = \core\event\course_backup_created::create([ 'objectid' => $course->id, 'context' => \context_course::instance($course->id), 'other' => $otherarray ]); $event->trigger(); // If the only action since last backup was a backup then no backup. $this->assertFalse(testable_backup_cron_automated_helper::testable_is_course_modified($course->id, $timepriortobackup)); $course->groupmode = SEPARATEGROUPS; $course->groupmodeforce = true; update_course($course); // Updated courses should be backed up. $this->assertTrue(testable_backup_cron_automated_helper::testable_is_course_modified($course->id, $timepriortobackup)); } /** * Create courses and backup records for tests. * * @return array Created courses. */ private function course_setup() { global $DB; // Create test courses. $course1 = $this->getDataGenerator()->create_course(array('timecreated' => 1553402000)); // Newest. $course2 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600)); $course3 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600)); $course4 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600)); // Create backup course records for the courses that need them. $backupcourse3 = new \stdClass; $backupcourse3->courseid = $course3->id; $backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK; $backupcourse3->nextstarttime = 1554822160; $DB->insert_record('backup_courses', $backupcourse3); $backupcourse4 = new \stdClass; $backupcourse4->courseid = $course4->id; $backupcourse4->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK; $backupcourse4->nextstarttime = 1554858160; $DB->insert_record('backup_courses', $backupcourse4); return array($course1, $course2, $course3, $course4); } /** * Test the selection and ordering of courses to be backed up. */ public function test_get_courses() { $this->resetAfterTest(); list($course1, $course2, $course3, $course4) = $this->course_setup(); $now = 1559215025; // Get the courses in order. $courseset = testable_backup_cron_automated_helper::testable_get_courses($now); $coursearray = array(); foreach ($courseset as $course) { if ($course->id != SITEID) { // Skip system course for test. $coursearray[] = $course->id; } } $courseset->close(); // First should be course 1, it is the more recently modified without a backup. $this->assertEquals($course1->id, $coursearray[0]); // Second should be course 2, it is the next more recently modified without a backup. $this->assertEquals($course2->id, $coursearray[1]); // Third should be course 3, it is the course with the oldest backup. $this->assertEquals($course3->id, $coursearray[2]); // Fourth should be course 4, it is the course with the newest backup. $this->assertEquals($course4->id, $coursearray[3]); } /** * Test the selection and ordering of courses to be backed up. * Where it is not yet time to start backups for courses with existing backups. */ public function test_get_courses_starttime() { $this->resetAfterTest(); list($course1, $course2, $course3, $course4) = $this->course_setup(); $now = 1554858000; // Get the courses in order. $courseset = testable_backup_cron_automated_helper::testable_get_courses($now); $coursearray = array(); foreach ($courseset as $course) { if ($course->id != SITEID) { // Skip system course for test. $coursearray[] = $course->id; } } $courseset->close(); // Should only be two courses. // First should be course 1, it is the more recently modified without a backup. $this->assertEquals($course1->id, $coursearray[0]); // Second should be course 2, it is the next more recently modified without a backup. $this->assertEquals($course2->id, $coursearray[1]); } } /** * Provides access to protected methods we want to explicitly test * * @copyright 2015 Jean-Philippe Gaudreau <jp.gaudreau@umontreal.ca> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class testable_backup_cron_automated_helper extends backup_cron_automated_helper { /** * Provides access to protected method get_backups_to_remove. * * @param array $backupfiles Existing backup files * @param int $now Starting time of the process * @return array Backup files to remove */ public static function testable_get_backups_to_delete($backupfiles, $now) { return parent::get_backups_to_delete($backupfiles, $now); } /** * Provides access to protected method get_backups_to_remove. * * @param int $courseid course id to check * @param int $since timestamp, from which to check * * @return bool true if the course was modified, false otherwise. This also returns false if no readers are enabled. This is * intentional, since we cannot reliably determine if any modification was made or not. */ public static function testable_is_course_modified($courseid, $since) { return parent::is_course_modified($courseid, $since); } /** * Provides access to protected method get_courses. * * @param int $now Timestamp to use. * @return moodle_recordset The returned courses as a Moodle recordest. */ public static function testable_get_courses($now) { return parent::get_courses($now); } } tests/async_helper_test.php 0000644 00000022112 15152170610 0012126 0 ustar 00 <?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 async_helper; use backup; use backup_controller; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Asyncronhous helper tests. * * @package core_backup * @covers \async_helper * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class async_helper_test extends \advanced_testcase { /** * Tests sending message for asynchronous backup. */ public function test_send_message() { global $DB, $USER; $this->preventResetByRollback(); $this->resetAfterTest(true); $this->setAdminUser(); set_config('backup_async_message_users', '1', 'backup'); set_config('backup_async_message_subject', 'Moodle {operation} completed sucessfully', 'backup'); set_config('backup_async_message', 'Dear {user_firstname} {user_lastname}, your {operation} (ID: {backupid}) has completed successfully!', 'backup'); set_config('allowedemaildomains', 'example.com'); $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Create a course with some availability data set. $user2 = $generator->create_user(array('firstname' => 'test', 'lastname' => 'human', 'maildisplay' => 1)); $generator->enrol_user($user2->id, $course->id, 'editingteacher'); $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'"); set_user_preference('message_provider_moodle_asyncbackupnotification', 'email', $user2); // Make the backup controller for an async backup. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_ASYNC, $user2->id); $bc->finish_ui(); $backupid = $bc->get_backupid(); $bc->destroy(); $sink = $this->redirectEmails(); // Send message. $asynchelper = new async_helper('backup', $backupid); $messageid = $asynchelper->send_message(); $emails = $sink->get_messages(); $this->assertCount(1, $emails); $email = reset($emails); $this->assertGreaterThan(0, $messageid); $sink->clear(); $this->assertSame($USER->email, $email->from); $this->assertSame($user2->email, $email->to); $this->assertSame('Moodle Backup completed sucessfully', $email->subject); // Assert body placeholders have all been replaced. $this->assertStringContainsString('Dear test human, your Backup', $email->body); $this->assertStringContainsString("(ID: {$backupid})", $email->body); $this->assertStringNotContainsString('{', $email->body); } /** * Tests getting the asynchronous backup table items. */ public function test_get_async_backups() { global $DB, $CFG, $USER, $PAGE; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Create a course with some availability data set. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $forum = $generator->create_module('forum', array( 'course' => $course->id)); $forum2 = $generator->create_module('forum', array( 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); // We need a grade, easiest is to add an assignment. $assignrow = $generator->create_module('assign', array( 'course' => $course->id)); $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); $item = $assign->get_grade_item(); // Make a test grouping as well. $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); $availability = '{"op":"|","show":false,"c":[' . '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . '{"type":"grouping","id":' . $grouping->id . '}' . ']}'; $DB->set_field('course_modules', 'availability', $availability, array( 'id' => $forum->cmid)); $DB->set_field('course_sections', 'availability', $availability, array( 'course' => $course->id, 'section' => 1)); // Make the backup controller for an async backup. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id); $bc->finish_ui(); $bc->destroy(); unset($bc); $coursecontext = \context_course::instance($course->id); $renderer = $PAGE->get_renderer('core', 'backup'); $result = \async_helper::get_async_backups($renderer, $coursecontext->instanceid); $this->assertEquals(1, count($result)); $this->assertEquals('backup.mbz', $result[0][0]); } /** * Tests getting the backup record. */ public function test_get_backup_record() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Create the initial backupcontoller. $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); $backupid = $bc->get_backupid(); $bc->destroy(); $copyrec = \async_helper::get_backup_record($backupid); $this->assertEquals($backupid, $copyrec->backupid); } /** * Tests is async pending conditions. */ public function test_is_async_pending() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); set_config('enableasyncbackup', '0'); $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); // Should be false as there are no backups and async backup is false. $this->assertFalse($ispending); // Create the initial backupcontoller. $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_ASYNC, $USER->id, \backup::RELEASESESSION_YES); $bc->destroy(); $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); // Should be false as there as async backup is false. $this->assertFalse($ispending); set_config('enableasyncbackup', '1'); // Should be true as there as async backup is true and there is a pending backup. $this->assertFalse($ispending); } /** * Tests is async pending conditions for course copies. */ public function test_is_async_pending_copy() { global $USER; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); set_config('enableasyncbackup', '0'); $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); // Should be false as there are no copies and async backup is false. $this->assertFalse($ispending); // Create the initial backupcontoller. $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); $bc->destroy(); $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); // Should be True as this a copy operation. $this->assertTrue($ispending); set_config('enableasyncbackup', '1'); $ispending = async_helper::is_async_pending($course->id, 'course', 'backup'); // Should be true as there as async backup is true and there is a pending copy. $this->assertTrue($ispending); } } tests/converterhelper_test.php 0000644 00000012757 15152170610 0012677 0 ustar 00 <?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/>. /** * Test the convert helper. * * @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 convert_helper; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); /** * Test the convert helper. * * @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 converterhelper_test extends \basic_testcase { public function test_choose_conversion_path() { // no converters available $descriptions = array(); $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); $this->assertEquals($path, array()); // missing source and/or targets $descriptions = array( // some custom converter 'exporter' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => 'some_custom_format', 'cost' => 10, ), // another custom converter 'converter' => array( 'from' => 'yet_another_crazy_custom_format', 'to' => backup::FORMAT_MOODLE, 'cost' => 10, ), ); $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); $this->assertEquals($path, array()); $path = testable_convert_helper::choose_conversion_path('some_other_custom_format', $descriptions); $this->assertEquals($path, array()); // single step conversion $path = testable_convert_helper::choose_conversion_path('yet_another_crazy_custom_format', $descriptions); $this->assertEquals($path, array('converter')); // no conversion needed - this is supposed to be detected by the caller $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE, $descriptions); $this->assertEquals($path, array()); // two alternatives $descriptions = array( // standard moodle 1.9 -> 2.x converter 'moodle1' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => backup::FORMAT_MOODLE, 'cost' => 10, ), // alternative moodle 1.9 -> 2.x converter 'alternative' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => backup::FORMAT_MOODLE, 'cost' => 8, ) ); $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); $this->assertEquals($path, array('alternative')); // complex case $descriptions = array( // standard moodle 1.9 -> 2.x converter 'moodle1' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => backup::FORMAT_MOODLE, 'cost' => 10, ), // alternative moodle 1.9 -> 2.x converter 'alternative' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => backup::FORMAT_MOODLE, 'cost' => 8, ), // custom converter from 1.9 -> custom 'CFv1' format 'cc1' => array( 'from' => backup::FORMAT_MOODLE1, 'to' => 'CFv1', 'cost' => 2, ), // custom converter from custom 'CFv1' format -> moodle 2.0 format 'cc2' => array( 'from' => 'CFv1', 'to' => backup::FORMAT_MOODLE, 'cost' => 5, ), // custom converter from CFv1 -> CFv2 format 'cc3' => array( 'from' => 'CFv1', 'to' => 'CFv2', 'cost' => 2, ), // custom converter from CFv2 -> moodle 2.0 format 'cc4' => array( 'from' => 'CFv2', 'to' => backup::FORMAT_MOODLE, 'cost' => 2, ), ); // ask the helper to find the most effective way $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); $this->assertEquals($path, array('cc1', 'cc3', 'cc4')); } } /** * Provides access to the protected methods we need to test */ class testable_convert_helper extends convert_helper { public static function choose_conversion_path($format, array $descriptions) { return parent::choose_conversion_path($format, $descriptions); } } tests/restore_structure_parser_processor_test.php 0000644 00000011276 15152170610 0016741 0 ustar 00 <?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/>. /** * Tests for restore_structure_parser_processor class. * * @package core_backup * @category test * @copyright 2017 Dmitrii Metelkin (dmitriim@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_structure_parser_processor.class.php'); /** * Tests for restore_structure_parser_processor class. * * @package core_backup * @copyright 2017 Dmitrii Metelkin (dmitriim@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_structure_parser_processor_test extends advanced_testcase { /** * Initial set up. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Data provider for ::test_process_cdata. * * @return array */ public function process_cdata_data_provider() { return array( array(null, null, true), array("$@NULL@$", null, true), array("$@NULL@$ ", "$@NULL@$ ", true), array(1, 1, true), array(" ", " ", true), array("1", "1", true), array("$@FILEPHP@$1.jpg", "$@FILEPHP@$1.jpg", true), array( "http://test.test/$@SLASH@$", "http://test.test/$@SLASH@$", true ), array( "<a href='$@FILEPHP@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/11.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/1/1.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/1//1.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F11.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F1.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F%2F1.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg$@FORCEDOWNLOAD@$'>Image</a>", "<a href='http://test.test/file.php/1/1.jpg?forcedownload=1'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg$@FORCEDOWNLOAD@$'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F1.jpg&forcedownload=1'>Image</a>", false ), array( "<iframe src='$@H5PEMBED@$?url=testurl'></iframe>", "<iframe src='http://test.test/h5p/embed.php?url=testurl'></iframe>", true ), ); } /** * Test that restore_structure_parser_processor replaces $@FILEPHP@$ to correct file php links. * * @dataProvider process_cdata_data_provider * @param string $content Testing content. * @param string $expected Expected result. * @param bool $slasharguments A value for $CFG->slasharguments setting. */ public function test_process_cdata($content, $expected, $slasharguments) { global $CFG; $CFG->slasharguments = $slasharguments; $CFG->wwwroot = 'http://test.test'; $processor = new restore_structure_parser_processor(1, 1); $this->assertEquals($expected, $processor->process_cdata($content)); } } tests/helper_test.php 0000644 00000003053 15152170610 0010734 0 ustar 00 <?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; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. global $CFG; require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_general_helper.class.php'); /** * backup_helper tests (all) * * @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 helper_test extends \basic_testcase { /* * test backup_helper class */ function test_backup_helper() { } /* * test backup_general_helper class */ function test_backup_general_helper() { } } tests/backup_encode_content_test.php 0000644 00000006213 15152170610 0013772 0 ustar 00 <?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_course_task; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_course_task.class.php'); /** * Tests for encoding content links in backup_course_task. * * The code that this tests is actually in backup/moodle2/backup_course_task.class.php, * but there is no place for unit tests near there, and perhaps one day it will * be refactored so it becomes more generic. * * @package core_backup * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_encode_content_test extends \basic_testcase { /** * Test the encode_content_links method for course. */ public function test_course_encode_content_links() { global $CFG; $httpsroot = "https://moodle.org"; $httproot = "http://moodle.org"; $oldroot = $CFG->wwwroot; // HTTPS root and links of both types in content. $CFG->wwwroot = $httpsroot; $encoded = backup_course_task::encode_content_links( $httproot . '/course/view.php?id=123, ' . $httpsroot . '/course/view.php?id=123, ' . $httpsroot . '/grade/index.php?id=123, ' . $httpsroot . '/grade/report/index.php?id=123, ' . $httpsroot . '/badges/view.php?type=2&id=123 and ' . $httpsroot . '/user/index.php?id=123.'); $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' . '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded); // HTTP root and links of both types in content. $CFG->wwwroot = $httproot; $encoded = backup_course_task::encode_content_links( $httproot . '/course/view.php?id=123, ' . $httpsroot . '/course/view.php?id=123, ' . $httproot . '/grade/index.php?id=123, ' . $httproot . '/grade/report/index.php?id=123, ' . $httproot . '/badges/view.php?type=2&id=123 and ' . $httproot . '/user/index.php?id=123.'); $this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' . '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded); $CFG->wwwroot = $oldroot; } } copy_helper.class.php 0000644 00000032165 15152170610 0010677 0 ustar 00 <?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/>. defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Copy helper class. * * @package core_backup * @copyright 2022 Catalyst IT Australia Pty Ltd * @author Cameron Ball <cameron@cameron1729.xyz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class copy_helper { /** * Process raw form data from copy_form. * * @param \stdClass $formdata Raw formdata * @return \stdClass Processed data for use with create_copy */ public static function process_formdata(\stdClass $formdata): \stdClass { $requiredfields = [ 'courseid', // Course id integer. 'fullname', // Fullname of the destination course. 'shortname', // Shortname of the destination course. 'category', // Category integer ID that contains the destination course. 'visible', // Integer to detrmine of the copied course will be visible. 'startdate', // Integer timestamp of the start of the destination course. 'enddate', // Integer timestamp of the end of the destination course. 'idnumber', // ID of the destination course. 'userdata', // Integer to determine if the copied course will contain user data. ]; $missingfields = array_diff($requiredfields, array_keys((array)$formdata)); if ($missingfields) { throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, implode(", ", $missingfields)); } // Remove any extra stuff in the form data. $processed = (object)array_intersect_key((array)$formdata, array_flip($requiredfields)); $processed->keptroles = []; // Extract roles from the form data and add to keptroles. foreach ($formdata as $key => $value) { if ((substr($key, 0, 5) === 'role_') && ($value != 0)) { $processed->keptroles[] = $value; } } return $processed; } /** * Creates a course copy. * Sets up relevant controllers and adhoc task. * * @param \stdClass $copydata Course copy data from process_formdata * @return array $copyids The backup and restore controller ids */ public static function create_copy(\stdClass $copydata): array { global $USER; $copyids = []; // Create the initial backupcontoller. $bc = new \backup_controller(\backup::TYPE_1COURSE, $copydata->courseid, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); $copyids['backupid'] = $bc->get_backupid(); // Create the initial restore contoller. list($fullname, $shortname) = \restore_dbops::calculate_course_names( 0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup')); $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $copydata->category); $rc = new \restore_controller($copyids['backupid'], $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::TARGET_NEW_COURSE, null, \backup::RELEASESESSION_NO, $copydata); $copyids['restoreid'] = $rc->get_restoreid(); $bc->set_status(\backup::STATUS_AWAITING); $bc->get_status(); $rc->save_controller(); // Create the ad-hoc task to perform the course copy. $asynctask = new \core\task\asynchronous_copy_task(); $asynctask->set_blocking(false); $asynctask->set_custom_data($copyids); \core\task\manager::queue_adhoc_task($asynctask); // Clean up the controller. $bc->destroy(); return $copyids; } /** * Get the in progress course copy operations for a user. * * @param int $userid User id to get the course copies for. * @param int|null $courseid The optional source course id to get copies for. * @return array $copies Details of the inprogress copies. */ public static function get_copies(int $userid, ?int $courseid = null): array { global $DB; $copies = []; [$insql, $inparams] = $DB->get_in_or_equal([\backup::STATUS_FINISHED_OK, \backup::STATUS_FINISHED_ERR]); $params = [ $userid, \backup::EXECUTION_DELAYED, \backup::MODE_COPY, \backup::OPERATION_BACKUP, \backup::STATUS_FINISHED_OK, \backup::OPERATION_RESTORE ]; // We exclude backups that finished with OK. Therefore if a backup is missing, // we can assume it finished properly. // // We exclude both failed and successful restores because both of those indicate that the whole // operation has completed. $sql = 'SELECT backupid, itemid, operation, status, timecreated, purpose FROM {backup_controllers} WHERE userid = ? AND execution = ? AND purpose = ? AND ((operation = ? AND status <> ?) OR (operation = ? AND status NOT ' . $insql .')) ORDER BY timecreated DESC'; $copyrecords = $DB->get_records_sql($sql, array_merge($params, $inparams)); $idtorc = self::map_backupids_to_restore_controller($copyrecords); // Our SQL only gets controllers that have not finished successfully. // So, no restores => all restores have finished (either failed or OK) => all backups have too // Therefore there are no in progress copy operations, return early. if (empty($idtorc)) { return []; } foreach ($copyrecords as $copyrecord) { try { $isbackup = $copyrecord->operation == \backup::OPERATION_BACKUP; // The mapping is guaranteed to exist for restore controllers, but not // backup controllers. // // When processing backups we don't actually need it, so we just coalesce // to null. $rc = $idtorc[$copyrecord->backupid] ?? null; $cid = $isbackup ? $copyrecord->itemid : $rc->get_copy()->courseid; $course = get_course($cid); $copy = clone ($copyrecord); $copy->backupid = $isbackup ? $copyrecord->backupid : null; $copy->restoreid = $rc ? $rc->get_restoreid() : null; $copy->destination = $rc ? $rc->get_copy()->shortname : null; $copy->source = $course->shortname; $copy->sourceid = $course->id; } catch (\Exception $e) { continue; } // Filter out anything that's not relevant. if ($courseid) { if ($isbackup && $copyrecord->itemid != $courseid) { continue; } if (!$isbackup && $rc->get_copy()->courseid != $courseid) { continue; } } // A backup here means that the associated restore controller has not started. // // There's a few situations to consider: // // 1. The backup is waiting or in progress // 2. The backup failed somehow // 3. Something went wrong (e.g., solar flare) and the backup controller saved, but the restore controller didn't // 4. The restore hasn't been created yet (race condition) // // In the case of 1, we add it to the return list. In the case of 2, 3 and 4 we just ignore it and move on. // The backup cleanup task will take care of updating/deleting invalid controllers. if ($isbackup) { if ($copyrecord->status != \backup::STATUS_FINISHED_ERR && !is_null($rc)) { $copies[] = $copy; } continue; } // A backup in copyrecords, indicates that the associated backup has not // successfully finished. We shouldn't do anything with this restore record. if ($copyrecords[$rc->get_tempdir()] ?? null) { continue; } // This is a restore record, and the backup has finished. Return it. $copies[] = $copy; } return $copies; } /** * Returns a mapping between copy controller IDs and the restore controller. * For example if there exists a copy with backup ID abc and restore ID 123 * then this mapping will map both keys abc and 123 to the same (instantiated) * restore controller. * * @param array $backuprecords An array of records from {backup_controllers} * @return array An array of mappings between backup ids and restore controllers */ private static function map_backupids_to_restore_controller(array $backuprecords): array { // Needed for PHP 7.3 - array_merge only accepts 0 parameters in PHP >= 7.4. if (empty($backuprecords)) { return []; } return array_merge( ...array_map( function (\stdClass $backuprecord): array { $iscopyrestore = $backuprecord->operation == \backup::OPERATION_RESTORE && $backuprecord->purpose == \backup::MODE_COPY; $isfinished = $backuprecord->status == \backup::STATUS_FINISHED_OK; if (!$iscopyrestore || $isfinished) { return []; } $rc = \restore_controller::load_controller($backuprecord->backupid); return [$backuprecord->backupid => $rc, $rc->get_tempdir() => $rc]; }, array_values($backuprecords) ) ); } /** * Detects and deletes/fails controllers associated with a course copy that are * in an invalid state. * * @param array $backuprecords An array of records from {backup_controllers} * @param int $age How old a controller needs to be (in seconds) before its considered for cleaning * @return void */ public static function cleanup_orphaned_copy_controllers(array $backuprecords, int $age = MINSECS): void { global $DB; $idtorc = self::map_backupids_to_restore_controller($backuprecords); // Helpful to test if a backup exists in $backuprecords. $bidstorecord = array_combine( array_column($backuprecords, 'backupid'), $backuprecords ); foreach ($backuprecords as $record) { if ($record->purpose != \backup::MODE_COPY || $record->status == \backup::STATUS_FINISHED_OK) { continue; } $isbackup = $record->operation == \backup::OPERATION_BACKUP; $restoreexists = isset($idtorc[$record->backupid]); $nsecondsago = time() - $age; if ($isbackup) { // Sometimes the backup controller gets created, ""something happens"" (like a solar flare) // and the restore controller (and hence adhoc task) don't. // // If more than one minute has passed and the restore controller doesn't exist, it's likely that // this backup controller is orphaned, so we should remove it as the adhoc task to process it will // never be created. if (!$restoreexists && $record->timecreated <= $nsecondsago) { // It would be better to mark the backup as failed by loading the controller // and marking it as failed with $bc->set_status(), but we can't: MDL-74711. // // Deleting it isn't ideal either as maybe we want to inspect the backup // for debugging. So manually updating the column seems to be the next best. $record->status = \backup::STATUS_FINISHED_ERR; $DB->update_record('backup_controllers', $record); } continue; } if ($rc = $idtorc[$record->backupid] ?? null) { $backuprecord = $bidstorecord[$rc->get_tempdir()] ?? null; // Check the status of the associated backup. If it's failed, then mark this // restore as failed too. if ($backuprecord && $backuprecord->status == \backup::STATUS_FINISHED_ERR) { $rc->set_status(\backup::STATUS_FINISHED_ERR); } } } } } restore_decode_rule.class.php 0000644 00000020545 15152170610 0012402 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class used to decode links back to their original form * * This class allows each restore task to specify the changes that * will be applied to any encoded (by backup) link to revert it back * to its original form, recoding any parameter as needed. * * TODO: Complete phpdocs */ class restore_decode_rule { protected $linkname; // How the link has been encoded in backup (CHOICEVIEWBYID, COURSEVIEWBYID...) protected $urltemplate; // How the original URL looks like, with dollar placeholders protected $mappings; // Which backup_ids mappings do we need to apply for replacing the placeholders protected $restoreid; // The unique restoreid we are executing protected $sourcewwwroot; // The original wwwroot of the backup file protected $targetwwwroot; // The targer wwwroot of the restore operation protected $cregexp; // Calculated regular expresion we'll be looking for matches public function __construct($linkname, $urltemplate, $mappings) { // Validate all the params are ok $this->mappings = $this->validate_params($linkname, $urltemplate, $mappings); $this->linkname = $linkname; $this->urltemplate = $urltemplate; $this->restoreid = 0; $this->sourcewwwroot = ''; $this->targetwwwroot = ''; // yes, uses to be $CFG->wwwroot, and? ;-) $this->cregexp = $this->get_calculated_regexp(); } public function set_restoreid($restoreid) { $this->restoreid = $restoreid; } public function set_wwwroots($sourcewwwroot, $targetwwwroot) { $this->sourcewwwroot = $sourcewwwroot; $this->targetwwwroot = $targetwwwroot; } public function decode($content) { if (preg_match_all($this->cregexp, $content, $matches) === 0) { // 0 matches, nothing to change return $content; } // Have found matches, iterate over them foreach ($matches[0] as $key => $tosearch) { $mappingsok = true; // To detect if any mapping has failed $placeholdersarr = array(); // The placeholders to be replaced $mappingssourcearr = array(); // To store the original mappings values $mappingstargetarr = array(); // To store the target mappings values $toreplace = $this->urltemplate;// The template used to build the replacement foreach ($this->mappings as $mappingkey => $mappingsource) { $source = $matches[$mappingkey][$key]; // get source $mappingssourcearr[$mappingkey] = $source; // set source arr $mappingstargetarr[$mappingkey] = 0; // apply default mapping $placeholdersarr[$mappingkey] = '$'.$mappingkey;// set the placeholders arr if (!$mappingsok) { // already missing some mapping, continue continue; } if (!$target = $this->get_mapping($mappingsource, $source)) {// mapping not found, mark and continue $mappingsok = false; continue; } $mappingstargetarr[$mappingkey] = $target; // store found mapping } $toreplace = $this->apply_modifications($toreplace, $mappingsok); // Apply other changes before replacement if (!$mappingsok) { // Some mapping has failed, apply original values to the template $toreplace = str_replace($placeholdersarr, $mappingssourcearr, $toreplace); } else { // All mappings found, apply target values to the template $toreplace = str_replace($placeholdersarr, $mappingstargetarr, $toreplace); } // Finally, perform the replacement in original content $content = str_replace($tosearch, $toreplace, $content); } return $content; // return the decoded content, pointing to original or target values } // Protected API starts here /** * Looks for mapping values in backup_ids table, simple wrapper over get_backup_ids_record */ protected function get_mapping($itemname, $itemid) { // Check restoreid is set if (!$this->restoreid) { throw new restore_decode_rule_exception('decode_rule_restoreid_not_set'); } if (!$found = restore_dbops::get_backup_ids_record($this->restoreid, $itemname, $itemid)) { return false; } return $found->newitemid; } /** * Apply other modifications, based in the result of $mappingsok before placeholder replacements * * Right now, simply prefix with the proper wwwroot (source/target) */ protected function apply_modifications($toreplace, $mappingsok) { // Check wwwroots are set if (!$this->targetwwwroot || !$this->sourcewwwroot) { throw new restore_decode_rule_exception('decode_rule_wwwroots_not_set'); } return ($mappingsok ? $this->targetwwwroot : $this->sourcewwwroot) . $toreplace; } /** * Perform all the validations and checks on the rule attributes */ protected function validate_params($linkname, $urltemplate, $mappings) { // Check linkname is A-Z0-9 if (empty($linkname) || preg_match('/[^A-Z0-9]/', $linkname)) { throw new restore_decode_rule_exception('decode_rule_incorrect_name', $linkname); } // Look urltemplate starts by / if (empty($urltemplate) || substr($urltemplate, 0, 1) != '/') { throw new restore_decode_rule_exception('decode_rule_incorrect_urltemplate', $urltemplate); } if (!is_array($mappings)) { $mappings = array($mappings); } // Look for placeholders in template $countph = preg_match_all('/(\$\d+)/', $urltemplate, $matches); $countma = count($mappings); // Check mappings number matches placeholders if ($countph != $countma) { $a = new stdClass(); $a->placeholders = $countph; $a->mappings = $countma; throw new restore_decode_rule_exception('decode_rule_mappings_incorrect_count', $a); } // Verify they are consecutive (starting on 1) $smatches = str_replace('$', '', $matches[1]); sort($smatches, SORT_NUMERIC); if (reset($smatches) != 1 || end($smatches) != $countma) { throw new restore_decode_rule_exception('decode_rule_nonconsecutive_placeholders', implode(', ', $smatches)); } // No dupes in placeholders if (count($smatches) != count(array_unique($smatches))) { throw new restore_decode_rule_exception('decode_rule_duplicate_placeholders', implode(', ', $smatches)); } // Return one array of placeholders as keys and mappings as values return array_combine($smatches, $mappings); } /** * based on rule definition, build the regular expression to execute on decode */ protected function get_calculated_regexp() { $regexp = '/\$@' . $this->linkname; foreach ($this->mappings as $key => $value) { $regexp .= '\*(\d+)'; } $regexp .= '@\$/'; return $regexp; } } /* * Exception class used by all the @restore_decode_rule stuff */ class restore_decode_rule_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } restore_moodlexml_parser_processor.class.php 0000644 00000004421 15152170610 0015576 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * return all the information present in the moodle_backup.xml file * accumulating it for later generation of controller->info * * TODO: Complete phpdocs */ class restore_moodlexml_parser_processor extends grouped_parser_processor { protected $accumchunks; public function __construct() { $this->accumchunks = array(); parent::__construct(); // Let's add all the paths we are interested on $this->add_path('/moodle_backup/information', true); // Everything will be grouped below this $this->add_path('/moodle_backup/information/details/detail'); $this->add_path('/moodle_backup/information/contents/activities/activity'); $this->add_path('/moodle_backup/information/contents/sections/section'); $this->add_path('/moodle_backup/information/contents/course'); $this->add_path('/moodle_backup/information/settings/setting'); } protected function dispatch_chunk($data) { $this->accumchunks[] = $data; } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } public function get_all_chunks() { return $this->accumchunks; } } restore_prechecks_helper.class.php 0000644 00000023074 15152170610 0013436 0 ustar 00 <?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-helper * @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 support for restore prechecks * * This class contains various prechecks to be performed before executing * the restore plan. Its entry point is execute_prechecks() that will * call various stuff. At the end, it will return one array(), if empty * all the prechecks have passed ok. If not empty, you'll find 1/2 elements * in the array, warnings and errors, each one containing one description * of the problem. Warnings aren't stoppers so the restore execution can * continue after displaying them. In the other side, if errors are returned * then restore execution cannot continue * * TODO: Finish phpdocs */ abstract class restore_prechecks_helper { /** * Entry point for all the prechecks to be performed before restore * * Returns empty array or warnings/errors array */ public static function execute_prechecks(restore_controller $controller, $droptemptablesafter = false) { global $CFG; $errors = array(); $warnings = array(); // Some handy vars to be used along the prechecks $samesite = $controller->is_samesite(); $restoreusers = $controller->get_plan()->get_setting('users')->get_value(); $hasmnetusers = (int)$controller->get_info()->mnet_remoteusers; $restoreid = $controller->get_restoreid(); $courseid = $controller->get_courseid(); $userid = $controller->get_userid(); $rolemappings = $controller->get_info()->role_mappings; $progress = $controller->get_progress(); // Start tracking progress. There are currently 8 major steps, corresponding // to $majorstep++ lines in this code; we keep track of the total so as to // verify that it's still correct. If you add a major step, you need to change // the total here. $majorstep = 1; $majorsteps = 8; $progress->start_progress('Carrying out pre-restore checks', $majorsteps); // Load all the included tasks to look for inforef.xml files $inforeffiles = array(); $tasks = restore_dbops::get_included_tasks($restoreid); $progress->start_progress('Listing inforef files', count($tasks)); $minorstep = 1; foreach ($tasks as $task) { // Add the inforef.xml file if exists $inforefpath = $task->get_taskbasepath() . '/inforef.xml'; if (file_exists($inforefpath)) { $inforeffiles[] = $inforefpath; } $progress->progress($minorstep++); } $progress->end_progress(); $progress->progress($majorstep++); // Create temp tables restore_controller_dbops::create_restore_temp_tables($controller->get_restoreid()); // Check we are restoring one backup >= $min20version (very first ok ever) $min20version = 2010072300; if ($controller->get_info()->backup_version < $min20version) { $message = new stdclass(); $message->backup = $controller->get_info()->backup_version; $message->min = $min20version; $errors[] = get_string('errorminbackup20version', 'backup', $message); } // Compare Moodle's versions if ($CFG->version < $controller->get_info()->moodle_version) { $message = new stdclass(); $message->serverversion = $CFG->version; $message->serverrelease = $CFG->release; $message->backupversion = $controller->get_info()->moodle_version; $message->backuprelease = $controller->get_info()->moodle_release; $warnings[] = get_string('noticenewerbackup','',$message); } // The original_course_format var was introduced in Moodle 2.9. $originalcourseformat = null; if (!empty($controller->get_info()->original_course_format)) { $originalcourseformat = $controller->get_info()->original_course_format; } // We can't restore other course's backups on the front page. if ($controller->get_courseid() == SITEID && $originalcourseformat != 'site' && $controller->get_type() == backup::TYPE_1COURSE) { $errors[] = get_string('errorrestorefrontpagebackup', 'backup'); } // We can't restore front pages over other courses. if ($controller->get_courseid() != SITEID && $originalcourseformat == 'site' && $controller->get_type() == backup::TYPE_1COURSE) { $errors[] = get_string('errorrestorefrontpagebackup', 'backup'); } // If restoring to different site and restoring users and backup has mnet users warn/error if (!$samesite && $restoreusers && $hasmnetusers) { // User is admin (can create users at sysctx), warn if (has_capability('moodle/user:create', context_system::instance(), $controller->get_userid())) { $warnings[] = get_string('mnetrestore_extusers_admin', 'admin'); // User not admin } else { $errors[] = get_string('mnetrestore_extusers_noadmin', 'admin'); } } // Load all the inforef files, we are going to need them $progress->start_progress('Loading temporary IDs', count($inforeffiles)); $minorstep = 1; foreach ($inforeffiles as $inforeffile) { // Load each inforef file to temp_ids. restore_dbops::load_inforef_to_tempids($restoreid, $inforeffile, $progress); $progress->progress($minorstep++); } $progress->end_progress(); $progress->progress($majorstep++); // If restoring users, check we are able to create all them if ($restoreusers) { $file = $controller->get_plan()->get_basepath() . '/users.xml'; // Load needed users to temp_ids. restore_dbops::load_users_to_tempids($restoreid, $file, $progress); $progress->progress($majorstep++); if ($problems = restore_dbops::precheck_included_users($restoreid, $courseid, $userid, $samesite, $progress)) { $errors = array_merge($errors, $problems); } } else { // To ensure consistent number of steps in progress tracking, // mark progress even though we didn't do anything. $progress->progress($majorstep++); } $progress->progress($majorstep++); // Note: restore won't create roles at all. Only mapping/skip! $file = $controller->get_plan()->get_basepath() . '/roles.xml'; restore_dbops::load_roles_to_tempids($restoreid, $file); // Load needed roles to temp_ids if ($problems = restore_dbops::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings)) { $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } $progress->progress($majorstep++); // Check we are able to restore and the categories and questions $file = $controller->get_plan()->get_basepath() . '/questions.xml'; restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file); if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) { $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } $progress->progress($majorstep++); // Prepare results. $results = array(); if (!empty($errors)) { $results['errors'] = $errors; } if (!empty($warnings)) { $results['warnings'] = $warnings; } // Warnings/errors detected or want to do so explicitly, drop temp tables if (!empty($results) || $droptemptablesafter) { restore_controller_dbops::drop_restore_temp_tables($controller->get_restoreid()); } // Finish progress and check we got the initial number of steps right. $progress->progress($majorstep++); if ($majorstep != $majorsteps) { throw new coding_exception('Progress step count wrong: ' . $majorstep); } $progress->end_progress(); return $results; } } /* * Exception class used by all the @restore_prechecks_helper stuff */ class restore_prechecks_helper_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } backup_general_helper.class.php 0000644 00000033524 15152170610 0012667 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Non instantiable helper class providing general helper methods for backup/restore * * This class contains various general helper static methods available for backup/restore * * TODO: Finish phpdocs */ abstract class backup_general_helper extends backup_helper { /** * Calculate one checksum for any array/object. Works recursively */ public static function array_checksum_recursive($arr) { $checksum = ''; // Init checksum // Check we are going to process one array always, objects must be cast before if (!is_array($arr)) { throw new backup_helper_exception('array_expected'); } foreach ($arr as $key => $value) { if ($value instanceof checksumable) { $checksum = md5($checksum . '-' . $key . '-' . $value->calculate_checksum()); } else if (is_object($value)) { $checksum = md5($checksum . '-' . $key . '-' . self::array_checksum_recursive((array)$value)); } else if (is_array($value)) { $checksum = md5($checksum . '-' . $key . '-' . self::array_checksum_recursive($value)); } else { $checksum = md5($checksum . '-' . $key . '-' . $value); } } return $checksum; } /** * Load all the blocks information needed for a given path within moodle2 backup * * This function, given one full path (course, activities/xxxx) will look for all the * blocks existing in the backup file, returning one array used to build the * proper restore plan by the @restore_plan_builder */ public static function get_blocks_from_path($path) { global $DB; $blocks = array(); // To return results static $availableblocks = array(); // Get and cache available blocks if (empty($availableblocks)) { $availableblocks = array_keys(core_component::get_plugin_list('block')); } $path = $path . '/blocks'; // Always look under blocks subdir if (!is_dir($path)) { return array(); } if (!$dir = opendir($path)) { return array(); } while (false !== ($file = readdir($dir))) { if ($file == '.' || $file == '..') { // Skip dots continue; } if (is_dir($path .'/' . $file)) { // Dir found, check it's a valid block if (!file_exists($path .'/' . $file . '/block.xml')) { // Skip if xml file not found continue; } // Extract block name $blockname = preg_replace('/(.*)_\d+/', '\\1', $file); // Check block exists and is installed if (in_array($blockname, $availableblocks) && $DB->record_exists('block', array('name' => $blockname))) { $blocks[$path .'/' . $file] = $blockname; } } } closedir($dir); return $blocks; } /** * Load and format all the needed information from moodle_backup.xml * * This function loads and process all the moodle_backup.xml * information, composing a big information structure that will * be the used by the plan builder in order to generate the * appropiate tasks / steps / settings */ public static function get_backup_information($tempdir) { global $CFG; // Make a request cache and store the data in there. static $cachesha1 = null; static $cache = null; $info = new stdclass(); // Final information goes here $backuptempdir = make_backup_temp_directory('', false); $moodlefile = $backuptempdir . '/' . $tempdir . '/moodle_backup.xml'; if (!file_exists($moodlefile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_moodle_backup_xml_file', $moodlefile); } $moodlefilesha1 = sha1_file($moodlefile); if ($moodlefilesha1 === $cachesha1) { return clone $cache; } // Load the entire file to in-memory array $xmlparser = new progressive_parser(); $xmlparser->set_file($moodlefile); $xmlprocessor = new restore_moodlexml_parser_processor(); $xmlparser->set_processor($xmlprocessor); $xmlparser->process(); $infoarr = $xmlprocessor->get_all_chunks(); if (count($infoarr) !== 1) { // Shouldn't happen ever, but... throw new backup_helper_exception('problem_parsing_moodle_backup_xml_file'); } $infoarr = $infoarr[0]['tags']; // for commodity // Let's build info $info->moodle_version = $infoarr['moodle_version']; $info->moodle_release = $infoarr['moodle_release']; $info->backup_version = $infoarr['backup_version']; $info->backup_release = $infoarr['backup_release']; $info->backup_date = $infoarr['backup_date']; $info->mnet_remoteusers = $infoarr['mnet_remoteusers']; $info->original_wwwroot = $infoarr['original_wwwroot']; $info->original_site_identifier_hash = $infoarr['original_site_identifier_hash']; $info->original_course_id = $infoarr['original_course_id']; $info->original_course_fullname = $infoarr['original_course_fullname']; $info->original_course_shortname = $infoarr['original_course_shortname']; $info->original_course_startdate = $infoarr['original_course_startdate']; // Old versions may not have this. if (isset($infoarr['original_course_enddate'])) { $info->original_course_enddate = $infoarr['original_course_enddate']; } $info->original_course_contextid = $infoarr['original_course_contextid']; $info->original_system_contextid = $infoarr['original_system_contextid']; // Moodle backup file don't have this option before 2.3 if (!empty($infoarr['include_file_references_to_external_content'])) { $info->include_file_references_to_external_content = 1; } else { $info->include_file_references_to_external_content = 0; } // Introduced in Moodle 2.9. $info->original_course_format = ''; if (!empty($infoarr['original_course_format'])) { $info->original_course_format = $infoarr['original_course_format']; } // include_files is a new setting in 2.6. if (isset($infoarr['include_files'])) { $info->include_files = $infoarr['include_files']; } else { $info->include_files = 1; } $info->type = $infoarr['details']['detail'][0]['type']; $info->format = $infoarr['details']['detail'][0]['format']; $info->mode = $infoarr['details']['detail'][0]['mode']; // Build the role mappings custom object $rolemappings = new stdclass(); $rolemappings->modified = false; $rolemappings->mappings = array(); $info->role_mappings = $rolemappings; // Some initially empty containers $info->sections = array(); $info->activities = array(); // Now the contents $contentsarr = $infoarr['contents']; if (isset($contentsarr['course']) && isset($contentsarr['course'][0])) { $info->course = new stdclass(); $info->course = (object)$contentsarr['course'][0]; $info->course->settings = array(); } if (isset($contentsarr['sections']) && isset($contentsarr['sections']['section'])) { $sectionarr = $contentsarr['sections']['section']; foreach ($sectionarr as $section) { $section = (object)$section; $section->settings = array(); $sections[basename($section->directory)] = $section; } $info->sections = $sections; } if (isset($contentsarr['activities']) && isset($contentsarr['activities']['activity'])) { $activityarr = $contentsarr['activities']['activity']; foreach ($activityarr as $activity) { $activity = (object)$activity; $activity->settings = array(); $activities[basename($activity->directory)] = $activity; } $info->activities = $activities; } $info->root_settings = array(); // For root settings // Now the settings, putting each one under its owner $settingsarr = $infoarr['settings']['setting']; foreach($settingsarr as $setting) { switch ($setting['level']) { case 'root': $info->root_settings[$setting['name']] = $setting['value']; break; case 'course': $info->course->settings[$setting['name']] = $setting['value']; break; case 'section': $info->sections[$setting['section']]->settings[$setting['name']] = $setting['value']; break; case 'activity': $info->activities[$setting['activity']]->settings[$setting['name']] = $setting['value']; break; default: // Shouldn't happen but tolerated for portability of customized backups. debugging("Unknown backup setting level: {$setting['level']}", DEBUG_DEVELOPER); break; } } $cache = clone $info; $cachesha1 = $moodlefilesha1; return $info; } /** * Load and format all the needed information from a backup file. * * This will only extract the moodle_backup.xml file from an MBZ * file and then call {@link self::get_backup_information()}. * * This can be a long-running (multi-minute) operation for large backups. * Pass a $progress value to receive progress updates. * * @param string $filepath absolute path to the MBZ file. * @param file_progress $progress Progress updates * @return stdClass containing information. * @since Moodle 2.4 */ public static function get_backup_information_from_mbz($filepath, file_progress $progress = null) { global $CFG; if (!is_readable($filepath)) { throw new backup_helper_exception('missing_moodle_backup_file', $filepath); } // Extract moodle_backup.xml. $tmpname = 'info_from_mbz_' . time() . '_' . random_string(4); $tmpdir = make_backup_temp_directory($tmpname); $fp = get_file_packer('application/vnd.moodle.backup'); $extracted = $fp->extract_to_pathname($filepath, $tmpdir, array('moodle_backup.xml'), $progress); $moodlefile = $tmpdir . '/' . 'moodle_backup.xml'; if (!$extracted || !is_readable($moodlefile)) { throw new backup_helper_exception('missing_moodle_backup_xml_file', $moodlefile); } // Read the information and delete the temporary directory. $info = self::get_backup_information($tmpname); remove_dir($tmpdir); return $info; } /** * Given the information fetched from moodle_backup.xml file * decide if we are restoring in the same site the backup was * generated or no. Behavior of various parts of restore are * dependent of this. * * Backups created natively in 2.0 and later declare the hashed * site identifier. Backups created by conversion from a 1.9 * backup do not declare such identifier, so there is a fallback * to wwwroot comparison. See MDL-16614. */ public static function backup_is_samesite($info) { global $CFG; $hashedsiteid = md5(get_site_identifier()); if (isset($info->original_site_identifier_hash) && !empty($info->original_site_identifier_hash)) { return $info->original_site_identifier_hash == $hashedsiteid; } else { return $info->original_wwwroot == $CFG->wwwroot; } } /** * Detects the format of the given unpacked backup directory * * @param string $tempdir the name of the backup directory * @return string one of backup::FORMAT_xxx constants */ public static function detect_backup_format($tempdir) { global $CFG; require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); if (convert_helper::detect_moodle2_format($tempdir)) { return backup::FORMAT_MOODLE; } // see if a converter can identify the format $converters = convert_helper::available_converters(); foreach ($converters as $name) { $classname = "{$name}_converter"; if (!class_exists($classname)) { throw new coding_exception("available_converters() is supposed to load converter classes but class $classname not found"); } $detected = call_user_func($classname .'::detect_format', $tempdir); if (!empty($detected)) { return $detected; } } return backup::FORMAT_UNKNOWN; } } backup_anonymizer_helper.class.php 0000644 00000014224 15152170610 0013441 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Helper class for anonymization of data * * This functions includes a collection of methods that are invoked * from the backup process when anonymization services have been * requested. * * The name of each method must be "process_parentname_name", as defined * byt the @anonymizer_final_element final element class, where * parentname is the name ob the parent tag and name the name of the tag * contents to be anonymized (i.e. process_user_username) with one param * being the value to anonymize. * * Note: current implementation of anonymization is pretty simple, just some * sequential values are used. If we want more elaborated generation, it * can be replaced later (using generators or wathever). Don't forget we must * ensure some fields (username, idnumber, email) are unique always. * * TODO: Improve to use more advanced anonymization * * TODO: Finish phpdocs */ class backup_anonymizer_helper { /** * Determine if the given user is an 'anonymous' user, based on their username, firstname, lastname * and email address. * @param stdClass $user the user record to test * @return bool true if this is an 'anonymous' user */ public static function is_anonymous_user($user) { if (preg_match('/^anon\d*$/', $user->username)) { $match = preg_match('/^anonfirstname\d*$/', $user->firstname); $match = $match && preg_match('/^anonlastname\d*$/', $user->lastname); // Check .com for backwards compatibility. $emailmatch = preg_match('/^anon\d*@doesntexist\.com$/', $user->email) || preg_match('/^anon\d*@doesntexist\.invalid$/', $user->email); if ($match && $emailmatch) { return true; } } return false; } public static function process_user_auth($value) { return 'manual'; // Set them to manual always } public static function process_user_username($value) { static $counter = 0; $counter++; return 'anon' . $counter; // Just a counter } public static function process_user_idnumber($value) { return ''; // Just blank it } public static function process_user_firstname($value) { static $counter = 0; $counter++; return 'anonfirstname' . $counter; // Just a counter } public static function process_user_lastname($value) { static $counter = 0; $counter++; return 'anonlastname' . $counter; // Just a counter } public static function process_user_email($value) { static $counter = 0; $counter++; return 'anon' . $counter . '@doesntexist.invalid'; // Just a counter. } public static function process_user_phone1($value) { return ''; // Clean phone1 } public static function process_user_phone2($value) { return ''; // Clean phone2 } public static function process_user_institution($value) { return ''; // Clean institution } public static function process_user_department($value) { return ''; // Clean department } public static function process_user_address($value) { return ''; // Clean address } public static function process_user_city($value) { return 'Perth'; // Set city } public static function process_user_country($value) { return 'AU'; // Set country } public static function process_user_lastip($value) { return '127.0.0.1'; // Set lastip to localhost } public static function process_user_picture($value) { return 0; // No picture } public static function process_user_description($value) { return ''; // No user description } public static function process_user_descriptionformat($value) { return 0; // Format moodle } public static function process_user_imagealt($value) { return ''; // No user imagealt } /** * Anonymises user's phonetic name field * @param string $value value of the user field * @return string anonymised phonetic name */ public static function process_user_firstnamephonetic($value) { static $counter = 0; $counter++; return 'anonfirstnamephonetic' . $counter; // Just a counter. } /** * Anonymises user's phonetic last name field * @param string $value value of the user field * @return string anonymised last phonetic name */ public static function process_user_lastnamephonetic($value) { static $counter = 0; $counter++; return 'anonlastnamephonetic' . $counter; // Just a counter. } /** * Anonymises user's middle name field * @param string $value value of the user field * @return string anonymised middle name */ public static function process_user_middlename($value) { static $counter = 0; $counter++; return 'anonmiddlename' . $counter; // Just a counter. } /** * Anonymises user's alternate name field * @param string $value value of the user field * @return string anonymised alternate name */ public static function process_user_alternatename($value) { static $counter = 0; $counter++; return 'anonalternatename' . $counter; // Just a counter. } } restore_decode_processor.class.php 0000644 00000015451 15152170610 0013452 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class that will perform all the necessary decoding tasks on restore * * This class will register all the restore_decode_content and * restore_decode_rule instances defined by the restore tasks * in order to perform the complete decoding of links in the * final task of the restore_plan execution. * * By visiting each content provider will apply all the defined rules * * TODO: Complete phpdocs */ class restore_decode_processor { protected $contents; // Array of restore_decode_content providers protected $rules; // Array of restore_decode_rule workers protected $restoreid; // The unique restoreid we are executing protected $sourcewwwroot; // The original wwwroot of the backup file protected $targetwwwroot; // The target wwwroot of the restore operation public function __construct($restoreid, $sourcewwwroot, $targetwwwroot) { $this->restoreid = $restoreid; $this->sourcewwwroot = $sourcewwwroot; $this->targetwwwroot = $targetwwwroot; $this->contents = array(); $this->rules = array(); } public function add_content($content) { if (!$content instanceof restore_decode_content) { throw new restore_decode_processor_exception('incorrect_restore_decode_content', get_class($content)); } $content->set_restoreid($this->restoreid); $this->contents[] = $content; } public function add_rule($rule) { if (!$rule instanceof restore_decode_rule) { throw new restore_decode_processor_exception('incorrect_restore_decode_rule', get_class($rule)); } $rule->set_restoreid($this->restoreid); $rule->set_wwwroots($this->sourcewwwroot, $this->targetwwwroot); $this->rules[] = $rule; } /** * Visit all the restore_decode_content providers * that will cause decode_content() to be called * for each content */ public function execute() { // Iterate over all contents, visiting them /** @var restore_decode_content $content */ foreach ($this->contents as $content) { $content->process($this); } } /** * Receive content from restore_decode_content objects * and apply all the restore_decode_rules to them */ public function decode_content($content) { if (!$content = $this->precheck_content($content)) { // Perform some prechecks return false; } // Iterate over all rules, chaining results foreach ($this->rules as $rule) { $content = $rule->decode($content); } return $content; } /** * Adds all the course/section/activity/block contents and rules */ public static function register_link_decoders($processor) { $tasks = array(); // To get the list of tasks having decoders // Add the course task $tasks[] = 'restore_course_task'; // Add the section task $tasks[] = 'restore_section_task'; // Add the module tasks $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { if (class_exists('restore_' . $mod . '_activity_task')) { $tasks[] = 'restore_' . $mod . '_activity_task'; } } // Add the default block task $tasks[] = 'restore_default_block_task'; // Add the custom block tasks $blocks = core_component::get_plugin_list('block'); foreach ($blocks as $block => $blockdir) { if (class_exists('restore_' . $block . '_block_task')) { $tasks[] = 'restore_' . $block . '_block_task'; } } // We have all the tasks registered, let's iterate over them, getting // contents and rules and adding them to the processor foreach ($tasks as $classname) { // Get restore_decode_content array and add to processor $contents = call_user_func(array($classname, 'define_decode_contents')); if (!is_array($contents)) { throw new restore_decode_processor_exception('define_decode_contents_not_array', $classname); } foreach ($contents as $content) { $processor->add_content($content); } // Get restore_decode_rule array and add to processor $rules = call_user_func(array($classname, 'define_decode_rules')); if (!is_array($rules)) { throw new restore_decode_processor_exception('define_decode_rules_not_array', $classname); } foreach ($rules as $rule) { $processor->add_rule($rule); } } // Now process all the plugins contents (note plugins don't have support for rules) // TODO: Add other plugin types (course formats, local...) here if we add them to backup/restore $plugins = array('qtype'); foreach ($plugins as $plugin) { $contents = restore_plugin::get_restore_decode_contents($plugin); if (!is_array($contents)) { throw new restore_decode_processor_exception('get_restore_decode_contents_not_array', $plugin); } foreach ($contents as $content) { $processor->add_content($content); } } } // Protected API starts here /** * Perform some general checks in content. Returning false rules processing is skipped */ protected function precheck_content($content) { // Look for $@ in content (all interlinks contain that) return (strpos($content ?? '', '$@') === false) ? false : $content; } } /* * Exception class used by all the @restore_decode_content stuff */ class restore_decode_processor_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } convert_helper.class.php 0000644 00000034300 15152170610 0011376 0 ustar 00 <?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/>. /** * Provides {@link convert_helper} and {@link convert_helper_exception} classes * * @package core * @subpackage backup-convert * @copyright 2011 Mark Nielsen <mark@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); /** * Provides various functionality via its static methods */ abstract class convert_helper { /** * @param string $entropy * @return string random identifier */ public static function generate_id($entropy) { return md5(time() . '-' . $entropy . '-' . random_string(20)); } /** * Returns the list of all available converters and loads their classes * * Converter must be installed as a directory in backup/converter/ and its * method is_available() must return true to get to the list. * * @see base_converter::is_available() * @return array of strings */ public static function available_converters($restore=true) { global $CFG; $converters = array(); $plugins = get_list_of_plugins('backup/converter'); foreach ($plugins as $name) { $filename = $restore ? 'lib.php' : 'backuplib.php'; $classuf = $restore ? '_converter' : '_export_converter'; $classfile = "{$CFG->dirroot}/backup/converter/{$name}/{$filename}"; $classname = "{$name}{$classuf}"; $zip_contents = "{$name}_zip_contents"; $store_backup_file = "{$name}_store_backup_file"; $convert = "{$name}_backup_convert"; if (!file_exists($classfile)) { throw new convert_helper_exception('converter_classfile_not_found', $classfile); } require_once($classfile); if (!class_exists($classname)) { throw new convert_helper_exception('converter_classname_not_found', $classname); } if (call_user_func($classname .'::is_available')) { if (!$restore) { if (!class_exists($zip_contents)) { throw new convert_helper_exception('converter_classname_not_found', $zip_contents); } if (!class_exists($store_backup_file)) { throw new convert_helper_exception('converter_classname_not_found', $store_backup_file); } if (!class_exists($convert)) { throw new convert_helper_exception('converter_classname_not_found', $convert); } } $converters[] = $name; } } return $converters; } public static function export_converter_dependencies($converter, $dependency) { global $CFG; $result = array(); $filename = 'backuplib.php'; $classuf = '_export_converter'; $classfile = "{$CFG->dirroot}/backup/converter/{$converter}/{$filename}"; $classname = "{$converter}{$classuf}"; if (!file_exists($classfile)) { throw new convert_helper_exception('converter_classfile_not_found', $classfile); } require_once($classfile); if (!class_exists($classname)) { throw new convert_helper_exception('converter_classname_not_found', $classname); } if (call_user_func($classname .'::is_available')) { $deps = call_user_func($classname .'::get_deps'); if (array_key_exists($dependency, $deps)) { $result = $deps[$dependency]; } } return $result; } /** * Detects if the given folder contains an unpacked moodle2 backup * * @param string $tempdir the name of the backup directory * @return boolean true if moodle2 format detected, false otherwise */ public static function detect_moodle2_format($tempdir) { $dirpath = make_backup_temp_directory($tempdir, false); if (!is_dir($dirpath)) { throw new convert_helper_exception('tmp_backup_directory_not_found', $dirpath); } $filepath = $dirpath . '/moodle_backup.xml'; if (!file_exists($filepath)) { return false; } $handle = fopen($filepath, 'r'); $firstchars = fread($handle, 200); $status = fclose($handle); // Look for expected XML elements (case-insensitive to account for encoding attribute). if (stripos($firstchars, '<?xml version="1.0" encoding="UTF-8"?>') !== false && strpos($firstchars, '<moodle_backup>') !== false && strpos($firstchars, '<information>') !== false) { return true; } return false; } /** * Converts the given directory with the backup into moodle2 format * * @param string $tempdir The directory to convert * @param string $format The current format, if already detected * @param base_logger|null if the conversion should be logged, use this logger * @throws convert_helper_exception * @return bool false if unable to find the conversion path, true otherwise */ public static function to_moodle2_format($tempdir, $format = null, $logger = null) { if (is_null($format)) { $format = backup_general_helper::detect_backup_format($tempdir); } // get the supported conversion paths from all available converters $converters = self::available_converters(); $descriptions = array(); foreach ($converters as $name) { $classname = "{$name}_converter"; if (!class_exists($classname)) { throw new convert_helper_exception('class_not_loaded', $classname); } if ($logger instanceof base_logger) { backup_helper::log('available converter', backup::LOG_DEBUG, $classname, 1, false, $logger); } $descriptions[$name] = call_user_func($classname .'::description'); } // choose the best conversion path for the given format $path = self::choose_conversion_path($format, $descriptions); if (empty($path)) { if ($logger instanceof base_logger) { backup_helper::log('unable to find the conversion path', backup::LOG_ERROR, null, 0, false, $logger); } return false; } if ($logger instanceof base_logger) { backup_helper::log('conversion path established', backup::LOG_INFO, implode(' => ', array_merge($path, array('moodle2'))), 0, false, $logger); } foreach ($path as $name) { if ($logger instanceof base_logger) { backup_helper::log('running converter', backup::LOG_INFO, $name, 0, false, $logger); } $converter = convert_factory::get_converter($name, $tempdir, $logger); $converter->convert(); } // make sure we ended with moodle2 format if (!self::detect_moodle2_format($tempdir)) { throw new convert_helper_exception('conversion_failed'); } return true; } /** * Inserts an inforef into the conversion temp table */ public static function set_inforef($contextid) { global $DB; } public static function get_inforef($contextid) { } /// end of public API ////////////////////////////////////////////////////// /** * Choose the best conversion path for the given format * * Given the source format and the list of available converters and their properties, * this methods picks the most effective way how to convert the source format into * the target moodle2 format. The method returns a list of converters that should be * called, in order. * * This implementation uses Dijkstra's algorithm to find the shortest way through * the oriented graph. * * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm * @author David Mudrak <david@moodle.com> * @param string $format the source backup format, one of backup::FORMAT_xxx * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name * @return array ordered list of converter names to call (may be empty if not reachable) */ protected static function choose_conversion_path($format, array $descriptions) { // construct an oriented graph of conversion paths. backup formats are nodes // and the the converters are edges of the graph. $paths = array(); // [fromnode][tonode] => converter foreach ($descriptions as $converter => $description) { $from = $description['from']; $to = $description['to']; $cost = $description['cost']; if (is_null($from) or $from === backup::FORMAT_UNKNOWN or is_null($to) or $to === backup::FORMAT_UNKNOWN or is_null($cost) or $cost <= 0) { throw new convert_helper_exception('invalid_converter_description', $converter); } if (!isset($paths[$from][$to])) { $paths[$from][$to] = $converter; } else { // if there are two converters available for the same conversion // path, choose the one with the lowest cost. if there are more // available converters with the same cost, the chosen one is // undefined (depends on the order of processing) if ($descriptions[$paths[$from][$to]]['cost'] > $cost) { $paths[$from][$to] = $converter; } } } if (empty($paths)) { // no conversion paths available return array(); } // now use Dijkstra's algorithm and find the shortest conversion path $dist = array(); // list of nodes and their distances from the source format $prev = array(); // list of previous nodes in optimal path from the source format foreach ($paths as $fromnode => $tonodes) { $dist[$fromnode] = null; // infinitive distance, can't be reached $prev[$fromnode] = null; // unknown foreach ($tonodes as $tonode => $converter) { $dist[$tonode] = null; // infinitive distance, can't be reached $prev[$tonode] = null; // unknown } } if (!array_key_exists($format, $dist)) { return array(); } else { $dist[$format] = 0; } $queue = array_flip(array_keys($dist)); while (!empty($queue)) { // find the node with the smallest distance from the source in the queue // in the first iteration, this will find the original format node itself $closest = null; foreach ($queue as $node => $undefined) { if (is_null($dist[$node])) { continue; } if (is_null($closest) or ($dist[$node] < $dist[$closest])) { $closest = $node; } } if (is_null($closest) or is_null($dist[$closest])) { // all remaining nodes are inaccessible from source break; } if ($closest === backup::FORMAT_MOODLE) { // bingo we can break now break; } unset($queue[$closest]); // visit all neighbors and update distances to them eventually if (!isset($paths[$closest])) { continue; } $neighbors = array_keys($paths[$closest]); // keep just neighbors that are in the queue yet foreach ($neighbors as $ix => $neighbor) { if (!array_key_exists($neighbor, $queue)) { unset($neighbors[$ix]); } } foreach ($neighbors as $neighbor) { // the alternative distance to the neighbor if we went thru the // current $closest node $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost']; if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) { // we found a shorter way to the $neighbor, remember it $dist[$neighbor] = $alt; $prev[$neighbor] = $closest; } } } if (is_null($dist[backup::FORMAT_MOODLE])) { // unable to find a conversion path, the target format not reachable return array(); } // reconstruct the optimal path from the source format to the target one $conversionpath = array(); $target = backup::FORMAT_MOODLE; while (isset($prev[$target])) { array_unshift($conversionpath, $paths[$prev[$target]][$target]); $target = $prev[$target]; } return $conversionpath; } } /** * General convert_helper related exception * * @author David Mudrak <david@moodle.com> */ class convert_helper_exception extends moodle_exception { /** * Constructor * * @param string $errorcode key for the corresponding error string * @param object $a extra words and phrases that might be required in the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $a = null, $debuginfo = null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } restore_roles_parser_processor.class.php 0000644 00000005044 15152170610 0014724 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one roles.xml (roles description) file to the backup_ids table * storing the whole structure there for later processing. * Note: only "needed" roles are loaded (must have roleref record in backup_ids) * * TODO: Complete phpdocs */ class restore_roles_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Set the paths we are interested on, returning all them grouped under user $this->add_path('/roles_definition/role'); } protected function dispatch_chunk($data) { // Received one role chunck, we are going to store it into backup_ids // table, with name = role $itemname = 'role'; $itemid = $data['tags']['id']; $info = $data['tags']; // Only load it if needed (exist same roleref itemid in table) if (restore_dbops::get_backup_ids_record($this->restoreid, 'roleref', $itemid)) { restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, null, $info); } } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } /** * Provide NULL decoding */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } } restore_questions_parser_processor.class.php 0000644 00000006774 15152170610 0015645 0 ustar 00 <?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-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the categories and questions (header info only) from the questions.xml file * to the backup_ids table storing the whole structure there for later processing. * Note: only "needed" categories are loaded (must have question_categoryref record in backup_ids) * Note: parentitemid will contain the category->contextid for categories * Note: parentitemid will contain the category->id for questions * * TODO: Complete phpdocs */ class restore_questions_parser_processor extends grouped_parser_processor { protected $restoreid; protected $lastcatid; public function __construct($restoreid) { $this->restoreid = $restoreid; $this->lastcatid = 0; parent::__construct(array()); // Set the paths we are interested on $this->add_path('/question_categories/question_category'); $this->add_path('/question_categories/question_category/questions/question'); } protected function dispatch_chunk($data) { // Prepare question_category record if ($data['path'] == '/question_categories/question_category') { $info = (object)$data['tags']; $itemname = 'question_category'; $itemid = $info->id; $parentitemid = $info->contextid; $this->lastcatid = $itemid; // Prepare question record } else if ($data['path'] == '/question_categories/question_category/questions/question') { $info = (object)$data['tags']; $itemname = 'question'; $itemid = $info->id; $parentitemid = $this->lastcatid; // Not question_category nor question, impossible. Throw exception. } else { throw new progressive_parser_exception('restore_questions_parser_processor_unexpected_path', $data['path']); } // Only load it if needed (exist same question_categoryref itemid in table) if (restore_dbops::get_backup_ids_record($this->restoreid, 'question_categoryref', $this->lastcatid)) { restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, $parentitemid, $info); } } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } /** * Provide NULL decoding */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�