From c6cc9726955dc3e968bd97dc6bc71c9ec3b998d5 Mon Sep 17 00:00:00 2001 From: Eloy Lafuente Date: Wed, 21 Apr 2010 09:26:06 +0000 Subject: [PATCH] MDL-21432 backup 2.0 - initial commit. controller --- backup/backup.class.php | 106 ++++++ backup/controller/backup_controller.class.php | 307 ++++++++++++++++++ .../controller/simpletest/testcontroller.php | 167 ++++++++++ 3 files changed, 580 insertions(+) create mode 100644 backup/backup.class.php create mode 100644 backup/controller/backup_controller.class.php create mode 100644 backup/controller/simpletest/testcontroller.php diff --git a/backup/backup.class.php b/backup/backup.class.php new file mode 100644 index 00000000000..2f932d35e93 --- /dev/null +++ b/backup/backup.class.php @@ -0,0 +1,106 @@ +. + +/** + * @package moodlecore + * @subpackage backup + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Abstract class defining common stuff to be used by the backup stuff + * + * This class defines various constants and methods that will be used + * by different classes, all related with the backup process. Just provides + * the top hierarchy of the backup controller/worker stuff. + * + * TODO: Finish phpdocs + */ +abstract class backup implements checksumable { + + // Backup type + const TYPE_1ACTIVITY = '1act'; + const TYPE_1SECTION = '1sec'; + const TYPE_1COURSE = '1cou'; + + // Backup format + const FORMAT_MOODLE = 'moodle2'; + const FORMAT_IMSCC = 'imscc'; + + // Interactive + const INTERACTIVE_YES = true; + const INTERACTIVE_NO = false; + + // Predefined modes (purposes) of the backup + const MODE_GENERAL = 10; + const MODE_IMPORT = 20; + const MODE_HUB = 30; + const MODE_SAMESITE = 40; + + // Execution mode + const EXECUTION_INMEDIATE = 1; + const EXECUTION_DELAYED = 2; + + // Status of the backup_controller + const STATUS_CREATED = 100; + const STATUS_PLANNED = 200; + const STATUS_CONFIGURED = 300; + const STATUS_SETTING_UI = 400; + const STATUS_AWAITING = 500; + const STATUS_EXECUTING = 600; + const STATUS_FINISHED_OK = 700; + const STATUS_FINISHED_ERR= 800; + + // Logging levels + const LOG_DEBUG = 50; + const LOG_INFO = 40; + const LOG_WARNING = 30; + const LOG_ERROR = 20; + const LOG_NONE = 10; + + // Some constants used to identify some helpfull processor variables + // (using negative numbers to avoid any collision posibility + // To be used when defining backup structures + const VAR_COURSEID = -1; // To reference id of course in a processor + const VAR_SECTIONID = -11; // To reference id of section in a processor + const VAR_ACTIVITYID = -21; // To reference id of activity in a processor + const VAR_MODID = -31; // To reference id of course_module in a processor + const VAR_MODNAME = -41; // To reference name of module in a processor + const VAR_BLOCKID = -51; // To reference id of block in a processor + const VAR_BLOCKNAME = -61; // To reference name of block in a processor + const VAR_CONTEXTID = -71; // To reference context id in a processor + const VAR_PARENTID = -81; // To reference the first parent->id in a backup structure + + // Used internally by the backup process + const VAR_BACKUPID = -1001; // To reference the backupid being processed + const VAR_BASEPATH = -1011; // To reference the dir where the file is generated + + // Version (to keep CFG->backup_version (and release) updated automatically) + const VERSION = 2010022500; + const RELEASE = '2.0 dev'; +} + +/* + * Exception class used by all the @backup stuff + */ +class backup_exception extends moodle_exception { + + public function __construct($errorcode, $a=NULL, $debuginfo=null) { + parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); + } +} diff --git a/backup/controller/backup_controller.class.php b/backup/controller/backup_controller.class.php new file mode 100644 index 00000000000..c3580cadb6a --- /dev/null +++ b/backup/controller/backup_controller.class.php @@ -0,0 +1,307 @@ +. + +/** + * @package moodlecore + * @subpackage backup-controller + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class implementing the controller of any backup process + * + * This final class is in charge of controlling all the backup architecture, for any + * type of backup. Based in type, format, interactivity and target, it stores the + * whole execution plan and settings that will be used later by the @backup_worker, + * applies all the defaults, performs all the security contraints and is in charge + * of handling the ui if necessary. Also logging strategy is defined here. + * + * Note the class is 100% neutral and usable for *any* backup. It just stores/requests + * all the needed information from other backup classes in order to have everything well + * structured in order to allow the @backup_worker classes to do their job. + * + * In other words, a mammoth class, but don't worry, practically everything is delegated/ + * aggregated!) + * + * TODO: Finish phpdocs + */ +class backup_controller extends backup implements loggable { + + protected $backupid; // Unique identificator for this backup + + protected $type; // Type of backup (activity, section, course) + protected $id; // Course/section/course_module id to backup + protected $courseid; // courseid where the id belongs to + protected $format; // Format of backup (moodle, imscc) + protected $interactive; // yes/no + protected $mode; // Purpose of the backup (default settings) + protected $userid; // user id executing the backup + + protected $status; // Current status of the controller (created, planned, configured...) + + protected $plan; // Backup execution plan + + protected $execution; // inmediate/delayed + protected $executiontime; // epoch time when we want the backup to be executed (requires cron to run) + + protected $destination; // Destination chain object (fs_moodle, fs_os, db, email...) + protected $logger; // Logging chain object (moodle, inline, fs, db, syslog) + + protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses + + public function __construct($type, $id, $format, $interactive, $mode, $userid){ + $this->type = $type; + $this->id = $id; + $this->courseid = backup_controller_dbops::get_courseid_from_type_id($this->type, $this->id); + $this->format = $format; + $this->interactive = $interactive; + $this->mode = $mode; + $this->userid = $userid; + + // Apply some defaults + $this->execution = backup::EXECUTION_INMEDIATE; + $this->executiontime = 0; + $this->checksum = ''; + + // Apply current backup version and release if necessary + backup_controller_dbops::apply_version_and_release(); + + // Check format and type are correct + backup_check::check_format_and_type($this->format, $this->type); + + // Check id is correct + backup_check::check_id($this->type, $this->id); + + // Check user is correct + backup_check::check_user($this->userid); + + // Calculate unique $backupid + $this->calculate_backupid(); + + // Default logger chain (based on interactive/execution) + $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid); + + // Instantiate the output_controller singleton and active it if interactive and inmediate + $oc = output_controller::get_instance(); + if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) { + $oc->set_active(true); + } + + $this->log('instantiating backup controller', backup::LOG_INFO, $this->backupid); + + // Default destination chain (based on type/mode/execution) + $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution); + + // Set initial status + $this->set_status(backup::STATUS_CREATED); + + // Load plan (based on type/format) + $this->load_plan(); + + // Apply all default settings (based on type/format/mode) + $this->apply_defaults(); + + // Perform all initial security checks and apply (2nd param) them to settings automatically + backup_check::check_security($this, true); + + // Set status based on interactivity + if ($this->interactive == backup::INTERACTIVE_YES) { + $this->set_status(backup::STATUS_SETTING_UI); + } else { + $this->set_status(backup::STATUS_AWAITING); + } + } + + public function show_ui() { + } + + public function process_ui() { + } + + public function finish_ui() { + if ($this->status != backup::STATUS_SETTING_UI) { + throw new backup_controller_exception('cannot_finish_ui_if_not_setting_ui'); + } + $this->set_status(backup::STATUS_AWAITING); + } + + public function process_ui_event() { + + // Perform security checks throwing exceptions (2nd param) if something is wrong + backup_check::check_security($this, false); + } + + public function set_status($status) { + $this->log('setting controller status to', backup::LOG_DEBUG, $status); + // TODO: Check it's a correct status + $this->status = $status; + // Ensure that, once set to backup::STATUS_AWAITING, controller is stored in DB + if ($status == backup::STATUS_AWAITING) { + $this->save_controller(); + $this->logger = self::load_controller($this->backupid)->logger; // wakeup loggers + } + } + + public function set_execution($execution, $executiontime = 0) { + $this->log('setting controller execution', backup::LOG_DEBUG); + // TODO: Check valid execution mode + // TODO: Check time in future + // TODO: Check time = 0 if inmediate + $this->execution = $execution; + $this->executiontime = $executiontime; + + // Default destination chain (based on type/mode/execution) + $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution); + + // Default logger chain (based on interactive/execution) + $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid); + } + +// checksumable interface methods + + public function calculate_checksum() { + // Reset current checksum to take it out from calculations! + $this->checksum = ''; + // Init checksum + $tempchecksum = md5('backupid-' . $this->backupid . + 'type-' . $this->type . + 'id-' . $this->id . + 'format-' . $this->format . + 'interactive-'. $this->interactive . + 'mode-' . $this->mode . + 'userid-' . $this->userid . + 'status-' . $this->status . + 'execution-' . $this->execution . + 'plan-' . backup_general_helper::array_checksum_recursive(array($this->plan)) . + 'destination-'. backup_general_helper::array_checksum_recursive(array($this->destination)) . + 'logger-' . backup_general_helper::array_checksum_recursive(array($this->logger))); + $this->log('calculating controller checksum', backup::LOG_DEBUG, $tempchecksum); + return $tempchecksum; + } + + public function is_checksum_correct($checksum) { + return $this->checksum === $checksum; + } + + public function get_backupid() { + return $this->backupid; + } + + public function get_type() { + return $this->type; + } + + public function get_id() { + return $this->id; + } + + public function get_courseid() { + return $this->courseid; + } + + public function get_format() { + return $this->format; + } + + public function get_interactive() { + return $this->interactive; + } + + public function get_mode() { + return $this->mode; + } + + public function get_userid() { + return $this->userid; + } + + public function get_status() { + return $this->status; + } + + public function get_execution() { + return $this->execution; + } + + public function get_executiontime() { + return $this->executiontime; + } + + public function get_plan() { + return $this->plan; + } + + public function get_logger() { + return $this->logger; + } + + public function execute_plan() { + return $this->plan->execute(); + } + + public function log($message, $level, $a = null, $depth = null, $display = false) { + backup_helper::log($message, $level, $a, $depth, $display, $this->logger); + } + + +// Protected API starts here + + protected function calculate_backupid() { + // Current epoch time + type + id + format + interactive + mode + userid + // should be unique enough. Add one random part at the end + $this->backupid = md5(time() . '-' . $this->type . '-' . $this->id . '-' . $this->format . '-' . + $this->interactive . '-' . $this->mode . '-' . $this->userid . '-' . + random_string(20)); + } + + public function save_controller() { + // Going to save controller to persistent storage, calculate checksum for later checks and save it + $this->log('saving controller to db', backup::LOG_DEBUG); + $this->checksum = $this->calculate_checksum(); + backup_controller_dbops::save_controller($this, $this->checksum); + } + + public static function load_controller($backupid) { + // Load controller from persistent storage + $controller = backup_controller_dbops::load_controller($backupid); + $controller->log('loading controller from db', backup::LOG_DEBUG); + return $controller; + } + + protected function load_plan() { + $this->log('loading controller plan', backup::LOG_DEBUG); + $this->plan = new backup_plan($this); + $this->plan->build(); // Build plan for this controller + $this->set_status(backup::STATUS_PLANNED); + } + + protected function apply_defaults() { + $this->log('applying plan defaults', backup::LOG_DEBUG); + $this->set_status(backup::STATUS_CONFIGURED); + } +} + +/* + * Exception class used by all the @backup_controller stuff + */ +class backup_controller_exception extends backup_exception { + + public function __construct($errorcode, $a=NULL, $debuginfo=null) { + parent::__construct($errorcode, $a, $debuginfo); + } +} diff --git a/backup/controller/simpletest/testcontroller.php b/backup/controller/simpletest/testcontroller.php new file mode 100644 index 00000000000..cf3add9c7c9 --- /dev/null +++ b/backup/controller/simpletest/testcontroller.php @@ -0,0 +1,167 @@ +. + +/** + * @package moodlecore + * @subpackage backup-tests + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Prevent direct access to this file +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); +} + +// Include all the needed stuff +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + +/* + * controller tests (all) + */ +class backup_controller_test extends UnitTestCase { + + public static $includecoverage = array('backup/controller'); + public static $excludecoverage = array('backup/controller/simpletest'); + + protected $moduleid; // course_modules id used for testing + protected $sectionid; // course_sections id used for testing + protected $courseid; // course id used for testing + protected $user; // user record used for testing + + protected $todelete; // array of records to be deleted after tests + protected $errorlogloggerlevel; // To store level $CFG->backup_error_log_logger_level + protected $outputindentedloggerlevel; // To store level $CFG->backup_output_indented_logger_level + protected $fileloggerlevel; // To store level $CFG->backup_file_logger_level + protected $databaseloggerlevel; // To store level $CFG->backup_database_logger_level + protected $fileloggerlevelextra;// To store level $CFG->backup_file_logger_level_extra + + function __construct() { + global $DB, $USER, $CFG; + + $this->moduleid = 0; + $this->sectionid = 0; + $this->courseid = 0; + $this->userid = $USER->id; + $this->todelete = array(); + + // Check we have (at least) one course_module + if ($coursemodule = $DB->get_record('course_modules', array(), '*', IGNORE_MULTIPLE)) { + $this->moduleid = $coursemodule->id; + $this->sectionid = $coursemodule->section; + $this->courseid = $coursemodule->course; + } + + // Avoid any logger to be created, we'll restore original settings on tearDown() + $this->errorlogloggerlevel = isset($CFG->backup_error_log_logger_level) ? $CFG->backup_error_log_logger_level : null; + $this->outputindentedloggerlevel = isset($CFG->backup_output_indented_logger_level) ? $CFG->backup_output_indented_logger_level : null; + $this->fileloggerlevel = isset($CFG->backup_file_logger_level) ? $CFG->backup_file_logger_level : null; + $this->databaseloggerlevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : null; + $this->fileloggerlevelextra = isset($CFG->backup_file_logger_level_extra) ? $CFG->backup_file_logger_level_extra : null; + + parent::__construct(); + } + + function skip() { + $this->skipIf(empty($this->moduleid), 'backup_controller_test require at least one course module to exist'); + $this->skipIf(empty($this->sectionid),'backup_controller_test require at least one course section to exist'); + $this->skipIf(empty($this->courseid), 'backup_controller_test require at least one course to exist'); + $this->skipIf(empty($this->userid),'backup_controller_test require one valid user to exist'); + } + + function setUp() { + global $CFG; + + // 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; + } + + function tearDown() { + global $DB, $CFG; + // Delete all the records marked to + foreach ($this->todelete as $todelete) { + $DB->delete_records($todelete[0], array('id' => $todelete[1])); + } + // Restore original file_logger levels + if ($this->errorlogloggerlevel !== null) { + $CFG->backup_error_log_logger_level = $this->errorlogloggerlevel; + } else { + unset($CFG->backup_error_log_logger_level); + } + if ($this->outputindentedloggerlevel !== null) { + $CFG->backup_output_indented_logger_level = $this->outputindentedloggerlevel; + } else { + unset($CFG->backup_output_indented_logger_level); + } + if ($this->fileloggerlevel !== null) { + $CFG->backup_file_logger_level = $this->fileloggerlevel; + } else { + unset($CFG->backup_file_logger_level); + } + if ($this->databaseloggerlevel !== null) { + $CFG->backup_database_logger_level = $this->databaseloggerlevel; + } else { + unset($CFG->backup_database_logger_level); + } + if ($this->fileloggerlevelextra !== null) { + $CFG->backup_file_logger_level_extra = $this->fileloggerlevelextra; + } else { + unset($CFG->backup_file_logger_level_extra); + } + } + + /* + * test base_setting class + */ + function test_backup_controller() { + + // Instantiate non interactive backup_controller + $bc = new mock_backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); + $this->assertTrue($bc instanceof backup_controller); + $this->assertEqual($bc->get_status(), backup::STATUS_AWAITING); + // Instantiate interactive backup_controller + $bc = new mock_backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_YES, backup::MODE_GENERAL, $this->userid); + $this->assertTrue($bc instanceof backup_controller); + $this->assertEqual($bc->get_status(), backup::STATUS_SETTING_UI); + $this->assertEqual(strlen($bc->get_backupid()), 32); // is one md5 + + // Save and load one backup controller to check everything is in place + $bc = new mock_backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); + $recid = $bc->save_controller(); + $newbc = mock_backup_controller::load_controller($bc->get_backupid()); + $this->assertTrue($newbc instanceof backup_controller); // This means checksum and load worked ok + + $this->todelete[] = array('backup_controllers', $recid); // mark this record for deletion + } +} + +/* + * helper extended @backup_controller class that makes some methods public for testing + */ +class mock_backup_controller extends backup_controller { + + public function save_controller() { + parent::save_controller(); + } +}