Files
moodle/backup/util/helper/backup_helper.class.php
Matt Porritt d7e4481e98 MDL-35773 Backup: API should have option to not backup files
Allow both UI and automated backups to be created without
including files.  Instead include only file references.
This is essentially implementing "SAMESITE" to backup files
instead of only for import and export functionality.
A new backup setting to include files (defaults to yes)
has been included.

The restore process will also look for and attempt to
restore files from the trashdir as part of restoring
backups.  Additionally to support this process the
ammount of time files are kept in trashdir before they
are cleaned up via cron is also adjustable via admin
setting.
2019-07-26 11:59:56 +08:00

392 lines
16 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* @package moodlecore
* @subpackage backup-helper
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Base abstract class for all the helper classes providing various operations
*
* TODO: Finish phpdocs
*/
abstract class backup_helper {
/**
* Given one backupid, create all the needed dirs to have one backup temp dir available
*/
static public function check_and_create_backup_dir($backupid) {
$backupiddir = make_backup_temp_directory($backupid, false);
if (empty($backupiddir)) {
throw new backup_helper_exception('cannot_create_backup_temp_dir');
}
}
/**
* Given one backupid, ensure its temp dir is completely empty
*
* If supplied, progress object should be ready to receive indeterminate
* progress reports.
*
* @param string $backupid Backup id
* @param \core\progress\base $progress Optional progress reporting object
*/
static public function clear_backup_dir($backupid, \core\progress\base $progress = null) {
$backupiddir = make_backup_temp_directory($backupid, false);
if (!self::delete_dir_contents($backupiddir, '', $progress)) {
throw new backup_helper_exception('cannot_empty_backup_temp_dir');
}
return true;
}
/**
* Given one backupid, delete completely its temp dir
*
* If supplied, progress object should be ready to receive indeterminate
* progress reports.
*
* @param string $backupid Backup id
* @param \core\progress\base $progress Optional progress reporting object
*/
static public function delete_backup_dir($backupid, \core\progress\base $progress = null) {
$backupiddir = make_backup_temp_directory($backupid, false);
self::clear_backup_dir($backupid, $progress);
return rmdir($backupiddir);
}
/**
* Given one fullpath to directory, delete its contents recursively
* Copied originally from somewhere in the net.
* TODO: Modernise this
*
* If supplied, progress object should be ready to receive indeterminate
* progress reports.
*
* @param string $dir Directory to delete
* @param string $excludedir Exclude this directory
* @param \core\progress\base $progress Optional progress reporting object
*/
static public function delete_dir_contents($dir, $excludeddir='', \core\progress\base $progress = null) {
global $CFG;
if ($progress) {
$progress->progress();
}
if (!is_dir($dir)) {
// if we've been given a directory that doesn't exist yet, return true.
// this happens when we're trying to clear out a course that has only just
// been created.
return true;
}
$slash = "/";
// Create arrays to store files and directories
$dir_files = array();
$dir_subdirs = array();
// Make sure we can delete it
chmod($dir, $CFG->directorypermissions);
if ((($handle = opendir($dir))) == false) {
// The directory could not be opened
return false;
}
// Loop through all directory entries, and construct two temporary arrays containing files and sub directories
while (false !== ($entry = readdir($handle))) {
if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != "." && $entry != $excludeddir) {
$dir_subdirs[] = $dir. $slash .$entry;
} else if ($entry != ".." && $entry != "." && $entry != $excludeddir) {
$dir_files[] = $dir. $slash .$entry;
}
}
// Delete all files in the curent directory return false and halt if a file cannot be removed
for ($i=0; $i<count($dir_files); $i++) {
chmod($dir_files[$i], $CFG->directorypermissions);
if (((unlink($dir_files[$i]))) == false) {
return false;
}
}
// Empty sub directories and then remove the directory
for ($i=0; $i<count($dir_subdirs); $i++) {
chmod($dir_subdirs[$i], $CFG->directorypermissions);
if (self::delete_dir_contents($dir_subdirs[$i], '', $progress) == false) {
return false;
} else {
if (remove_dir($dir_subdirs[$i]) == false) {
return false;
}
}
}
// Close directory
closedir($handle);
// Success, every thing is gone return true
return true;
}
/**
* Delete all the temp dirs older than the time specified.
*
* If supplied, progress object should be ready to receive indeterminate
* progress reports.
*
* @param int $deletefrom Time to delete from
* @param \core\progress\base $progress Optional progress reporting object
*/
static public function delete_old_backup_dirs($deletefrom, \core\progress\base $progress = null) {
$status = true;
// Get files and directories in the backup temp dir without descend.
$backuptempdir = make_backup_temp_directory('');
$list = get_directory_list($backuptempdir, '', false, true, true);
foreach ($list as $file) {
$file_path = $backuptempdir . '/' . $file;
$moddate = filemtime($file_path);
if ($status && $moddate < $deletefrom) {
//If directory, recurse
if (is_dir($file_path)) {
// $file is really the backupid
$status = self::delete_backup_dir($file, $progress);
//If file
} else {
unlink($file_path);
}
}
}
if (!$status) {
throw new backup_helper_exception('problem_deleting_old_backup_temp_dirs');
}
}
/**
* This function will be invoked by any log() method in backup/restore, acting
* as a simple forwarder to the standard loggers but also, if the $display
* parameter is true, supporting translation via get_string() and sending to
* standard output.
*/
static public function log($message, $level, $a, $depth, $display, $logger) {
// Send to standard loggers
$logmessage = $message;
$options = empty($depth) ? array() : array('depth' => $depth);
if (!empty($a)) {
$logmessage = $logmessage . ' ' . implode(', ', (array)$a);
}
$logger->process($logmessage, $level, $options);
// If $display specified, send translated string to output_controller
if ($display) {
output_controller::get_instance()->output($message, 'backup', $a, $depth);
}
}
/**
* Given one backupid and the (FS) final generated file, perform its final storage
* into Moodle file storage. For stored files it returns the complete file_info object
*
* Note: the $filepath is deleted if the backup file is created successfully
*
* If you specify the progress monitor, this will start a new progress section
* to track progress in processing (in case this task takes a long time).
*
* @param int $backupid
* @param string $filepath zip file containing the backup
* @param \core\progress\base $progress Optional progress monitor
* @return stored_file if created, null otherwise
*
* @throws moodle_exception in case of any problems
*/
static public function store_backup_file($backupid, $filepath, \core\progress\base $progress = null) {
global $CFG;
// First of all, get some information from the backup_controller to help us decide
list($dinfo, $cinfo, $sinfo) = backup_controller_dbops::get_moodle_backup_information(
$backupid, $progress);
// Extract useful information to decide
$hasusers = (bool)$sinfo['users']->value; // Backup has users
$isannon = (bool)$sinfo['anonymize']->value; // Backup is anonymised
$filename = $sinfo['filename']->value; // Backup filename
$backupmode= $dinfo[0]->mode; // Backup mode backup::MODE_GENERAL/IMPORT/HUB
$backuptype= $dinfo[0]->type; // Backup type backup::TYPE_1ACTIVITY/SECTION/COURSE
$userid = $dinfo[0]->userid; // User->id executing the backup
$id = $dinfo[0]->id; // Id of activity/section/course (depends of type)
$courseid = $dinfo[0]->courseid; // Id of the course
$format = $dinfo[0]->format; // Type of backup file
// Quick hack. If for any reason, filename is blank, fix it here.
// TODO: This hack will be out once MDL-22142 - P26 gets fixed
if (empty($filename)) {
$filename = backup_plan_dbops::get_default_backup_filename('moodle2', $backuptype, $id, $hasusers, $isannon);
}
// Backups of type IMPORT aren't stored ever
if ($backupmode == backup::MODE_IMPORT) {
return null;
}
if (!is_readable($filepath)) {
// we have a problem if zip file does not exist
throw new coding_exception('backup_helper::store_backup_file() expects valid $filepath parameter');
}
// Calculate file storage options of id being backup
$ctxid = 0;
$filearea = '';
$component = '';
$itemid = 0;
switch ($backuptype) {
case backup::TYPE_1ACTIVITY:
$ctxid = context_module::instance($id)->id;
$component = 'backup';
$filearea = 'activity';
$itemid = 0;
break;
case backup::TYPE_1SECTION:
$ctxid = context_course::instance($courseid)->id;
$component = 'backup';
$filearea = 'section';
$itemid = $id;
break;
case backup::TYPE_1COURSE:
$ctxid = context_course::instance($courseid)->id;
$component = 'backup';
$filearea = 'course';
$itemid = 0;
break;
}
if ($backupmode == backup::MODE_AUTOMATED) {
// Automated backups have there own special area!
$filearea = 'automated';
// If we're keeping the backup only in a chosen path, just move it there now
// this saves copying from filepool to here later and filling trashdir.
$config = get_config('backup');
$dir = $config->backup_auto_destination;
if ($config->backup_auto_storage == 1 and $dir and is_dir($dir) and is_writable($dir)) {
$filedest = $dir.'/'
.backup_plan_dbops::get_default_backup_filename(
$format,
$backuptype,
$courseid,
$hasusers,
$isannon,
!$config->backup_shortname,
(bool)$config->backup_auto_files);
// first try to move the file, if it is not possible copy and delete instead
if (@rename($filepath, $filedest)) {
return null;
}
umask($CFG->umaskpermissions);
if (copy($filepath, $filedest)) {
@chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot
unlink($filepath);
return null;
} else {
$bc = backup_controller::load_controller($backupid);
$bc->log('Attempt to copy backup file to the specified directory using filesystem failed - ',
backup::LOG_WARNING, $dir);
$bc->destroy();
}
// bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system
}
}
// Backups of type HUB (by definition never have user info)
// are sent to user's "user_tohub" file area. The upload process
// will be responsible for cleaning that filearea once finished
if ($backupmode == backup::MODE_HUB) {
$ctxid = context_user::instance($userid)->id;
$component = 'user';
$filearea = 'tohub';
$itemid = 0;
}
// Backups without user info or with the anonymise functionality
// enabled are sent to user's "user_backup"
// file area. Maintenance of such area is responsibility of
// the user via corresponding file manager frontend
if ($backupmode == backup::MODE_GENERAL && (!$hasusers || $isannon)) {
$ctxid = context_user::instance($userid)->id;
$component = 'user';
$filearea = 'backup';
$itemid = 0;
}
// Let's send the file to file storage, everything already defined
$fs = get_file_storage();
$fr = array(
'contextid' => $ctxid,
'component' => $component,
'filearea' => $filearea,
'itemid' => $itemid,
'filepath' => '/',
'filename' => $filename,
'userid' => $userid,
'timecreated' => time(),
'timemodified'=> time());
// If file already exists, delete if before
// creating it again. This is BC behaviour - copy()
// overwrites by default
if ($fs->file_exists($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename'])) {
$pathnamehash = $fs->get_pathname_hash($fr['contextid'], $fr['component'], $fr['filearea'], $fr['itemid'], $fr['filepath'], $fr['filename']);
$sf = $fs->get_file_by_hash($pathnamehash);
$sf->delete();
}
$file = $fs->create_file_from_pathname($fr, $filepath);
unlink($filepath);
return $file;
}
/**
* This function simply marks one param to be considered as straight sql
* param, so it won't be searched in the structure tree nor converted at
* all. Useful for better integration of definition of sources in structure
* and DB stuff
*/
public static function is_sqlparam($value) {
return array('sqlparam' => $value);
}
/**
* This function returns one array of itemnames that are being handled by
* inforef.xml files. Used both by backup and restore
*/
public static function get_inforef_itemnames() {
return array('user', 'grouping', 'group', 'role', 'file', 'scale', 'outcome', 'grade_item', 'question_category');
}
}
/*
* Exception class used by all the @helper stuff
*/
class backup_helper_exception extends backup_exception {
public function __construct($errorcode, $a=NULL, $debuginfo=null) {
parent::__construct($errorcode, $a, $debuginfo);
}
}