first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+439
View File
@@ -0,0 +1,439 @@
<?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.
*/
public static 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.
*/
public static 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 string $filearea The filearea to get backup data for.
* @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($filearea, $instanceid) {
global $DB;
$backups = [];
$table = 'backup_controllers';
$select = 'execution = :execution AND status < :status1 AND status > :status2 ' .
'AND operation = :operation';
$params = [
'execution' => backup::EXECUTION_DELAYED,
'status1' => backup::STATUS_FINISHED_ERR,
'status2' => backup::STATUS_NEED_PRECHECK,
'operation' => 'backup',
];
$sort = 'timecreated DESC';
$fields = 'id, backupid, status, timecreated';
if ($filearea == 'backup') {
// Get relevant backup ids based on user id.
$params['userid'] = $instanceid;
$select = 'userid = :userid AND ' . $select;
$records = $DB->get_records_select($table, $select, $params, $sort, $fields);
foreach ($records as $record) {
$bc = \backup_controller::load_controller($record->backupid);
// Get useful info to render async status in correct area.
list($hasusers, $isannon) = self::get_userdata_backup_settings($bc);
// Backup has users and is not anonymised -> don't show it in users backup file area.
if ($hasusers && !$isannon) {
continue;
}
$record->filename = $bc->get_plan()->get_setting('filename')->get_value();
$bc->destroy();
array_push($backups, $record);
}
} else {
if ($filearea == 'course' || $filearea == 'activity') {
// Get relevant backup ids based on context instance id.
$params['itemid'] = $instanceid;
$select = 'itemid = :itemid AND ' . $select;
$records = $DB->get_records_select($table, $select, $params, $sort, $fields);
foreach ($records as $record) {
$bc = \backup_controller::load_controller($record->backupid);
// Get useful info to render async status in correct area.
list($hasusers, $isannon) = self::get_userdata_backup_settings($bc);
// Backup has no user or is anonymised -> don't show it in course/activity backup file area.
if (!$hasusers || $isannon) {
continue;
}
$record->filename = $bc->get_plan()->get_setting('filename')->get_value();
$bc->destroy();
array_push($backups, $record);
}
}
}
return $backups;
}
/**
* Get the user data settings for backups.
*
* @param \backup_controller $backupcontroller The backup controller object.
* @return array Array of user data settings.
*/
public static function get_userdata_backup_settings(\backup_controller $backupcontroller): array {
$hasusers = (bool)$backupcontroller->get_plan()->get_setting('users')->get_value(); // Backup has users.
$isannon = (bool)$backupcontroller->get_plan()->get_setting('anonymize')->get_value(); // Backup is anonymised.
return [$hasusers, $isannon];
}
/**
* 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;
}
}
@@ -0,0 +1,192 @@
<?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.
}
}
@@ -0,0 +1,68 @@
<?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
}
}
@@ -0,0 +1,825 @@
<?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.
*
* This class is an abstract class with methods that can be called to aid the
* running of automated backups over cron.
*
* @package core
* @subpackage backup
* @copyright 2010 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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...');
\core\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.
set_config(
'backup_auto_emailpending',
$emailpending ? 1 : 0,
'backup',
);
} 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
*/
public 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_custom_data(array(
'courseid' => $backupcourse->courseid,
'adminid' => $admin->id
));
$taskid = \core\task\manager::queue_adhoc_task($asynctask);
// Get the queued tasks.
$queuedtasks = [];
if ($value = get_config('backup', 'backup_auto_adhoctasks')) {
$queuedtasks = explode(',', $value);
}
if ($taskid) {
$queuedtasks[] = (int) $taskid;
}
// Save the queued tasks.
set_config(
'backup_auto_adhoctasks',
implode(',', $queuedtasks),
'backup',
);
$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) {
global $DB;
/** @var \core\log\sql_reader[] */
$readers = get_log_manager()->get_readers('core\log\sql_reader');
// Exclude events defined by hook.
$hook = new \core_backup\hook\before_course_modified_check();
\core\di::get(\core\hook\manager::class)->dispatch($hook);
foreach ($readers as $readerpluginname => $reader) {
$params = [
'courseid' => $courseid,
'since' => $since,
];
$where = "courseid = :courseid and timecreated > :since and crud <> 'r'";
$excludeevents = $hook->get_excluded_events();
// Prevent logs of previous backups causing a false positive.
if ($readerpluginname !== 'logstore_legacy') {
$excludeevents[] = '\core\event\course_backup_created';
}
if ($excludeevents) {
[$notinsql, $notinparams] = $DB->get_in_or_equal($excludeevents, SQL_PARAMS_NAMED, 'eventname', false);
$where .= 'AND eventname ' . $notinsql;
$params = array_merge($params, $notinparams);
}
if ($reader->get_events_select_exists($where, $params)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,103 @@
<?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);
}
}
}
@@ -0,0 +1,338 @@
<?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;
}
}
+449
View File
@@ -0,0 +1,449 @@
<?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
*/
public static 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
*/
public static 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
*/
public static 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
*/
public static 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
*/
public static 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.
*/
public static 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
*/
public static 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 || $backupmode == backup::MODE_ASYNC) && (!$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');
}
/**
* Print the course reuse dropdown.
*
* @param string $current The current course reuse option where the header is modified
*/
public static function print_coursereuse_selector(string $current): void {
global $OUTPUT, $PAGE;
if ($coursereusenode = $PAGE->settingsnav->find('coursereuse', \navigation_node::TYPE_CONTAINER)) {
$menuarray = \core\navigation\views\secondary::create_menu_element([$coursereusenode]);
if (empty($menuarray)) {
return;
}
$coursereuse = get_string('coursereuse');
$activeurl = '';
if (isset($menuarray[0])) {
// Remove the "Course reuse" entry.
$result = array_search($coursereuse, $menuarray[0][$coursereuse]);
unset($menuarray[0][$coursereuse][$result]);
// Find the active node.
foreach ($menuarray[0] as $key => $value) {
$check = array_search($current, $value);
if ($check !== false) {
$activeurl = $check;
}
}
} else {
$result = array_search($coursereuse, $menuarray);
unset($menuarray[$result]);
$check = array_search(get_string($current), $menuarray);
if ($check !== false) {
$activeurl = $check;
}
}
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl);
$selectmenu->set_label(get_string('coursereusenavigationmenu'), ['class' => 'sr-only']);
$options = \html_writer::tag(
'div',
$OUTPUT->render_from_template('core/tertiary_navigation_selector', $selectmenu->export_for_template($OUTPUT)),
['class' => 'row pb-3']
);
echo \html_writer::tag(
'div',
$options,
['class' => 'container-fluid tertiary-navigation full-width-bottom-border', 'id' => 'tertiary-navigation']);
} else {
echo $OUTPUT->heading(get_string($current), 2, 'mb-3');
}
}
}
/*
* 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);
}
}
@@ -0,0 +1,57 @@
<?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
}
}
+387
View File
@@ -0,0 +1,387 @@
<?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);
}
}
+309
View File
@@ -0,0 +1,309 @@
<?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_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);
}
}
}
}
}
@@ -0,0 +1,128 @@
<?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);
}
}
@@ -0,0 +1,186 @@
<?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);
}
}
@@ -0,0 +1,210 @@
<?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
/** @var bool $urlencoded Whether to use urlencode() on the final URL. */
protected bool $urlencoded;
/**
* Constructor
*
* @param string $linkname How the link has been encoded in backup (CHOICEVIEWBYID, COURSEVIEWBYID...)
* @param string $urltemplate How the original URL looks like, with dollar placeholders
* @param array|string $mappings Which backup_ids mappings do we need to apply for replacing the placeholders
* @param bool $urlencoded Whether to use urlencode() on the final URL (defaults to false)
*/
public function __construct(string $linkname, string $urltemplate, $mappings, bool $urlencoded = false) {
// 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->urlencoded = $urlencoded;
$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);
}
if ($this->urlencoded) {
$toreplace = urlencode($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);
}
}
@@ -0,0 +1,64 @@
<?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
}
}
@@ -0,0 +1,249 @@
<?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 . '~';
}
}
@@ -0,0 +1,144 @@
<?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);
}
}
@@ -0,0 +1,66 @@
<?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;
}
}
@@ -0,0 +1,216 @@
<?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);
}
}
@@ -0,0 +1,109 @@
<?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 {
/** @var string XML path in the questions.xml backup file to question categories. */
protected const CATEGORY_PATH = '/question_categories/question_category';
/** @var string XML path in the questions.xml to question elements within question_category (Moodle 4.0+). */
protected const QUESTION_SUBPATH =
'/question_bank_entries/question_bank_entry/question_version/question_versions/questions/question';
/** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */
protected const LEGACY_QUESTION_SUBPATH = '/questions/question';
/** @var string identifies the current restore. */
protected string $restoreid;
/** @var int during the restore, this tracks the last category we saw. Any questions we see will be in here. */
protected int $lastcatid;
public function __construct($restoreid) {
$this->restoreid = $restoreid;
$this->lastcatid = 0;
parent::__construct();
// Set the paths we are interested on
$this->add_path(self::CATEGORY_PATH);
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH);
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH);
}
protected function dispatch_chunk($data) {
// Prepare question_category record
if ($data['path'] == self::CATEGORY_PATH) {
$info = (object)$data['tags'];
$itemname = 'question_category';
$itemid = $info->id;
$parentitemid = $info->contextid;
$this->lastcatid = $itemid;
// Prepare question record
} else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH ||
$data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) {
$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;
}
}
@@ -0,0 +1,75 @@
<?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;
}
}
@@ -0,0 +1,133 @@
<?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 = '&amp;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
}
}
@@ -0,0 +1,86 @@
<?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;
}
}
@@ -0,0 +1,236 @@
<?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(): void {
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(): void {
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);
$result = \async_helper::get_async_backups('course', $coursecontext->instanceid);
$this->assertEquals(1, count($result));
$this->assertEquals('backup.mbz', $result[0]->filename);
}
/**
* Tests getting the backup record.
*/
public function test_get_backup_record(): void {
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(): void {
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(): void {
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);
}
}
@@ -0,0 +1,83 @@
<?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(): void {
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, ' .
$httpsroot . '/user/index.php?id=123, ' .
$httpsroot . '/pluginfile.php/123 and ' .
urlencode($httpsroot . '/pluginfile.php/123') . '.'
);
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*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, ' .
$httproot . '/user/index.php?id=123, ' .
$httproot . '/pluginfile.php/123 and ' .
urlencode($httproot . '/pluginfile.php/123') . '.'
);
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded);
$CFG->wwwroot = $oldroot;
}
}
@@ -0,0 +1,155 @@
<?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(): void {
// 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);
}
}
@@ -0,0 +1,838 @@
<?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(): void {
$this->expectException(\moodle_exception::class);
\copy_helper::process_formdata(new \stdClass);
}
/**
* Test processing form data.
*
* @covers ::process_formdata
*/
public function test_process_formdata(): void {
$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(): void {
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(): void {
// 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());
\core\task\manager::adhoc_task_complete($task);
}
/**
* Test getting the current copies.
*
* @covers ::get_copies
*/
public function test_get_copies(): void {
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(): void {
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(): void {
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(): void {
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(): void {
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(): void {
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(): void {
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(): void {
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(): void {
// 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);
}
}
@@ -0,0 +1,520 @@
<?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(): void {
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(): void {
$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(): void {
$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(): void {
$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(): void {
$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);
}
}
+200
View File
@@ -0,0 +1,200 @@
<?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(): void {
// 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(): void {
// TODO: restore_decode_content tests
}
/**
* test restore_decode_processor class
*/
function test_restore_decode_processor(): void {
// 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;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?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(): void {
}
/*
* test backup_general_helper class
*/
function test_backup_general_helper(): void {
}
}
@@ -0,0 +1,70 @@
<?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(): void {
// 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(): void {
$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');
$tokens = $method->invoke($lr, $original);
$method = $class->getMethod('build_regexp');
$this->assertSame($expectation, $method->invoke($lr, $original, $tokens));
}
}
@@ -0,0 +1,137 @@
<?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&amp;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): void {
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));
}
}