File manager - Edit - /home/u466501803/domains/qurdis.my.id/public_html/backup.tar
Back
util/destinations/tests/destinations_test.php 0000644 00000002263 15215711721 0015650 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; /** * @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 */ final class destinations_test extends \basic_testcase { /** * test backup_destination class */ function test_backup_destination(): void { } /** * test backup_destination_osfs class */ function test_backup_destination_osfs(): void { } } util/interfaces/checksumable.class.php 0000644 00000003272 15215711721 0014115 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-interfaces * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Interface to apply to all the classes we want to calculate their checksum * * Each class being part of @backup_controller will implement this interface * in order to be able to calculate one objective and unique checksum for * the whole controller class. * * TODO: Finish phpdocs */ interface checksumable { /** * This function will return one unique and stable checksum for one instance * of the class implementing it. It's each implementation responsibility to * do it recursively if needed and use optional store (caching) of the checksum if * necessary/possible */ public function calculate_checksum(); /** * Given one checksum, returns if matches object's checksum (true) or no (false) */ public function is_checksum_correct($checksum); } util/interfaces/executable.class.php 0000644 00000002277 15215711721 0013614 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-interfaces * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Interface to apply to all the classes we want to be executable (plan/part/task) * * TODO: Finish phpdocs */ interface executable { /** * This function will perform all the actions necessary to achieve the execution * of the plan/part/task */ public function execute(); } util/interfaces/loggable.class.php 0000644 00000002746 15215711721 0013250 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-interfaces * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Interface to apply to all the classes we want to be able to write to logs * * Any class being part of one backup/restore and needing to senf informatio * to logs must implement this interface (and have access to the @logger * instantiated object) * * TODO: Finish phpdocs */ interface loggable { /** * This function will be responsible for handling the params, and to call * to the corresponding logger->process() once all modifications in params * have been performed */ public function log($message, $level, $a = null, $depth = null, $display = false); } util/interfaces/annotable.class.php 0000644 00000002514 15215711721 0013430 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-interfaces * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Interface to apply to all the classes we want to be annotable in the backup/restore process * * TODO: Finish phpdocs */ interface annotable { /** * This function implements the annotation of the current value associating it with $itemname */ public function annotate($backupid); /** * This function sets the $itemname to be used when annotating */ public function set_annotation_item($itemname); } util/interfaces/processable.class.php 0000644 00000002603 15215711721 0013766 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-interfaces * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Interface to apply to all the classes we want to be processable by one @base_processor * * Any class being part of one backup/restore structure must implement this interface * in order to be able to be processed by a given processor (visitor pattern) * * TODO: Finish phpdocs */ interface processable { /** * This function will call to the corresponding processor method in other to * make them perform the desired tasks. */ public function process($processor); } util/output/output_controller.class.php 0000644 00000005401 15215711721 0014503 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-output * @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 decides, based in environment/backup controller settings about * the best way to send information to output, independently of the process * and the loggers. Instantiated/configured by @backup_controller constructor * * Mainly used by backup_helper::log() (that receives all the log requests from * the rest of backup objects) to split messages both to loggers and to output. * * This class adopts the singleton pattern to be able to provide some persistency * and global access. */ class output_controller { private static $instance; // The unique instance of output_controller available along the request private $list; // progress_trace object we are going to use for output private $active; // To be able to stop output completely or active it again private function __construct() { // Private constructor if (defined('STDOUT')) { // text mode $this->list = new text_progress_trace(); } else { $this->list = new html_list_progress_trace(); } $this->active = false; // Somebody has to active me before outputing anything } public static function get_instance() { if (!isset(self::$instance)) { self::$instance = new output_controller(); } return self::$instance; } public function set_active($active) { if ($this->active && (bool)$active == false) { // Stopping, call finished() $this->list->finished(); } $this->active = (bool)$active; } public function output($message, $langfile, $a, $depth) { if ($this->active) { $stringkey = preg_replace('/\s/', '', $message); // String key is message without whitespace $message = get_string($stringkey, $langfile, $a); $this->list->output($message, $depth); } } } util/settings/base_setting.class.php 0000644 00000051105 15215711721 0013651 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-settings * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This abstract class defines one basic setting * * Each setting will be able to control its name, value (from a list), ui * representation (check box, drop down, text field...), visibility, status * (editable/locked...) and its hierarchy with other settings (using one * like-observer pattern. * * TODO: Finish phpdocs */ abstract class base_setting { // Some constants defining different ui representations for the setting const UI_NONE = 0; const UI_HTML_CHECKBOX = 10; const UI_HTML_RADIOBUTTON = 20; const UI_HTML_DROPDOWN = 30; const UI_HTML_TEXTFIELD = 40; // Type of validation to perform against the value (relaying in PARAM_XXX validations) const IS_BOOLEAN = 'bool'; const IS_INTEGER = 'int'; const IS_FILENAME= 'file'; const IS_PATH = 'path'; const IS_TEXT = 'text'; // Visible/hidden const VISIBLE = 1; const HIDDEN = 0; // Editable/locked (by different causes) const NOT_LOCKED = 3; const LOCKED_BY_CONFIG = 5; const LOCKED_BY_HIERARCHY = 7; const LOCKED_BY_PERMISSION = 9; // Type of change to inform dependencies const CHANGED_VALUE = 1; const CHANGED_VISIBILITY = 2; const CHANGED_STATUS = 3; protected $name; // name of the setting protected $value; // value of the setting protected $unlockedvalue; // Value to set after the setting is unlocked. protected $vtype; // type of value (setting_base::IS_BOOLEAN/setting_base::IS_INTEGER...) protected $visibility; // visibility of the setting (setting_base::VISIBLE/setting_base::HIDDEN) protected $status; // setting_base::NOT_LOCKED/setting_base::LOCKED_BY_PERMISSION... /** @var setting_dependency[] */ protected $dependencies = array(); // array of dependent (observer) objects (usually setting_base ones) protected $dependenton = array(); /** * The user interface for this setting * @var backup_setting_ui|backup_setting_ui_checkbox|backup_setting_ui_radio|backup_setting_ui_select|backup_setting_ui_text */ protected $uisetting; /** * An array that contains the identifier and component of a help string if one * has been set * @var array */ protected $help = array(); /** * Instantiates a setting object * * @param string $name Name of the setting * @param string $vtype Type of the setting, eg {@link self::IS_TEXT} * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, eg {@link self::VISIBLE} * @param int $status Status of the setting with regards to the locking, eg {@link self::NOT_LOCKED} */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { // Check vtype if ($vtype !== self::IS_BOOLEAN && $vtype !== self::IS_INTEGER && $vtype !== self::IS_FILENAME && $vtype !== self::IS_PATH && $vtype !== self::IS_TEXT) { throw new base_setting_exception('setting_invalid_type'); } // Validate value $value = $this->validate_value($vtype, $value); // Check visibility $visibility = $this->validate_visibility($visibility); // Check status $status = $this->validate_status($status); $this->name = $name; $this->vtype = $vtype; $this->value = $value; $this->visibility = $visibility; $this->status = $status; $this->unlockedvalue = $this->value; // Generate a default ui $this->uisetting = new base_setting_ui($this); } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // Before reseting anything, call destroy recursively foreach ($this->dependencies as $dependency) { $dependency->destroy(); } foreach ($this->dependenton as $dependenton) { $dependenton->destroy(); } if ($this->uisetting) { $this->uisetting->destroy(); } // Everything has been destroyed recursively, now we can reset safely $this->dependencies = array(); $this->dependenton = array(); $this->uisetting = null; } public function get_name() { return $this->name; } public function get_value() { return $this->value; } public function get_visibility() { return $this->visibility; } public function get_status() { return $this->status; } public function set_value($value) { // Validate value $value = $this->validate_value($this->vtype, $value); // Only can change value if setting is not locked if ($this->status != self::NOT_LOCKED) { switch ($this->status) { case self::LOCKED_BY_PERMISSION: throw new base_setting_exception('setting_locked_by_permission'); case self::LOCKED_BY_CONFIG: throw new base_setting_exception('setting_locked_by_config'); } } $oldvalue = $this->value; $this->value = $value; if ($value !== $oldvalue) { // Value has changed, let's inform dependencies $this->inform_dependencies(self::CHANGED_VALUE, $oldvalue); } } public function set_visibility($visibility) { $visibility = $this->validate_visibility($visibility); // If this setting is dependent on other settings first check that all // of those settings are visible if (count($this->dependenton) > 0 && $visibility == base_setting::VISIBLE) { foreach ($this->dependenton as $dependency) { if ($dependency->get_setting()->get_visibility() != base_setting::VISIBLE) { $visibility = base_setting::HIDDEN; break; } } } $oldvisibility = $this->visibility; $this->visibility = $visibility; if ($visibility !== $oldvisibility) { // Visibility has changed, let's inform dependencies $this->inform_dependencies(self::CHANGED_VISIBILITY, $oldvisibility); } } public function set_status($status) { $status = $this->validate_status($status); if (($this->status == base_setting::LOCKED_BY_PERMISSION || $this->status == base_setting::LOCKED_BY_CONFIG) && $status == base_setting::LOCKED_BY_HIERARCHY) { // Lock by permission or config can not be overriden by lock by hierarchy. return; } // If the setting is being unlocked first check whether an other settings // this setting is dependent on are locked. If they are then we still don't // want to lock this setting. if (count($this->dependenton) > 0 && $status == base_setting::NOT_LOCKED) { foreach ($this->dependenton as $dependency) { if ($dependency->is_locked()) { // It still needs to be locked $status = base_setting::LOCKED_BY_HIERARCHY; break; } } } $oldstatus = $this->status; $this->status = $status; if ($status !== $oldstatus) { // Status has changed, let's inform dependencies $this->inform_dependencies(self::CHANGED_STATUS, $oldstatus); if ($status == base_setting::NOT_LOCKED) { // When setting gets unlocked set it to the original value. $this->set_value($this->unlockedvalue); } } } /** * Gets an array of properties for all of the dependencies that will affect * this setting. * * This method returns an array rather than the dependencies in order to * minimise the memory footprint of for the potentially huge recursive * dependency structure that we may be dealing with. * * This method also ensures that all dependencies are transmuted to affect * the setting in question and that we don't provide any duplicates. * * @param string|null $settingname * @return array */ public function get_my_dependency_properties($settingname=null) { if ($settingname == null) { $settingname = $this->get_ui_name(); } $dependencies = array(); foreach ($this->dependenton as $dependenton) { $properties = $dependenton->get_moodleform_properties(); $properties['setting'] = $settingname; $dependencies[$properties['setting'].'-'.$properties['dependenton']] = $properties; $dependencies = array_merge($dependencies, $dependenton->get_setting()->get_my_dependency_properties($settingname)); } return $dependencies; } /** * Returns all of the dependencies that affect this setting. * e.g. settings this setting depends on. * * @return array Array of setting_dependency's */ public function get_settings_depended_on() { return $this->dependenton; } /** * Checks if there are other settings that are dependent on this setting * * @return bool True if there are other settings that are dependent on this setting */ public function has_dependent_settings() { return (count($this->dependencies)>0); } /** * Checks if this setting is dependent on any other settings * * @return bool True if this setting is dependent on any other settings */ public function has_dependencies_on_settings() { return (count($this->dependenton)>0); } /** * Sets the user interface for this setting * * @param base_setting_ui $ui */ public function set_ui(backup_setting_ui $ui) { $this->uisetting = $ui; } /** * Gets the user interface for this setting * * @return base_setting_ui */ public function get_ui() { return $this->uisetting; } /** * Adds a dependency where another setting depends on this setting. * @param setting_dependency $dependency */ public function register_dependency(setting_dependency $dependency) { if ($this->is_circular_reference($dependency->get_dependent_setting())) { $a = new stdclass(); $a->alreadydependent = $this->name; $a->main = $dependency->get_dependent_setting()->get_name(); throw new base_setting_exception('setting_circular_reference', $a); } $this->dependencies[$dependency->get_dependent_setting()->get_name()] = $dependency; $dependency->get_dependent_setting()->register_dependent_dependency($dependency); } /** * Adds a dependency where this setting is dependent on another. * * This should only be called internally once we are sure it is not cicrular. * * @param setting_dependency $dependency */ protected function register_dependent_dependency(setting_dependency $dependency) { $this->dependenton[$dependency->get_setting()->get_name()] = $dependency; } /** * Quick method to add a dependency to this setting. * * The dependency created is done so by inspecting this setting and the * setting that is passed in as the dependent setting. * * @param base_setting $dependentsetting * @param int $type One of setting_dependency::* * @param array $options */ public function add_dependency(base_setting $dependentsetting, $type=null, $options=array()) { if ($this->is_circular_reference($dependentsetting)) { $a = new stdclass(); $a->alreadydependent = $this->name; $a->main = $dependentsetting->get_name(); throw new base_setting_exception('setting_circular_reference', $a); } // Check the settings hasn't been already added if (array_key_exists($dependentsetting->get_name(), $this->dependencies)) { throw new base_setting_exception('setting_already_added'); } $options = (array)$options; if (!array_key_exists('defaultvalue', $options)) { $options['defaultvalue'] = false; } if ($type == null) { switch ($this->vtype) { case self::IS_BOOLEAN : if ($this->get_ui_type() == self::UI_HTML_CHECKBOX) { if ($this->value) { $type = setting_dependency::DISABLED_NOT_CHECKED; } else { $type = setting_dependency::DISABLED_CHECKED; } } else { if ($this->value) { $type = setting_dependency::DISABLED_FALSE; } else { $type = setting_dependency::DISABLED_TRUE; } } break; case self::IS_FILENAME : case self::IS_PATH : case self::IS_INTEGER : default : $type = setting_dependency::DISABLED_VALUE; break; } } switch ($type) { case setting_dependency::DISABLED_VALUE : if (!array_key_exists('value', $options)) { throw new base_setting_exception('dependency_needs_value'); } $dependency = new setting_dependency_disabledif_equals($this, $dependentsetting, $options['value'], $options['defaultvalue']); break; case setting_dependency::DISABLED_TRUE : $dependency = new setting_dependency_disabledif_equals($this, $dependentsetting, true, $options['defaultvalue']); break; case setting_dependency::DISABLED_FALSE : $dependency = new setting_dependency_disabledif_equals($this, $dependentsetting, false, $options['defaultvalue']); break; case setting_dependency::DISABLED_CHECKED : $dependency = new setting_dependency_disabledif_checked($this, $dependentsetting, $options['defaultvalue']); break; case setting_dependency::DISABLED_NOT_CHECKED : $dependency = new setting_dependency_disabledif_not_checked($this, $dependentsetting, $options['defaultvalue']); break; case setting_dependency::DISABLED_EMPTY : $dependency = new setting_dependency_disabledif_empty($this, $dependentsetting, $options['defaultvalue']); break; case setting_dependency::DISABLED_NOT_EMPTY : $dependency = new setting_dependency_disabledif_not_empty($this, $dependentsetting, $options['defaultvalue']); break; } $this->dependencies[$dependentsetting->get_name()] = $dependency; $dependency->get_dependent_setting()->register_dependent_dependency($dependency); } /** * Get the PARAM_XXXX validation to be applied to the setting * * @return string The PARAM_XXXX constant of null if the setting type is not defined */ public function get_param_validation() { switch ($this->vtype) { case self::IS_BOOLEAN: return PARAM_BOOL; case self::IS_INTEGER: return PARAM_INT; case self::IS_FILENAME: return PARAM_FILE; case self::IS_PATH: return PARAM_PATH; case self::IS_TEXT: return PARAM_TEXT; } return null; } // Protected API starts here protected function validate_value($vtype, $value) { if (is_null($value)) { // Nulls aren't validated return null; } $oldvalue = $value; switch ($vtype) { case self::IS_BOOLEAN: $value = clean_param($oldvalue, PARAM_BOOL); // Just clean break; case self::IS_INTEGER: $value = clean_param($oldvalue, PARAM_INT); if ($value != $oldvalue) { throw new base_setting_exception('setting_invalid_integer', $oldvalue); } break; case self::IS_FILENAME: $value = clean_param($oldvalue, PARAM_FILE); if ($value != $oldvalue) { throw new base_setting_exception('setting_invalid_filename', $oldvalue); } break; case self::IS_PATH: $value = clean_param($oldvalue, PARAM_PATH); if ($value != $oldvalue) { throw new base_setting_exception('setting_invalid_path', $oldvalue); } break; case self::IS_TEXT: $value = clean_param($oldvalue, PARAM_TEXT); if ($value != $oldvalue) { throw new base_setting_exception('setting_invalid_text', $oldvalue); } break; } return $value; } protected function validate_visibility($visibility) { if (is_null($visibility)) { $visibility = self::VISIBLE; } if ($visibility !== self::VISIBLE && $visibility !== self::HIDDEN) { throw new base_setting_exception('setting_invalid_visibility'); } return $visibility; } protected function validate_status($status) { if (is_null($status)) { $status = self::NOT_LOCKED; } if ($status !== self::NOT_LOCKED && $status !== self::LOCKED_BY_CONFIG && $status !== self::LOCKED_BY_PERMISSION && $status !== self::LOCKED_BY_HIERARCHY) { throw new base_setting_exception('setting_invalid_status', $status); } return $status; } protected function inform_dependencies($ctype, $oldv) { foreach ($this->dependencies as $dependency) { $dependency->process_change($ctype, $oldv); } } protected function is_circular_reference($obj) { // Get object dependencies recursively and check (by name) if $this is already there $dependencies = $obj->get_dependencies(); if (array_key_exists($this->name, $dependencies) || $obj == $this) { return true; } // Recurse the dependent settings one by one foreach ($dependencies as $dependency) { if ($dependency->get_dependent_setting()->is_circular_reference($obj)) { return true; } } return false; } public function get_dependencies() { return $this->dependencies; } public function get_ui_name() { return $this->uisetting->get_name(); } public function get_ui_type() { return $this->uisetting->get_type(); } /** * Sets a help string for this setting * * @param string $identifier * @param string $component */ public function set_help($identifier, $component='moodle') { $this->help = array($identifier, $component); } /** * Gets the help string params for this setting if it has been set * @return array|false An array (identifier, component) or false if not set */ public function get_help() { if ($this->has_help()) { return $this->help; } return false; } /** * Returns true if help has been set for this setting * @return cool */ public function has_help() { return (!empty($this->help)); } } /* * Exception class used by all the @setting_base stuff */ class base_setting_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/settings/root/root_backup_setting.class.php 0000644 00000002577 15215711721 0016243 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines root_backup_setting class * * @package core_backup * @category backup * @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(); /** * Abstract class containing all the common stuff for root backup settings */ abstract class root_backup_setting extends backup_setting { /** * {@inheritdoc} */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { $this->level = self::ROOT_LEVEL; parent::__construct($name, $vtype, $value, $visibility, $status); } } util/settings/tests/settings_test.php 0000644 00000057266 15215711721 0014155 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Setting 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 */ namespace core_backup; use activity_backup_setting; use backup_setting; use backup_setting_exception; use base_setting; use base_setting_exception; use course_backup_setting; use section_backup_setting; use setting_dependency; use setting_dependency_disabledif_empty; 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/settings/base_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php'); require_once($CFG->dirroot . '/backup/util/settings/root/root_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/activity/activity_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/section/section_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/course/course_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_setting.class.php'); /** * Setting 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 */ final class settings_test extends \basic_testcase { /** * test base_setting class */ public function test_base_setting(): void { // Instantiate base_setting and check everything $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN); $this->assertTrue($bs instanceof base_setting); $this->assertEquals($bs->get_name(), 'test'); $this->assertEquals($bs->get_vtype(), base_setting::IS_BOOLEAN); $this->assertTrue(is_null($bs->get_value())); $this->assertEquals($bs->get_visibility(), base_setting::VISIBLE); $this->assertEquals($bs->get_status(), base_setting::NOT_LOCKED); // Instantiate base_setting with explicit nulls $bs = new mock_base_setting('test', base_setting::IS_FILENAME, 'filename.txt', null, null); $this->assertEquals($bs->get_value() , 'filename.txt'); $this->assertEquals($bs->get_visibility(), base_setting::VISIBLE); $this->assertEquals($bs->get_status(), base_setting::NOT_LOCKED); // Instantiate base_setting and set value, visibility and status $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN); $bs->set_value(true); $this->assertNotEmpty($bs->get_value()); $bs->set_visibility(base_setting::HIDDEN); $this->assertEquals($bs->get_visibility(), base_setting::HIDDEN); $bs->set_status(base_setting::LOCKED_BY_HIERARCHY); $this->assertEquals($bs->get_status(), base_setting::LOCKED_BY_HIERARCHY); // Instantiate with wrong vtype try { $bs = new mock_base_setting('test', 'one_wrong_type'); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_invalid_type'); } // Instantiate with wrong integer value try { $bs = new mock_base_setting('test', base_setting::IS_INTEGER, 99.99); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_invalid_integer'); } // Instantiate with wrong filename value try { $bs = new mock_base_setting('test', base_setting::IS_FILENAME, '../../filename.txt'); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_invalid_filename'); } // Instantiate with wrong visibility try { $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN, null, 'one_wrong_visibility'); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_invalid_visibility'); } // Instantiate with wrong status try { $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN, null, null, 'one_wrong_status'); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_invalid_status'); } // Instantiate base_setting and try to set wrong ui_type // We need a custom error handler to catch the type hinting error // that should return incorrect_object_passed $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN); set_error_handler('\core_backup\backup_setting_error_handler', E_RECOVERABLE_ERROR); try { $bs->set_ui('one_wrong_ui_type', 'label', array(), array()); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'incorrect_object_passed'); } catch (\TypeError $e) { // On PHP7+ we get a TypeError raised, lets check we've the right error. $this->assertMatchesRegularExpression('/must be (of type|an instance of) backup_setting_ui/', $e->getMessage()); } restore_error_handler(); // Instantiate base_setting and try to set wrong ui_label // We need a custom error handler to catch the type hinting error // that should return incorrect_object_passed $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN); set_error_handler('\core_backup\backup_setting_error_handler', E_RECOVERABLE_ERROR); try { $bs->set_ui(base_setting::UI_HTML_CHECKBOX, 'one/wrong/label', array(), array()); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'incorrect_object_passed'); } catch (\TypeError $e) { // On PHP7+ we get a TypeError raised, lets check we've the right error. $this->assertMatchesRegularExpression('/must be (of type|an instance of) backup_setting_ui/', $e->getMessage()); } restore_error_handler(); // Try to change value of locked setting by permission $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN, null, null, base_setting::LOCKED_BY_PERMISSION); try { $bs->set_value(true); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_locked_by_permission'); } // Try to change value of locked setting by config $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN, null, null, base_setting::LOCKED_BY_CONFIG); try { $bs->set_value(true); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_locked_by_config'); } // Try to add same setting twice $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); $bs2 = new mock_base_setting('test2', base_setting::IS_INTEGER, null); $bs1->add_dependency($bs2, null, array('value'=>0)); try { $bs1->add_dependency($bs2); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_already_added'); } // Try to create one circular reference $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); try { $bs1->add_dependency($bs1); // self $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_circular_reference'); $this->assertTrue($e->a instanceof \stdClass); $this->assertEquals($e->a->main, 'test1'); $this->assertEquals($e->a->alreadydependent, 'test1'); } $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); $bs2 = new mock_base_setting('test2', base_setting::IS_INTEGER, null); $bs3 = new mock_base_setting('test3', base_setting::IS_INTEGER, null); $bs4 = new mock_base_setting('test4', base_setting::IS_INTEGER, null); $bs1->add_dependency($bs2, null, array('value'=>0)); $bs2->add_dependency($bs3, null, array('value'=>0)); $bs3->add_dependency($bs4, null, array('value'=>0)); try { $bs4->add_dependency($bs1, null, array('value'=>0)); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_circular_reference'); $this->assertTrue($e->a instanceof \stdClass); $this->assertEquals($e->a->main, 'test1'); $this->assertEquals($e->a->alreadydependent, 'test4'); } $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); $bs2 = new mock_base_setting('test2', base_setting::IS_INTEGER, null); $bs1->register_dependency(new setting_dependency_disabledif_empty($bs1, $bs2)); try { // $bs1 is already dependent on $bs2 so this should fail. $bs2->register_dependency(new setting_dependency_disabledif_empty($bs2, $bs1)); $this->assertTrue(false, 'base_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_setting_exception); $this->assertEquals($e->errorcode, 'setting_circular_reference'); $this->assertTrue($e->a instanceof \stdClass); $this->assertEquals($e->a->main, 'test1'); $this->assertEquals($e->a->alreadydependent, 'test2'); } // Create 3 settings and observe between them, last one must // automatically inherit all the settings defined in the main one $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); $bs2 = new mock_base_setting('test2', base_setting::IS_INTEGER, null); $bs3 = new mock_base_setting('test3', base_setting::IS_INTEGER, null); $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY); $bs2->add_dependency($bs3, setting_dependency::DISABLED_NOT_EMPTY); // Check values are spreaded ok $bs1->set_value(123); $this->assertEquals($bs1->get_value(), 123); $this->assertEquals($bs2->get_value(), $bs1->get_value()); $this->assertEquals($bs3->get_value(), $bs1->get_value()); // Add one more setting and set value again $bs4 = new mock_base_setting('test4', base_setting::IS_INTEGER, null); $bs2->add_dependency($bs4, setting_dependency::DISABLED_NOT_EMPTY); $bs2->set_value(321); // The above change should change $this->assertEquals($bs1->get_value(), 123); $this->assertEquals($bs2->get_value(), 321); $this->assertEquals($bs3->get_value(), 321); $this->assertEquals($bs4->get_value(), 321); // Check visibility is spreaded ok $bs1->set_visibility(base_setting::HIDDEN); $this->assertEquals($bs2->get_visibility(), $bs1->get_visibility()); $this->assertEquals($bs3->get_visibility(), $bs1->get_visibility()); // Check status is spreaded ok $bs1->set_status(base_setting::LOCKED_BY_HIERARCHY); $this->assertEquals($bs2->get_status(), $bs1->get_status()); $this->assertEquals($bs3->get_status(), $bs1->get_status()); // Create 3 settings and observe between them, put them in one array, // force serialize/deserialize to check the observable pattern continues // working after that $bs1 = new mock_base_setting('test1', base_setting::IS_INTEGER, null); $bs2 = new mock_base_setting('test2', base_setting::IS_INTEGER, null); $bs3 = new mock_base_setting('test3', base_setting::IS_INTEGER, null); $bs1->add_dependency($bs2, null, array('value'=>0)); $bs2->add_dependency($bs3, null, array('value'=>0)); // Serialize $arr = array($bs1, $bs2, $bs3); $ser = base64_encode(serialize($arr)); // Unserialize and copy to new objects $newarr = unserialize(base64_decode($ser)); $ubs1 = $newarr[0]; $ubs2 = $newarr[1]; $ubs3 = $newarr[2]; // Must continue being base settings $this->assertTrue($ubs1 instanceof base_setting); $this->assertTrue($ubs2 instanceof base_setting); $this->assertTrue($ubs3 instanceof base_setting); // Set parent setting $ubs1->set_value(1234); $ubs1->set_visibility(base_setting::HIDDEN); $ubs1->set_status(base_setting::LOCKED_BY_HIERARCHY); // Check changes have been spreaded $this->assertEquals($ubs2->get_visibility(), $ubs1->get_visibility()); $this->assertEquals($ubs3->get_visibility(), $ubs1->get_visibility()); $this->assertEquals($ubs2->get_status(), $ubs1->get_status()); $this->assertEquals($ubs3->get_status(), $ubs1->get_status()); } /** * Test that locked and unlocked states on dependent backup settings at the same level * correctly do not flow from the parent to the child setting when the setting is locked by permissions. */ public function test_dependency_empty_locked_by_permission_child_is_not_unlocked(): void { // Check dependencies are working ok. $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 2); $bs1->set_level(1); $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 2); $bs2->set_level(1); // Same level *must* work. $bs1->add_dependency($bs2, setting_dependency::DISABLED_EMPTY); $bs1->set_status(base_setting::LOCKED_BY_PERMISSION); $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status()); $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status()); $bs2->set_status(base_setting::LOCKED_BY_PERMISSION); $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs1->get_status()); // Unlocking the parent should NOT unlock the child. $bs1->set_status(base_setting::NOT_LOCKED); $this->assertEquals(base_setting::LOCKED_BY_PERMISSION, $bs2->get_status()); } /** * Test that locked and unlocked states on dependent backup settings at the same level * correctly do flow from the parent to the child setting when the setting is locked by config. */ public function test_dependency_not_empty_locked_by_config_parent_is_unlocked(): void { $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, 0); $bs1->set_level(1); $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, 0); $bs2->set_level(1); // Same level *must* work. $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY); $bs1->set_status(base_setting::LOCKED_BY_CONFIG); $this->assertEquals(base_setting::LOCKED_BY_HIERARCHY, $bs2->get_status()); $this->assertEquals(base_setting::LOCKED_BY_CONFIG, $bs1->get_status()); // Unlocking the parent should unlock the child. $bs1->set_status(base_setting::NOT_LOCKED); $this->assertEquals(base_setting::NOT_LOCKED, $bs2->get_status()); } /** * test backup_setting class */ public function test_backup_setting(): void { // Instantiate backup_setting class and set level $bs = new mock_backup_setting('test', base_setting::IS_INTEGER, null); $bs->set_level(1); $this->assertEquals($bs->get_level(), 1); // Instantiate backup setting class and try to add one non backup_setting dependency set_error_handler('\core_backup\backup_setting_error_handler', E_RECOVERABLE_ERROR); $bs = new mock_backup_setting('test', base_setting::IS_INTEGER, null); try { $bs->add_dependency(new \stdClass()); $this->assertTrue(false, 'backup_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_setting_exception); $this->assertEquals($e->errorcode, 'incorrect_object_passed'); } catch (\TypeError $e) { // On PHP7+ we get a TypeError raised, lets check we've the right error. $this->assertMatchesRegularExpression('/must be (an instance of|of type) base_setting/', $e->getMessage()); } restore_error_handler(); // Try to assing upper level dependency $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, null); $bs1->set_level(1); $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, null); $bs2->set_level(2); try { $bs2->add_dependency($bs1); $this->assertTrue(false, 'backup_setting_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_setting_exception); $this->assertEquals($e->errorcode, 'cannot_add_upper_level_dependency'); } // Check dependencies are working ok $bs1 = new mock_backup_setting('test1', base_setting::IS_INTEGER, null); $bs1->set_level(1); $bs2 = new mock_backup_setting('test2', base_setting::IS_INTEGER, null); $bs2->set_level(1); // Same level *must* work $bs1->add_dependency($bs2, setting_dependency::DISABLED_NOT_EMPTY); $bs1->set_value(123456); $this->assertEquals($bs2->get_value(), $bs1->get_value()); } /** * test activity_backup_setting class */ public function test_activity_backup_setting(): void { $bs = new mock_activity_backup_setting('test', base_setting::IS_INTEGER, null); $this->assertEquals($bs->get_level(), backup_setting::ACTIVITY_LEVEL); // Check checksum implementation is working $bs1 = new mock_activity_backup_setting('test', base_setting::IS_INTEGER, null); $bs1->set_value(123); $checksum = $bs1->calculate_checksum(); $this->assertNotEmpty($checksum); $this->assertTrue($bs1->is_checksum_correct($checksum)); } /** * test section_backup_setting class */ public function test_section_backup_setting(): void { $bs = new mock_section_backup_setting('test', base_setting::IS_INTEGER, null); $this->assertEquals($bs->get_level(), backup_setting::SECTION_LEVEL); // Check checksum implementation is working $bs1 = new mock_section_backup_setting('test', base_setting::IS_INTEGER, null); $bs1->set_value(123); $checksum = $bs1->calculate_checksum(); $this->assertNotEmpty($checksum); $this->assertTrue($bs1->is_checksum_correct($checksum)); } /** * test course_backup_setting class */ public function test_course_backup_setting(): void { $bs = new mock_course_backup_setting('test', base_setting::IS_INTEGER, null); $this->assertEquals($bs->get_level(), backup_setting::COURSE_LEVEL); // Check checksum implementation is working $bs1 = new mock_course_backup_setting('test', base_setting::IS_INTEGER, null); $bs1->set_value(123); $checksum = $bs1->calculate_checksum(); $this->assertNotEmpty($checksum); $this->assertTrue($bs1->is_checksum_correct($checksum)); } } /** * helper extended base_setting class that makes some methods public for testing */ class mock_base_setting extends base_setting { public function get_vtype() { return $this->vtype; } public function process_change($setting, $ctype, $oldv) { // Simply, inherit from the main object $this->set_value($setting->get_value()); $this->set_visibility($setting->get_visibility()); $this->set_status($setting->get_status()); } } /** * helper extended backup_setting class that makes some methods public for testing */ class mock_backup_setting extends backup_setting { public function set_level($level) { $this->level = $level; } public function process_change($setting, $ctype, $oldv) { // Simply, inherit from the main object $this->set_value($setting->get_value()); $this->set_visibility($setting->get_visibility()); $this->set_status($setting->get_status()); } } /** * helper extended activity_backup_setting class that makes some methods public for testing */ class mock_activity_backup_setting extends activity_backup_setting { public function process_change($setting, $ctype, $oldv) { // Do nothing } } /** * helper extended section_backup_setting class that makes some methods public for testing */ class mock_section_backup_setting extends section_backup_setting { public function process_change($setting, $ctype, $oldv) { // Do nothing } } /** * helper extended course_backup_setting class that makes some methods public for testing */ class mock_course_backup_setting extends course_backup_setting { public function process_change($setting, $ctype, $oldv) { // Do nothing } } /** * This error handler is used to convert errors to excpetions so that simepltest can * catch them. * * This is required in order to catch type hint mismatches that result in a error * being thrown. It should only ever be used to catch E_RECOVERABLE_ERROR's. * * It throws a backup_setting_exception with 'incorrect_object_passed' * * @param int $errno E_RECOVERABLE_ERROR * @param string $errstr * @param string $errfile * @param int $errline * @return null */ function backup_setting_error_handler($errno, $errstr, $errfile, $errline) { if ($errno !== E_RECOVERABLE_ERROR) { // Currently we only want to deal with type hinting errors return false; } throw new backup_setting_exception('incorrect_object_passed'); } util/settings/setting_dependency.class.php 0000644 00000042630 15215711721 0015060 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Generic abstract dependency class * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class setting_dependency { /** * Used to define the type of a dependency. * * Note with these that checked and true, and not checked and false are equal. * This is because the terminology differs but the resulting action is the same. * Reduces code! */ const DISABLED_VALUE = 0; const DISABLED_NOT_VALUE = 1; const DISABLED_TRUE = 2; const DISABLED_FALSE = 3; const DISABLED_CHECKED = 4; const DISABLED_NOT_CHECKED = 5; const DISABLED_EMPTY = 6; const DISABLED_NOT_EMPTY = 7; /** * The parent setting (primary) * @var base_setting */ protected $setting; /** * The dependent setting (secondary) * @var base_setting */ protected $dependentsetting; /** * The default setting * @var mixed */ protected $defaultvalue; /** * The last value the dependent setting had * @var mixed */ protected $lastvalue; /** * Creates the dependency object * @param base_setting $setting The parent setting or the primary setting if you prefer * @param base_setting $dependentsetting The dependent setting * @param mixed $defaultvalue The default value to assign if the dependency is unmet */ public function __construct(base_setting $setting, base_setting $dependentsetting, $defaultvalue = false) { $this->setting = $setting; $this->dependentsetting = $dependentsetting; $this->defaultvalue = $defaultvalue; $this->lastvalue = $dependentsetting->get_value(); } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset. $this->setting = null; $this->dependentsetting = null; } /** * Processes a change is setting called by the primary setting * @param int $changetype * @param mixed $oldvalue * @return bool */ final public function process_change($changetype, $oldvalue) { // Check the type of change requested. switch ($changetype) { // Process a status change. case base_setting::CHANGED_STATUS: return $this->process_status_change($oldvalue); // Process a visibility change. case base_setting::CHANGED_VISIBILITY: return $this->process_visibility_change($oldvalue); // Process a value change. case base_setting::CHANGED_VALUE: return $this->process_value_change($oldvalue); } // Throw an exception if we get this far. throw new backup_ui_exception('unknownchangetype'); } /** * Processes a visibility change * @param bool $oldvisibility * @return bool */ protected function process_visibility_change($oldvisibility) { // Store the current dependent settings visibility for comparison. $prevalue = $this->dependentsetting->get_visibility(); // Set it regardless of whether we need to. $this->dependentsetting->set_visibility($this->setting->get_visibility()); // Return true if it changed. return ($prevalue != $this->dependentsetting->get_visibility()); } /** * All dependencies must define how they would like to deal with a status change * @param int $oldstatus */ abstract protected function process_status_change($oldstatus); /** * All dependencies must define how they would like to process a value change */ abstract protected function process_value_change($oldvalue); /** * Gets the primary setting * @return backup_setting */ public function get_setting() { return $this->setting; } /** * Gets the dependent setting * @return backup_setting */ public function get_dependent_setting() { return $this->dependentsetting; } /** * This function enforces the dependency */ abstract public function enforce(); /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ abstract public function get_moodleform_properties(); /** * Returns true if the dependent setting is locked by this setting_dependency. * @return bool */ abstract public function is_locked(); } /** * A dependency that disables the secondary setting if the primary setting is * equal to the provided value * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_equals extends setting_dependency { /** * The value to compare to * @var mixed */ protected $value; /** * Creates the dependency * * @param base_setting $setting * @param base_setting $dependentsetting * @param mixed $value * @param mixed $defaultvalue */ public function __construct(base_setting $setting, base_setting $dependentsetting, $value, $defaultvalue = false) { parent::__construct($setting, $dependentsetting, $defaultvalue); $this->value = ($value) ? (string)$value : 0; } /** * Returns true if the dependent setting is locked by this setting_dependency. * @return bool */ public function is_locked() { // If the setting is locked or the dependent setting should be locked then return true. if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->evaluate_disabled_condition($this->setting->get_value())) { return true; } // Else the dependent setting is not locked by this setting_dependency. return false; } /** * Processes a value change in the primary setting * @param mixed $oldvalue * @return bool */ protected function process_value_change($oldvalue) { if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_PERMISSION || $this->dependentsetting->get_status() == base_setting::LOCKED_BY_CONFIG) { // When setting is locked by permission or config do not apply dependencies. return false; } $prevalue = $this->dependentsetting->get_value(); // If the setting is the desired value enact the dependency. $settingvalue = $this->setting->get_value(); if ($this->evaluate_disabled_condition($settingvalue)) { // The dependent setting needs to be locked by hierachy and set to the // default value. $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY); // For checkboxes the default value is false, but when the setting is // locked, the value should inherit from the parent setting. if ($this->defaultvalue === false) { $this->dependentsetting->set_value($settingvalue); } else { $this->dependentsetting->set_value($this->defaultvalue); } } else if ($this->dependentsetting->get_status() == base_setting::LOCKED_BY_HIERARCHY) { // We can unlock the dependent setting. $this->dependentsetting->set_status(base_setting::NOT_LOCKED); } // Return true if the value has changed for the dependent setting. return ($prevalue != $this->dependentsetting->get_value()); } /** * Processes a status change in the primary setting * @param mixed $oldstatus * @return bool */ protected function process_status_change($oldstatus) { // Store the dependent status. $prevalue = $this->dependentsetting->get_status(); // Store the current status. $currentstatus = $this->setting->get_status(); if ($currentstatus == base_setting::NOT_LOCKED) { if ($prevalue == base_setting::LOCKED_BY_HIERARCHY && !$this->evaluate_disabled_condition($this->setting->get_value())) { // Dependency has changes, is not fine, unlock the dependent setting. $this->dependentsetting->set_status(base_setting::NOT_LOCKED); } } else { // Make sure the dependent setting is also locked, in this case by hierarchy. $this->dependentsetting->set_status(base_setting::LOCKED_BY_HIERARCHY); } // Return true if the dependent setting has changed. return ($prevalue != $this->dependentsetting->get_status()); } /** * Enforces the dependency if required. * @return bool True if there were changes */ public function enforce() { // This will be set to true if ANYTHING changes. $changes = false; // First process any value changes. if ($this->process_value_change($this->setting->get_value())) { $changes = true; } // Second process any status changes. if ($this->process_status_change($this->setting->get_status())) { $changes = true; } // Finally process visibility changes. if ($this->process_visibility_change($this->setting->get_visibility())) { $changes = true; } return $changes; } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'eq', 'value' => $this->value ); } /** * Evaluate the current value of the setting and return true if the dependent setting should be locked or false. * This function should be abstract, but there will probably be existing sub-classes so we must provide a default * implementation. * @param mixed $value The value of the parent setting. * @return bool */ protected function evaluate_disabled_condition($value) { return $value == $this->value; } } /** * A dependency that disables the secondary setting if the primary setting is * not equal to the provided value * * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_not_equals extends setting_dependency_disabledif_equals { /** * Evaluate the current value of the setting and return true if the dependent setting should be locked or false. * @param mixed $value The value of the parent setting. * @return bool */ protected function evaluate_disabled_condition($value) { return $value != $this->value; } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'notequal', 'value' => $this->value ); } } /** * Disable if a value is in a list. */ class setting_dependency_disabledif_in_array extends setting_dependency_disabledif_equals { /** * Evaluate the current value of the setting and return true if the dependent setting should be locked or false. * @param mixed $value The value of the parent setting. * @return bool */ protected function evaluate_disabled_condition($value) { return in_array($value, $this->value); } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'eq', 'value' => $this->value ); } } /** * This class is here for backwards compatibility (terrible name). */ class setting_dependency_disabledif_equals2 extends setting_dependency_disabledif_in_array { } /** * A dependency that disables the secondary element if the primary element is * true or checked * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_checked extends setting_dependency_disabledif_equals { public function __construct(base_setting $setting, base_setting $dependentsetting, $defaultvalue = false) { parent::__construct($setting, $dependentsetting, true, $defaultvalue); $this->value = true; } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'checked' ); } } /** * A dependency that disables the secondary element if the primary element is * false or not checked * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_not_checked extends setting_dependency_disabledif_equals { public function __construct(base_setting $setting, base_setting $dependentsetting, $defaultvalue = false) { parent::__construct($setting, $dependentsetting, false, $defaultvalue); $this->value = false; } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'notchecked' ); } } /** * A dependency that disables the secondary setting if the value of the primary setting * is not empty. * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_not_empty extends setting_dependency_disabledif_equals { public function __construct(base_setting $setting, base_setting $dependentsetting, $defaultvalue = false) { parent::__construct($setting, $dependentsetting, false, $defaultvalue); $this->value = false; } /** * Evaluate the current value of the setting and return true if the dependent setting should be locked or false. * @param mixed $value The value of the parent setting. * @return bool */ protected function evaluate_disabled_condition($value) { return !empty($value); } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'notequal', 'value' => '' ); } } /** * A dependency that disables the secondary setting if the value of the primary setting * is empty. * * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_equals { public function __construct(base_setting $setting, base_setting $dependentsetting, $defaultvalue = false) { parent::__construct($setting, $dependentsetting, false, $defaultvalue); $this->value = false; } /** * Evaluate the current value of the setting and return true if the dependent setting should be locked or false. * @param mixed $value The value of the parent setting. * @return bool */ protected function evaluate_disabled_condition($value) { return empty($value); } /** * Returns an array of properties suitable to be used to define a moodleforms * disabled command * @return array */ public function get_moodleform_properties() { return array( 'setting' => $this->dependentsetting->get_ui_name(), 'dependenton' => $this->setting->get_ui_name(), 'condition' => 'notequal', 'value' => '' ); } } util/settings/section/section_backup_setting.class.php 0000644 00000002464 15215711721 0017400 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-settings * @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 containing all the common stuff for section backup settings * * TODO: Finish phpdocs */ abstract class section_backup_setting extends backup_setting { public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { $this->level = self::SECTION_LEVEL; parent::__construct($name, $vtype, $value, $visibility, $status); } } util/settings/activity/activity_backup_setting.class.php 0000644 00000002467 15215711721 0017763 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-settings * @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 containing all the common stuff for activity backup settings * * TODO: Finish phpdocs */ abstract class activity_backup_setting extends backup_setting { public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { $this->level = self::ACTIVITY_LEVEL; parent::__construct($name, $vtype, $value, $visibility, $status); } } util/settings/backup_setting.class.php 0000644 00000011165 15215711721 0014206 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_setting class * * @package core_backup * @category backup * @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(); /** * Parent class for all backup settings */ abstract class backup_setting extends base_setting implements checksumable { // Some constants defining levels of setting const ROOT_LEVEL = 1; const COURSE_LEVEL = 5; const SECTION_LEVEL = 9; const ACTIVITY_LEVEL = 13; /** @var int the subsection level. */ const SUBSECTION_LEVEL = 17; /** @var int the activity inside a subsection level. */ const SUBACTIVITY_LEVEL = 21; /** @var int Level of the setting, eg {@link self::ROOT_LEVEL} */ protected $level; /** * {@inheritdoc} */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); // Generate a default ui $this->uisetting = new backup_setting_ui_checkbox($this, $name); } /** * @return int Level of the setting, eg {@link self::ROOT_LEVEL} */ public function get_level() { return $this->level; } /** * Creates and sets a user interface for this setting given appropriate arguments * * @param int $type * @param string $label * @param array $attributes * @param array $options */ public function make_ui($type, $label, ?array $attributes = null, ?array $options = null) { $this->uisetting = backup_setting_ui::make($this, $type, $label, $attributes, $options); if (is_array($options) || is_object($options)) { $options = (array)$options; switch (get_class($this->uisetting)) { case 'backup_setting_ui_radio' : // text if (array_key_exists('text', $options)) { $this->uisetting->set_text($options['text']); } case 'backup_setting_ui_checkbox' : // value if (array_key_exists('value', $options)) { $this->uisetting->set_value($options['value']); } break; case 'backup_setting_ui_select' : // options if (array_key_exists('options', $options)) { $this->uisetting->set_values($options['options']); } break; } } } public function add_dependency(base_setting $dependentsetting, $type=setting_dependency::DISABLED_VALUE, $options=array()) { if (!($dependentsetting instanceof backup_setting)) { throw new backup_setting_exception('invalid_backup_setting_parameter'); } // Check the dependency level is >= current level if ($dependentsetting->get_level() < $this->level) { throw new backup_setting_exception('cannot_add_upper_level_dependency', [ $dependentsetting->get_level(), $dependentsetting->get_name(), $this->level, $this->get_name(), ]); } parent::add_dependency($dependentsetting, $type, $options); } // checksumable interface methods public function calculate_checksum() { // Checksum is a simple md5 hash of name, value, level // Not following dependencies at all. Each setting will // calculate its own checksum return md5($this->name . '-' . $this->value . '-' . $this->level); } public function is_checksum_correct($checksum) { return $this->calculate_checksum() === $checksum; } } /** * Exception class used by all the @backup_setting stuff */ class backup_setting_exception extends base_setting_exception { } util/settings/course/course_backup_setting.class.php 0000644 00000002461 15215711721 0017065 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-settings * @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 containing all the common stuff for course backup settings * * TODO: Finish phpdocs */ abstract class course_backup_setting extends backup_setting { public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { $this->level = self::COURSE_LEVEL; parent::__construct($name, $vtype, $value, $visibility, $status); } } util/loggers/base_logger.class.php 0000644 00000014142 15215711721 0013255 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @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 loggers to be used in backup/restore * * Any message passed will be processed by all the loggers in the defined chain * (note some implementations may be not strictly "loggers" but classes performing * other sort of tasks (avoiding browser/php timeouts, painters...). One simple 1-way * basic chain of commands/responsibility pattern. * * TODO: Finish phpdocs */ abstract class base_logger implements checksumable { protected $level; // minimum level of logging this logger must handle (valid level from @backup class) protected $showdate; // flag to decide if the logger must output the date (true) or no (false) protected $showlevel; // flag to decide if the logger must output the level (true) or no (false) protected $next; // next logger in the chain public function __construct($level, $showdate = false, $showlevel = false) { // TODO: check level is correct $this->level = $level; $this->showdate = $showdate; $this->showlevel = $showlevel; $this->next = null; } final public function set_next($next) { // TODO: Check is a base logger // TODO: Check next hasn't been set already // TODO: Avoid circular dependencies if ($this->is_circular_reference($next)) { $a = new stdclass(); $a->alreadyinchain = get_class($this); $a->main = get_class($next); throw new base_logger_exception('logger_circular_reference', $a); } $this->next = $next; } public function get_next() { return $this->next; } public function get_level() { return $this->level; } /** * Destroy (nullify) the chain of loggers references, also closing resources when needed. * * @since Moodle 3.1 */ final public function destroy() { // Recursively destroy the chain. if ($this->next !== null) { $this->next->destroy(); $this->next = null; } // And close every logger. $this->close(); } /** * Close any resource the logger may have open. * * @since Moodle 3.1 */ public function close() { // Nothing to do by default. Only loggers using resources (files, own connections...) need to override this. } // checksumable interface methods public function calculate_checksum() { // Checksum is a simple md5 hash of classname, level and // on each specialised logger, its own atrributes // Not following the chain at all. return md5(get_class($this) . '-' . $this->level); } public function is_checksum_correct($checksum) { return $this->calculate_checksum() === $checksum; } // Protected API starts here abstract protected function action($message, $level, $options = null); // To implement final public function process($message, $level, $options = null) { $result = true; if ($this->level != backup::LOG_NONE && $this->level >= $level && !(defined('BEHAT_TEST') && BEHAT_TEST)) { // Perform action conditionally. $result = $this->action($message, $level, $options); } if ($result === false) { // Something was wrong, stop the chain return $result; } if ($this->next !== null) { // The chain continues being processed $result = $this->next->process($message, $level, $options); } return $result; } protected function is_circular_reference($obj) { // Get object all nexts recursively and check if $this is already there $nexts = $obj->get_nexts(); if (array_key_exists($this->calculate_checksum(), $nexts) || $obj == $this) { return true; } return false; } protected function get_nexts() { $nexts = array(); if ($this->next !== null) { $nexts[$this->next->calculate_checksum()] = $this->next->calculate_checksum(); $nexts = array_merge($nexts, $this->next->get_nexts()); } return $nexts; } protected function get_datestr() { return userdate(time(), '%c'); } protected function get_levelstr($level) { $result = 'undefined'; switch ($level) { case backup::LOG_ERROR: $result = 'error'; break; case backup::LOG_WARNING: $result = 'warn'; break; case backup::LOG_INFO: $result = 'info'; break; case backup::LOG_DEBUG: $result = 'debug'; break; } return $result; } protected function get_prefix($level, $options) { $prefix = ''; if ($this->showdate) { $prefix .= '[' . $this->get_datestr() . '] '; } if ($this->showlevel) { $prefix .= '[' . $this->get_levelstr($level) . '] '; } return $prefix; } } /* * Exception class used by all the @base_logger stuff */ class base_logger_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/loggers/file_logger.class.php 0000644 00000007615 15215711721 0013271 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Logger implementation that sends indented messages (depth option) to one file * * TODO: Finish phpdocs */ class file_logger extends base_logger { protected $fullpath; // Full path to OS file where contents will be stored protected $fhandle; // File handle where all write operations happen public function __construct($level, $showdate = false, $showlevel = false, $fullpath = null) { if (empty($fullpath)) { throw new base_logger_exception('missing_fullpath_parameter', $fullpath); } if (!is_writable(dirname($fullpath))) { throw new base_logger_exception('file_not_writable', $fullpath); } // Open the OS file for writing (append) $this->fullpath = $fullpath; if ($level > backup::LOG_NONE) { // Only create the file if we are going to log something if (! $this->fhandle = fopen($this->fullpath, 'a')) { throw new base_logger_exception('error_opening_file', $fullpath); } } parent::__construct($level, $showdate, $showlevel); } public function __destruct() { if (is_resource($this->fhandle)) { // Blindy close the file handler (no exceptions in destruct). @fclose($this->fhandle); } } public function __sleep() { if (is_resource($this->fhandle)) { // Blindy close the file handler before serialization. @fclose($this->fhandle); $this->fhandle = null; } return array('level', 'showdate', 'showlevel', 'next', 'fullpath'); } public function __wakeup() { if ($this->level > backup::LOG_NONE) { // Only create the file if we are going to log something if (! $this->fhandle = fopen($this->fullpath, 'a')) { throw new base_logger_exception('error_opening_file', $this->fullpath); } } } /** * Close the logger resources (file handle) if still open. * * @since Moodle 3.1 */ public function close() { // Close the file handle if hasn't been closed already. if (is_resource($this->fhandle)) { fclose($this->fhandle); $this->fhandle = null; } } // Protected API starts here protected function action($message, $level, $options = null) { $prefix = $this->get_prefix($level, $options); $depth = isset($options['depth']) ? $options['depth'] : 0; // Depending of the type (extension of the file), format differently if (substr($this->fullpath, -5) !== '.html') { $content = $prefix . str_repeat(' ', $depth) . $message . PHP_EOL; } else { $content = $prefix . str_repeat(' ', $depth) . htmlentities($message, ENT_QUOTES, 'UTF-8') . '<br/>' . PHP_EOL; } if (!is_resource($this->fhandle) || (false === fwrite($this->fhandle, $content))) { throw new base_logger_exception('error_writing_file', $this->fullpath); } return true; } } util/loggers/core_backup_html_logger.class.php 0000644 00000003013 15215711721 0015637 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Logger that stores HTML log data in memory, ready for later display. * * @package core_backup * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_backup_html_logger extends base_logger { /** * @var string HTML output */ protected $html = ''; protected function action($message, $level, $options = null) { $prefix = $this->get_prefix($level, $options); $depth = isset($options['depth']) ? $options['depth'] : 0; $this->html .= $prefix . str_repeat(' ', $depth) . s($message) . '<br/>' . PHP_EOL; return true; } /** * Gets the full HTML content of the log. * * @return string HTML content of log */ public function get_html() { return $this->html; } } util/loggers/output_indented_logger.class.php 0000644 00000003216 15215711721 0015555 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Logger implementation that sends indented messages (depth option) to output * * TODO: Finish phpdocs */ class output_indented_logger extends base_logger { // Protected API starts here protected function action($message, $level, $options = null) { $prefix = $this->get_prefix($level, $options); $depth = isset($options['depth']) ? $options['depth'] : 0; // Depending of running from browser/command line, format differently if (defined('STDOUT')) { echo $prefix . str_repeat(' ', $depth) . $message . PHP_EOL; } else { echo $prefix . str_repeat(' ', $depth) . htmlentities($message, ENT_QUOTES, 'UTF-8') . '<br/>' . PHP_EOL; } flush(); return true; } } util/loggers/database_logger.class.php 0000644 00000005623 15215711721 0014113 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Logger implementation that sends messages to database * * TODO: Finish phpdocs */ class database_logger extends base_logger { protected $datecol; // Name of the field where the timestamp will be stored protected $levelcol; // Name of the field where the level of the message will be stored protected $messagecol; // Name of the field where the message will be stored protected $logtable; // Table, without prefix where information must be logged protected $columns; // Array of columns and values to set in all actions logged // Protected API starts here public function __construct($level, $datecol = false, $levelcol = false, $messagecol = null, $logtable = null, $columns = null) { // TODO check $datecol exists // TODO check $levelcol exists // TODO check $logtable exists // TODO check $messagecol exists // TODO check all $columns exist $this->datecol = $datecol; $this->levelcol = $levelcol; $this->messagecol = $messagecol; $this->logtable = $logtable; $this->columns = $columns; parent::__construct($level, (bool)$datecol, (bool)$levelcol); } protected function action($message, $level, $options = null) { $columns = $this->columns; if ($this->datecol) { $columns[$this->datecol] = time(); } if ($this->levelcol) { $columns[$this->levelcol] = $level; } $columns[$this->messagecol] = clean_param($message, PARAM_NOTAGS); return $this->insert_log_record($this->logtable, $columns); } protected function insert_log_record($table, $columns) { // TODO: Allow to use an alternate connection (created in constructor) // based in some CFG->backup_database_logger_newconn = true in order // to preserve DB logs if the whole backup/restore transaction is // rollback global $DB; return $DB->insert_record($table, $columns, false); // Don't return inserted id } } util/loggers/tests/logger_test.php 0000644 00000041576 15215711721 0013373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Logger 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 */ namespace core_backup; use backup; use base_logger; use base_logger_exception; use database_logger; use error_log_logger; use file_logger; use output_indented_logger; use output_text_logger; 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/loggers/base_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_text_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); /** * Logger 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 */ final class logger_test extends \basic_testcase { /** * test base_logger class */ function test_base_logger(): void { // Test logger with simple action (message * level) $lo = new mock_base_logger1(backup::LOG_ERROR); $msg = 13; $this->assertEquals($lo->process($msg, backup::LOG_ERROR), $msg * backup::LOG_ERROR); // With lowest level must return true $lo = new mock_base_logger1(backup::LOG_ERROR); $msg = 13; $this->assertTrue($lo->process($msg, backup::LOG_DEBUG)); // Chain 2 loggers, we must get as result the result of the inner one $lo1 = new mock_base_logger1(backup::LOG_ERROR); $lo2 = new mock_base_logger2(backup::LOG_ERROR); $lo1->set_next($lo2); $msg = 13; $this->assertEquals($lo1->process($msg, backup::LOG_ERROR), $msg + backup::LOG_ERROR); // Try circular reference $lo1 = new mock_base_logger1(backup::LOG_ERROR); try { $lo1->set_next($lo1); //self $this->assertTrue(false, 'base_logger_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_logger_exception); $this->assertEquals($e->errorcode, 'logger_circular_reference'); $this->assertTrue($e->a instanceof \stdClass); $this->assertEquals($e->a->main, get_class($lo1)); $this->assertEquals($e->a->alreadyinchain, get_class($lo1)); } $lo1 = new mock_base_logger1(backup::LOG_ERROR); $lo2 = new mock_base_logger2(backup::LOG_ERROR); $lo3 = new mock_base_logger3(backup::LOG_ERROR); $lo1->set_next($lo2); $lo2->set_next($lo3); try { $lo3->set_next($lo1); $this->assertTrue(false, 'base_logger_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_logger_exception); $this->assertEquals($e->errorcode, 'logger_circular_reference'); $this->assertTrue($e->a instanceof \stdClass); $this->assertEquals($e->a->main, get_class($lo1)); $this->assertEquals($e->a->alreadyinchain, get_class($lo3)); } // Test stopper logger $lo1 = new mock_base_logger1(backup::LOG_ERROR); $lo2 = new mock_base_logger2(backup::LOG_ERROR); $lo3 = new mock_base_logger3(backup::LOG_ERROR); $lo1->set_next($lo2); $lo2->set_next($lo3); $msg = 13; $this->assertFalse($lo1->process($msg, backup::LOG_ERROR)); // Test checksum correct $lo1 = new mock_base_logger1(backup::LOG_ERROR); $lo1->is_checksum_correct(get_class($lo1) . '-' . backup::LOG_ERROR); // Test get_levelstr() $lo1 = new mock_base_logger1(backup::LOG_ERROR); $this->assertEquals($lo1->get_levelstr(backup::LOG_NONE), 'undefined'); $this->assertEquals($lo1->get_levelstr(backup::LOG_ERROR), 'error'); $this->assertEquals($lo1->get_levelstr(backup::LOG_WARNING), 'warn'); $this->assertEquals($lo1->get_levelstr(backup::LOG_INFO), 'info'); $this->assertEquals($lo1->get_levelstr(backup::LOG_DEBUG), 'debug'); // Test destroy. $lo1 = new mock_base_logger1(backup::LOG_ERROR); $lo2 = new mock_base_logger2(backup::LOG_ERROR); $lo1->set_next($lo2); $this->assertInstanceOf('base_logger', $lo1->get_next()); $this->assertNull($lo2->get_next()); $lo1->destroy(); $this->assertNull($lo1->get_next()); $this->assertNull($lo2->get_next()); } /** * test error_log_logger class */ function test_error_log_logger(): void { // Not much really to test, just instantiate and execute, should return true $lo = new error_log_logger(backup::LOG_ERROR); $this->assertTrue($lo instanceof error_log_logger); $message = 'This log exists because you have run Moodle unit tests: Ignore it'; $result = $lo->process($message, backup::LOG_ERROR); $this->assertTrue($result); } /** * test output_text_logger class */ function test_output_text_logger(): void { // Instantiate without date nor level output $lo = new output_text_logger(backup::LOG_ERROR); $this->assertTrue($lo instanceof output_text_logger); $message = 'testing output_text_logger'; ob_start(); // Capture output $result = $lo->process($message, backup::LOG_ERROR); $contents = ob_get_contents(); ob_end_clean(); // End capture and discard $this->assertTrue($result); $this->assertTrue(strpos($contents, $message) !== false); // Instantiate with date and level output $lo = new output_text_logger(backup::LOG_ERROR, true, true); $this->assertTrue($lo instanceof output_text_logger); $message = 'testing output_text_logger'; ob_start(); // Capture output $result = $lo->process($message, backup::LOG_ERROR); $contents = ob_get_contents(); ob_end_clean(); // End capture and discard $this->assertTrue($result); $this->assertTrue(strpos($contents,'[') === 0); $this->assertTrue(strpos($contents,'[error]') !== false); $this->assertTrue(strpos($contents, $message) !== false); $this->assertTrue(substr_count($contents , '] ') >= 2); } /** * test output_indented_logger class */ function test_output_indented_logger(): void { // Instantiate without date nor level output $options = array('depth' => 2); $lo = new output_indented_logger(backup::LOG_ERROR); $this->assertTrue($lo instanceof output_indented_logger); $message = 'testing output_indented_logger'; ob_start(); // Capture output $result = $lo->process($message, backup::LOG_ERROR, $options); $contents = ob_get_contents(); ob_end_clean(); // End capture and discard $this->assertTrue($result); if (defined('STDOUT')) { $check = ' '; } else { $check = ' '; } $this->assertTrue(strpos($contents, str_repeat($check, $options['depth']) . $message) !== false); // Instantiate with date and level output $options = array('depth' => 3); $lo = new output_indented_logger(backup::LOG_ERROR, true, true); $this->assertTrue($lo instanceof output_indented_logger); $message = 'testing output_indented_logger'; ob_start(); // Capture output $result = $lo->process($message, backup::LOG_ERROR, $options); $contents = ob_get_contents(); ob_end_clean(); // End capture and discard $this->assertTrue($result); $this->assertTrue(strpos($contents,'[') === 0); $this->assertTrue(strpos($contents,'[error]') !== false); $this->assertTrue(strpos($contents, $message) !== false); $this->assertTrue(substr_count($contents , '] ') >= 2); if (defined('STDOUT')) { $check = ' '; } else { $check = ' '; } $this->assertTrue(strpos($contents, str_repeat($check, $options['depth']) . $message) !== false); } /** * test database_logger class */ function test_database_logger(): void { // Instantiate with date and level output (and with specs from the global moodle "log" table so checks will pass $now = time(); $datecol = 'time'; $levelcol = 'action'; $messagecol = 'info'; $logtable = 'log'; $columns = array('url' => 'http://127.0.0.1'); $loglevel = backup::LOG_ERROR; $lo = new mock_database_logger(backup::LOG_ERROR, $datecol, $levelcol, $messagecol, $logtable, $columns); $this->assertTrue($lo instanceof database_logger); $message = 'testing database_logger'; $result = $lo->process($message, $loglevel); // Check everything is ready to be inserted to DB $this->assertEquals($result['table'], $logtable); $this->assertTrue($result['columns'][$datecol] >= $now); $this->assertEquals($result['columns'][$levelcol], $loglevel); $this->assertEquals($result['columns'][$messagecol], $message); $this->assertEquals($result['columns']['url'], $columns['url']); } /** * test file_logger class */ function test_file_logger(): void { global $CFG; $file = $CFG->tempdir . '/test/test_file_logger.txt'; // Remove the test dir and any content @remove_dir(dirname($file)); // Recreate test dir if (!check_dir_exists(dirname($file), true, true)) { throw new \moodle_exception('error_creating_temp_dir', 'error', dirname($file)); } // Instantiate with date and level output, and also use the depth option $options = array('depth' => 3); $lo1 = new file_logger(backup::LOG_ERROR, true, true, $file); $this->assertTrue($lo1 instanceof file_logger); $message1 = 'testing file_logger'; $result = $lo1->process($message1, backup::LOG_ERROR, $options); $this->assertTrue($result); // Another file_logger is going towrite there too without closing $options = array(); $lo2 = new file_logger(backup::LOG_WARNING, true, true, $file); $this->assertTrue($lo2 instanceof file_logger); $message2 = 'testing file_logger2'; $result = $lo2->process($message2, backup::LOG_WARNING, $options); $this->assertTrue($result); // Destroy loggers. $lo1->destroy(); $lo2->destroy(); // Load file results to analyze them $fcontents = file_get_contents($file); $acontents = explode(PHP_EOL, $fcontents); // Split by line $this->assertTrue(strpos($acontents[0], $message1) !== false); $this->assertTrue(strpos($acontents[0], '[error]') !== false); $this->assertTrue(strpos($acontents[0], ' ') !== false); $this->assertTrue(substr_count($acontents[0] , '] ') >= 2); $this->assertTrue(strpos($acontents[1], $message2) !== false); $this->assertTrue(strpos($acontents[1], '[warn]') !== false); $this->assertTrue(strpos($acontents[1], ' ') === false); $this->assertTrue(substr_count($acontents[1] , '] ') >= 2); unlink($file); // delete file // Try one html file check_dir_exists($CFG->tempdir . '/test'); $file = $CFG->tempdir . '/test/test_file_logger.html'; $options = array('depth' => 1); $lo = new file_logger(backup::LOG_ERROR, true, true, $file); $this->assertTrue($lo instanceof file_logger); $this->assertTrue(file_exists($file)); $message = 'testing file_logger'; $result = $lo->process($message, backup::LOG_ERROR, $options); $lo->close(); // Closes logger. // Get file contents and inspect them $fcontents = file_get_contents($file); $this->assertTrue($result); $this->assertTrue(strpos($fcontents, $message) !== false); $this->assertTrue(strpos($fcontents, '[error]') !== false); $this->assertTrue(strpos($fcontents, ' ') !== false); $this->assertTrue(substr_count($fcontents , '] ') >= 2); unlink($file); // delete file // Instantiate, write something, force deletion, try to write again check_dir_exists($CFG->tempdir . '/test'); $file = $CFG->tempdir . '/test/test_file_logger.html'; $lo = new mock_file_logger(backup::LOG_ERROR, true, true, $file); $this->assertTrue(file_exists($file)); $message = 'testing file_logger'; $result = $lo->process($message, backup::LOG_ERROR); $lo->close(); $this->assertNull($lo->get_fhandle()); try { $result = @$lo->process($message, backup::LOG_ERROR); // Try to write again $this->assertTrue(false, 'base_logger_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_logger_exception); $this->assertEquals($e->errorcode, 'error_writing_file'); } // Instantiate without file try { $lo = new file_logger(backup::LOG_WARNING, true, true, ''); $this->assertTrue(false, 'base_logger_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_logger_exception); $this->assertEquals($e->errorcode, 'missing_fullpath_parameter'); } // Instantiate in (near) impossible path $file = $CFG->tempdir . '/test_azby/test_file_logger.txt'; try { $lo = new file_logger(backup::LOG_WARNING, true, true, $file); $this->assertTrue(false, 'base_logger_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_logger_exception); $this->assertEquals($e->errorcode, 'file_not_writable'); $this->assertEquals($e->a, $file); } // Instantiate one file logger with level = backup::LOG_NONE $file = $CFG->tempdir . '/test/test_file_logger.txt'; $lo = new file_logger(backup::LOG_NONE, true, true, $file); $this->assertTrue($lo instanceof file_logger); $this->assertFalse(file_exists($file)); $lo->close(); // Remove the test dir and any content @remove_dir(dirname($file)); } } /** * helper extended base_logger class that implements some methods for testing * Simply return the product of message and level */ class mock_base_logger1 extends base_logger { protected function action($message, $level, $options = null) { return $message * $level; // Simply return that, for testing } public function get_levelstr($level) { return parent::get_levelstr($level); } } /** * helper extended base_logger class that implements some methods for testing * Simply return the sum of message and level */ class mock_base_logger2 extends base_logger { protected function action($message, $level, $options = null) { return $message + $level; // Simply return that, for testing } } /** * helper extended base_logger class that implements some methods for testing * Simply return 8 */ class mock_base_logger3 extends base_logger { protected function action($message, $level, $options = null) { return false; // Simply return false, for testing stopper } } /** * helper extended database_logger class that implements some methods for testing * Returns the complete info that normally will be used by insert record calls */ class mock_database_logger extends database_logger { protected function insert_log_record($table, $columns) { return array('table' => $table, 'columns' => $columns); } } /** * helper extended file_logger class that implements some methods for testing * Returns the, usually protected, handle */ class mock_file_logger extends file_logger { function get_fhandle() { return $this->fhandle; } } util/loggers/error_log_logger.class.php 0000644 00000002465 15215711721 0014342 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Logger implementation that sends messages to error_log() * * TODO: Finish phpdocs */ class error_log_logger extends base_logger { // Protected API starts here protected function action($message, $level, $options = null) { if (PHPUNIT_TEST) { // no logging from PHPUnit, it is admins fault if it does not work!!! return true; } return error_log($message); } } util/loggers/output_text_logger.class.php 0000644 00000002764 15215711721 0014756 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-logger * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Logger implementation that sends text messages to output * * TODO: Finish phpdocs */ class output_text_logger extends base_logger { // Protected API starts here protected function action($message, $level, $options = null) { $prefix = $this->get_prefix($level, $options); // Depending of running from browser/command line, format differently if (defined('STDOUT')) { echo $prefix . $message . PHP_EOL; } else { echo $prefix . htmlentities($message, ENT_QUOTES, 'UTF-8') . '<br/>' . PHP_EOL; } flush(); return true; } } util/includes/backup_includes.php 0000644 00000015405 15215711721 0013202 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-includes * @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 backup needed stuff require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/executable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/processable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/annotable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/loggable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); require_once($CFG->dirroot . '/backup/util/output/output_controller.class.php'); require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_structure_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_plan_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_question_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/checks/backup_check.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_atom.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_attribute.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_final_element.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_nested_element.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_optigroup.class.php'); require_once($CFG->dirroot . '/backup/util/structure/base_processor.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_attribute.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_final_element.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_nested_element.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_optigroup_element.class.php'); require_once($CFG->dirroot . '/backup/util/structure/backup_structure_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/async_helper.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'); require_once($CFG->dirroot . '/backup/util/helper/backup_null_iterator.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_array_iterator.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php'); require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php'); // Required by backup_general_helper::get_backup_information(). require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/base_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/core_backup_html_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php'); require_once($CFG->dirroot . '/backup/util/settings/base_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/root/root_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/activity/activity_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/section/section_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/course/course_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_plan.class.php'); require_once($CFG->dirroot . '/backup/util/plan/backup_plan.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_task.class.php'); require_once($CFG->dirroot . '/backup/util/plan/backup_task.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/backup_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/backup_structure_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/backup_execution_step.class.php'); require_once($CFG->dirroot . '/backup/controller/base_controller.class.php'); require_once($CFG->dirroot . '/backup/controller/backup_controller.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_moodleform.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_ui.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_ui_stage.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_moodleform.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_stage.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_setting.class.php'); // And some moodle stuff too require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/gradelib.php'); util/includes/convert_includes.php 0000644 00000002747 15215711721 0013422 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Makes sure that all general code needed by backup-convert code is included * * @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/interfaces/loggable.class.php'); // converters are loggable require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); // req by backup.class.php require_once($CFG->dirroot . '/backup/backup.class.php'); // provides backup::FORMAT_xxx constants require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); require_once($CFG->dirroot . '/backup/util/factories/convert_factory.class.php'); require_once($CFG->libdir . '/filelib.php'); util/includes/restore_includes.php 0000644 00000015017 15215711721 0013417 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-includes * @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 backup needed stuff require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/loggable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/executable.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/processable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); require_once($CFG->dirroot . '/backup/util/structure/restore_path_element.class.php'); require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_anonymizer_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_file_manager.class.php'); require_once($CFG->dirroot . '/backup/util/helper/copy_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_prechecks_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_moodlexml_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_inforef_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_users_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_roles_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_questions_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_structure_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_decode_rule.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_decode_content.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_decode_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_logs_processor.class.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_log_rule.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php'); require_once($CFG->dirroot . '/backup/util/output/output_controller.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/restore_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/restore_controller_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/checks/restore_check.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/base_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/core_backup_html_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php'); require_once($CFG->dirroot . '/backup/util/factories/restore_factory.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'); require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php'); require_once($CFG->dirroot . '/backup/util/settings/base_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/root/root_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/activity/activity_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/section/section_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/settings/course/course_backup_setting.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_plan.class.php'); require_once($CFG->dirroot . '/backup/util/plan/restore_plan.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_task.class.php'); require_once($CFG->dirroot . '/backup/util/plan/restore_task.class.php'); require_once($CFG->dirroot . '/backup/util/plan/base_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/restore_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/restore_structure_step.class.php'); require_once($CFG->dirroot . '/backup/util/plan/restore_execution_step.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_plan_builder.class.php'); require_once($CFG->dirroot . '/backup/controller/base_controller.class.php'); require_once($CFG->dirroot . '/backup/controller/restore_controller.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_moodleform.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_ui.class.php'); require_once($CFG->dirroot . '/backup/util/ui/base_ui_stage.class.php'); require_once($CFG->dirroot . '/backup/util/ui/backup_ui_setting.class.php'); require_once($CFG->dirroot . '/backup/util/ui/restore_ui_stage.class.php'); require_once($CFG->dirroot . '/backup/util/ui/restore_ui.class.php'); require_once($CFG->dirroot . '/backup/util/ui/restore_moodleform.class.php'); require_once($CFG->dirroot . '/backup/util/ui/restore_ui_components.php'); // And some moodle stuff too require_once($CFG->dirroot . '/tag/lib.php'); require_once($CFG->dirroot . '/lib/gradelib.php'); require_once($CFG->dirroot . '/lib//questionlib.php'); require_once($CFG->dirroot . '/course/lib.php'); require_once ($CFG->dirroot . '/blocks/moodleblock.class.php'); util/factories/convert_factory.class.php 0000644 00000003727 15215711721 0014537 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core * @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(); /** * Factory class to create new instances of backup converters */ abstract class convert_factory { /** * Instantinates the given converter operating on a given directory * * @throws coding_exception * @param $name The converter name * @param $tempdir The temp directory to operate on * @param base_logger|null if the conversion should be logged, use this logger * @return base_converter */ public static function get_converter($name, $tempdir, $logger = null) { global $CFG; $name = clean_param($name, PARAM_SAFEDIR); $classfile = "$CFG->dirroot/backup/converter/$name/lib.php"; $classname = "{$name}_converter"; if (!file_exists($classfile)) { throw new coding_exception("Converter factory error: class file not found $classfile"); } require_once($classfile); if (!class_exists($classname)) { throw new coding_exception("Converter factory error: class not found $classname"); } return new $classname($tempdir, $logger); } } util/factories/tests/factories_test.php 0000644 00000014610 15215711721 0014375 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_factory; use database_logger; use error_log_logger; use file_logger; use output_indented_logger; 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/loggers/base_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php'); require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php'); require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php'); /** * @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 */ final class factories_test extends \advanced_testcase { public function setUp(): void { global $CFG; parent::setUp(); $this->resetAfterTest(true); $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; unset($CFG->backup_file_logger_extra); $CFG->backup_file_logger_level_extra = backup::LOG_NONE; } /** * test get_logger_chain() method */ public function test_backup_factory(): void { global $CFG; // Default instantiate, all levels = backup::LOG_NONE // With debugdisplay enabled $CFG->debugdisplay = true; $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test'); $this->assertTrue($logger1 instanceof error_log_logger); // 1st logger is error_log_logger $this->assertEquals($logger1->get_level(), backup::LOG_NONE); $logger2 = $logger1->get_next(); $this->assertTrue($logger2 instanceof output_indented_logger); // 2nd logger is output_indented_logger $this->assertEquals($logger2->get_level(), backup::LOG_NONE); $logger3 = $logger2->get_next(); $this->assertTrue($logger3 instanceof file_logger); // 3rd logger is file_logger $this->assertEquals($logger3->get_level(), backup::LOG_NONE); $logger4 = $logger3->get_next(); $this->assertTrue($logger4 instanceof database_logger); // 4th logger is database_logger $this->assertEquals($logger4->get_level(), backup::LOG_NONE); $logger5 = $logger4->get_next(); $this->assertTrue($logger5 === null); // With debugdisplay disabled $CFG->debugdisplay = false; $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test'); $this->assertTrue($logger1 instanceof error_log_logger); // 1st logger is error_log_logger $this->assertEquals($logger1->get_level(), backup::LOG_NONE); $logger2 = $logger1->get_next(); $this->assertTrue($logger2 instanceof file_logger); // 2nd logger is file_logger $this->assertEquals($logger2->get_level(), backup::LOG_NONE); $logger3 = $logger2->get_next(); $this->assertTrue($logger3 instanceof database_logger); // 3rd logger is database_logger $this->assertEquals($logger3->get_level(), backup::LOG_NONE); $logger4 = $logger3->get_next(); $this->assertTrue($logger4 === null); // Instantiate with debugging enabled and $CFG->backup_error_log_logger_level not set $CFG->debugdisplay = true; unset($CFG->backup_error_log_logger_level); $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test'); $this->assertTrue($logger1 instanceof error_log_logger); // 1st logger is error_log_logger $this->assertEquals($logger1->get_level(), backup::LOG_DEBUG); // and must have backup::LOG_DEBUG level // Set $CFG->backup_error_log_logger_level to backup::LOG_WARNING and test again $CFG->backup_error_log_logger_level = backup::LOG_WARNING; $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test'); $this->assertTrue($logger1 instanceof error_log_logger); // 1st logger is error_log_logger $this->assertEquals($logger1->get_level(), backup::LOG_WARNING); // and must have backup::LOG_WARNING level // Instantiate in non-interactive mode, output_indented_logger must be out $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_NO, backup::EXECUTION_INMEDIATE, 'test'); $logger2 = $logger1->get_next(); $this->assertTrue($logger2 instanceof file_logger); // 2nd logger is file_logger (output_indented_logger skiped) // Define extra file logger and instantiate, should be 5th and last logger $CFG->backup_file_logger_extra = $CFG->tempdir.'/test.html'; $CFG->backup_file_logger_level_extra = backup::LOG_NONE; $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test'); $logger2 = $logger1->get_next(); $logger3 = $logger2->get_next(); $logger4 = $logger3->get_next(); $logger5 = $logger4->get_next(); $this->assertTrue($logger5 instanceof file_logger); // 5rd logger is file_logger (extra) $this->assertEquals($logger3->get_level(), backup::LOG_NONE); $logger6 = $logger5->get_next(); $this->assertTrue($logger6 === null); } } util/factories/restore_factory.class.php 0000644 00000004471 15215711721 0014537 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-factories * @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 factory class providing different restore object instances * * This class contains various methods available in order to easily * create different parts of the restore architecture in an easy way * * TODO: Finish phpdocs */ abstract class restore_factory { public static function get_restore_activity_task($info) { $classname = 'restore_' . $info->modulename . '_activity_task'; if (class_exists($classname)) { return new $classname($info->title, $info); } } public static function get_restore_block_task($blockname, $basepath) { $classname = 'restore_default_block_task'; $testname = 'restore_' . $blockname . '_block_task'; // If the block has custom backup/restore task class (testname), use it if (class_exists($testname)) { $classname = $testname; } return new $classname($blockname, $basepath); } public static function get_restore_section_task($info) { return new restore_section_task($info->title, $info); } public static function get_restore_course_task($info, $courseid) { global $DB; // Check course exists if (!$course = $DB->get_record('course', array('id' => $courseid))) { throw new restore_task_exception('course_task_course_not_found', $courseid); } return new restore_course_task($info->title, $info); } } util/factories/backup_factory.class.php 0000644 00000015463 15215711721 0014324 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-factories * @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 factory class providing different backup object instances * * This class contains various methods available in order to easily * create different parts of the backup architecture in an easy way * * TODO: Finish phpdocs */ abstract class backup_factory { public static function get_destination_chain($type, $id, $mode, $execution) { return null; } public static function get_logger_chain($interactive, $execution, $backupid) { global $CFG; $dfltloglevel = backup::LOG_WARNING; // Default logging level if ($CFG->debugdeveloper) { // Debug developer raises default logging level $dfltloglevel = backup::LOG_DEBUG; } $enabledloggers = array(); // Array to store all enabled loggers // Create error_log_logger, observing $CFG->backup_error_log_logger_level, // defaulting to $dfltloglevel $elllevel = isset($CFG->backup_error_log_logger_level) ? $CFG->backup_error_log_logger_level : $dfltloglevel; $enabledloggers[] = new error_log_logger($elllevel); // Create output_indented_logger, observing $CFG->backup_output_indented_logger_level and $CFG->debugdisplay, // defaulting to LOG_ERROR. Only if interactive and inmediate if ($CFG->debugdisplay && $interactive == backup::INTERACTIVE_YES && $execution == backup::EXECUTION_INMEDIATE) { $oillevel = isset($CFG->backup_output_indented_logger_level) ? $CFG->backup_output_indented_logger_level : backup::LOG_ERROR; $enabledloggers[] = new output_indented_logger($oillevel, false, false); } // Create file_logger, observing $CFG->backup_file_logger_level // defaulting to $dfltloglevel $backuptempdir = make_backup_temp_directory(''); // Need to ensure that $CFG->backuptempdir already exists. $fllevel = isset($CFG->backup_file_logger_level) ? $CFG->backup_file_logger_level : $dfltloglevel; $enabledloggers[] = new file_logger($fllevel, true, true, $backuptempdir . '/' . $backupid . '.log'); // Create database_logger, observing $CFG->backup_database_logger_level and defaulting to LOG_WARNING // and pointing to the backup_logs table $dllevel = isset($CFG->backup_database_logger_level) ? $CFG->backup_database_logger_level : $dfltloglevel; $columns = array('backupid' => $backupid); $enabledloggers[] = new database_logger($dllevel, 'timecreated', 'loglevel', 'message', 'backup_logs', $columns); // Create extra file_logger, observing $CFG->backup_file_logger_extra and $CFG->backup_file_logger_extra_level // defaulting to $fllevel (normal file logger) if (isset($CFG->backup_file_logger_extra)) { $flelevel = isset($CFG->backup_file_logger_extra_level) ? $CFG->backup_file_logger_extra_level : $fllevel; $enabledloggers[] = new file_logger($flelevel, true, true, $CFG->backup_file_logger_extra); } // Build the chain $loggers = null; foreach ($enabledloggers as $currentlogger) { if ($loggers == null) { $loggers = $currentlogger; } else { $lastlogger->set_next($currentlogger); } $lastlogger = $currentlogger; } return $loggers; } /** * Given one format and one course module id, return the corresponding * backup_xxxx_activity_task() */ public static function get_backup_activity_task($format, $moduleid) { global $CFG, $DB; // Check moduleid exists if (!$coursemodule = get_coursemodule_from_id(false, $moduleid)) { throw new backup_task_exception('activity_task_coursemodule_not_found', $moduleid); } $classname = 'backup_' . $coursemodule->modname . '_activity_task'; return new $classname($coursemodule->name, $moduleid); } /** * Given one format, one block id and, optionally, one moduleid, return the corresponding backup_xxx_block_task() */ public static function get_backup_block_task($format, $blockid, $moduleid = null) { global $CFG, $DB; // Check blockid exists if (!$block = $DB->get_record('block_instances', array('id' => $blockid))) { throw new backup_task_exception('block_task_block_instance_not_found', $blockid); } // Set default block backup task $classname = 'backup_default_block_task'; $testname = 'backup_' . $block->blockname . '_block_task'; // If the block has custom backup/restore task class (testname), use it if (class_exists($testname)) { $classname = $testname; } return new $classname($block->blockname, $blockid, $moduleid); } /** * Given one format and one section id, return the corresponding backup_section_task() */ public static function get_backup_section_task($format, $sectionid) { global $DB; // Check section exists if (!$section = $DB->get_record('course_sections', array('id' => $sectionid))) { throw new backup_task_exception('section_task_section_not_found', $sectionid); } return new backup_section_task((string)$section->name === '' ? $section->section : $section->name, $sectionid); } /** * Given one format and one course id, return the corresponding backup_course_task() */ public static function get_backup_course_task($format, $courseid) { global $DB; // Check course exists if (!$course = $DB->get_record('course', array('id' => $courseid))) { throw new backup_task_exception('course_task_course_not_found', $courseid); } return new backup_course_task($course->shortname, $courseid); } /** * Dispatches the creation of the @backup_plan to the proper format builder */ public static function build_plan($controller) { backup_plan_builder::build_plan($controller); } } util/checks/restore_check.class.php 0000644 00000025673 15215711721 0013435 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-factories * @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 different restore checks * * This class contains various static methods available in order to easily * perform a bunch of restore architecture tests * * TODO: Finish phpdocs */ abstract class restore_check { public static function check_courseid($courseid) { global $DB; // id must exist in course table if (! $DB->record_exists('course', array('id' => $courseid))) { throw new restore_controller_exception('restore_check_course_not_exists', $courseid); } return true; } public static function check_user($userid) { global $DB; // userid must exist in user table if (! $DB->record_exists('user', array('id' => $userid))) { throw new restore_controller_exception('restore_check_user_not_exists', $userid); } return true; } public static function check_security($restore_controller, $apply) { global $DB; if (! $restore_controller instanceof restore_controller) { throw new restore_controller_exception('restore_check_security_requires_restore_controller'); } $restore_controller->log('checking plan security', backup::LOG_INFO); // Some handy vars $type = $restore_controller->get_type(); $mode = $restore_controller->get_mode(); $courseid = $restore_controller->get_courseid(); $coursectx= context_course::instance($courseid); $userid = $restore_controller->get_userid(); // Note: all the checks along the function MUST be performed for $userid, that // is the user who "requested" the course restore, not current $USER at all!! // First of all, decide which caps/contexts are we going to check // for common backups (general, automated...) based exclusively // in the type (course, section, activity). And store them into // one capability => context array structure $typecapstocheck = array(); switch ($type) { case backup::TYPE_1COURSE : $typecapstocheck['moodle/restore:restorecourse'] = $coursectx; break; case backup::TYPE_1SECTION : $typecapstocheck['moodle/restore:restoresection'] = $coursectx; break; case backup::TYPE_1ACTIVITY : $typecapstocheck['moodle/restore:restoreactivity'] = $coursectx; break; default : throw new restore_controller_exception('restore_unknown_restore_type', $type); } // Now, if restore mode is hub or import, check userid has permissions for those modes // other modes will perform common checks only (restorexxxx capabilities in $typecapstocheck) switch ($mode) { case backup::MODE_IMPORT: if (!has_capability('moodle/restore:restoretargetimport', $coursectx, $userid)) { $a = new stdclass(); $a->userid = $userid; $a->courseid = $courseid; $a->capability = 'moodle/restore:restoretargetimport'; throw new restore_controller_exception('restore_user_missing_capability', $a); } break; // Common backup (general, automated...), let's check all the $typecapstocheck // capability => context pairs default: foreach ($typecapstocheck as $capability => $context) { if (!has_capability($capability, $context, $userid)) { $a = new stdclass(); $a->userid = $userid; $a->courseid = $courseid; $a->capability = $capability; throw new restore_controller_exception('restore_user_missing_capability', $a); } } } // Now, enforce 'moodle/restore:userinfo' to 'users' setting, applying changes if allowed, // else throwing exception $userssetting = $restore_controller->get_plan()->get_setting('users'); $prevvalue = $userssetting->get_value(); $prevstatus = $userssetting->get_status(); $hasusercap = has_capability('moodle/restore:userinfo', $coursectx, $userid); // If setting is enabled but user lacks permission if (!$hasusercap && $prevvalue) { // If user has not the capability and setting is enabled // Now analyse if we are allowed to apply changes or must stop with exception if (!$apply) { // Cannot apply changes, throw exception $a = new stdclass(); $a->setting = 'users'; $a->value = $prevvalue; $a->capability = 'moodle/restore:userinfo'; throw new restore_controller_exception('restore_setting_value_wrong_for_capability', $a); } else { // Can apply changes $userssetting->set_value(false); // Set the value to false $userssetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm } } // Now, if mode is HUB or IMPORT, and still we are including users in restore, turn them off // Defaults processing should have handled this, but we need to be 100% sure if ($mode == backup::MODE_IMPORT || $mode == backup::MODE_HUB) { $userssetting = $restore_controller->get_plan()->get_setting('users'); if ($userssetting->get_value()) { $userssetting->set_value(false); // Set the value to false $userssetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm } } // Check the user has the ability to configure the restore. If not then we need // to lock all settings by permission so that no changes can be made. This does // not apply to the import facility, where all the activities (picked on backup) // are restored automatically without restore UI if ($mode != backup::MODE_IMPORT) { $hasconfigcap = has_capability('moodle/restore:configure', $coursectx, $userid); if (!$hasconfigcap) { $settings = $restore_controller->get_plan()->get_settings(); foreach ($settings as $setting) { $setting->set_status(base_setting::LOCKED_BY_PERMISSION); } } } if ($type == backup::TYPE_1COURSE) { // Ensure the user has the rolldates capability. If not we want to lock this // settings so that they cannot change it. $hasrolldatescap = has_capability('moodle/restore:rolldates', $coursectx, $userid); if (!$hasrolldatescap) { $startdatesetting = $restore_controller->get_plan()->get_setting('course_startdate'); if ($startdatesetting) { $startdatesetting->set_status(base_setting::NOT_LOCKED); // Permission lock overrides config lock. $startdatesetting->set_value(false); $startdatesetting->set_status(base_setting::LOCKED_BY_PERMISSION); } } // Ensure the user has the changefullname capability. If not we want to lock // the setting so that they cannot change it. $haschangefullnamecap = has_capability('moodle/course:changefullname', $coursectx, $userid); if (!$haschangefullnamecap) { $fullnamesetting = $restore_controller->get_plan()->get_setting('course_fullname'); $fullnamesetting->set_status(base_setting::NOT_LOCKED); // Permission lock overrides config lock. $fullnamesetting->set_value(false); $fullnamesetting->set_status(base_setting::LOCKED_BY_PERMISSION); } // Ensure the user has the changeshortname capability. If not we want to lock // the setting so that they cannot change it. $haschangeshortnamecap = has_capability('moodle/course:changeshortname', $coursectx, $userid); if (!$haschangeshortnamecap) { $shortnamesetting = $restore_controller->get_plan()->get_setting('course_shortname'); $shortnamesetting->set_status(base_setting::NOT_LOCKED); // Permission lock overrides config lock. $shortnamesetting->set_value(false); $shortnamesetting->set_status(base_setting::LOCKED_BY_PERMISSION); } // Ensure the user has the update capability. If not we want to lock // the overwrite setting so that they cannot change it. $hasupdatecap = has_capability('moodle/course:update', $coursectx, $userid); if (!$hasupdatecap) { $overwritesetting = $restore_controller->get_plan()->get_setting('overwrite_conf'); $overwritesetting->set_status(base_setting::NOT_LOCKED); // Permission lock overrides config lock. $overwritesetting->set_value(false); $overwritesetting->set_status(base_setting::LOCKED_BY_PERMISSION); } // Ensure the user has the capability to manage enrolment methods. If not we want to unset and lock // the setting so that they cannot change it. $hasmanageenrolcap = has_capability('moodle/course:enrolconfig', $coursectx, $userid); if (!$hasmanageenrolcap) { if ($restore_controller->get_plan()->setting_exists('enrolments')) { $enrolsetting = $restore_controller->get_plan()->get_setting('enrolments'); if ($enrolsetting->get_value() != backup::ENROL_NEVER) { $enrolsetting->set_status(base_setting::NOT_LOCKED); // In case it was locked earlier. $enrolsetting->set_value(backup::ENROL_NEVER); } $enrolsetting->set_status(base_setting::LOCKED_BY_PERMISSION); } } } return true; } } util/checks/tests/checks_test.php 0000644 00000013257 15215711721 0013145 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_check; use backup_controller; use backup_controller_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * Check 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 */ final class checks_test extends \advanced_testcase { protected $moduleid; // course_modules id used for testing protected $sectionid; // course_sections id used for testing protected $courseid; // course id used for testing protected $userid; // user record id protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(array(), array('createsections' => true)); $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3)); $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid)); $this->moduleid = $coursemodule->id; $this->sectionid = $coursemodule->section; $this->courseid = $coursemodule->course; $this->userid = 2; // admin $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; unset($CFG->backup_file_logger_extra); $CFG->backup_file_logger_level_extra = backup::LOG_NONE; } /* * test backup_check class */ public function test_backup_check(): void { // Check against existing course module/section course or fail $this->assertTrue(backup_check::check_id(backup::TYPE_1ACTIVITY, $this->moduleid)); $this->assertTrue(backup_check::check_id(backup::TYPE_1SECTION, $this->sectionid)); $this->assertTrue(backup_check::check_id(backup::TYPE_1COURSE, $this->courseid)); $this->assertTrue(backup_check::check_user($this->userid)); // Check against non-existing course module/section/course (0) try { backup_check::check_id(backup::TYPE_1ACTIVITY, 0); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_module_not_exists'); } try { backup_check::check_id(backup::TYPE_1SECTION, 0); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_section_not_exists'); } try { backup_check::check_id(backup::TYPE_1COURSE, 0); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_course_not_exists'); } // Try wrong type try { backup_check::check_id(12345678,0); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_incorrect_type'); } // Test non-existing user $userid = 0; try { backup_check::check_user($userid); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_user_not_exists'); } // Security check tests // Try to pass wrong controller try { backup_check::check_security(new \stdClass(), true); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_security_requires_backup_controller'); } // Pass correct controller, check must return true in any case with $apply enabled // and $bc must continue being mock_backup_controller $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $this->assertTrue(backup_check::check_security($bc, true)); $this->assertTrue($bc instanceof backup_controller); $bc->destroy(); } } util/checks/backup_check.class.php 0000644 00000026060 15215711721 0013206 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-factories * @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 different backup checks * * This class contains various static methods available in order to easily * perform a bunch of backup architecture tests * * TODO: Finish phpdocs */ abstract class backup_check { public static function check_format_and_type($format, $type) { global $CFG; $file = $CFG->dirroot . '/backup/' . $format . '/backup_plan_builder.class.php'; if (! file_exists($file)) { throw new backup_controller_exception('backup_check_unsupported_format', $format); } require_once($file); if (!in_array($type, backup_plan_builder::supported_backup_types())) { throw new backup_controller_exception('backup_check_unsupported_type', $type); } require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); } public static function check_id($type, $id) { global $DB; switch ($type) { case backup::TYPE_1ACTIVITY: // id must exist in course_modules table if (! $DB->record_exists('course_modules', array('id' => $id))) { throw new backup_controller_exception('backup_check_module_not_exists', $id); } break; case backup::TYPE_1SECTION: // id must exist in course_sections table if (! $DB->record_exists('course_sections', array('id' => $id))) { throw new backup_controller_exception('backup_check_section_not_exists', $id); } break; case backup::TYPE_1COURSE: // id must exist in course table if (! $DB->record_exists('course', array('id' => $id))) { throw new backup_controller_exception('backup_check_course_not_exists', $id); } break; default: throw new backup_controller_exception('backup_check_incorrect_type', $type); } return true; } public static function check_user($userid) { global $DB; // userid must exist in user table if (! $DB->record_exists('user', array('id' => $userid))) { throw new backup_controller_exception('backup_check_user_not_exists', $userid); } return true; } public static function check_security($backup_controller, $apply) { global $DB; if (! $backup_controller instanceof backup_controller) { throw new backup_controller_exception('backup_check_security_requires_backup_controller'); } $backup_controller->log('checking plan security', backup::LOG_INFO); // Some handy vars $type = $backup_controller->get_type(); $mode = $backup_controller->get_mode(); $courseid = $backup_controller->get_courseid(); $coursectx= context_course::instance($courseid); $userid = $backup_controller->get_userid(); $id = $backup_controller->get_id(); // courseid / sectionid / cmid // Note: all the checks along the function MUST be performed for $userid, that // is the user who "requested" the course backup, not current $USER at all!! // First of all, decide which caps/contexts are we going to check // for common backups (general, automated...) based exclusively // in the type (course, section, activity). And store them into // one capability => context array structure $typecapstocheck = array(); switch ($type) { case backup::TYPE_1COURSE : $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); // course exists $typecapstocheck['moodle/backup:backupcourse'] = $coursectx; break; case backup::TYPE_1SECTION : $DB->get_record('course_sections', array('course' => $courseid, 'id' => $id), '*', MUST_EXIST); // sec exists $typecapstocheck['moodle/backup:backupsection'] = $coursectx; break; case backup::TYPE_1ACTIVITY : get_coursemodule_from_id(null, $id, $courseid, false, MUST_EXIST); // cm exists $modulectx = context_module::instance($id); $typecapstocheck['moodle/backup:backupactivity'] = $modulectx; break; default : throw new backup_controller_exception('backup_unknown_backup_type', $type); } // Now, if backup mode is hub or import, check userid has permissions for those modes // other modes will perform common checks only (backupxxxx capabilities in $typecapstocheck) switch ($mode) { case backup::MODE_IMPORT: if (!has_capability('moodle/backup:backuptargetimport', $coursectx, $userid)) { $a = new stdclass(); $a->userid = $userid; $a->courseid = $courseid; $a->capability = 'moodle/backup:backuptargetimport'; throw new backup_controller_exception('backup_user_missing_capability', $a); } break; // Common backup (general, automated...), let's check all the $typecapstocheck // capability => context pairs default: foreach ($typecapstocheck as $capability => $context) { if (!has_capability($capability, $context, $userid)) { $a = new stdclass(); $a->userid = $userid; $a->courseid = $courseid; $a->capability = $capability; throw new backup_controller_exception('backup_user_missing_capability', $a); } } } // Now, enforce 'moodle/backup:userinfo' to 'users' setting, applying changes if allowed, // else throwing exception $userssetting = $backup_controller->get_plan()->get_setting('users'); $prevvalue = $userssetting->get_value(); $prevstatus = $userssetting->get_status(); $hasusercap = has_capability('moodle/backup:userinfo', $coursectx, $userid); // If setting is enabled but user lacks permission if (!$hasusercap) { // If user has not the capability // Now analyse if we are allowed to apply changes or must stop with exception if (!$apply && $prevvalue) { // Cannot apply changes and the value is set, throw exception $a = new stdclass(); $a->setting = 'users'; $a->value = $prevvalue; $a->capability = 'moodle/backup:userinfo'; throw new backup_controller_exception('backup_setting_value_wrong_for_capability', $a); } else { // Can apply changes // If it is already false, we don't want to try and set it again, because if it is // already locked, and exception will occur. The side benifit is if it is true and locked // we will get an exception... if ($prevvalue) { $userssetting->set_value(false); // Set the value to false } $userssetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm } } // Now, enforce 'moodle/backup:anonymise' to 'anonymise' setting, applying changes if allowed, // else throwing exception $anonsetting = $backup_controller->get_plan()->get_setting('anonymize'); $prevvalue = $anonsetting->get_value(); $prevstatus = $anonsetting->get_status(); $hasanoncap = has_capability('moodle/backup:anonymise', $coursectx, $userid); // If setting is enabled but user lacks permission if (!$hasanoncap) { // If user has not the capability // Now analyse if we are allowed to apply changes or must stop with exception if (!$apply && $prevvalue) { // Cannot apply changes and the value is set, throw exception $a = new stdclass(); $a->setting = 'anonymize'; $a->value = $prevvalue; $a->capability = 'moodle/backup:anonymise'; throw new backup_controller_exception('backup_setting_value_wrong_for_capability', $a); } else { // Can apply changes if ($prevvalue) { // If we try and set it back to false and it has already been locked, error will occur $anonsetting->set_value(false); // Set the value to false } $anonsetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm } } // Now, if mode is HUB or IMPORT, and still we are including users in backup, turn them off // Defaults processing should have handled this, but we need to be 100% sure if ($mode == backup::MODE_IMPORT || $mode == backup::MODE_HUB) { $userssetting = $backup_controller->get_plan()->get_setting('users'); if ($userssetting->get_value()) { $userssetting->set_value(false); // Set the value to false $userssetting->set_status(base_setting::LOCKED_BY_PERMISSION);// Set the status to locked by perm } } // Check the user has the ability to configure the backup. If not then we need // to lock all settings by permission so that no changes can be made. This does // not apply to the import facility, where the activities must be always enabled // to be able to pick them if ($mode != backup::MODE_IMPORT) { $hasconfigcap = has_capability('moodle/backup:configure', $coursectx, $userid); if (!$hasconfigcap) { $settings = $backup_controller->get_plan()->get_settings(); foreach ($settings as $setting) { if ($setting->get_name() == 'filename') { continue; } $setting->set_status(base_setting::LOCKED_BY_PERMISSION); } } } return true; } } util/helper/backup_anonymizer_helper.class.php 0000644 00000014224 15215711721 0015701 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Helper class for anonymization of data * * This functions includes a collection of methods that are invoked * from the backup process when anonymization services have been * requested. * * The name of each method must be "process_parentname_name", as defined * byt the @anonymizer_final_element final element class, where * parentname is the name ob the parent tag and name the name of the tag * contents to be anonymized (i.e. process_user_username) with one param * being the value to anonymize. * * Note: current implementation of anonymization is pretty simple, just some * sequential values are used. If we want more elaborated generation, it * can be replaced later (using generators or wathever). Don't forget we must * ensure some fields (username, idnumber, email) are unique always. * * TODO: Improve to use more advanced anonymization * * TODO: Finish phpdocs */ class backup_anonymizer_helper { /** * Determine if the given user is an 'anonymous' user, based on their username, firstname, lastname * and email address. * @param stdClass $user the user record to test * @return bool true if this is an 'anonymous' user */ public static function is_anonymous_user($user) { if (preg_match('/^anon\d*$/', $user->username)) { $match = preg_match('/^anonfirstname\d*$/', $user->firstname); $match = $match && preg_match('/^anonlastname\d*$/', $user->lastname); // Check .com for backwards compatibility. $emailmatch = preg_match('/^anon\d*@doesntexist\.com$/', $user->email) || preg_match('/^anon\d*@doesntexist\.invalid$/', $user->email); if ($match && $emailmatch) { return true; } } return false; } public static function process_user_auth($value) { return 'manual'; // Set them to manual always } public static function process_user_username($value) { static $counter = 0; $counter++; return 'anon' . $counter; // Just a counter } public static function process_user_idnumber($value) { return ''; // Just blank it } public static function process_user_firstname($value) { static $counter = 0; $counter++; return 'anonfirstname' . $counter; // Just a counter } public static function process_user_lastname($value) { static $counter = 0; $counter++; return 'anonlastname' . $counter; // Just a counter } public static function process_user_email($value) { static $counter = 0; $counter++; return 'anon' . $counter . '@doesntexist.invalid'; // Just a counter. } public static function process_user_phone1($value) { return ''; // Clean phone1 } public static function process_user_phone2($value) { return ''; // Clean phone2 } public static function process_user_institution($value) { return ''; // Clean institution } public static function process_user_department($value) { return ''; // Clean department } public static function process_user_address($value) { return ''; // Clean address } public static function process_user_city($value) { return 'Perth'; // Set city } public static function process_user_country($value) { return 'AU'; // Set country } public static function process_user_lastip($value) { return '127.0.0.1'; // Set lastip to localhost } public static function process_user_picture($value) { return 0; // No picture } public static function process_user_description($value) { return ''; // No user description } public static function process_user_descriptionformat($value) { return 0; // Format moodle } public static function process_user_imagealt($value) { return ''; // No user imagealt } /** * Anonymises user's phonetic name field * @param string $value value of the user field * @return string anonymised phonetic name */ public static function process_user_firstnamephonetic($value) { static $counter = 0; $counter++; return 'anonfirstnamephonetic' . $counter; // Just a counter. } /** * Anonymises user's phonetic last name field * @param string $value value of the user field * @return string anonymised last phonetic name */ public static function process_user_lastnamephonetic($value) { static $counter = 0; $counter++; return 'anonlastnamephonetic' . $counter; // Just a counter. } /** * Anonymises user's middle name field * @param string $value value of the user field * @return string anonymised middle name */ public static function process_user_middlename($value) { static $counter = 0; $counter++; return 'anonmiddlename' . $counter; // Just a counter. } /** * Anonymises user's alternate name field * @param string $value value of the user field * @return string anonymised alternate name */ public static function process_user_alternatename($value) { static $counter = 0; $counter++; return 'anonalternatename' . $counter; // Just a counter. } } util/helper/restore_questions_parser_processor.class.php 0000644 00000025131 15215711721 0020071 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the categories and questions (header info only) from the questions.xml file * to the backup_ids table storing the whole structure there for later processing. * Note: only "needed" categories are loaded (must have question_categoryref record in backup_ids) * Note: parentitemid will contain the category->contextid for categories * Note: parentitemid will contain the category->id for questions * * TODO: Complete phpdocs */ class restore_questions_parser_processor extends grouped_parser_processor { /** @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 String for concatenating data into a string for hashing.*/ protected const HASHDATA_SEPARATOR = '|HASHDATA|'; /** @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) { global $CFG; $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, true); $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH, true); // Add all sub-elements, including those from plugins, as grouped paths with the question tag so that // we can create a hash of all question data for comparison with questions in the database. $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints'); $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints'); $this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints/question_hint'); $this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints/question_hint'); $connectionpoint = new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH); foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) { $restore = $this->get_qtype_restore($qtype->name); if (!$restore) { continue; } $structure = $restore->define_plugin_structure($connectionpoint); foreach ($structure as $element) { $subpath = str_replace(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/', '', $element->get_path()); $pathparts = explode('/', $subpath); $path = self::CATEGORY_PATH . self::QUESTION_SUBPATH; $legacypath = self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH; foreach ($pathparts as $part) { $path .= '/' . $part; $legacypath .= '/' . $part; if (!in_array($path, $this->paths)) { $this->add_path($path); $this->add_path($legacypath); } } } } } 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) { // Remove sub-elements from the question info we're going to save. $info = (object) array_filter($data['tags'], fn($tag) => !is_array($tag)); $itemname = 'question'; $itemid = $info->id; $parentitemid = $this->lastcatid; $restore = $this->get_qtype_restore($data['tags']['qtype']); if ($restore) { $questiondata = $restore->convert_backup_to_questiondata($data['tags']); } else { $questiondata = restore_qtype_plugin::convert_backup_to_questiondata($data['tags']); } // Store a hash of question fields for comparison with existing questions. $info->questionhash = $this->generate_question_identity_hash($questiondata); // 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; } /** * Load and instantiate the restore class for the given question type. * * If there is no restore class, null is returned. * * @param string $qtype The question type name (no qtype_ prefix) * @return ?restore_qtype_plugin */ protected static function get_qtype_restore(string $qtype): ?restore_qtype_plugin { global $CFG; $step = new restore_quiz_activity_structure_step('questions', 'question.xml'); $filepath = "{$CFG->dirroot}/question/type/{$qtype}/backup/moodle2/restore_qtype_{$qtype}_plugin.class.php"; if (!file_exists($filepath)) { return null; } require_once($filepath); $restoreclass = "restore_qtype_{$qtype}_plugin"; if (!class_exists($restoreclass)) { return null; } return new $restoreclass('qtype', $qtype, $step); } /** * Given a data structure containing the data for a question, reduce it to a flat array and return a sha1 hash of the data. * * @param stdClass $questiondata An array containing all the data for a question, including hints and qtype plugin data. * @param ?backup_xml_transformer $transformer If provided, run the backup transformer process on all text fields. This ensures * that values from the database are compared like-for-like with encoded values from the backup. * @return string A sha1 hash of all question data, normalised and concatenated together. */ public static function generate_question_identity_hash( stdClass $questiondata, ?backup_xml_transformer $transformer = null, ): string { $questiondata = clone($questiondata); $restore = self::get_qtype_restore($questiondata->qtype); if ($restore) { $restore->define_plugin_structure(new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH)); // Combine default exclusions with those specified by the plugin. $questiondata = $restore->remove_excluded_question_data($questiondata, $restore->get_excluded_identity_hash_fields()); } else { // The qtype has no restore class, use the default reduction method. $questiondata = restore_qtype_plugin::remove_excluded_question_data($questiondata); } // Convert questiondata to a flat array of values. $hashdata = []; // Convert the object to a multi-dimensional array for compatibility with array_walk_recursive. $questiondata = json_decode(json_encode($questiondata), true); array_walk_recursive($questiondata, function($value) use (&$hashdata) { // Normalise data types. Depending on where the data comes from, it may be a mixture of nulls, strings, // ints and floats. Convert everything to strings, then all numbers to floats to ensure we are doing // like-for-like comparisons without losing accuracy. $value = (string) $value; if (is_numeric($value)) { $value = (float) ($value); } else if (str_contains($value, "\r\n")) { // Normalise line breaks. $value = str_replace("\r\n", "\n", $value); } $hashdata[] = $value; }); sort($hashdata, SORT_STRING); $hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata); if ($transformer) { $hashstring = $transformer->process($hashstring); // Need to re-sort the hashdata with the transformed strings. $hashdata = explode(self::HASHDATA_SEPARATOR, $hashstring); sort($hashdata, SORT_STRING); $hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata); } return sha1($hashstring); } } util/helper/restore_decode_content.class.php 0000644 00000011755 15215711721 0015350 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class in charge of providing the contents to be processed by restore_decode_rules * * This class is in charge of looking (in DB) for the contents needing to be * processed by the declared restore_decode_rules. Basically it iterates over * one recordset (optimised by joining them with backup_ids records), retrieving * them from DB, delegating process to the restore_plan and storing results back * to DB. * * Implements one visitor-like pattern so the decode_processor will visit it * to get all the contents processed by its defined rules * * TODO: Complete phpdocs */ class restore_decode_content implements processable { protected $tablename; // Name, without prefix, of the table we are going to retrieve contents protected $fields; // Array of fields we are going to decode in that table (usually 1) protected $mapping; // Mapping (itemname) in backup_ids used to determine target ids (defaults to $tablename) protected $restoreid; // Unique id of the restore operation we are running protected $iterator; // The iterator for this content (usually one recordset) public function __construct($tablename, $fields, $mapping = null) { // TODO: check table exists // TODO: check fields exist $this->tablename = $tablename; $this->fields = !is_array($fields) ? array($fields) : $fields; // Accept string/array $this->mapping = is_null($mapping) ? $tablename : $mapping; // Default to tableanme $this->restoreid = 0; } public function set_restoreid($restoreid) { $this->restoreid = $restoreid; } public function process($processor) { if (!$processor instanceof restore_decode_processor) { // No correct processor, throw exception throw new restore_decode_content_exception('incorrect_restore_decode_processor', get_class($processor)); } if (!$this->restoreid) { // Check restoreid is set throw new restore_decode_rule_exception('decode_content_restoreid_not_set'); } // Get the iterator of contents $it = $this->get_iterator(); foreach ($it as $itrow) { // Iterate over rows $itrowarr = (array)$itrow; // Array-ize for clean access $rowchanged = false; // To track changes in the row foreach ($this->fields as $field) { // Iterate for each field $content = $this->preprocess_field($itrowarr[$field]); // Apply potential pre-transformations if ($result = $processor->decode_content($content)) { $itrowarr[$field] = $this->postprocess_field($result); // Apply potential post-transformations $rowchanged = true; } } if ($rowchanged) { // Change detected, perform update in the row $this->update_iterator_row($itrowarr); } } $it->close(); // Always close the iterator at the end } // Protected API starts here protected function get_iterator() { global $DB; // Build the SQL dynamically here $fieldslist = 't.' . implode(', t.', $this->fields); $sql = "SELECT t.id, $fieldslist FROM {" . $this->tablename . "} t JOIN {backup_ids_temp} b ON b.newitemid = t.id WHERE b.backupid = ? AND b.itemname = ?"; $params = array($this->restoreid, $this->mapping); return ($DB->get_recordset_sql($sql, $params)); } protected function update_iterator_row($row) { global $DB; $DB->update_record($this->tablename, $row); } protected function preprocess_field($field) { return $field; } protected function postprocess_field($field) { return $field; } } /* * Exception class used by all the @restore_decode_content stuff */ class restore_decode_content_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } util/helper/restore_moodlexml_parser_processor.class.php 0000644 00000004421 15215711721 0020036 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * return all the information present in the moodle_backup.xml file * accumulating it for later generation of controller->info * * TODO: Complete phpdocs */ class restore_moodlexml_parser_processor extends grouped_parser_processor { protected $accumchunks; public function __construct() { $this->accumchunks = array(); parent::__construct(); // Let's add all the paths we are interested on $this->add_path('/moodle_backup/information', true); // Everything will be grouped below this $this->add_path('/moodle_backup/information/details/detail'); $this->add_path('/moodle_backup/information/contents/activities/activity'); $this->add_path('/moodle_backup/information/contents/sections/section'); $this->add_path('/moodle_backup/information/contents/course'); $this->add_path('/moodle_backup/information/settings/setting'); } protected function dispatch_chunk($data) { $this->accumchunks[] = $data; } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } public function get_all_chunks() { return $this->accumchunks; } } util/helper/backup_null_iterator.class.php 0000644 00000003247 15215711721 0015035 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementation of iterator interface to work without information * * This class implementes the iterator but does nothing (as far as it * doesn't handle real data at all). It's here to provide one common * API when we want to skip some elements from structure, while also * working with array/db iterators at the same time. * * TODO: Finish phpdocs */ class backup_null_iterator implements iterator { public function rewind(): void { } #[\ReturnTypeWillChange] public function current() { } #[\ReturnTypeWillChange] public function key() { } public function next(): void { } public function valid(): bool { return false; } public function close() { // Added to provide compatibility with recordset iterators } } util/helper/copy_helper.class.php 0000644 00000032114 15215711721 0013131 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Copy helper class. * * @package core_backup * @copyright 2022 Catalyst IT Australia Pty Ltd * @author Cameron Ball <cameron@cameron1729.xyz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class copy_helper { /** * Process raw form data from copy_form. * * @param \stdClass $formdata Raw formdata * @return \stdClass Processed data for use with create_copy */ public static function process_formdata(\stdClass $formdata): \stdClass { $requiredfields = [ 'courseid', // Course id integer. 'fullname', // Fullname of the destination course. 'shortname', // Shortname of the destination course. 'category', // Category integer ID that contains the destination course. 'visible', // Integer to detrmine of the copied course will be visible. 'startdate', // Integer timestamp of the start of the destination course. 'enddate', // Integer timestamp of the end of the destination course. 'idnumber', // ID of the destination course. 'userdata', // Integer to determine if the copied course will contain user data. ]; $missingfields = array_diff($requiredfields, array_keys((array)$formdata)); if ($missingfields) { throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, implode(", ", $missingfields)); } // Remove any extra stuff in the form data. $processed = (object)array_intersect_key((array)$formdata, array_flip($requiredfields)); $processed->keptroles = []; // Extract roles from the form data and add to keptroles. foreach ($formdata as $key => $value) { if ((substr($key, 0, 5) === 'role_') && ($value != 0)) { $processed->keptroles[] = $value; } } return $processed; } /** * Creates a course copy. * Sets up relevant controllers and adhoc task. * * @param \stdClass $copydata Course copy data from process_formdata * @return array $copyids The backup and restore controller ids */ public static function create_copy(\stdClass $copydata): array { global $USER; $copyids = []; // Create the initial backupcontoller. $bc = new \backup_controller(\backup::TYPE_1COURSE, $copydata->courseid, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES); $copyids['backupid'] = $bc->get_backupid(); // Create the initial restore contoller. list($fullname, $shortname) = \restore_dbops::calculate_course_names( 0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup')); $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $copydata->category); $rc = new \restore_controller($copyids['backupid'], $newcourseid, \backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::TARGET_NEW_COURSE, null, \backup::RELEASESESSION_NO, $copydata); $copyids['restoreid'] = $rc->get_restoreid(); $bc->set_status(\backup::STATUS_AWAITING); $bc->get_status(); $rc->save_controller(); // Create the ad-hoc task to perform the course copy. $asynctask = new \core\task\asynchronous_copy_task(); $asynctask->set_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); } } } } } util/helper/async_helper.class.php 0000644 00000036731 15215711721 0013305 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Helper functions for asynchronous backups and restores. * * @package core * @copyright 2019 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/user/lib.php'); /** * Helper functions for asynchronous backups and restores. * * @package core * @copyright 2019 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class async_helper { /** * @var string $type The type of async operation. */ protected $type = 'backup'; /** * @var string $backupid The id of the backup or restore. */ protected $backupid; /** * @var object $user The user who created the backup record. */ protected $user; /** * @var object $backuprec The backup controller record from the database. */ protected $backuprec; /** * Class constructor. * * @param string $type The type of async operation. * @param string $id The id of the backup or restore. */ public function __construct($type, $id) { $this->type = $type; $this->backupid = $id; $this->backuprec = self::get_backup_record($id); $this->user = $this->get_user(); } /** * Given a backup id return a the record from the database. * We use this method rather than 'load_controller' as the controller may * not exist if this backup/restore has completed. * * @param int $id The backup id to get. * @return object $backuprec The backup controller record. */ 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; } } util/helper/restore_users_parser_processor.class.php 0000644 00000006364 15215711721 0017207 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one users.xml file to the backup_ids table * storing the whole structure there for later processing. * Note: only "needed" users are loaded (must have userref record in backup_ids) * Note: parentitemid will contain the user->contextid * Note: althought included in backup, we don't restore user context ras/caps * in same site they will be already there and it doesn't seem a good idea * to make them "transportable" arround sites. * * TODO: Complete phpdocs */ class restore_users_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Set the paths we are interested on, returning all them grouped under user $this->add_path('/users/user', true); $this->add_path('/users/user/custom_fields/custom_field'); $this->add_path('/users/user/tags/tag'); $this->add_path('/users/user/preferences/preference'); // As noted above, we skip user context ras and caps // $this->add_path('/users/user/roles/role_overrides/override'); // $this->add_path('/users/user/roles/role_assignments/assignment'); } protected function dispatch_chunk($data) { // Received one user chunck, we are going to store it into backup_ids // table, with name = user and parentid = contextid for later use $itemname = 'user'; $itemid = $data['tags']['id']; $parentitemid = $data['tags']['contextid']; $info = $data['tags']; // Only load it if needed (exist same userref itemid in table) if (restore_dbops::get_backup_ids_record($this->restoreid, 'userref', $itemid)) { restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, $parentitemid, $info); } } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } /** * Provide NULL decoding */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } } util/helper/backup_helper.class.php 0000644 00000044067 15215711721 0013436 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * 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); } } util/helper/backup_array_iterator.class.php 0000644 00000003660 15215711721 0015200 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementation of iterator interface to work with common arrays * * This class implements the iterator interface in order to provide one * common API to be used in backup and restore when, within the same code, * both database recordsets (already iteratorors) and arrays of information * are used. * * TODO: Finish phpdocs */ class backup_array_iterator implements iterator { private $arr; public function __construct(array $arr) { $this->arr = $arr; } public function rewind(): void { reset($this->arr); } #[\ReturnTypeWillChange] public function current() { return current($this->arr); } #[\ReturnTypeWillChange] public function key() { return key($this->arr); } public function next(): void { next($this->arr); } public function valid(): bool { return key($this->arr) !== null; } public function close() { // Added to provide compatibility with recordset iterators reset($this->arr); // Just reset the array } } util/helper/restore_structure_parser_processor.class.php 0000644 00000011540 15215711721 0020076 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * support the process of all the moodle2 backup files, with * complete specs about what to load (grouped or no), dispatching * to corresponding methods and basic decoding of contents * (NULLs and legacy file.php uses) * * TODO: Complete phpdocs */ class restore_structure_parser_processor extends grouped_parser_processor { protected $courseid; // Course->id we are restoring to protected $step; // @restore_structure_step using this processor public function __construct($courseid, $step) { $this->courseid = $courseid; $this->step = $step; parent::__construct(); } /** * Provide NULL and legacy file.php uses decoding */ public function process_cdata($cdata) { global $CFG; if ($cdata === '$@NULL@$') { // Some cases we know we can skip complete processing return null; } else if ($cdata === '') { return ''; } else if (is_numeric($cdata)) { return $cdata; } else if (strlen($cdata ?? '') < 32) { // Impossible to have one link in 32cc. // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=). return $cdata; } if (strpos($cdata, '$@FILEPHP@$') !== false) { // We need to convert $@FILEPHP@$. if ($CFG->slasharguments) { $slash = '/'; $forcedownload = '?forcedownload=1'; } else { $slash = '%2F'; $forcedownload = '&forcedownload=1'; } // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash. $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash); // Decode file.php calls. $search = array ("$@FILEPHP@$"); $replace = array($basefileurl); $result = str_replace($search, $replace, $cdata); // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799. $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$'); $replace = array($slash, $forcedownload); $cdata = str_replace($search, $replace, $result); } if (strpos($cdata, '$@H5PEMBED@$') !== false) { // We need to convert $@H5PEMBED@$. // Decode embed.php calls. $cdata = str_replace('$@H5PEMBED@$', $CFG->wwwroot.'/h5p/embed.php', $cdata); } return $cdata; } /** * Override this method so we'll be able to skip * dispatching some well-known chunks, like the * ones being 100% part of subplugins stuff. Useful * for allowing development without having all the * possible restore subplugins defined */ protected function postprocess_chunk($data) { // Iterate over all the data tags, if any of them is // not 'subplugin_XXXX' or has value, then it's a valid chunk, // pass it to standard (parent) processing of chunks. foreach ($data['tags'] as $key => $value) { if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) { parent::postprocess_chunk($data); return; } } // Arrived here, all the tags correspond to sublplugins and are empty, // skip the chunk, and debug_developer notice $this->chunks--; // not counted debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) . ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER); } protected function dispatch_chunk($data) { $this->step->process($data); } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } } util/helper/restore_prechecks_helper.class.php 0000644 00000023074 15215711721 0015676 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Non instantiable helper class providing support for restore prechecks * * This class contains various prechecks to be performed before executing * the restore plan. Its entry point is execute_prechecks() that will * call various stuff. At the end, it will return one array(), if empty * all the prechecks have passed ok. If not empty, you'll find 1/2 elements * in the array, warnings and errors, each one containing one description * of the problem. Warnings aren't stoppers so the restore execution can * continue after displaying them. In the other side, if errors are returned * then restore execution cannot continue * * TODO: Finish phpdocs */ abstract class restore_prechecks_helper { /** * Entry point for all the prechecks to be performed before restore * * Returns empty array or warnings/errors array */ public static function execute_prechecks(restore_controller $controller, $droptemptablesafter = false) { global $CFG; $errors = array(); $warnings = array(); // Some handy vars to be used along the prechecks $samesite = $controller->is_samesite(); $restoreusers = $controller->get_plan()->get_setting('users')->get_value(); $hasmnetusers = (int)$controller->get_info()->mnet_remoteusers; $restoreid = $controller->get_restoreid(); $courseid = $controller->get_courseid(); $userid = $controller->get_userid(); $rolemappings = $controller->get_info()->role_mappings; $progress = $controller->get_progress(); // Start tracking progress. There are currently 8 major steps, corresponding // to $majorstep++ lines in this code; we keep track of the total so as to // verify that it's still correct. If you add a major step, you need to change // the total here. $majorstep = 1; $majorsteps = 8; $progress->start_progress('Carrying out pre-restore checks', $majorsteps); // Load all the included tasks to look for inforef.xml files $inforeffiles = array(); $tasks = restore_dbops::get_included_tasks($restoreid); $progress->start_progress('Listing inforef files', count($tasks)); $minorstep = 1; foreach ($tasks as $task) { // Add the inforef.xml file if exists $inforefpath = $task->get_taskbasepath() . '/inforef.xml'; if (file_exists($inforefpath)) { $inforeffiles[] = $inforefpath; } $progress->progress($minorstep++); } $progress->end_progress(); $progress->progress($majorstep++); // Create temp tables restore_controller_dbops::create_restore_temp_tables($controller->get_restoreid()); // Check we are restoring one backup >= $min20version (very first ok ever) $min20version = 2010072300; if ($controller->get_info()->backup_version < $min20version) { $message = new stdclass(); $message->backup = $controller->get_info()->backup_version; $message->min = $min20version; $errors[] = get_string('errorminbackup20version', 'backup', $message); } // Compare Moodle's versions if ($CFG->version < $controller->get_info()->moodle_version) { $message = new stdclass(); $message->serverversion = $CFG->version; $message->serverrelease = $CFG->release; $message->backupversion = $controller->get_info()->moodle_version; $message->backuprelease = $controller->get_info()->moodle_release; $warnings[] = get_string('noticenewerbackup','',$message); } // The original_course_format var was introduced in Moodle 2.9. $originalcourseformat = null; if (!empty($controller->get_info()->original_course_format)) { $originalcourseformat = $controller->get_info()->original_course_format; } // We can't restore other course's backups on the front page. if ($controller->get_courseid() == SITEID && $originalcourseformat != 'site' && $controller->get_type() == backup::TYPE_1COURSE) { $errors[] = get_string('errorrestorefrontpagebackup', 'backup'); } // We can't restore front pages over other courses. if ($controller->get_courseid() != SITEID && $originalcourseformat == 'site' && $controller->get_type() == backup::TYPE_1COURSE) { $errors[] = get_string('errorrestorefrontpagebackup', 'backup'); } // If restoring to different site and restoring users and backup has mnet users warn/error if (!$samesite && $restoreusers && $hasmnetusers) { // User is admin (can create users at sysctx), warn if (has_capability('moodle/user:create', context_system::instance(), $controller->get_userid())) { $warnings[] = get_string('mnetrestore_extusers_admin', 'admin'); // User not admin } else { $errors[] = get_string('mnetrestore_extusers_noadmin', 'admin'); } } // Load all the inforef files, we are going to need them $progress->start_progress('Loading temporary IDs', count($inforeffiles)); $minorstep = 1; foreach ($inforeffiles as $inforeffile) { // Load each inforef file to temp_ids. restore_dbops::load_inforef_to_tempids($restoreid, $inforeffile, $progress); $progress->progress($minorstep++); } $progress->end_progress(); $progress->progress($majorstep++); // If restoring users, check we are able to create all them if ($restoreusers) { $file = $controller->get_plan()->get_basepath() . '/users.xml'; // Load needed users to temp_ids. restore_dbops::load_users_to_tempids($restoreid, $file, $progress); $progress->progress($majorstep++); if ($problems = restore_dbops::precheck_included_users($restoreid, $courseid, $userid, $samesite, $progress)) { $errors = array_merge($errors, $problems); } } else { // To ensure consistent number of steps in progress tracking, // mark progress even though we didn't do anything. $progress->progress($majorstep++); } $progress->progress($majorstep++); // Note: restore won't create roles at all. Only mapping/skip! $file = $controller->get_plan()->get_basepath() . '/roles.xml'; restore_dbops::load_roles_to_tempids($restoreid, $file); // Load needed roles to temp_ids if ($problems = restore_dbops::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings)) { $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } $progress->progress($majorstep++); // Check we are able to restore and the categories and questions $file = $controller->get_plan()->get_basepath() . '/questions.xml'; restore_dbops::load_categories_and_questions_to_tempids($restoreid, $file); if ($problems = restore_dbops::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite)) { $errors = array_key_exists('errors', $problems) ? array_merge($errors, $problems['errors']) : $errors; $warnings = array_key_exists('warnings', $problems) ? array_merge($warnings, $problems['warnings']) : $warnings; } $progress->progress($majorstep++); // Prepare results. $results = array(); if (!empty($errors)) { $results['errors'] = $errors; } if (!empty($warnings)) { $results['warnings'] = $warnings; } // Warnings/errors detected or want to do so explicitly, drop temp tables if (!empty($results) || $droptemptablesafter) { restore_controller_dbops::drop_restore_temp_tables($controller->get_restoreid()); } // Finish progress and check we got the initial number of steps right. $progress->progress($majorstep++); if ($majorstep != $majorsteps) { throw new coding_exception('Progress step count wrong: ' . $majorstep); } $progress->end_progress(); return $results; } } /* * Exception class used by all the @restore_prechecks_helper stuff */ class restore_prechecks_helper_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/helper/restore_roles_parser_processor.class.php 0000644 00000005044 15215711721 0017164 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one roles.xml (roles description) file to the backup_ids table * storing the whole structure there for later processing. * Note: only "needed" roles are loaded (must have roleref record in backup_ids) * * TODO: Complete phpdocs */ class restore_roles_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Set the paths we are interested on, returning all them grouped under user $this->add_path('/roles_definition/role'); } protected function dispatch_chunk($data) { // Received one role chunck, we are going to store it into backup_ids // table, with name = role $itemname = 'role'; $itemid = $data['tags']['id']; $info = $data['tags']; // Only load it if needed (exist same roleref itemid in table) if (restore_dbops::get_backup_ids_record($this->restoreid, 'roleref', $itemid)) { restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid, 0, null, $info); } } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } /** * Provide NULL decoding */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } } util/helper/tests/copy_helper_test.php 0000644 00000102011 15215711721 0014220 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->libdir . '/completionlib.php'); /** * Course copy tests. * * @package core_backup * @copyright 2020 onward The Moodle Users Association <https://moodleassociation.org/> * @author Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \copy_helper */ final 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; parent::setUp(); $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); } } util/helper/tests/backup_encode_content_test.php 0000644 00000007406 15215711721 0016237 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup_course_task; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_course_task.class.php'); /** * Tests for encoding content links in backup_course_task. * * The code that this tests is actually in backup/moodle2/backup_course_task.class.php, * but there is no place for unit tests near there, and perhaps one day it will * be refactored so it becomes more generic. * * @package core_backup * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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 . '/course/section.php?id=123, ' . $httpsroot . '/grade/index.php?id=123, ' . $httpsroot . '/grade/report/index.php?id=123, ' . $httpsroot . '/badges/index.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@$, ' . '$@COURSESECTIONBYID*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 . '/course/section.php?id=123, ' . $httproot . '/grade/index.php?id=123, ' . $httproot . '/grade/report/index.php?id=123, ' . $httproot . '/badges/index.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@$, ' . '$@COURSESECTIONBYID*123@$, $@GRADEINDEXBYID*123@$, ' . '$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' . '$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded); $CFG->wwwroot = $oldroot; } } util/helper/tests/decode_test.php 0000644 00000017634 15215711721 0013152 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup; use restore_decode_rule; use restore_decode_rule_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Restore_decode tests (both rule and content) * * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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; } } util/helper/tests/restore_log_rule_test.php 0000644 00000004757 15215711721 0015304 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use restore_log_rule; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Test the backup and restore of logs using rules. * * @package core_backup * @category test * @copyright 2015 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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)); } } util/helper/tests/converterhelper_test.php 0000644 00000012773 15215711721 0015135 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Test the convert helper. * * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup; use backup; use convert_helper; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); /** * Test the convert helper. * * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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); } } util/helper/tests/restore_structure_parser_processor_test.php 0000644 00000011330 15215711721 0021170 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Tests for restore_structure_parser_processor class. * * @package core_backup * @category test * @copyright 2017 Dmitrii Metelkin (dmitriim@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/helper/restore_structure_parser_processor.class.php'); /** * Tests for restore_structure_parser_processor class. * * @package core_backup * @copyright 2017 Dmitrii Metelkin (dmitriim@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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 static function process_cdata_data_provider(): array { return array( array(null, null, true), array("$@NULL@$", null, true), array("$@NULL@$ ", "$@NULL@$ ", true), array(1, 1, true), array(" ", " ", true), array("1", "1", true), array("$@FILEPHP@$1.jpg", "$@FILEPHP@$1.jpg", true), array( "http://test.test/$@SLASH@$", "http://test.test/$@SLASH@$", true ), array( "<a href='$@FILEPHP@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/11.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/1/1.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php/1//1.jpg'>Image</a>", true ), array( "<a href='$@FILEPHP@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F11.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F1.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F%2F1.jpg'>Image</a>", false ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg$@FORCEDOWNLOAD@$'>Image</a>", "<a href='http://test.test/file.php/1/1.jpg?forcedownload=1'>Image</a>", true ), array( "<a href='$@FILEPHP@$$@SLASH@$1.jpg$@FORCEDOWNLOAD@$'>Image</a>", "<a href='http://test.test/file.php?file=%2F1%2F1.jpg&forcedownload=1'>Image</a>", false ), array( "<iframe src='$@H5PEMBED@$?url=testurl'></iframe>", "<iframe src='http://test.test/h5p/embed.php?url=testurl'></iframe>", true ), ); } /** * Test that restore_structure_parser_processor replaces $@FILEPHP@$ to correct file php links. * * @dataProvider process_cdata_data_provider * @param string $content Testing content. * @param string $expected Expected result. * @param bool $slasharguments A value for $CFG->slasharguments setting. */ public function test_process_cdata($content, $expected, $slasharguments): 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)); } } util/helper/tests/helper_test.php 0000644 00000003075 15215711721 0013200 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. global $CFG; require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php'); require_once($CFG->dirroot . '/backup/util/helper/backup_general_helper.class.php'); /** * backup_helper tests (all) * * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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 { } } util/helper/tests/async_helper_test.php 0000644 00000022071 15215711721 0014372 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use async_helper; use backup; use backup_controller; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Asyncronhous helper tests. * * @package core_backup * @covers \async_helper * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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); } } util/helper/tests/cronhelper_test.php 0000644 00000055107 15215711721 0014065 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Unit tests for backups cron helper. * * @package core_backup * @category test * @copyright 2012 Frédéric Massart <fred@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup; use backup; use backup_cron_automated_helper; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php'); require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once("$CFG->dirroot/backup/backup.class.php"); /** * Unit tests for backups cron helper. * * @package core_backup * @category test * @copyright 2012 Frédéric Massart <fred@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final 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); } } util/helper/restore_log_rule.class.php 0000644 00000023511 15215711721 0014174 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class used to restore logs, converting all the information as needed * * This class allows each restore task to specify which logs (by action) will * be handled on restore and which transformations will be performed in order * to accomodate them into their new destination * * TODO: Complete phpdocs */ class restore_log_rule implements processable { protected $module; // module of the log record protected $action; // action of the log record protected $urlread; // url format of the log record in backup file protected $inforead; // info format of the log record in backup file protected $modulewrite;// module of the log record to be written (defaults to $module if not specified) protected $actionwrite;// action of the log record to be written (defaults to $action if not specified) protected $urlwrite; // url format of the log record to be written (defaults to $urlread if not specified) protected $infowrite;// info format of the log record to be written (defaults to $inforead if not specified) protected $urlreadregexp; // Regexps for extracting information from url and info protected $inforeadregexp; protected $allpairs; // to acummulate all tokens and values pairs on each log record restored protected $urltokens; // tokens present int the $urlread attribute protected $infotokens;// tokens present in the $inforead attribute protected $fixedvalues; // Some values that will have precedence over mappings to save tons of DB mappings protected $restoreid; public function __construct($module, $action, $urlread, $inforead, $modulewrite = null, $actionwrite = null, $urlwrite = null, $infowrite = null) { $this->module = $module; $this->action = $action; $this->urlread = $urlread; $this->inforead = $inforead; $this->modulewrite = is_null($modulewrite) ? $module : $modulewrite; $this->actionwrite= is_null($actionwrite) ? $action : $actionwrite; $this->urlwrite = is_null($urlwrite) ? $urlread : $urlwrite; $this->infowrite= is_null($infowrite) ? $inforead : $infowrite; $this->allpairs = array(); $this->urltokens = array(); $this->infotokens= array(); $this->urlreadregexp = null; $this->inforeadregexp = null; $this->fixedvalues = array(); $this->restoreid = null; // TODO: validate module, action are valid => exception // Calculate regexps and tokens, both for urlread and inforead $this->calculate_url_regexp($this->urlread); $this->calculate_info_regexp($this->inforead); } public function set_restoreid($restoreid) { $this->restoreid = $restoreid; } public function set_fixed_values($values) { //TODO: check $values is array => exception $this->fixedvalues = $values; } public function get_key_name() { return $this->module . '-' . $this->action; } public function process($inputlog) { // There might be multiple rules that process this log, we can't alter it in the process of checking it. $log = clone($inputlog); // Reset the allpairs array $this->allpairs = array(); $urlmatches = array(); $infomatches = array(); // Apply urlreadregexp to the $log->url if necessary if ($this->urlreadregexp) { preg_match($this->urlreadregexp, $log->url, $urlmatches); if (empty($urlmatches)) { return false; } } else { if (!is_null($this->urlread)) { // If not null, use it (null means unmodified) $log->url = $this->urlread; } } // Apply inforeadregexp to the $log->info if necessary if ($this->inforeadregexp) { preg_match($this->inforeadregexp, $log->info, $infomatches); if (empty($infomatches)) { return false; } } else { if (!is_null($this->inforead)) { // If not null, use it (null means unmodified) $log->info = $this->inforead; } } // If there are $urlmatches, let's process them if (!empty($urlmatches)) { array_shift($urlmatches); // Take out first element if (count($urlmatches) !== count($this->urltokens)) { // Number of matches must be number of tokens return false; } // Let's process all the tokens and matches, using them to parse the urlwrite $log->url = $this->parse_tokens_and_matches($this->urltokens, $urlmatches, $this->urlwrite); } // If there are $infomatches, let's process them if (!empty($infomatches)) { array_shift($infomatches); // Take out first element if (count($infomatches) !== count($this->infotokens)) { // Number of matches must be number of tokens return false; } // Let's process all the tokens and matches, using them to parse the infowrite $log->info = $this->parse_tokens_and_matches($this->infotokens, $infomatches, $this->infowrite); } // Arrived here, if there is any pending token in $log->url or $log->info, stop if ($this->extract_tokens($log->url) || $this->extract_tokens($log->info)) { return false; } // Finally, set module and action $log->module = $this->modulewrite; $log->action = $this->actionwrite; return $log; } // Protected API starts here protected function parse_tokens_and_matches($tokens, $values, $content) { $pairs = array_combine($tokens, $values); ksort($pairs); // First literals, then mappings foreach ($pairs as $token => $value) { // If one token has already been processed, continue if (array_key_exists($token, $this->allpairs)) { continue; } // If the pair is one literal token, just keep it unmodified if (substr($token, 0, 1) == '[') { $this->allpairs[$token] = $value; // If the pair is one mapping token, let's process it } else if (substr($token, 0, 1) == '{') { $ctoken = $token; // First, resolve mappings to literals if necessary if (substr($token, 1, 1) == '[') { $literaltoken = trim($token, '{}'); if (array_key_exists($literaltoken, $this->allpairs)) { $ctoken = '{' . $this->allpairs[$literaltoken] . '}'; } } // Look for mapping in fixedvalues before going to DB $plaintoken = trim($ctoken, '{}'); if (array_key_exists($plaintoken, $this->fixedvalues)) { $this->allpairs[$token] = $this->fixedvalues[$plaintoken]; // Last chance, fetch value from backup_ids_temp, via mapping } else { if ($mapping = restore_dbops::get_backup_ids_record($this->restoreid, $plaintoken, $value)) { $this->allpairs[$token] = $mapping->newitemid; } } } } // Apply all the conversions array (allpairs) to content krsort($this->allpairs); // First mappings, then literals $content = str_replace(array_keys($this->allpairs), $this->allpairs, $content); return $content; } protected function calculate_url_regexp($urlexpression) { // Detect all the tokens in the expression if ($tokens = $this->extract_tokens($urlexpression)) { $this->urltokens = $tokens; // Now, build the regexp $this->urlreadregexp = $this->build_regexp($urlexpression, $this->urltokens); } } protected function calculate_info_regexp($infoexpression) { // Detect all the tokens in the expression if ($tokens = $this->extract_tokens($infoexpression)) { $this->infotokens = $tokens; // Now, build the regexp $this->inforeadregexp = $this->build_regexp($infoexpression, $this->infotokens); } } protected function extract_tokens($expression) { // Extract all the tokens enclosed in square and curly brackets preg_match_all('~\[[^\]]+\]|\{[^\}]+\}~', $expression, $matches); return $matches[0]; } protected function build_regexp($expression, $tokens) { // Replace to temp (and preg_quote() safe) placeholders foreach ($tokens as $token) { $expression = preg_replace('~' . preg_quote($token, '~') . '~', '%@@%@@%', $expression, 1); } // quote the expression $expression = preg_quote($expression, '~'); // Replace all the placeholders $expression = preg_replace('~%@@%@@%~', '(.*)', $expression); return '~' . $expression . '~'; } } util/helper/restore_logs_processor.class.php 0000644 00000013364 15215711721 0015434 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * TODO: Finish phpdocs */ /** * This class is one varying singleton that, for all the logs corresponding to * one task, is in charge of storing all its {@link restore_log_rule} rules, * dispatching to the correct one and insert/log the resulting information. * * Each time the task getting the instance changes, the rules are completely * reloaded with the ones in the new task. And all rules are informed with * new fixed values if explicity set. * * This class adopts the singleton pattern to be able to provide some persistency * of rules along the restore of all the logs corresponding to one restore_task */ class restore_logs_processor { private static $instance; // The current instance of restore_logs_processor private static $task; // The current restore_task instance this processor belongs to private $rules; // Array of restore_log_rule rules (module-action being keys), supports multiple per key private function __construct($values) { // Private constructor // Constructor has been called, so we need to reload everything // Process rules $this->rules = array(); $rules = call_user_func(array(self::$task, 'define_restore_log_rules')); foreach ($rules as $rule) { // TODO: Check it is one restore_log_rule // Set rule restoreid $rule->set_restoreid(self::$task->get_restoreid()); // Set rule fixed values if needed if (is_array($values) and !empty($values)) { $rule->set_fixed_values($values); } // Add the rule to the associative array if (array_key_exists($rule->get_key_name(), $this->rules)) { $this->rules[$rule->get_key_name()][] = $rule; } else { $this->rules[$rule->get_key_name()] = array($rule); } } } public static function get_instance($task, $values) { // If the singleton isn't set or if the task is another one, create new instance if (!isset(self::$instance) || self::$task !== $task) { self::$task = $task; self::$instance = new restore_logs_processor($values); } return self::$instance; } public function process_log_record($log) { // Check we have one restore_log_rule for this log record $keyname = $log->module . '-' . $log->action; if (array_key_exists($keyname, $this->rules)) { // Try it for each rule available foreach ($this->rules[$keyname] as $rule) { $newlog = $rule->process($log); // Some rule has been able to perform the conversion, exit from loop if (!empty($newlog)) { break; } } // Arrived here log is empty, no rule was able to perform the conversion, log the problem if (empty($newlog)) { self::$task->log('Log module-action "' . $keyname . '" process problem. Not restored. ' . json_encode($log), backup::LOG_DEBUG); } } else { // Action not found log the problem self::$task->log('Log module-action "' . $keyname . '" unknown. Not restored. '.json_encode($log), backup::LOG_DEBUG); $newlog = false; } return $newlog; } /** * Adds all the activity {@link restore_log_rule} rules * defined in activity task but corresponding to log * records at course level (cmid = 0). */ public static function register_log_rules_for_course() { $tasks = array(); // To get the list of tasks having log rules for course $rules = array(); // To accumulate rules for course // Add the module tasks $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { if (class_exists('restore_' . $mod . '_activity_task')) { $tasks[$mod] = 'restore_' . $mod . '_activity_task'; } } foreach ($tasks as $mod => $classname) { if (!method_exists($classname, 'define_restore_log_rules_for_course')) { continue; // This method is optional } // Get restore_log_rule array and accumulate $taskrules = call_user_func(array($classname, 'define_restore_log_rules_for_course')); if (!is_array($taskrules)) { throw new restore_logs_processor_exception('define_restore_log_rules_for_course_not_array', $classname); } $rules = array_merge($rules, $taskrules); } return $rules; } } /* * Exception class used by all the @restore_logs_processor stuff */ class restore_logs_processor_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } util/helper/backup_cron_helper.class.php 0000644 00000101466 15215711721 0014454 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Utility helper for automated backups run through cron. * * 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; } } util/helper/restore_decode_rule.class.php 0000644 00000022116 15215711721 0014636 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class used to decode links back to their original form * * This class allows each restore task to specify the changes that * will be applied to any encoded (by backup) link to revert it back * to its original form, recoding any parameter as needed. * * TODO: Complete phpdocs */ class restore_decode_rule { protected $linkname; // How the link has been encoded in backup (CHOICEVIEWBYID, COURSEVIEWBYID...) protected $urltemplate; // How the original URL looks like, with dollar placeholders protected $mappings; // Which backup_ids mappings do we need to apply for replacing the placeholders protected $restoreid; // The unique restoreid we are executing protected $sourcewwwroot; // The original wwwroot of the backup file protected $targetwwwroot; // The targer wwwroot of the restore operation protected $cregexp; // Calculated regular expresion we'll be looking for matches /** @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); } } util/helper/restore_decode_processor.class.php 0000644 00000015451 15215711721 0015712 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Helper class that will perform all the necessary decoding tasks on restore * * This class will register all the restore_decode_content and * restore_decode_rule instances defined by the restore tasks * in order to perform the complete decoding of links in the * final task of the restore_plan execution. * * By visiting each content provider will apply all the defined rules * * TODO: Complete phpdocs */ class restore_decode_processor { protected $contents; // Array of restore_decode_content providers protected $rules; // Array of restore_decode_rule workers protected $restoreid; // The unique restoreid we are executing protected $sourcewwwroot; // The original wwwroot of the backup file protected $targetwwwroot; // The target wwwroot of the restore operation public function __construct($restoreid, $sourcewwwroot, $targetwwwroot) { $this->restoreid = $restoreid; $this->sourcewwwroot = $sourcewwwroot; $this->targetwwwroot = $targetwwwroot; $this->contents = array(); $this->rules = array(); } public function add_content($content) { if (!$content instanceof restore_decode_content) { throw new restore_decode_processor_exception('incorrect_restore_decode_content', get_class($content)); } $content->set_restoreid($this->restoreid); $this->contents[] = $content; } public function add_rule($rule) { if (!$rule instanceof restore_decode_rule) { throw new restore_decode_processor_exception('incorrect_restore_decode_rule', get_class($rule)); } $rule->set_restoreid($this->restoreid); $rule->set_wwwroots($this->sourcewwwroot, $this->targetwwwroot); $this->rules[] = $rule; } /** * Visit all the restore_decode_content providers * that will cause decode_content() to be called * for each content */ public function execute() { // Iterate over all contents, visiting them /** @var restore_decode_content $content */ foreach ($this->contents as $content) { $content->process($this); } } /** * Receive content from restore_decode_content objects * and apply all the restore_decode_rules to them */ public function decode_content($content) { if (!$content = $this->precheck_content($content)) { // Perform some prechecks return false; } // Iterate over all rules, chaining results foreach ($this->rules as $rule) { $content = $rule->decode($content); } return $content; } /** * Adds all the course/section/activity/block contents and rules */ public static function register_link_decoders($processor) { $tasks = array(); // To get the list of tasks having decoders // Add the course task $tasks[] = 'restore_course_task'; // Add the section task $tasks[] = 'restore_section_task'; // Add the module tasks $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { if (class_exists('restore_' . $mod . '_activity_task')) { $tasks[] = 'restore_' . $mod . '_activity_task'; } } // Add the default block task $tasks[] = 'restore_default_block_task'; // Add the custom block tasks $blocks = core_component::get_plugin_list('block'); foreach ($blocks as $block => $blockdir) { if (class_exists('restore_' . $block . '_block_task')) { $tasks[] = 'restore_' . $block . '_block_task'; } } // We have all the tasks registered, let's iterate over them, getting // contents and rules and adding them to the processor foreach ($tasks as $classname) { // Get restore_decode_content array and add to processor $contents = call_user_func(array($classname, 'define_decode_contents')); if (!is_array($contents)) { throw new restore_decode_processor_exception('define_decode_contents_not_array', $classname); } foreach ($contents as $content) { $processor->add_content($content); } // Get restore_decode_rule array and add to processor $rules = call_user_func(array($classname, 'define_decode_rules')); if (!is_array($rules)) { throw new restore_decode_processor_exception('define_decode_rules_not_array', $classname); } foreach ($rules as $rule) { $processor->add_rule($rule); } } // Now process all the plugins contents (note plugins don't have support for rules) // TODO: Add other plugin types (course formats, local...) here if we add them to backup/restore $plugins = array('qtype'); foreach ($plugins as $plugin) { $contents = restore_plugin::get_restore_decode_contents($plugin); if (!is_array($contents)) { throw new restore_decode_processor_exception('get_restore_decode_contents_not_array', $plugin); } foreach ($contents as $content) { $processor->add_content($content); } } } // Protected API starts here /** * Perform some general checks in content. Returning false rules processing is skipped */ protected function precheck_content($content) { // Look for $@ in content (all interlinks contain that) return (strpos($content ?? '', '$@') === false) ? false : $content; } } /* * Exception class used by all the @restore_decode_content stuff */ class restore_decode_processor_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { return parent::__construct($errorcode, $a, $debuginfo); } } util/helper/backup_general_helper.class.php 0000644 00000033525 15215711721 0015130 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Non instantiable helper class providing general helper methods for backup/restore * * This class contains various general helper static methods available for backup/restore * * TODO: Finish phpdocs */ abstract class backup_general_helper extends backup_helper { /** * Calculate one checksum for any array/object. Works recursively */ public static function array_checksum_recursive($arr) { $checksum = ''; // Init checksum // Check we are going to process one array always, objects must be cast before if (!is_array($arr)) { throw new backup_helper_exception('array_expected'); } foreach ($arr as $key => $value) { if ($value instanceof checksumable) { $checksum = md5($checksum . '-' . $key . '-' . $value->calculate_checksum()); } else if (is_object($value)) { $checksum = md5($checksum . '-' . $key . '-' . self::array_checksum_recursive((array)$value)); } else if (is_array($value)) { $checksum = md5($checksum . '-' . $key . '-' . self::array_checksum_recursive($value)); } else { $checksum = md5($checksum . '-' . $key . '-' . $value); } } return $checksum; } /** * Load all the blocks information needed for a given path within moodle2 backup * * This function, given one full path (course, activities/xxxx) will look for all the * blocks existing in the backup file, returning one array used to build the * proper restore plan by the @restore_plan_builder */ public static function get_blocks_from_path($path) { global $DB; $blocks = array(); // To return results static $availableblocks = array(); // Get and cache available blocks if (empty($availableblocks)) { $availableblocks = array_keys(core_component::get_plugin_list('block')); } $path = $path . '/blocks'; // Always look under blocks subdir if (!is_dir($path)) { return array(); } if (!$dir = opendir($path)) { return array(); } while (false !== ($file = readdir($dir))) { if ($file == '.' || $file == '..') { // Skip dots continue; } if (is_dir($path .'/' . $file)) { // Dir found, check it's a valid block if (!file_exists($path .'/' . $file . '/block.xml')) { // Skip if xml file not found continue; } // Extract block name $blockname = preg_replace('/(.*)_\d+/', '\\1', $file); // Check block exists and is installed if (in_array($blockname, $availableblocks) && $DB->record_exists('block', array('name' => $blockname))) { $blocks[$path .'/' . $file] = $blockname; } } } closedir($dir); return $blocks; } /** * Load and format all the needed information from moodle_backup.xml * * This function loads and process all the moodle_backup.xml * information, composing a big information structure that will * be the used by the plan builder in order to generate the * appropiate tasks / steps / settings */ public static function get_backup_information($tempdir) { global $CFG; // Make a request cache and store the data in there. static $cachesha1 = null; static $cache = null; $info = new stdclass(); // Final information goes here $backuptempdir = make_backup_temp_directory('', false); $moodlefile = $backuptempdir . '/' . $tempdir . '/moodle_backup.xml'; if (!file_exists($moodlefile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_moodle_backup_xml_file', $moodlefile); } $moodlefilesha1 = sha1_file($moodlefile); if ($moodlefilesha1 === $cachesha1) { return clone $cache; } // Load the entire file to in-memory array $xmlparser = new progressive_parser(); $xmlparser->set_file($moodlefile); $xmlprocessor = new restore_moodlexml_parser_processor(); $xmlparser->set_processor($xmlprocessor); $xmlparser->process(); $infoarr = $xmlprocessor->get_all_chunks(); if (count($infoarr) !== 1) { // Shouldn't happen ever, but... throw new backup_helper_exception('problem_parsing_moodle_backup_xml_file'); } $infoarr = $infoarr[0]['tags']; // for commodity // Let's build info $info->moodle_version = $infoarr['moodle_version']; $info->moodle_release = $infoarr['moodle_release']; $info->backup_version = $infoarr['backup_version']; $info->backup_release = $infoarr['backup_release']; $info->backup_date = $infoarr['backup_date']; $info->mnet_remoteusers = $infoarr['mnet_remoteusers']; $info->original_wwwroot = $infoarr['original_wwwroot']; $info->original_site_identifier_hash = $infoarr['original_site_identifier_hash']; $info->original_course_id = $infoarr['original_course_id']; $info->original_course_fullname = $infoarr['original_course_fullname']; $info->original_course_shortname = $infoarr['original_course_shortname']; $info->original_course_startdate = $infoarr['original_course_startdate']; // Old versions may not have this. if (isset($infoarr['original_course_enddate'])) { $info->original_course_enddate = $infoarr['original_course_enddate']; } $info->original_course_contextid = $infoarr['original_course_contextid']; $info->original_system_contextid = $infoarr['original_system_contextid']; // Moodle backup file don't have this option before 2.3 if (!empty($infoarr['include_file_references_to_external_content'])) { $info->include_file_references_to_external_content = 1; } else { $info->include_file_references_to_external_content = 0; } // Introduced in Moodle 2.9. $info->original_course_format = ''; if (!empty($infoarr['original_course_format'])) { $info->original_course_format = $infoarr['original_course_format']; } // include_files is a new setting in 2.6. if (isset($infoarr['include_files'])) { $info->include_files = $infoarr['include_files']; } else { $info->include_files = 1; } $info->type = $infoarr['details']['detail'][0]['type']; $info->format = $infoarr['details']['detail'][0]['format']; $info->mode = $infoarr['details']['detail'][0]['mode']; // Build the role mappings custom object $rolemappings = new stdclass(); $rolemappings->modified = false; $rolemappings->mappings = array(); $info->role_mappings = $rolemappings; // Some initially empty containers $info->sections = array(); $info->activities = array(); // Now the contents $contentsarr = $infoarr['contents']; if (isset($contentsarr['course']) && isset($contentsarr['course'][0])) { $info->course = new stdclass(); $info->course = (object)$contentsarr['course'][0]; $info->course->settings = array(); } if (isset($contentsarr['sections']) && isset($contentsarr['sections']['section'])) { $sectionarr = $contentsarr['sections']['section']; foreach ($sectionarr as $section) { $section = (object)$section; $section->settings = array(); $sections[basename($section->directory)] = $section; } $info->sections = $sections; } if (isset($contentsarr['activities']) && isset($contentsarr['activities']['activity'])) { $activityarr = $contentsarr['activities']['activity']; foreach ($activityarr as $activity) { $activity = (object)$activity; $activity->settings = array(); $activities[basename($activity->directory)] = $activity; } $info->activities = $activities; } $info->root_settings = array(); // For root settings // Now the settings, putting each one under its owner $settingsarr = $infoarr['settings']['setting']; foreach($settingsarr as $setting) { switch ($setting['level']) { case 'root': $info->root_settings[$setting['name']] = $setting['value']; break; case 'course': $info->course->settings[$setting['name']] = $setting['value']; break; case 'section': $info->sections[$setting['section']]->settings[$setting['name']] = $setting['value']; break; case 'activity': $info->activities[$setting['activity']]->settings[$setting['name']] = $setting['value']; break; default: // Shouldn't happen but tolerated for portability of customized backups. debugging("Unknown backup setting level: {$setting['level']}", DEBUG_DEVELOPER); break; } } $cache = clone $info; $cachesha1 = $moodlefilesha1; return $info; } /** * Load and format all the needed information from a backup file. * * This will only extract the moodle_backup.xml file from an MBZ * file and then call {@link self::get_backup_information()}. * * This can be a long-running (multi-minute) operation for large backups. * Pass a $progress value to receive progress updates. * * @param string $filepath absolute path to the MBZ file. * @param file_progress $progress Progress updates * @return stdClass containing information. * @since Moodle 2.4 */ public static function get_backup_information_from_mbz($filepath, ?file_progress $progress = null) { global $CFG; if (!is_readable($filepath)) { throw new backup_helper_exception('missing_moodle_backup_file', $filepath); } // Extract moodle_backup.xml. $tmpname = 'info_from_mbz_' . time() . '_' . random_string(4); $tmpdir = make_backup_temp_directory($tmpname); $fp = get_file_packer('application/vnd.moodle.backup'); $extracted = $fp->extract_to_pathname($filepath, $tmpdir, array('moodle_backup.xml'), $progress); $moodlefile = $tmpdir . '/' . 'moodle_backup.xml'; if (!$extracted || !is_readable($moodlefile)) { throw new backup_helper_exception('missing_moodle_backup_xml_file', $moodlefile); } // Read the information and delete the temporary directory. $info = self::get_backup_information($tmpname); remove_dir($tmpdir); return $info; } /** * Given the information fetched from moodle_backup.xml file * decide if we are restoring in the same site the backup was * generated or no. Behavior of various parts of restore are * dependent of this. * * Backups created natively in 2.0 and later declare the hashed * site identifier. Backups created by conversion from a 1.9 * backup do not declare such identifier, so there is a fallback * to wwwroot comparison. See MDL-16614. */ public static function backup_is_samesite($info) { global $CFG; $hashedsiteid = md5(get_site_identifier()); if (isset($info->original_site_identifier_hash) && !empty($info->original_site_identifier_hash)) { return $info->original_site_identifier_hash == $hashedsiteid; } else { return $info->original_wwwroot == $CFG->wwwroot; } } /** * Detects the format of the given unpacked backup directory * * @param string $tempdir the name of the backup directory * @return string one of backup::FORMAT_xxx constants */ public static function detect_backup_format($tempdir) { global $CFG; require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); if (convert_helper::detect_moodle2_format($tempdir)) { return backup::FORMAT_MOODLE; } // see if a converter can identify the format $converters = convert_helper::available_converters(); foreach ($converters as $name) { $classname = "{$name}_converter"; if (!class_exists($classname)) { throw new coding_exception("available_converters() is supposed to load converter classes but class $classname not found"); } $detected = call_user_func($classname .'::detect_format', $tempdir); if (!empty($detected)) { return $detected; } } return backup::FORMAT_UNKNOWN; } } util/helper/restore_inforef_parser_processor.class.php 0000644 00000004343 15215711721 0017471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot.'/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * helper implementation of grouped_parser_processor that will * load all the contents of one inforef.xml file to the backup_ids table * * TODO: Complete phpdocs */ class restore_inforef_parser_processor extends grouped_parser_processor { protected $restoreid; public function __construct($restoreid) { $this->restoreid = $restoreid; parent::__construct(array()); // Get itemnames handled by inforef files $items = backup_helper::get_inforef_itemnames(); // Let's add all them as target paths for the processor foreach($items as $itemname) { $pathvalue = '/inforef/' . $itemname . 'ref/' . $itemname; $this->add_path($pathvalue); } } protected function dispatch_chunk($data) { // Received one inforef chunck, we are going to store it into backup_ids // table, with name = itemname + "ref" for later use $itemname = basename($data['path']). 'ref'; $itemid = $data['tags']['id']; restore_dbops::set_backup_ids_record($this->restoreid, $itemname, $itemid); } protected function notify_path_start($path) { // nothing to do } protected function notify_path_end($path) { // nothing to do } } util/helper/convert_helper.class.php 0000644 00000034300 15215711721 0013636 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides {@link convert_helper} and {@link convert_helper_exception} classes * * @package core * @subpackage backup-convert * @copyright 2011 Mark Nielsen <mark@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); /** * Provides various functionality via its static methods */ abstract class convert_helper { /** * @param string $entropy * @return string random identifier */ public static function generate_id($entropy) { return md5(time() . '-' . $entropy . '-' . random_string(20)); } /** * Returns the list of all available converters and loads their classes * * Converter must be installed as a directory in backup/converter/ and its * method is_available() must return true to get to the list. * * @see base_converter::is_available() * @return array of strings */ public static function available_converters($restore=true) { global $CFG; $converters = array(); $plugins = get_list_of_plugins('backup/converter'); foreach ($plugins as $name) { $filename = $restore ? 'lib.php' : 'backuplib.php'; $classuf = $restore ? '_converter' : '_export_converter'; $classfile = "{$CFG->dirroot}/backup/converter/{$name}/{$filename}"; $classname = "{$name}{$classuf}"; $zip_contents = "{$name}_zip_contents"; $store_backup_file = "{$name}_store_backup_file"; $convert = "{$name}_backup_convert"; if (!file_exists($classfile)) { throw new convert_helper_exception('converter_classfile_not_found', $classfile); } require_once($classfile); if (!class_exists($classname)) { throw new convert_helper_exception('converter_classname_not_found', $classname); } if (call_user_func($classname .'::is_available')) { if (!$restore) { if (!class_exists($zip_contents)) { throw new convert_helper_exception('converter_classname_not_found', $zip_contents); } if (!class_exists($store_backup_file)) { throw new convert_helper_exception('converter_classname_not_found', $store_backup_file); } if (!class_exists($convert)) { throw new convert_helper_exception('converter_classname_not_found', $convert); } } $converters[] = $name; } } return $converters; } public static function export_converter_dependencies($converter, $dependency) { global $CFG; $result = array(); $filename = 'backuplib.php'; $classuf = '_export_converter'; $classfile = "{$CFG->dirroot}/backup/converter/{$converter}/{$filename}"; $classname = "{$converter}{$classuf}"; if (!file_exists($classfile)) { throw new convert_helper_exception('converter_classfile_not_found', $classfile); } require_once($classfile); if (!class_exists($classname)) { throw new convert_helper_exception('converter_classname_not_found', $classname); } if (call_user_func($classname .'::is_available')) { $deps = call_user_func($classname .'::get_deps'); if (array_key_exists($dependency, $deps)) { $result = $deps[$dependency]; } } return $result; } /** * Detects if the given folder contains an unpacked moodle2 backup * * @param string $tempdir the name of the backup directory * @return boolean true if moodle2 format detected, false otherwise */ public static function detect_moodle2_format($tempdir) { $dirpath = make_backup_temp_directory($tempdir, false); if (!is_dir($dirpath)) { throw new convert_helper_exception('tmp_backup_directory_not_found', $dirpath); } $filepath = $dirpath . '/moodle_backup.xml'; if (!file_exists($filepath)) { return false; } $handle = fopen($filepath, 'r'); $firstchars = fread($handle, 200); $status = fclose($handle); // Look for expected XML elements (case-insensitive to account for encoding attribute). if (stripos($firstchars, '<?xml version="1.0" encoding="UTF-8"?>') !== false && strpos($firstchars, '<moodle_backup>') !== false && strpos($firstchars, '<information>') !== false) { return true; } return false; } /** * Converts the given directory with the backup into moodle2 format * * @param string $tempdir The directory to convert * @param string $format The current format, if already detected * @param base_logger|null if the conversion should be logged, use this logger * @throws convert_helper_exception * @return bool false if unable to find the conversion path, true otherwise */ public static function to_moodle2_format($tempdir, $format = null, $logger = null) { if (is_null($format)) { $format = backup_general_helper::detect_backup_format($tempdir); } // get the supported conversion paths from all available converters $converters = self::available_converters(); $descriptions = array(); foreach ($converters as $name) { $classname = "{$name}_converter"; if (!class_exists($classname)) { throw new convert_helper_exception('class_not_loaded', $classname); } if ($logger instanceof base_logger) { backup_helper::log('available converter', backup::LOG_DEBUG, $classname, 1, false, $logger); } $descriptions[$name] = call_user_func($classname .'::description'); } // choose the best conversion path for the given format $path = self::choose_conversion_path($format, $descriptions); if (empty($path)) { if ($logger instanceof base_logger) { backup_helper::log('unable to find the conversion path', backup::LOG_ERROR, null, 0, false, $logger); } return false; } if ($logger instanceof base_logger) { backup_helper::log('conversion path established', backup::LOG_INFO, implode(' => ', array_merge($path, array('moodle2'))), 0, false, $logger); } foreach ($path as $name) { if ($logger instanceof base_logger) { backup_helper::log('running converter', backup::LOG_INFO, $name, 0, false, $logger); } $converter = convert_factory::get_converter($name, $tempdir, $logger); $converter->convert(); } // make sure we ended with moodle2 format if (!self::detect_moodle2_format($tempdir)) { throw new convert_helper_exception('conversion_failed'); } return true; } /** * Inserts an inforef into the conversion temp table */ public static function set_inforef($contextid) { global $DB; } public static function get_inforef($contextid) { } /// end of public API ////////////////////////////////////////////////////// /** * Choose the best conversion path for the given format * * Given the source format and the list of available converters and their properties, * this methods picks the most effective way how to convert the source format into * the target moodle2 format. The method returns a list of converters that should be * called, in order. * * This implementation uses Dijkstra's algorithm to find the shortest way through * the oriented graph. * * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm * @author David Mudrak <david@moodle.com> * @param string $format the source backup format, one of backup::FORMAT_xxx * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name * @return array ordered list of converter names to call (may be empty if not reachable) */ protected static function choose_conversion_path($format, array $descriptions) { // construct an oriented graph of conversion paths. backup formats are nodes // and the the converters are edges of the graph. $paths = array(); // [fromnode][tonode] => converter foreach ($descriptions as $converter => $description) { $from = $description['from']; $to = $description['to']; $cost = $description['cost']; if (is_null($from) or $from === backup::FORMAT_UNKNOWN or is_null($to) or $to === backup::FORMAT_UNKNOWN or is_null($cost) or $cost <= 0) { throw new convert_helper_exception('invalid_converter_description', $converter); } if (!isset($paths[$from][$to])) { $paths[$from][$to] = $converter; } else { // if there are two converters available for the same conversion // path, choose the one with the lowest cost. if there are more // available converters with the same cost, the chosen one is // undefined (depends on the order of processing) if ($descriptions[$paths[$from][$to]]['cost'] > $cost) { $paths[$from][$to] = $converter; } } } if (empty($paths)) { // no conversion paths available return array(); } // now use Dijkstra's algorithm and find the shortest conversion path $dist = array(); // list of nodes and their distances from the source format $prev = array(); // list of previous nodes in optimal path from the source format foreach ($paths as $fromnode => $tonodes) { $dist[$fromnode] = null; // infinitive distance, can't be reached $prev[$fromnode] = null; // unknown foreach ($tonodes as $tonode => $converter) { $dist[$tonode] = null; // infinitive distance, can't be reached $prev[$tonode] = null; // unknown } } if (!array_key_exists($format, $dist)) { return array(); } else { $dist[$format] = 0; } $queue = array_flip(array_keys($dist)); while (!empty($queue)) { // find the node with the smallest distance from the source in the queue // in the first iteration, this will find the original format node itself $closest = null; foreach ($queue as $node => $undefined) { if (is_null($dist[$node])) { continue; } if (is_null($closest) or ($dist[$node] < $dist[$closest])) { $closest = $node; } } if (is_null($closest) or is_null($dist[$closest])) { // all remaining nodes are inaccessible from source break; } if ($closest === backup::FORMAT_MOODLE) { // bingo we can break now break; } unset($queue[$closest]); // visit all neighbors and update distances to them eventually if (!isset($paths[$closest])) { continue; } $neighbors = array_keys($paths[$closest]); // keep just neighbors that are in the queue yet foreach ($neighbors as $ix => $neighbor) { if (!array_key_exists($neighbor, $queue)) { unset($neighbors[$ix]); } } foreach ($neighbors as $neighbor) { // the alternative distance to the neighbor if we went thru the // current $closest node $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost']; if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) { // we found a shorter way to the $neighbor, remember it $dist[$neighbor] = $alt; $prev[$neighbor] = $closest; } } } if (is_null($dist[backup::FORMAT_MOODLE])) { // unable to find a conversion path, the target format not reachable return array(); } // reconstruct the optimal path from the source format to the target one $conversionpath = array(); $target = backup::FORMAT_MOODLE; while (isset($prev[$target])) { array_unshift($conversionpath, $paths[$prev[$target]][$target]); $target = $prev[$target]; } return $conversionpath; } } /** * General convert_helper related exception * * @author David Mudrak <david@moodle.com> */ class convert_helper_exception extends moodle_exception { /** * Constructor * * @param string $errorcode key for the corresponding error string * @param object $a extra words and phrases that might be required in the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $a = null, $debuginfo = null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } util/helper/backup_file_manager.class.php 0000644 00000006705 15215711721 0014565 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-helper * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Collection of helper functions to handle files * * This class implements various functions related with moodle storage * handling (get file from storage, put it there...) and some others * to use the zip/unzip facilities. * * Note: It's supposed that, some day, files implementation will offer * those functions without needeing to know storage internals at all. * That day, we'll move related functions here to proper file api ones. * * TODO: Finish phpdocs */ class backup_file_manager { /** * Returns the full path to backup storage base dir */ public static function get_backup_storage_base_dir($backupid) { global $CFG; $backupiddir = make_backup_temp_directory($backupid); return $backupiddir . '/files'; } /** * Given one file content hash, returns the path (relative to filedir) * to the file. */ public static function get_backup_content_file_location($contenthash) { $l1 = $contenthash[0].$contenthash[1]; return "$l1/$contenthash"; } /** * Copy one file from moodle storage to backup storage */ public static function copy_file_moodle2backup($backupid, $filerecorid) { global $DB; if (!backup_controller_dbops::backup_includes_files($backupid)) { // Only include the files if required by the controller. return; } // Normalise param if (!is_object($filerecorid)) { $filerecorid = $DB->get_record('files', array('id' => $filerecorid)); } // Directory, nothing to do if ($filerecorid->filename === '.') { return; } $fs = get_file_storage(); $file = $fs->get_file_instance($filerecorid); // If the file is external file, skip copying. if ($file->is_external_file()) { return; } // Calculate source and target paths (use same subdirs strategy for both) $targetfilepath = self::get_backup_storage_base_dir($backupid) . '/' . self::get_backup_content_file_location($filerecorid->contenthash); // Create target dir if necessary if (!file_exists(dirname($targetfilepath))) { if (!check_dir_exists(dirname($targetfilepath), true, true)) { throw new backup_helper_exception('cannot_create_directory', dirname($targetfilepath)); } } // And copy the file (if doesn't exist already) if (!file_exists($targetfilepath)) { $file->copy_content_to($targetfilepath); } } } util/ui/restore_ui_stage.class.php 0000644 00000120411 15215711721 0013317 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * restore user interface stages * * This file contains the classes required to manage the stages that make up the * restore user interface. * These will be primarily operated a {@link restore_ui} instance. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Abstract stage class * * This class should be extended by all restore stages (a requirement of many restore ui functions). * Each stage must then define two abstract methods * - process : To process the stage * - initialise_stage_form : To get a restore_moodleform instance for the stage * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_ui_stage extends base_ui_stage { /** * Constructor * @param restore_ui $ui * @param array $params */ public function __construct(restore_ui $ui, ?array $params = null) { $this->ui = $ui; $this->params = $params; } /** * The restore id from the restore controller * @return string */ final public function get_restoreid() { return $this->get_uniqueid(); } /** * This is an independent stage * @return int */ final public function is_independent() { return false; } /** * No sub stages for this stage * @return false */ public function has_sub_stages() { return false; } /** * The name of this stage * @return string */ final public function get_name() { return get_string('restorestage'.$this->stage, 'backup'); } /** * Returns true if this is the settings stage * @return bool */ final public function is_first_stage() { return $this->stage == restore_ui::STAGE_SETTINGS; } } /** * Abstract class used to represent a restore stage that is indenependent. * * An independent stage is a judged to be so because it doesn't require, and has * no use for the restore controller. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_ui_independent_stage { /** * @var \core\progress\base Optional progress reporter */ private $progressreporter; /** * Constructs the restore stage. * @param int $contextid */ abstract public function __construct($contextid); /** * Processes the current restore stage. * @return mixed */ abstract public function process(); /** * Displays this restore stage. * @param core_backup_renderer $renderer * @return mixed */ abstract public function display(core_backup_renderer $renderer); /** * Returns the current restore stage. * @return int */ abstract public function get_stage(); /** * Gets the progress reporter object in use for this restore UI stage. * * IMPORTANT: This progress reporter is used only for UI progress that is * outside the restore controller. The restore controller has its own * progress reporter which is used for progress during the main restore. * Use the restore controller's progress reporter to report progress during * a restore operation, not this one. * * This extra reporter is necessary because on some restore UI screens, * there are long-running tasks even though there is no restore controller * in use. There is a similar function in restore_ui. but that class is not * used on some stages. * * @return \core\progress\none */ public function get_progress_reporter() { if (!$this->progressreporter) { $this->progressreporter = new \core\progress\none(); } return $this->progressreporter; } /** * Sets the progress reporter that will be returned by get_progress_reporter. * * @param \core\progress\base $progressreporter Progress reporter */ public function set_progress_reporter(\core\progress\base $progressreporter) { $this->progressreporter = $progressreporter; } /** * Gets an array of progress bar items that can be displayed through the restore renderer. * @return array Array of items for the progress bar */ public function get_progress_bar() { global $PAGE; $stage = restore_ui::STAGE_COMPLETE; $currentstage = $this->get_stage(); $items = array(); while ($stage > 0) { $classes = array('backup_stage'); if (floor($stage / 2) == $currentstage) { $classes[] = 'backup_stage_next'; } else if ($stage == $currentstage) { $classes[] = 'backup_stage_current'; } else if ($stage < $currentstage) { $classes[] = 'backup_stage_complete'; } $item = array('text' => strlen(decbin($stage)).'. '.get_string('restorestage'.$stage, 'backup'), 'class' => join(' ', $classes)); if ($stage < $currentstage && $currentstage < restore_ui::STAGE_COMPLETE) { // By default you can't go back to independent stages, if that changes in the future uncomment the next line. // $item['link'] = new moodle_url($PAGE->url, array('restore' => $this->get_restoreid(), 'stage' => $stage)); } array_unshift($items, $item); $stage = floor($stage / 2); } return $items; } /** * Returns the restore stage name. * @return string */ abstract public function get_stage_name(); /** * Obviously true * @return true */ final public function is_independent() { return true; } /** * Handles the destruction of this object. */ public function destroy() { // Nothing to destroy here!. } } /** * The confirmation stage. * * This is the first stage, it is independent. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_confirm extends restore_ui_independent_stage implements file_progress { /** * The context ID. * @var int */ protected $contextid; /** * The file name. * @var string */ protected $filename = null; /** * The file path. * @var string */ protected $filepath = null; /** * @var string Content hash of archive file to restore (if specified by hash) */ protected $contenthash = null; /** * @var string Pathname hash of stored_file object to restore */ protected $pathnamehash = null; /** * @var array */ protected $details; /** * @var bool True if we have started reporting progress */ protected $startedprogress = false; /** * Constructor * @param int $contextid * @throws coding_exception */ public function __construct($contextid) { $this->contextid = $contextid; $this->filename = optional_param('filename', null, PARAM_FILE); if ($this->filename === null) { // Identify file object by its pathname hash. $this->pathnamehash = required_param('pathnamehash', PARAM_ALPHANUM); // The file content hash is also passed for security; users // cannot guess the content hash (unless they know the file contents), // so this guarantees that either the system generated this link or // else the user has access to the restore archive anyhow. $this->contenthash = required_param('contenthash', PARAM_ALPHANUM); } } /** * Processes this restore stage * @return bool * @throws restore_ui_exception */ public function process() { $backuptempdir = make_backup_temp_directory(''); if ($this->filename) { $archivepath = $backuptempdir . '/' . $this->filename; if (!file_exists($archivepath)) { throw new restore_ui_exception('invalidrestorefile'); } $outcome = $this->extract_file_to_dir($archivepath); if ($outcome) { fulldelete($archivepath); } } else { $fs = get_file_storage(); $storedfile = $fs->get_file_by_hash($this->pathnamehash); if (!$storedfile || $storedfile->get_contenthash() !== $this->contenthash) { throw new restore_ui_exception('invalidrestorefile'); } $outcome = $this->extract_file_to_dir($storedfile); } return $outcome; } /** * Extracts the file. * * @param string|stored_file $source Archive file to extract * @return bool */ protected function extract_file_to_dir($source) { global $USER; $this->filepath = restore_controller::get_tempdir_name($this->contextid, $USER->id); $backuptempdir = make_backup_temp_directory('', false); $fb = get_file_packer('application/vnd.moodle.backup'); $result = $fb->extract_to_pathname($source, $backuptempdir . '/' . $this->filepath . '/', null, $this); // If any progress happened, end it. if ($this->startedprogress) { $this->get_progress_reporter()->end_progress(); } return $result; } /** * Implementation for file_progress interface to display unzip progress. * * @param int $progress Current progress * @param int $max Max value */ public function progress($progress = file_progress::INDETERMINATE, $max = file_progress::INDETERMINATE) { $reporter = $this->get_progress_reporter(); // Start tracking progress if necessary. if (!$this->startedprogress) { $reporter->start_progress('extract_file_to_dir', ($max == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $max); $this->startedprogress = true; } // Pass progress through to whatever handles it. $reporter->progress( ($progress == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $progress); } /** * Renders the confirmation stage screen * * @param core_backup_renderer $renderer renderer instance to use * @return string HTML code */ public function display(core_backup_renderer $renderer) { $prevstageurl = new moodle_url('/backup/restorefile.php', array('contextid' => $this->contextid)); $nextstageurl = new moodle_url('/backup/restore.php', array( 'contextid' => $this->contextid, 'filepath' => $this->filepath, 'stage' => restore_ui::STAGE_DESTINATION)); $format = backup_general_helper::detect_backup_format($this->filepath); if ($format === backup::FORMAT_UNKNOWN) { // Unknown format - we can't do anything here. return $renderer->backup_details_unknown($prevstageurl); } else if ($format !== backup::FORMAT_MOODLE) { // Non-standard format to be converted. $details = array('format' => $format, 'type' => backup::TYPE_1COURSE); // todo type to be returned by a converter return $renderer->backup_details_nonstandard($nextstageurl, $details); } else { // Standard MBZ backup, let us get information from it and display. $this->details = backup_general_helper::get_backup_information($this->filepath); return $renderer->backup_details($this->details, $nextstageurl); } } /** * The restore stage name. * @return string * @throws coding_exception */ public function get_stage_name() { return get_string('restorestage'.restore_ui::STAGE_CONFIRM, 'backup'); } /** * The restore stage this class is for. * @return int */ public function get_stage() { return restore_ui::STAGE_CONFIRM; } } /** * This is the destination stage. * * This stage is the second stage and is also independent * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_destination extends restore_ui_independent_stage { /** * The context ID. * @var int */ protected $contextid; /** * The backup file path. * @var mixed|null */ protected $filepath = null; /** * The course ID. * @var null */ protected $courseid = null; /** * The restore target. One of backup::TARGET_NEW * @var int */ protected $target = backup::TARGET_NEW_COURSE; /** * The course search component. * @var null|restore_course_search */ protected $coursesearch = null; /** * The category search component. * @var null|restore_category_search */ protected $categorysearch = null; /** * Constructs the destination stage. * @param int $contextid * @throws coding_exception */ public function __construct($contextid) { global $PAGE; $this->contextid = $contextid; $this->filepath = required_param('filepath', PARAM_ALPHANUM); $url = new moodle_url($PAGE->url, array( 'filepath' => $this->filepath, 'contextid' => $this->contextid, 'stage' => restore_ui::STAGE_DESTINATION)); // The context level can be course category, course or module. We need to make sure that we always use correct one. $context = context::instance_by_id($contextid); if ($context->contextlevel != CONTEXT_COURSE && $coursecontext = $context->get_course_context(false)) { $context = $coursecontext; } $this->coursesearch = new restore_course_search(array('url' => $url), $context->instanceid); $this->categorysearch = new restore_category_search(array('url' => $url)); } /** * Processes the destination stage. * @return bool * @throws coding_exception * @throws restore_ui_exception */ public function process() { global $DB; $filepathdir = make_backup_temp_directory($this->filepath, false); if (!file_exists($filepathdir) || !is_dir($filepathdir)) { throw new restore_ui_exception('invalidrestorepath'); } if (optional_param('searchcourses', false, PARAM_BOOL)) { return false; } $this->target = optional_param('target', backup::TARGET_NEW_COURSE, PARAM_INT); $targetid = optional_param('targetid', null, PARAM_INT); if (!is_null($this->target) && !is_null($targetid) && confirm_sesskey()) { if ($this->target == backup::TARGET_NEW_COURSE) { list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'), get_string('restoringcourseshortname', 'backup')); $this->courseid = restore_dbops::create_new_course($fullname, $shortname, $targetid); } else { $this->courseid = $targetid; } return ($DB->record_exists('course', array('id' => $this->courseid))); } return false; } /** * Renders the destination stage screen * * @param core_backup_renderer $renderer renderer instance to use * @return string HTML code */ public function display(core_backup_renderer $renderer) { $format = backup_general_helper::detect_backup_format($this->filepath); if ($format === backup::FORMAT_MOODLE) { // Standard Moodle 2 format, let use get the type of the backup. $details = backup_general_helper::get_backup_information($this->filepath); if ($details->type === backup::TYPE_1COURSE) { $wholecourse = true; } else { $wholecourse = false; } } else { // Non-standard format to be converted. We assume it contains the // whole course for now. However, in the future there might be a callback // to the installed converters. $wholecourse = true; } $nextstageurl = new moodle_url('/backup/restore.php', array( 'contextid' => $this->contextid, 'filepath' => $this->filepath, 'stage' => restore_ui::STAGE_SETTINGS)); $context = context::instance_by_id($this->contextid); if ($context->contextlevel == CONTEXT_COURSE and has_capability('moodle/restore:restorecourse', $context)) { $currentcourse = $context->instanceid; } else { $currentcourse = false; } return $renderer->course_selector($nextstageurl, $wholecourse, $this->categorysearch, $this->coursesearch, $currentcourse); } /** * Returns the stage name. * @return string * @throws coding_exception */ public function get_stage_name() { return get_string('restorestage'.restore_ui::STAGE_DESTINATION, 'backup'); } /** * Returns the backup file path * @return mixed|null */ public function get_filepath() { return $this->filepath; } /** * Returns the course id. * @return null */ public function get_course_id() { return $this->courseid; } /** * Returns the current restore stage * @return int */ public function get_stage() { return restore_ui::STAGE_DESTINATION; } /** * Returns the target for this restore. * One of backup::TARGET_* * @return int */ public function get_target() { return $this->target; } } /** * This stage is the settings stage. * * This stage is the third stage, it is dependent on a restore controller and * is the first stage as such. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_settings extends restore_ui_stage { /** * Initial restore stage constructor * @param restore_ui $ui * @param array $params */ public function __construct(restore_ui $ui, ?array $params = null) { $this->stage = restore_ui::STAGE_SETTINGS; parent::__construct($ui, $params); } /** * Process the settings stage. * * @param base_moodleform $form * @return bool|int */ public function process(?base_moodleform $form = null) { $form = $this->initialise_stage_form(); if ($form->is_cancelled()) { $this->ui->cancel_process(); } $data = $form->get_data(); if ($data && confirm_sesskey()) { $tasks = $this->ui->get_tasks(); $changes = 0; foreach ($tasks as &$task) { // We are only interesting in the backup root task for this stage. if ($task instanceof restore_root_task || $task instanceof restore_course_task) { // Get all settings into a var so we can iterate by reference. $settings = $task->get_settings(); foreach ($settings as &$setting) { $name = $setting->get_ui_name(); if (isset($data->$name) && $data->$name != $setting->get_value()) { $setting->set_value($data->$name); $changes++; } else if (!isset($data->$name) && $setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX && $setting->get_value()) { $setting->set_value(0); $changes++; } } } } // Return the number of changes the user made. return $changes; } else { return false; } } /** * Initialise the stage form. * * @return backup_moodleform|base_moodleform|restore_settings_form * @throws coding_exception */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { $form = new restore_settings_form($this, $PAGE->url); // Store as a variable so we can iterate by reference. $tasks = $this->ui->get_tasks(); $headingprinted = false; // Iterate all tasks by reference. foreach ($tasks as &$task) { // For the initial stage we are only interested in the root settings. if ($task instanceof restore_root_task) { if (!$headingprinted) { $form->add_heading('rootsettings', get_string('restorerootsettings', 'backup')); $headingprinted = true; } $settings = $task->get_settings(); // First add all settings except the filename setting. foreach ($settings as &$setting) { if ($setting->get_name() == 'filename') { continue; } $form->add_setting($setting, $task); } // Then add all dependencies. foreach ($settings as &$setting) { if ($setting->get_name() == 'filename') { continue; } $form->add_dependencies($setting); } } } $this->stageform = $form; } // Return the form. return $this->stageform; } } /** * Schema stage of backup process * * During the schema stage the user is required to set the settings that relate * to the area that they are backing up as well as its children. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_schema extends restore_ui_stage { /** * @var int Maximum number of settings to add to form at once */ const MAX_SETTINGS_BATCH = 1000; /** * Schema stage constructor * @param restore_ui $ui * @param array $params */ public function __construct(restore_ui $ui, ?array $params = null) { $this->stage = restore_ui::STAGE_SCHEMA; parent::__construct($ui, $params); } /** * Processes the schema stage * * @param base_moodleform $form * @return int The number of changes the user made */ public function process(?base_moodleform $form = null) { $form = $this->initialise_stage_form(); // Check it wasn't cancelled. if ($form->is_cancelled()) { $this->ui->cancel_process(); } // Check it has been submit. $data = $form->get_data(); if ($data && confirm_sesskey()) { // Get the tasks into a var so we can iterate by reference. $tasks = $this->ui->get_tasks(); $changes = 0; // Iterate all tasks by reference. foreach ($tasks as &$task) { // We are only interested in schema settings. if (!($task instanceof restore_root_task)) { // Store as a variable so we can iterate by reference. $settings = $task->get_settings(); // Iterate by reference. foreach ($settings as &$setting) { $name = $setting->get_ui_name(); if (isset($data->$name) && $data->$name != $setting->get_value()) { $setting->set_value($data->$name); $changes++; } else if (!isset($data->$name) && $setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX && $setting->get_value()) { $setting->set_value(0); $changes++; } } } } // Return the number of changes the user made. return $changes; } else { return false; } } /** * Creates the backup_schema_form instance for this stage * * @return backup_schema_form */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { $form = new restore_schema_form($this, $PAGE->url); $tasks = $this->ui->get_tasks(); $courseheading = false; // Track progress through each stage. $progress = $this->ui->get_progress_reporter(); $progress->start_progress('Initialise schema stage form', 3); $progress->start_progress('', count($tasks)); $done = 1; $allsettings = array(); foreach ($tasks as $task) { if (!($task instanceof restore_root_task)) { if (!$courseheading) { // If we haven't already display a course heading to group nicely. $form->add_heading('coursesettings', get_string('coursesettings', 'backup')); $courseheading = true; } // Put each setting into an array of settings to add. Adding // a setting individually is a very slow operation, so we add. // them all in a batch later on. foreach ($task->get_settings() as $setting) { $allsettings[] = array($setting, $task); } } else if ($this->ui->enforce_changed_dependencies()) { // Only show these settings if dependencies changed them. // Add a root settings heading to group nicely. $form->add_heading('rootsettings', get_string('rootsettings', 'backup')); // Iterate all settings and add them to the form as a fixed // setting. We only want schema settings to be editable. foreach ($task->get_settings() as $setting) { if ($setting->get_name() != 'filename') { $form->add_fixed_setting($setting, $task); } } } // Update progress. $progress->progress($done++); } $progress->end_progress(); // Add settings for tasks in batches of up to 1000. Adding settings // in larger batches improves performance, but if it takes too long, // we won't be able to update the progress bar so the backup might. // time out. 1000 is chosen to balance this. $numsettings = count($allsettings); $progress->start_progress('', ceil($numsettings / self::MAX_SETTINGS_BATCH)); $start = 0; $done = 1; while ($start < $numsettings) { $length = min(self::MAX_SETTINGS_BATCH, $numsettings - $start); $form->add_settings(array_slice($allsettings, $start, $length)); $start += $length; $progress->progress($done++); } $progress->end_progress(); // Add the dependencies for all the settings. $progress->start_progress('', count($allsettings)); $done = 1; foreach ($allsettings as $settingtask) { $form->add_dependencies($settingtask[0]); $progress->progress($done++); } $progress->end_progress(); $progress->end_progress(); $this->stageform = $form; } return $this->stageform; } } /** * Confirmation stage * * On this stage the user reviews the setting for the backup and can change the filename * of the file that will be generated. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_review extends restore_ui_stage { /** * Constructs the stage * @param restore_ui $ui * @param array $params */ public function __construct($ui, ?array $params = null) { $this->stage = restore_ui::STAGE_REVIEW; parent::__construct($ui, $params); } /** * Processes the confirmation stage * * @param base_moodleform $form * @return int The number of changes the user made */ public function process(?base_moodleform $form = null) { $form = $this->initialise_stage_form(); // Check it hasn't been cancelled. if ($form->is_cancelled()) { $this->ui->cancel_process(); } $data = $form->get_data(); if ($data && confirm_sesskey()) { return 0; } else { return false; } } /** * Creates the backup_confirmation_form instance this stage requires * * @return backup_confirmation_form */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { // Get the form. $form = new restore_review_form($this, $PAGE->url); $content = ''; $courseheading = false; $progress = $this->ui->get_progress_reporter(); $tasks = $this->ui->get_tasks(); $progress->start_progress('initialise_stage_form', count($tasks)); $done = 1; foreach ($tasks as $task) { if ($task instanceof restore_root_task) { // If its a backup root add a root settings heading to group nicely. $form->add_heading('rootsettings', get_string('restorerootsettings', 'backup')); } else if (!$courseheading) { // We haven't already add a course heading. $form->add_heading('coursesettings', get_string('coursesettings', 'backup')); $courseheading = true; } // Iterate all settings, doesnt need to happen by reference. foreach ($task->get_settings() as $setting) { $form->add_fixed_setting($setting, $task); } // Update progress. $progress->progress($done++); } $progress->end_progress(); $this->stageform = $form; } return $this->stageform; } } /** * Final stage of backup * * This stage is special in that it is does not make use of a form. The reason for * this is the order of procession of backup at this stage. * The processesion is: * 1. The final stage will be intialise. * 2. The confirmation stage will be processed. * 3. The backup will be executed * 4. The complete stage will be loaded by execution * 5. The complete stage will be displayed * * This highlights that we neither need a form nor a display method for this stage * we simply need to process. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_process extends restore_ui_stage { /** * There is no substage required. */ const SUBSTAGE_NONE = 0; /** * The prechecks substage is required/the current substage. */ const SUBSTAGE_PRECHECKS = 2; /** * The current substage. * @var int */ protected $substage = 0; /** * Constructs the final stage * @param base_ui $ui * @param array $params */ public function __construct(base_ui $ui, ?array $params = null) { $this->stage = restore_ui::STAGE_PROCESS; parent::__construct($ui, $params); } /** * Processes the final stage. * * In this case it checks to see if there is a sub stage that we need to display * before execution, if there is we gear up to display the subpage, otherwise * we return true which will lead to execution of the restore and the loading * of the completed stage. * * @param base_moodleform $form */ public function process(?base_moodleform $form = null) { if (optional_param('cancel', false, PARAM_BOOL)) { redirect(new moodle_url('/course/view.php', array('id' => $this->get_ui()->get_controller()->get_courseid()))); } // First decide whether a substage is needed. $rc = $this->ui->get_controller(); if ($rc->get_status() == backup::STATUS_SETTING_UI) { $rc->finish_ui(); } if ($rc->get_status() == backup::STATUS_NEED_PRECHECK) { if (!$rc->precheck_executed()) { $rc->execute_precheck(true); } $results = $rc->get_precheck_results(); if (!empty($results)) { $this->substage = self::SUBSTAGE_PRECHECKS; } } $substage = optional_param('substage', null, PARAM_INT); if (empty($this->substage) && !empty($substage)) { $this->substage = $substage; // Now check whether that substage has already been submit. if ($this->substage == self::SUBSTAGE_PRECHECKS && optional_param('sesskey', null, PARAM_RAW) == sesskey()) { $info = $rc->get_info(); if (!empty($info->role_mappings->mappings)) { foreach ($info->role_mappings->mappings as $key => &$mapping) { $mapping->targetroleid = optional_param('mapping'.$key, $mapping->targetroleid, PARAM_INT); } $info->role_mappings->modified = true; } // We've processed the substage now setting it back to none so we // can move to the next stage. $this->substage = self::SUBSTAGE_NONE; } } return empty($this->substage); } /** * should NEVER be called... throws an exception */ protected function initialise_stage_form() { throw new backup_ui_exception('backup_ui_must_execute_first'); } /** * Renders the process stage screen * * @throws restore_ui_exception * @param core_backup_renderer $renderer renderer instance to use * @return string HTML code */ public function display(core_backup_renderer $renderer) { global $PAGE; $html = ''; $haserrors = false; $url = new moodle_url($PAGE->url, array( 'restore' => $this->get_uniqueid(), 'stage' => restore_ui::STAGE_PROCESS, 'substage' => $this->substage, 'sesskey' => sesskey())); $html .= html_writer::start_tag('form', array( 'action' => $url->out_omit_querystring(), 'class' => 'backup-restore', 'enctype' => 'application/x-www-form-urlencoded', // Enforce compatibility with our max_input_vars hack. 'method' => 'post')); foreach ($url->params() as $name => $value) { $html .= html_writer::empty_tag('input', array( 'type' => 'hidden', 'name' => $name, 'value' => $value)); } switch ($this->substage) { case self::SUBSTAGE_PRECHECKS : $results = $this->ui->get_controller()->get_precheck_results(); $info = $this->ui->get_controller()->get_info(); $haserrors = (!empty($results['errors'])); $html .= $renderer->precheck_notices($results); if (!empty($info->role_mappings->mappings)) { $context = context_course::instance($this->ui->get_controller()->get_courseid()); $assignableroles = get_assignable_roles($context, ROLENAME_ALIAS, false); // Get current role mappings. $currentroles = role_fix_names(get_all_roles(), $context); // Get backup role mappings. $rolemappings = $info->role_mappings->mappings; array_map(function($rolemapping) use ($currentroles) { foreach ($currentroles as $role) { // Find matching archetype to determine the backup's shortname for label display. if ($rolemapping->archetype == $role->archetype) { $rolemapping->name = $rolemapping->shortname; break; } } if ($rolemapping->name == null) { $rolemapping->name = get_string('undefinedrolemapping', 'backup', $rolemapping->archetype); } }, $rolemappings); $html .= $renderer->role_mappings($rolemappings, $assignableroles); } break; default: throw new restore_ui_exception('backup_ui_must_execute_first'); } $html .= $renderer->substage_buttons($haserrors); $html .= html_writer::end_tag('form'); return $html; } /** * Returns true if this stage can have sub-stages. * @return bool|false */ public function has_sub_stages() { return true; } } /** * This is the completed stage. * * Once this is displayed there is nothing more to do. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_stage_complete extends restore_ui_stage_process { /** * The results of the backup execution * @var array */ protected $results; /** * Constructs the complete backup stage * @param restore_ui $ui * @param array $params * @param array $results */ public function __construct(restore_ui $ui, ?array $params = null, ?array $results = null) { $this->results = $results; parent::__construct($ui, $params); $this->stage = restore_ui::STAGE_COMPLETE; } /** * Displays the completed backup stage. * * Currently this just envolves redirecting to the file browser with an * appropriate message. * * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { $html = ''; if (!empty($this->results['file_aliases_restore_failures'])) { $html .= $renderer->box_start('generalbox filealiasesfailures'); $html .= $renderer->heading_with_help(get_string('filealiasesrestorefailures', 'core_backup'), 'filealiasesrestorefailures', 'core_backup'); $html .= $renderer->container(get_string('filealiasesrestorefailuresinfo', 'core_backup')); $html .= $renderer->container_start('aliaseslist'); $html .= html_writer::start_tag('ul'); foreach ($this->results['file_aliases_restore_failures'] as $alias) { $html .= html_writer::tag('li', s($alias)); } $html .= html_writer::end_tag('ul'); $html .= $renderer->container_end(); $html .= $renderer->box_end(); } $html .= $renderer->box_start(); if (array_key_exists('file_missing_in_backup', $this->results)) { $html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem'); } $html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess'); $courseurl = course_get_url($this->get_ui()->get_controller()->get_courseid()); $html .= $renderer->continue_button($courseurl, 'get'); $html .= $renderer->box_end(); return $html; } } util/ui/backup_ui_stage.class.php 0000644 00000057635 15215711721 0013122 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Backup user interface stages * * This file contains the classes required to manage the stages that make up the * backup user interface. * These will be primarily operated a {@link backup_ui} instance. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Abstract stage class * * This class should be extended by all backup stages (a requirement of many backup ui functions). * Each stage must then define two abstract methods * - process : To process the stage * - initialise_stage_form : To get a backup_moodleform instance for the stage * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_ui_stage extends base_ui_stage { /** * Constructor. * * @param backup_ui $ui * @param array $params */ public function __construct(backup_ui $ui, ?array $params = null) { parent::__construct($ui, $params); } /** * The backup id from the backup controller * @return string */ final public function get_backupid() { return $this->get_uniqueid(); } } /** * Class representing the initial stage of a backup. * * In this stage the user is required to set the root level settings. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_stage_initial extends backup_ui_stage { /** * When set to true we skip all stages and jump to immediately processing the backup. * @var bool */ protected $oneclickbackup = false; /** * Initial backup stage constructor * @param backup_ui $ui * @param array $params */ public function __construct(backup_ui $ui, ?array $params = null) { $this->stage = backup_ui::STAGE_INITIAL; parent::__construct($ui, $params); } /** * Processes the initial backup stage * @param base_moodleform $m * @return int The number of changes */ public function process(?base_moodleform $m = null) { $form = $this->initialise_stage_form(); if ($form->is_cancelled()) { $this->ui->cancel_process(); } $data = $form->get_data(); if ($data && confirm_sesskey()) { if (isset($data->oneclickbackup)) { $this->oneclickbackup = true; } $tasks = $this->ui->get_tasks(); $changes = 0; foreach ($tasks as &$task) { // We are only interesting in the backup root task for this stage. if ($task instanceof backup_root_task) { // Get all settings into a var so we can iterate by reference. $settings = $task->get_settings(); foreach ($settings as &$setting) { $name = $setting->get_ui_name(); if (isset($data->$name) && $data->$name != $setting->get_value()) { $setting->set_value($data->$name); $changes++; } else if (!isset($data->$name) && $setting->get_value() && $setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX && $setting->get_status() !== backup_setting::LOCKED_BY_HIERARCHY) { $setting->set_value(0); $changes++; } } } } // Return the number of changes the user made. return $changes; } else { return false; } } /** * Gets the next stage for the backup. * * We override this function to implement the one click backup. * When the user performs a one click backup we jump straight to the final stage. * * @return int */ public function get_next_stage() { if ($this->oneclickbackup) { // Its a one click backup. // The default filename is backup.mbz, this normally gets set to something useful in the confirmation stage. // because we skipped that stage we must manually set this to a useful value. $tasks = $this->ui->get_tasks(); foreach ($tasks as $task) { if ($task instanceof backup_root_task) { // Find the filename setting. $setting = $task->get_setting('filename'); if ($setting) { // Use the helper objects to get a useful name. $filename = backup_plan_dbops::get_default_backup_filename( $this->ui->get_format(), $this->ui->get_type(), $this->ui->get_controller_id(), $this->ui->get_setting_value('users'), $this->ui->get_setting_value('anonymize'), false, (bool)$this->ui->get_setting_value('files') ); $setting->set_value($filename); } } } return backup_ui::STAGE_FINAL; } return parent::get_next_stage(); } /** * Initialises the backup_moodleform instance for this stage * * @return backup_initial_form */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { $form = new backup_initial_form($this, $PAGE->url); // Store as a variable so we can iterate by reference. $tasks = $this->ui->get_tasks(); // Iterate all tasks by reference. $add_settings = array(); $dependencies = array(); foreach ($tasks as &$task) { // For the initial stage we are only interested in the root settings. if ($task instanceof backup_root_task) { if ($this->ui instanceof import_ui) { $form->add_heading('rootsettings', get_string('importrootsettings', 'backup')); } else { $form->add_heading('rootsettings', get_string('rootsettings', 'backup')); } $settings = $task->get_settings(); // First add all settings except the filename setting. foreach ($settings as &$setting) { if ($setting->get_name() == 'filename') { continue; } $add_settings[] = array($setting, $task); } // Then add all dependencies. foreach ($settings as &$setting) { if ($setting->get_name() == 'filename') { continue; } $dependencies[] = $setting; } } } // Add all settings at once. $form->add_settings($add_settings); // Add dependencies. foreach ($dependencies as $depsetting) { $form->add_dependencies($depsetting); } $this->stageform = $form; } // Return the form. return $this->stageform; } } /** * Schema stage of backup process * * During the schema stage the user is required to set the settings that relate * to the area that they are backing up as well as its children. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_stage_schema extends backup_ui_stage { /** * @var int Maximum number of settings to add to form at once */ const MAX_SETTINGS_BATCH = 1000; /** * Schema stage constructor * @param backup_ui $ui * @param array $params */ public function __construct(backup_ui $ui, ?array $params = null) { $this->stage = backup_ui::STAGE_SCHEMA; parent::__construct($ui, $params); } /** * Processes the schema stage * * @param base_moodleform $form * @return int The number of changes the user made */ public function process(?base_moodleform $form = null) { $form = $this->initialise_stage_form(); // Check it wasn't cancelled. if ($form->is_cancelled()) { $this->ui->cancel_process(); } // Check it has been submit. $data = $form->get_data(); if ($data && confirm_sesskey()) { // Get the tasks into a var so we can iterate by reference. $tasks = $this->ui->get_tasks(); $changes = 0; // Iterate all tasks by reference. foreach ($tasks as &$task) { // We are only interested in schema settings. if (!($task instanceof backup_root_task)) { // Store as a variable so we can iterate by reference. $settings = $task->get_settings(); // Iterate by reference. foreach ($settings as &$setting) { $name = $setting->get_ui_name(); if (isset($data->$name) && $data->$name != $setting->get_value()) { $setting->set_value($data->$name); $changes++; } else if (!isset($data->$name) && $setting->get_ui_type() == backup_setting::UI_HTML_CHECKBOX && $setting->get_value()) { $setting->set_value(0); $changes++; } } } } // Return the number of changes the user made. return $changes; } else { return false; } } /** * Creates the backup_schema_form instance for this stage * * @return backup_schema_form */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { $form = new backup_schema_form($this, $PAGE->url); $tasks = $this->ui->get_tasks(); $content = ''; $courseheading = false; $add_settings = array(); $dependencies = array(); // Track progress through each stage. $progress = $this->ui->get_controller()->get_progress(); $progress->start_progress('Initialise stage form', 3); // Get settings for all tasks. $progress->start_progress('', count($tasks)); $done = 1; foreach ($tasks as $task) { if (!($task instanceof backup_root_task)) { if (!$courseheading) { // If we haven't already display a course heading to group nicely. $form->add_heading('coursesettings', get_string('includeactivities', 'backup')); $courseheading = true; } // First add each setting. foreach ($task->get_settings() as $setting) { $add_settings[] = array($setting, $task); } // The add all the dependencies. foreach ($task->get_settings() as $setting) { $dependencies[] = $setting; } } else if ($this->ui->enforce_changed_dependencies()) { // Only show these settings if dependencies changed them. // Add a root settings heading to group nicely. $form->add_heading('rootsettings', get_string('rootsettings', 'backup')); // Iterate all settings and add them to the form as a fixed // setting. We only want schema settings to be editable. foreach ($task->get_settings() as $setting) { if ($setting->get_name() != 'filename') { $form->add_fixed_setting($setting, $task); } } } // Update progress. $progress->progress($done++); } $progress->end_progress(); // Add settings for tasks in batches of up to 1000. Adding settings // in larger batches improves performance, but if it takes too long, // we won't be able to update the progress bar so the backup might. // time out. 1000 is chosen to balance this. $numsettings = count($add_settings); $progress->start_progress('', ceil($numsettings / self::MAX_SETTINGS_BATCH)); $start = 0; $done = 1; while ($start < $numsettings) { $length = min(self::MAX_SETTINGS_BATCH, $numsettings - $start); $form->add_settings(array_slice($add_settings, $start, $length)); $start += $length; $progress->progress($done++); } $progress->end_progress(); $progress->start_progress('', count($dependencies)); $done = 1; foreach ($dependencies as $depsetting) { $form->add_dependencies($depsetting); $progress->progress($done++); } $progress->end_progress(); // End overall progress through creating form. $progress->end_progress(); $this->stageform = $form; } return $this->stageform; } } /** * Confirmation stage * * On this stage the user reviews the setting for the backup and can change the filename * of the file that will be generated. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_stage_confirmation extends backup_ui_stage { /** * Constructs the stage * @param backup_ui $ui * @param array $params */ public function __construct($ui, ?array $params = null) { $this->stage = backup_ui::STAGE_CONFIRMATION; parent::__construct($ui, $params); } /** * Processes the confirmation stage * * @param base_moodleform $form * @return int The number of changes the user made */ public function process(?base_moodleform $form = null) { $form = $this->initialise_stage_form(); // Check it hasn't been cancelled. if ($form->is_cancelled()) { $this->ui->cancel_process(); } $data = $form->get_data(); if ($data && confirm_sesskey()) { // Collect into a variable so we can iterate by reference. $tasks = $this->ui->get_tasks(); $changes = 0; // Iterate each task by reference. foreach ($tasks as &$task) { if ($task instanceof backup_root_task) { // At this stage all we are interested in is the filename setting. $setting = $task->get_setting('filename'); $name = $setting->get_ui_name(); if (isset($data->$name) && $data->$name != $setting->get_value()) { $setting->set_value($data->$name); $changes++; } } } // Return the number of changes the user made. return $changes; } else { return false; } } /** * Creates the backup_confirmation_form instance this stage requires * * @return backup_confirmation_form */ protected function initialise_stage_form() { global $PAGE; if ($this->stageform === null) { // Get the form. $form = new backup_confirmation_form($this, $PAGE->url); $content = ''; $courseheading = false; foreach ($this->ui->get_tasks() as $task) { if ($setting = $task->get_setting('filename')) { $form->add_heading('filenamesetting', get_string('filename', 'backup')); if ($setting->get_value() == 'backup.mbz') { $format = $this->ui->get_format(); $type = $this->ui->get_type(); $id = $this->ui->get_controller_id(); $users = $this->ui->get_setting_value('users'); $anonymised = $this->ui->get_setting_value('anonymize'); $files = (bool)$this->ui->get_setting_value('files'); $filename = backup_plan_dbops::get_default_backup_filename( $format, $type, $id, $users, $anonymised, false, $files); $setting->set_value($filename); } $form->add_setting($setting, $task); break; } } // Track progress through tasks. $progress = $this->ui->get_controller()->get_progress(); $tasks = $this->ui->get_tasks(); $progress->start_progress('initialise_stage_form', count($tasks)); $done = 1; foreach ($tasks as $task) { if ($task instanceof backup_root_task) { // If its a backup root add a root settings heading to group nicely. if ($this->ui instanceof import_ui) { $form->add_heading('rootsettings', get_string('importrootsettings', 'backup')); } else { $form->add_heading('rootsettings', get_string('rootsettings', 'backup')); } } else if (!$courseheading) { // We haven't already add a course heading. $form->add_heading('coursesettings', get_string('includeditems', 'backup')); $courseheading = true; } // Iterate all settings, doesnt need to happen by reference. foreach ($task->get_settings() as $setting) { // For this stage only the filename setting should be editable. if ($setting->get_name() != 'filename') { $form->add_fixed_setting($setting, $task); } } // Update progress. $progress->progress($done++); } $progress->end_progress(); $this->stageform = $form; } return $this->stageform; } } /** * Final stage of backup * * This stage is special in that it is does not make use of a form. The reason for * this is the order of procession of backup at this stage. * The processesion is: * 1. The final stage will be intialise. * 2. The confirmation stage will be processed. * 3. The backup will be executed * 4. The complete stage will be loaded by execution * 5. The complete stage will be displayed * * This highlights that we neither need a form nor a display method for this stage * we simply need to process. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_stage_final extends backup_ui_stage { /** * Constructs the final stage * @param backup_ui $ui * @param array $params */ public function __construct(backup_ui $ui, ?array $params = null) { $this->stage = backup_ui::STAGE_FINAL; parent::__construct($ui, $params); } /** * Processes the final stage. * * In this case it ALWAYS passes processing to the previous stage (confirmation) * * @param base_moodleform $form * @return bool */ public function process(?base_moodleform $form = null) { return true; } /** * should NEVER be called... throws an exception */ protected function initialise_stage_form() { throw new backup_ui_exception('backup_ui_must_execute_first'); } /** * should NEVER be called... throws an exception * * @throws backup_ui_exception always * @param core_backup_renderer $renderer * @return void */ public function display(core_backup_renderer $renderer) { throw new backup_ui_exception('backup_ui_must_execute_first'); } } /** * The completed backup stage * * At this stage everything is done and the user will be redirected to view the * backup file in the file browser. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_stage_complete extends backup_ui_stage_final { /** * The results of the backup execution * @var array */ protected $results; /** * Constructs the complete backup stage * * @param backup_ui $ui * @param array $params * @param array $results */ public function __construct(backup_ui $ui, ?array $params = null, ?array $results = null) { $this->results = $results; parent::__construct($ui, $params); $this->stage = backup_ui::STAGE_COMPLETE; } /** * Displays the completed backup stage. * * Currently this just involves redirecting to the file browser with an * appropriate message. * * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { // Get the resulting stored_file record. $type = $this->get_ui()->get_controller()->get_type(); $courseid = $this->get_ui()->get_controller()->get_courseid(); switch ($type) { case 'activity': $cmid = $this->get_ui()->get_controller()->get_id(); $cm = get_coursemodule_from_id(null, $cmid, $courseid); $modcontext = context_module::instance($cm->id); $restorerul = new moodle_url('/backup/restorefile.php', array('contextid' => $modcontext->id)); break; case 'course': default: $coursecontext = context_course::instance($courseid); $restorerul = new moodle_url('/backup/restorefile.php', array('contextid' => $coursecontext->id)); } $output = ''; $output .= $renderer->box_start(); if (!empty($this->results['include_file_references_to_external_content'])) { $output .= $renderer->notification(get_string('filereferencesincluded', 'backup'), 'notifyproblem'); } if (!empty($this->results['missing_files_in_pool'])) { $output .= $renderer->notification(get_string('missingfilesinpool', 'backup'), 'notifyproblem'); } $output .= $renderer->get_samesite_notification(); $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess'); $output .= $renderer->continue_button($restorerul, 'get'); $output .= $renderer->box_end(); return $output; } } util/ui/classes/hook/before_course_modified_check.php 0000644 00000003616 15215711721 0017073 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup\hook; /** * Get a list of event names which are excluded to trigger from course changes in automated backup. * * @package core_backup * @copyright 2023 Tomo Tsuyuki <tomotsuyuki@catalyst-au.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ #[\core\attribute\label('Get a list of event names which are excluded to trigger from course changes in automated backup.')] #[\core\attribute\tags('backup')] final class before_course_modified_check { /** * @var string[] Array of event names. */ private $events = []; /** * Add an array of event names which are excluded to trigger from course changes in automated backup. * * @param string $events,... Array of event name strings */ public function exclude_events(string ...$events): void { $this->events = array_merge($this->events, $events); } /** * Get an array of event names which are excluded to trigger from course changes in automated backup. * This is called after dispatch for the hook and use values to exclude events for backup. * * @return array */ public function get_excluded_events(): array { return $this->events; } } util/ui/classes/output/copy_form.php 0000644 00000024331 15215711721 0013646 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Course copy form class. * * @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 */ namespace core_backup\output; defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Course copy form class. * * @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 */ class copy_form extends \moodleform { /** * Build form for the course copy settings. * * {@inheritDoc} * @see \moodleform::definition() */ public function definition() { global $CFG, $OUTPUT, $USER; $mform = $this->_form; $course = $this->_customdata['course']; $coursecontext = \context_course::instance($course->id); $courseconfig = get_config('moodlecourse'); $returnto = $this->_customdata['returnto']; $returnurl = $this->_customdata['returnurl']; if (empty($course->category)) { $course->category = $course->categoryid; } // Course ID. $mform->addElement('hidden', 'courseid', $course->id); $mform->setType('courseid', PARAM_INT); // Return to type. $mform->addElement('hidden', 'returnto', null); $mform->setType('returnto', PARAM_ALPHANUM); $mform->setConstant('returnto', $returnto); // Notifications of current copies. $copies = \copy_helper::get_copies($USER->id, $course->id); if (!empty($copies)) { $progresslink = new \moodle_url('/backup/copyprogress.php?', array('id' => $course->id)); $notificationmsg = get_string('copiesinprogress', 'backup', $progresslink->out()); $notification = $OUTPUT->notification($notificationmsg, 'notifymessage'); $mform->addElement('html', $notification); } // Return to URL. $mform->addElement('hidden', 'returnurl', null); $mform->setType('returnurl', PARAM_LOCALURL); $mform->setConstant('returnurl', $returnurl); // Form heading. $mform->addElement('html', \html_writer::div(get_string('copycoursedesc', 'backup'), 'form-description mb-6')); // Course fullname. $mform->addElement('text', 'fullname', get_string('fullnamecourse'), 'maxlength="254" size="50"'); $mform->addHelpButton('fullname', 'fullnamecourse'); $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); $mform->setType('fullname', PARAM_TEXT); // Course shortname. $mform->addElement('text', 'shortname', get_string('shortnamecourse'), 'maxlength="100" size="20"'); $mform->addHelpButton('shortname', 'shortnamecourse'); $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); $mform->setType('shortname', PARAM_TEXT); // Course category. $displaylist = \core_course_category::make_categories_list(\core_course\management\helper::get_course_copy_capabilities()); if (!isset($displaylist[$course->category])) { // Always keep current category. $displaylist[$course->category] = \core_course_category::get($course->category, MUST_EXIST, true)->get_formatted_name(); } $mform->addElement('autocomplete', 'category', get_string('coursecategory'), $displaylist); $mform->addRule('category', null, 'required', null, 'client'); $mform->addHelpButton('category', 'coursecategory'); // Course visibility. $choices = array(); $choices['0'] = get_string('hide'); $choices['1'] = get_string('show'); $mform->addElement('select', 'visible', get_string('coursevisibility'), $choices); $mform->addHelpButton('visible', 'coursevisibility'); $mform->setDefault('visible', $courseconfig->visible); if (!has_capability('moodle/course:visibility', $coursecontext)) { $mform->hardFreeze('visible'); $mform->setConstant('visible', $course->visible); } // Course start date. $mform->addElement('date_time_selector', 'startdate', get_string('startdate')); $mform->addHelpButton('startdate', 'startdate'); $date = (new \DateTime())->setTimestamp(usergetmidnight(time())); $date->modify('+1 day'); $mform->setDefault('startdate', $date->getTimestamp()); // Course enddate. $mform->addElement('date_time_selector', 'enddate', get_string('enddate'), array('optional' => true)); $mform->addHelpButton('enddate', 'enddate'); if (!empty($CFG->enablecourserelativedates)) { $attributes = [ 'aria-describedby' => 'relativedatesmode_warning' ]; if (!empty($course->id)) { $attributes['disabled'] = true; } $relativeoptions = [ 0 => get_string('no'), 1 => get_string('yes'), ]; $relativedatesmodegroup = []; $relativedatesmodegroup[] = $mform->createElement('select', 'relativedatesmode', get_string('relativedatesmode'), $relativeoptions, $attributes); $relativedatesmodegroup[] = $mform->createElement('html', \html_writer::span(get_string('relativedatesmode_warning'), '', ['id' => 'relativedatesmode_warning'])); $mform->addGroup($relativedatesmodegroup, 'relativedatesmodegroup', get_string('relativedatesmode'), null, false); $mform->addHelpButton('relativedatesmodegroup', 'relativedatesmode'); } // Course ID number (default to the current course ID number; blank for users who can't change ID numbers). $mform->addElement('text', 'idnumber', get_string('idnumbercourse'), 'maxlength="100" size="10"'); $mform->setDefault('idnumber', $course->idnumber); $mform->addHelpButton('idnumber', 'idnumbercourse'); $mform->setType('idnumber', PARAM_RAW); if (!has_capability('moodle/course:changeidnumber', $coursecontext)) { $mform->hardFreeze('idnumber'); $mform->setConstant('idnumber', ''); } // Keep source course user data. $mform->addElement('select', 'userdata', get_string('userdata', 'backup'), [0 => get_string('no'), 1 => get_string('yes')]); $mform->setDefault('userdata', 0); $mform->addHelpButton('userdata', 'userdata', 'backup'); $requiredcapabilities = array( 'moodle/restore:createuser', 'moodle/backup:userinfo', 'moodle/restore:userinfo' ); if (!has_all_capabilities($requiredcapabilities, $coursecontext)) { $mform->hardFreeze('userdata'); $mform->setConstant('userdata', 0); } // Keep manual enrolments. // Only get roles actually used in this course. $roles = role_fix_names(get_roles_used_in_context($coursecontext, false), $coursecontext); // Only add the option if there are roles in this course. if (!empty($roles) && has_capability('moodle/restore:createuser', $coursecontext)) { $rolearray = array(); foreach ($roles as $role) { $roleid = 'role_' . $role->id; $rolearray[] = $mform->createElement('advcheckbox', $roleid, $role->localname, '', array('group' => 2), array(0, $role->id)); } $mform->addGroup($rolearray, 'rolearray', get_string('keptroles', 'backup'), ' ', false); $mform->addHelpButton('rolearray', 'keptroles', 'backup'); $this->add_checkbox_controller(2); } $buttonarray = array(); $buttonarray[] = $mform->createElement('submit', 'submitreturn', get_string('copyreturn', 'backup')); $buttonarray[] = $mform->createElement('submit', 'submitdisplay', get_string('copyview', 'backup')); $buttonarray[] = $mform->createElement('cancel'); $mform->addGroup($buttonarray, 'buttonar', '', ' ', false); } /** * Validation of the form. * * @param array $data * @param array $files * @return array the errors that were found */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); // Add field validation check for duplicate shortname. $courseshortname = $DB->get_record('course', array('shortname' => $data['shortname']), 'fullname', IGNORE_MULTIPLE); if ($courseshortname) { $errors['shortname'] = get_string('shortnametaken', '', $courseshortname->fullname); } // Add field validation check for duplicate idnumber. if (!empty($data['idnumber'])) { $courseidnumber = $DB->get_record('course', array('idnumber' => $data['idnumber']), 'fullname', IGNORE_MULTIPLE); if ($courseidnumber) { $errors['idnumber'] = get_string('courseidnumbertaken', 'error', $courseidnumber->fullname); } } // Validate the dates (make sure end isn't greater than start). if ($errorcode = course_validate_dates($data)) { $errors['enddate'] = get_string($errorcode, 'error'); } return $errors; } } util/ui/classes/privacy/provider.php 0000644 00000034210 15215711721 0013615 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for core_backup. * * @package core_backup * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * Privacy Subsystem implementation for core_backup. * * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\subsystem\provider { /** * Return the fields which contain personal data. * * @param collection $items a reference to the collection to use to store the metadata. * @return collection the updated collection of metadata items. */ public static function get_metadata(collection $items): collection { $items->link_external_location( 'Backup', [ 'detailsofarchive' => 'privacy:metadata:backup:detailsofarchive' ], 'privacy:metadata:backup:externalpurpose' ); $items->add_database_table( 'backup_controllers', [ 'operation' => 'privacy:metadata:backup_controllers:operation', 'type' => 'privacy:metadata:backup_controllers:type', 'itemid' => 'privacy:metadata:backup_controllers:itemid', 'timecreated' => 'privacy:metadata:backup_controllers:timecreated', 'timemodified' => 'privacy:metadata:backup_controllers:timemodified' ], 'privacy:metadata:backup_controllers' ); return $items; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new contextlist(); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel AND bc.type = :type WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'userid' => $userid, 'type' => 'course', ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {course_sections} c ON bc.itemid = c.id AND bc.type = :type JOIN {context} ctx ON ctx.instanceid = c.course AND ctx.contextlevel = :contextlevel WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_COURSE, 'userid' => $userid, 'type' => 'section', ]; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel AND bc.type = :type WHERE bc.userid = :userid"; $params = [ 'contextlevel' => CONTEXT_MODULE, 'userid' => $userid, 'type' => 'activity', ]; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_course) { $params = ['courseid' => $context->instanceid]; $sql = "SELECT bc.userid FROM {backup_controllers} bc WHERE bc.itemid = :courseid AND bc.type = :typecourse"; $courseparams = ['typecourse' => 'course'] + $params; $userlist->add_from_sql('userid', $sql, $courseparams); $sql = "SELECT bc.userid FROM {backup_controllers} bc JOIN {course_sections} c ON bc.itemid = c.id WHERE c.course = :courseid AND bc.type = :typesection"; $sectionparams = ['typesection' => 'section'] + $params; $userlist->add_from_sql('userid', $sql, $sectionparams); } if ($context instanceof \context_module) { $params = [ 'cmid' => $context->instanceid, 'typeactivity' => 'activity' ]; $sql = "SELECT bc.userid FROM {backup_controllers} bc WHERE bc.itemid = :cmid AND bc.type = :typeactivity"; $userlist->add_from_sql('userid', $sql, $params); } } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); $sql = "SELECT bc.* FROM {backup_controllers} bc JOIN {context} ctx ON ctx.instanceid = bc.itemid AND ctx.contextlevel = :contextlevel WHERE ctx.id {$contextsql} AND bc.userid = :userid ORDER BY bc.timecreated ASC"; $params = ['contextlevel' => CONTEXT_COURSE, 'userid' => $user->id] + $contextparams; $backupcontrollers = $DB->get_recordset_sql($sql, $params); self::recordset_loop_and_export($backupcontrollers, 'itemid', [], function($carry, $record) { $carry[] = [ 'operation' => $record->operation, 'type' => $record->type, 'itemid' => $record->itemid, 'timecreated' => transform::datetime($record->timecreated), 'timemodified' => transform::datetime($record->timemodified), ]; return $carry; }, function($courseid, $data) { $context = \context_course::instance($courseid); $finaldata = (object) $data; writer::with_context($context)->export_data([get_string('backup'), $courseid], $finaldata); }); } /** * Delete all user data which matches the specified context. * Only dealing with the specific context - not it's child contexts. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { global $DB; if ($context instanceof \context_course) { $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = ?) AND type = ?"; $DB->delete_records_select('backup_controllers', $sectionsql, [$context->instanceid, \backup::TYPE_1SECTION]); $DB->delete_records('backup_controllers', ['itemid' => $context->instanceid, 'type' => \backup::TYPE_1COURSE]); } if ($context instanceof \context_module) { $DB->delete_records('backup_controllers', ['itemid' => $context->instanceid, 'type' => \backup::TYPE_1ACTIVITY]); } return; } /** * Delete multiple users within a single context. * Only dealing with the specific context - not it's child contexts. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { global $DB; if (empty($userlist->get_userids())) { return; } $context = $userlist->get_context(); if ($context instanceof \context_course) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid {$usersql} AND type = :type"; $params = $userparams; $params['itemid'] = $context->instanceid; $params['type'] = \backup::TYPE_1COURSE; $DB->delete_records_select('backup_controllers', $select, $params); $params = $userparams; $params['course'] = $context->instanceid; $params['type'] = \backup::TYPE_1SECTION; $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = :course)"; $select = $sectionsql . " AND userid {$usersql} AND type = :type"; $DB->delete_records_select('backup_controllers', $select, $params); } if ($context instanceof \context_module) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid {$usersql} AND type = :type"; $params = $userparams; $params['itemid'] = $context->instanceid; $params['type'] = \backup::TYPE_1ACTIVITY; // Delete activity backup data. $select = "itemid = :itemid AND type = :type AND userid {$usersql}"; $params = ['itemid' => $context->instanceid, 'type' => 'activity'] + $userparams; $DB->delete_records_select('backup_controllers', $select, $params); } } /** * Delete all user data for the specified user, in the specified contexts. * Only dealing with the specific context - not it's child contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context instanceof \context_course) { $select = "itemid = :itemid AND userid = :userid AND type = :type"; $params = [ 'userid' => $userid, 'itemid' => $context->instanceid, 'type' => \backup::TYPE_1COURSE ]; $DB->delete_records_select('backup_controllers', $select, $params); $params = [ 'userid' => $userid, 'course' => $context->instanceid, 'type' => \backup::TYPE_1SECTION ]; $sectionsql = "itemid IN (SELECT id FROM {course_sections} WHERE course = :course)"; $select = $sectionsql . " AND userid = :userid AND type = :type"; $DB->delete_records_select('backup_controllers', $select, $params); } if ($context instanceof \context_module) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "itemid = :itemid AND userid = :userid AND type = :type"; $params = [ 'itemid' => $context->instanceid, 'userid' => $userid, 'type' => \backup::TYPE_1ACTIVITY ]; $DB->delete_records_select('backup_controllers', $select, $params); } } } /** * Loop and export from a recordset. * * @param \moodle_recordset $recordset The recordset. * @param string $splitkey The record key to determine when to export. * @param mixed $initial The initial data to reduce from. * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. * @return void */ protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, callable $reducer, callable $export) { $data = $initial; $lastid = null; foreach ($recordset as $record) { if ($lastid && $record->{$splitkey} != $lastid) { $export($lastid, $data); $data = $initial; } $data = $reducer($data, $record); $lastid = $record->{$splitkey}; } $recordset->close(); if (!empty($lastid)) { $export($lastid, $data); } } } util/ui/backup_ui_setting.class.php 0000644 00000064464 15215711721 0013472 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the setting user interface classes that all backup/restore * settings use to represent the UI they have. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Abstract class used to represent the user interface that a setting has. * * @todo extend as required for restore * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class base_setting_ui { /** * Prefix applied to all inputs/selects */ const NAME_PREFIX = 'setting_'; /** * The name of the setting * @var string */ protected $name; /** * The label for the setting * @var string */ protected $label; /** * The optional accessible label for the setting. * @var string */ protected string $altlabel = ''; /** * An array of HTML attributes to apply to this setting * @var array */ protected $attributes = array(); /** * The backup_setting UI type this relates to. One of backup_setting::UI_*; * @var int */ protected $type; /** * An icon to display next to this setting in the UI * @var pix_icon */ protected $icon = false; /** * The setting this UI belongs to (parent reference) * @var base_setting|backup_setting */ protected $setting; /** * Constructors are sooooo cool * @param base_setting $setting */ public function __construct(base_setting $setting) { $this->setting = $setting; } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset. $this->setting = null; } /** * Gets the name of this item including its prefix * @return string */ public function get_name() { return self::NAME_PREFIX.$this->name; } /** * Gets the name of this item including its prefix * @return string */ public function get_label() { return $this->label; } /** * Get the visually hidden label for the UI setting. * * @return string */ public function get_visually_hidden_label(): ?string { global $PAGE; if ($this->altlabel === '') { return null; } $renderer = $PAGE->get_renderer('core_backup'); return $renderer->sr_text($this->altlabel); } /** * Gets the type of this element * @return int */ public function get_type() { return $this->type; } /** * Gets the HTML attributes for this item * @return array */ public function get_attributes() { return $this->attributes; } /** * Gets the value of this setting * @return mixed */ public function get_value() { return $this->setting->get_value(); } /** * Gets the value to display in a static quickforms element * @return mixed */ public function get_static_value() { return $this->setting->get_value(); } /** * Gets the the PARAM_XXXX validation to be applied to the setting * * return string The PARAM_XXXX constant of null if the setting type is not defined */ public function get_param_validation() { return $this->setting->get_param_validation(); } /** * Sets the label. * * @throws base_setting_ui_exception when the label is not valid. * @param string $label */ public function set_label(string $label): void { // Let's avoid empty/whitespace-only labels, so the html clean (that makes trim()) doesn't fail. if (trim($label) === '') { $label = ' '; // Will be converted to non-breaking utf-8 char 0xc2a0 by PARAM_CLEANHTML. } $label = clean_param($label, PARAM_CLEANHTML); if ($label === '') { throw new base_setting_ui_exception('setting_invalid_ui_label'); } $this->label = $label; } /** * Adds a visually hidden label to the UI setting. * * Some backup fields labels have unaccessible labels for screen readers. For example, * all schema activity user data uses '-' as label. This method adds extra information * for screen readers. * * @param string $label The accessible label to be added. * @return void */ public function set_visually_hidden_label(string $label): void { $this->altlabel = clean_param($label, PARAM_CLEANHTML); } /** * Disables the UI for this element */ public function disable() { $this->attributes['disabled'] = 'disabled'; } /** * Sets the icon to display next to this item * * @param pix_icon $icon */ public function set_icon(pix_icon $icon) { $this->icon = $icon; } /** * Returns the icon to display next to this item, or false if there isn't one. * * @return pix_icon|false */ public function get_icon() { if (!empty($this->icon)) { return $this->icon; } return false; } } /** * Abstract class to represent the user interface backup settings have * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_setting_ui extends base_setting_ui { /** * An array of options relating to this setting * @var array */ protected $options = array(); /** * JAC... Just Another Constructor * * @param backup_setting $setting * @param string $label The label to display with the setting ui * @param array $attributes Array of HTML attributes to apply to the element * @param array $options Array of options to apply to the setting ui object */ public function __construct(backup_setting $setting, $label = null, ?array $attributes = null, ?array $options = null) { parent::__construct($setting); // Improve the inputs name by appending the level to the name. switch ($setting->get_level()) { case backup_setting::ROOT_LEVEL : $this->name = 'root_'.$setting->get_name(); break; case backup_setting::COURSE_LEVEL : $this->name = 'course_'.$setting->get_name(); break; case backup_setting::SECTION_LEVEL : case backup_setting::SUBSECTION_LEVEL: $this->name = 'section_'.$setting->get_name(); break; case backup_setting::ACTIVITY_LEVEL : case backup_setting::SUBACTIVITY_LEVEL: $this->name = 'activity_'.$setting->get_name(); break; } $this->label = $label; if (is_array($attributes)) { $this->attributes = $attributes; } if (is_array($options)) { $this->options = $options; } } /** * Creates a new backup setting ui based on the setting it is given * * @throws backup_setting_ui_exception if the setting type is not supported, * @param backup_setting $setting * @param int $type The backup_setting UI type. One of backup_setting::UI_*; * @param string $label The label to display with the setting ui * @param array $attributes Array of HTML attributes to apply to the element * @param array $options Array of options to apply to the setting ui object * @return backup_setting_ui_text|backup_setting_ui_checkbox|backup_setting_ui_select|backup_setting_ui_radio */ final public static function make(backup_setting $setting, $type, $label, ?array $attributes = null, ?array $options = null) { // Base the decision we make on the type that was sent. switch ($type) { case backup_setting::UI_HTML_CHECKBOX : return new backup_setting_ui_checkbox($setting, $label, null, (array)$attributes, (array)$options); case backup_setting::UI_HTML_DROPDOWN : return new backup_setting_ui_select($setting, $label, null, (array)$attributes, (array)$options); case backup_setting::UI_HTML_RADIOBUTTON : return new backup_setting_ui_radio($setting, $label, null, null, (array)$attributes, (array)$options); case backup_setting::UI_HTML_TEXTFIELD : return new backup_setting_ui_text($setting, $label, $attributes, $options); default: throw new backup_setting_ui_exception('setting_invalid_ui_type'); } } /** * Get element properties that can be used to make a quickform element * * @param base_task $task * @param renderer_base $output * @return array */ abstract public function get_element_properties(?base_task $task = null, ?renderer_base $output = null); /** * Applies config options to a given properties array and then returns it * @param array $properties * @return array */ public function apply_options(array $properties) { if (!empty($this->options['size'])) { $properties['attributes']['size'] = $this->options['size']; } return $properties; } /** * Gets the label for this item * @param base_task $task Optional, if provided and the setting is an include * $task is used to set the setting label * @return string */ public function get_label(?base_task $task = null) { // If a task has been provided and the label is not already set meaningfully // we will attempt to improve it. if (!is_null($task) && $this->label == $this->setting->get_name() && strpos($this->setting->get_name(), '_include') !== false) { $level = $this->setting->get_level(); if ($level == backup_setting::SECTION_LEVEL || $level == backup_setting::SUBSECTION_LEVEL) { $this->label = get_string('includesection', 'backup', $task->get_name()); } else if ($level == backup_setting::ACTIVITY_LEVEL || $level == backup_setting::SUBACTIVITY_LEVEL) { $this->label = $task->get_name(); } } return $this->label; } /** * Returns true if the setting is changeable. * * A setting is changeable if it meets either of the two following conditions. * * 1. The setting is not locked * 2. The setting is locked but only by settings that are of the same level (same page) * * Condition 2 is really why we have this function * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered, * when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting, * they could be changeable in the same view. * @return bool */ public function is_changeable($level = null) { if ($this->setting->get_status() === backup_setting::NOT_LOCKED) { // Its not locked so its chanegable. return true; } else if ($this->setting->get_status() !== backup_setting::LOCKED_BY_HIERARCHY) { // Its not changeable because its locked by permission or config. return false; } else if ($this->setting->has_dependencies_on_settings()) { foreach ($this->setting->get_settings_depended_on() as $dependency) { if ($level && $dependency->get_setting()->get_level() >= $level) { continue; } if ($dependency->is_locked() && $dependency->get_setting()->get_level() !== $this->setting->get_level()) { // Its not changeable because one or more dependancies arn't changeable. return false; } } // Its changeable because all dependencies are changeable. return true; } // We should never get here but if we do return false to be safe. // The setting would need to be locked by hierarchy and not have any deps. return false; } } /** * A text input user interface element for backup settings * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_text extends backup_setting_ui { /** * @var int */ protected $type = backup_setting::UI_HTML_TEXTFIELD; /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, attributes) */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { $icon = $this->get_icon(); $context = context_course::instance($task->get_courseid()); $label = format_string($this->get_label($task), true, array('context' => $context)); if (!empty($icon)) { $label .= $output->render($icon); } // Name, label, attributes. return $this->apply_options(array( 'element' => 'text', 'name' => self::NAME_PREFIX.$this->name, 'label' => $label, 'attributes' => $this->attributes) ); } } /** * A checkbox user interface element for backup settings (default) * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_checkbox extends backup_setting_ui { /** * @var int */ protected $type = backup_setting::UI_HTML_CHECKBOX; /** * @var bool */ protected $changeable = true; /** * The text to show next to the checkbox * @var string */ protected $text; /** * Overridden constructor so we can take text argument * * @param backup_setting $setting * @param string $label * @param string $text * @param array $attributes * @param array $options */ public function __construct(backup_setting $setting, $label = null, $text = null, array $attributes = array(), array $options = array()) { parent::__construct($setting, $label, $attributes, $options); $this->text = $text; } /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, text, attributes); */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { // Name, label, text, attributes. $icon = $this->get_icon(); $context = context_course::instance($task->get_courseid()); $label = format_string($this->get_label($task), true, array('context' => $context)); if (!empty($icon)) { $label .= $output->render($icon); } $altlabel = $this->get_visually_hidden_label(); if (!empty($altlabel)) { $label = $altlabel . $label; } return $this->apply_options(array( 'element' => 'checkbox', 'name' => self::NAME_PREFIX.$this->name, 'label' => $label, 'text' => $this->text, 'attributes' => $this->attributes )); } /** * Sets the text for the element * @param string $text */ public function set_text($text) { $this->text = $text; } /** * Gets the static value for the element * @global core_renderer $OUTPUT * @return string */ public function get_static_value() { global $OUTPUT; // Checkboxes are always yes or no. if ($this->get_value()) { return $OUTPUT->pix_icon('i/valid', get_string('yes')); } else { return $OUTPUT->pix_icon('i/invalid', get_string('no')); } } /** * Returns true if the setting is changeable * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered, * when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting, * they could be changeable in the same view. * @return bool */ public function is_changeable($level = null) { if ($this->changeable === false) { return false; } else { return parent::is_changeable($level); } } /** * Sets whether the setting is changeable, * Note dependencies can still mark this setting changeable or not * @param bool $newvalue */ public function set_changeable($newvalue) { $this->changeable = ($newvalue); } } /** * Radio button user interface element for backup settings * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_radio extends backup_setting_ui { /** * @var int */ protected $type = backup_setting::UI_HTML_RADIOBUTTON; /** * The string shown next to the input * @var string */ protected $text; /** * The value for the radio input * @var string */ protected $value; /** * Constructor * * @param backup_setting $setting * @param string $label * @param string $text * @param string $value * @param array $attributes * @param array $options */ public function __construct(backup_setting $setting, $label = null, $text = null, $value = null, array $attributes = array(), array $options = array()) { parent::__construct($setting, $label, $attributes, $options); $this->text = $text; $this->value = (string)$value; } /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, text, value, attributes) */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { $icon = $this->get_icon(); $context = context_course::instance($task->get_courseid()); $label = format_string($this->get_label($task), true, array('context' => $context)); if (!empty($icon)) { $label .= $output->render($icon); } // Name, label, text, value, attributes. return $this->apply_options(array( 'element' => 'radio', 'name' => self::NAME_PREFIX.$this->name, 'label' => $label, 'text' => $this->text, 'value' => $this->value, 'attributes' => $this->attributes )); } /** * Sets the text next to this input * @param text $text */ public function set_text($text) { $this->text = $text; } /** * Sets the value for the input * @param string $value */ public function set_value($value) { $this->value = (string)$value; } /** * Gets the static value to show for the element */ public function get_static_value() { return $this->value; } } /** * A select box, drop down user interface for backup settings * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_select extends backup_setting_ui { /** * @var int */ protected $type = backup_setting::UI_HTML_DROPDOWN; /** * An array of options to display in the select * @var array */ protected $values; /** * Constructor * * @param backup_setting $setting * @param string $label * @param array $values * @param array $attributes * @param array $options */ public function __construct(backup_setting $setting, $label = null, $values = null, array $attributes = array(), array $options = array()) { parent::__construct($setting, $label, $attributes, $options); $this->values = $values; } /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, options, attributes) */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { $icon = $this->get_icon(); $context = context_course::instance($task->get_courseid()); $label = format_string($this->get_label($task), true, array('context' => $context)); if (!empty($icon)) { $label .= $output->render($icon); } // Name, label, options, attributes. return $this->apply_options(array( 'element' => 'select', 'name' => self::NAME_PREFIX.$this->name, 'label' => $label, 'options' => $this->values, 'attributes' => $this->attributes )); } /** * Sets the options for the select box * @param array $values Associative array of value => text options */ public function set_values(array $values) { $this->values = $values; } /** * Gets the static value for this select element * @return string */ public function get_static_value() { return $this->values[$this->get_value()]; } /** * Returns true if the setting is changeable, false otherwise * * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered, * when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting, * they could be changeable in the same view. * @return bool */ public function is_changeable($level = null) { if (count($this->values) == 1) { return false; } else { return parent::is_changeable($level); } } /** * Returns the list of available values * @return array */ public function get_values() { return $this->values; } } /** * A date selector user interface widget for backup settings. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_dateselector extends backup_setting_ui_text { /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, options, attributes) */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { if (!array_key_exists('optional', $this->attributes)) { $this->attributes['optional'] = false; } $properties = parent::get_element_properties($task, $output); $properties['element'] = 'date_selector'; return $properties; } /** * Gets the static value for this select element * @return string */ public function get_static_value() { $value = $this->get_value(); if (!empty($value)) { return userdate($value); } return parent::get_static_value(); } } /** * A wrapper for defaultcustom form element - can have either text or date_selector type * * @package core_backup * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_defaultcustom extends backup_setting_ui_text { /** * Constructor * * @param backup_setting $setting * @param string $label The label to display with the setting ui * @param array $attributes Array of HTML attributes to apply to the element * @param array $options Array of options to apply to the setting ui object */ public function __construct(backup_setting $setting, $label = null, ?array $attributes = null, ?array $options = null) { if (!is_array($attributes)) { $attributes = []; } $attributes += ['customlabel' => get_string('overwrite', 'backup'), 'type' => 'text']; parent::__construct($setting, $label, $attributes, $options); } /** * Returns an array of properties suitable for generating a quickforms element * @param base_task $task * @param renderer_base $output * @return array (element, name, label, options, attributes) */ public function get_element_properties(?base_task $task = null, ?renderer_base $output = null) { return ['element' => 'defaultcustom'] + parent::get_element_properties($task, $output); } /** * Gets the static value for this select element * @return string */ public function get_static_value() { $value = $this->get_value(); if ($value === false) { $value = $this->attributes['defaultvalue']; } if (!empty($value)) { if ($this->attributes['type'] === 'date_selector' || $this->attributes['type'] === 'date_time_selector') { return userdate($value); } } return $value; } } /** * Base setting UI exception class. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class base_setting_ui_exception extends base_setting_exception {} /** * Backup setting UI exception class. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_setting_ui_exception extends base_setting_ui_exception {}; util/ui/base_ui.class.php 0000644 00000025226 15215711721 0011373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the backup user interface class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This is the backup user interface class * * The backup user interface class manages the user interface and backup for * Moodle. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_ui { /** * The progress of this instance of the backup ui class * It is in the initial stage. */ const PROGRESS_INTIAL = 0; /** * The progress of this instance of the backup ui class * It is processed. */ const PROGRESS_PROCESSED = 1; /** * The progress of this instance of the backup ui class * It is saved. */ const PROGRESS_SAVED = 2; /** * The progress of this instance of the backup ui class * It has been executed. */ const PROGRESS_EXECUTED = 3; /** * The controller * @var backup_controller|restore_controller */ protected $controller; /** * The current stage * @var base_ui_stage */ protected $stage; /** * The current progress of the UI * @var int One of self::PROGRESS_* */ protected $progress; /** * The number of changes made by dependency enforcement * @var int */ protected $dependencychanges = 0; /** * Yay for constructors * @param backup_controller $controller * @param array $params */ public function __construct($controller, ?array $params = null) { $this->controller = $controller; $this->progress = self::PROGRESS_INTIAL; $this->stage = $this->initialise_stage(null, $params); if ($this->controller) { // Process UI event before to be safe. $this->controller->process_ui_event(); } } /** * Destorys the backup controller and the loaded stage. */ public function destroy() { if ($this->controller) { $this->controller->destroy(); } unset($this->stage); } /** * Intialises what ever stage is requested. If none are requested we check * params for 'stage' and default to initial * * @param int|null $stage The desired stage to intialise or null for the default * @param array $params * @return base_ui_stage */ abstract protected function initialise_stage($stage = null, ?array $params = null); /** * This processes the current stage of the backup * @throws backup_ui_exception * @return bool */ public function process() { if ($this->progress >= self::PROGRESS_PROCESSED) { throw new backup_ui_exception('backupuialreadyprocessed'); } $this->progress = self::PROGRESS_PROCESSED; if (optional_param('previous', false, PARAM_BOOL) && $this->stage->get_stage() > $this->get_first_stage_id()) { $this->stage = $this->initialise_stage($this->stage->get_prev_stage(), $this->stage->get_params()); return false; } // Process the stage. $processoutcome = $this->stage->process(); if ($processoutcome !== false) { $this->stage = $this->initialise_stage($this->stage->get_next_stage(), $this->stage->get_params()); } // Process UI event after to check changes are valid. $this->controller->process_ui_event(); return $processoutcome; } /** * Saves the backup controller. * * Once this has been called nothing else can be changed in the controller. * * @throws base_ui_exception * @return bool */ public function save_controller() { if ($this->progress >= self::PROGRESS_SAVED) { throw new base_ui_exception('backupuialreadysaved'); } $this->progress = self::PROGRESS_SAVED; // First enforce dependencies. $this->enforce_dependencies(); // Process UI event after to check any changes are valid. $this->controller->process_ui_event(); // Save the controller. $this->controller->save_controller(); return true; } /** * Displays the UI for the backup! * * @throws base_ui_exception * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { if ($this->progress < self::PROGRESS_SAVED) { throw new base_ui_exception('backupsavebeforedisplay'); } return $this->stage->display($renderer); } /** * Gets all backup tasks from the controller * @return array Array of backup_task */ public function get_tasks() { $plan = $this->controller->get_plan(); $tasks = $plan->get_tasks(); return $tasks; } /** * Gets the stage we are on * @return int */ public function get_stage() { return $this->stage->get_stage(); } /** * Gets the name of the stage we are on * @return string */ public function get_stage_name() { return $this->stage->get_name(); } /** * Gets the backup id from the controller * @return string */ abstract public function get_uniqueid(); /** * Executes the backup plan * @return bool */ abstract public function execute(); /** * Enforces dependencies on all settings. Call before save * @return bool True if dependencies were enforced and changes were made */ protected function enforce_dependencies() { // Get the plan. $plan = $this->controller->get_plan(); // Get the tasks as a var so we can iterate by reference. $tasks = $plan->get_tasks(); $changes = 0; foreach ($tasks as &$task) { // Store as a var so we can iterate by reference. $settings = $task->get_settings(); foreach ($settings as &$setting) { // Get all dependencies for iteration by reference. $dependencies = $setting->get_dependencies(); foreach ($dependencies as &$dependency) { // Enforce each dependency. if ($dependency->enforce()) { $changes++; } } } } // Store the number of settings that changed through enforcement. $this->dependencychanges = $changes; return ($changes > 0); } /** * Returns true if enforce_dependencies changed any settings * @return bool */ public function enforce_changed_dependencies() { return ($this->dependencychanges > 0); } /** * Loads the backup controller if we are tracking one * @throws coding_exception * @param string|bool $uniqueid * @return backup_controller|false */ public static function load_controller($uniqueid = false) { throw new coding_exception('load_controller() method needs to be overridden in each subclass of base_ui'); } /** * Cancels the current backup/restore and redirects the user back to the relevant place */ public function cancel_process() { global $PAGE; // Determine the appropriate URL to redirect the user to. if ($PAGE->context->contextlevel == CONTEXT_MODULE && $PAGE->cm !== null) { $relevanturl = new moodle_url('/mod/'.$PAGE->cm->modname.'/view.php', array('id' => $PAGE->cm->id)); } else { $relevanturl = new moodle_url('/course/view.php', array('id' => $PAGE->course->id)); } redirect($relevanturl); } /** * Gets an array of progress bar items that can be displayed through the backup renderer. * @return array Array of items for the progress bar */ abstract public function get_progress_bar(); /** * Gets the format for the backup * @return int */ public function get_format() { return $this->controller->get_format(); } /** * Gets the type of the backup * @return int */ public function get_type() { return $this->controller->get_type(); } /** * Returns the controller object. * @return backup_controller|restore_controller */ public function get_controller() { return $this->controller; } /** * Gets the ID used in creating the controller. Relates to course/section/cm * @return int */ public function get_controller_id() { return $this->controller->get_id(); } /** * Gets the requested setting * @param string $name * @param bool $default * @return base_setting */ public function get_setting($name, $default = false) { try { return $this->controller->get_plan()->get_setting($name); } catch (Exception $e) { debugging('Failed to find the setting: '.$name, DEBUG_DEVELOPER); return $default; } } /** * Gets the value for the requested setting * * @param string $name * @param bool $default * @return mixed */ public function get_setting_value($name, $default = false) { try { return $this->controller->get_plan()->get_setting($name)->get_value(); } catch (Exception $e) { debugging('Failed to find the setting: '.$name, DEBUG_DEVELOPER); return $default; } } /** * Returns the name of this stage. * @return mixed */ abstract public function get_name(); /** * Returns the first stage ID. * @return mixed */ abstract public function get_first_stage_id(); } /** * Backup user interface exception. Modelled off the backup_exception class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class base_ui_exception extends backup_exception {} util/ui/UPGRADING.md 0000644 00000001475 15215711721 0010011 0 ustar 00 # core_backup (subsystem) Upgrade notes ## 4.5 ### Removed - The `\core_backup\copy\copy` class has been deprecated and removed. Please use `\copy_helper` instead. For more information see [MDL-75022](https://tracker.moodle.org/browse/MDL-75022) - The following methods in the `\base_controller` class have been removed: | Method | Replacement | | --- | --- | | `\base_controller::set_copy()` | Use a restore controller for storing copy information instead. | | `\base_controller::get_copy()` | `\restore_controller::get_copy()` | For more information see [MDL-75025](https://tracker.moodle.org/browse/MDL-75025) util/ui/renderer.php 0000644 00000143471 15215711721 0010471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains backup and restore output renderers * * @package core_backup * @copyright 2010 Sam Hemelryk * @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/includes/restore_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); /** * The primary renderer for the backup. * * Can be retrieved with the following code: * <?php * $renderer = $PAGE->get_renderer('core', 'backup'); * ?> * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_backup_renderer extends plugin_renderer_base { /** * Same site notification display. * * @var string */ private $samesitenotification = ''; /** * Renderers a progress bar for the backup or restore given the items that make it up. * * @param array $items An array of items * @return string */ public function progress_bar(array $items) { foreach ($items as &$item) { $text = $item['text']; unset($item['text']); if (array_key_exists('link', $item)) { $link = $item['link']; unset($item['link']); $item = html_writer::link($link, $text, $item); } else { $item = html_writer::tag('span', $text, $item); } } return html_writer::tag('div', join(get_separator(), $items), array('class' => 'backup_progress clearfix')); } /** * The backup and restore pages may display a log (if any) in a scrolling box. * * @param string $loghtml Log content in HTML format * @return string HTML content that shows the log */ public function log_display($loghtml) { $out = html_writer::start_div('backup_log'); $out .= $this->output->heading(get_string('backuplog', 'backup')); $out .= html_writer::start_div('backup_log_contents'); $out .= $loghtml; $out .= html_writer::end_div(); $out .= html_writer::end_div(); return $out; } /** * Set the same site backup notification. * */ public function set_samesite_notification() { $this->samesitenotification = $this->output->notification(get_string('samesitenotification', 'backup'), 'info'); } /** * Get the same site backup notification. * */ public function get_samesite_notification() { return $this->samesitenotification; } /** * Prints a dependency notification * * @param string $message * @return string */ public function dependency_notification($message) { return html_writer::tag('div', $message, array('class' => 'notification dependencies_enforced')); } /** * Displays the details of a backup file * * @param stdClass $details * @param moodle_url $nextstageurl * @return string */ public function backup_details($details, $nextstageurl) { $yestick = $this->output->pix_icon('i/valid', get_string('yes')); $notick = $this->output->pix_icon('i/invalid', get_string('no')); $html = html_writer::start_tag('div', array('class' => 'backup-restore')); $html .= html_writer::start_tag('div', ['class' => 'backup-section', 'role' => 'table', 'aria-labelledby' => 'backupdetailsheader']); $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, 'header', 'backupdetailsheader'); $html .= $this->backup_detail_pair(get_string('backuptype', 'backup'), get_string('backuptype'.$details->type, 'backup')); $html .= $this->backup_detail_pair(get_string('backupformat', 'backup'), get_string('backupformat'.$details->format, 'backup')); $html .= $this->backup_detail_pair(get_string('backupmode', 'backup'), get_string('backupmode'.$details->mode, 'backup')); $html .= $this->backup_detail_pair(get_string('backupdate', 'backup'), userdate($details->backup_date)); $html .= $this->backup_detail_pair(get_string('moodleversion', 'backup'), html_writer::tag('span', s($details->moodle_release), array('class' => 'moodle_release')). html_writer::tag('span', '[' . s($details->moodle_version) .']', array('class' => 'moodle_version sub-detail'))); $html .= $this->backup_detail_pair(get_string('backupversion', 'backup'), html_writer::tag('span', s($details->backup_release), array('class' => 'moodle_release')). html_writer::tag('span', '[' . s($details->backup_version) . ']', array('class' => 'moodle_version sub-detail'))); $html .= $this->backup_detail_pair(get_string('originalwwwroot', 'backup'), html_writer::tag('span', s($details->original_wwwroot), array('class' => 'originalwwwroot')). html_writer::tag('span', '[' . s($details->original_site_identifier_hash) . ']', array('class' => 'sitehash sub-detail'))); if (!empty($details->include_file_references_to_external_content)) { $message = ''; if (backup_general_helper::backup_is_samesite($details)) { $message = $yestick . ' ' . get_string('filereferencessamesite', 'backup'); } else { $message = $notick . ' ' . get_string('filereferencesnotsamesite', 'backup'); } $html .= $this->backup_detail_pair(get_string('includefilereferences', 'backup'), $message); } $html .= html_writer::end_tag('div'); $html .= html_writer::start_tag('div', ['class' => 'backup-section settings-section', 'role' => 'table', 'aria-labelledby' => 'backupsettingsheader']); $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, 'header', 'backupsettingsheader'); foreach ($details->root_settings as $label => $value) { if ($label == 'filename' or $label == 'user_files') { continue; } $html .= $this->backup_detail_pair(get_string('rootsetting'.str_replace('_', '', $label), 'backup'), $value ? $yestick : $notick); } $html .= html_writer::end_tag('div'); if ($details->type === 'course') { $html .= html_writer::start_tag('div', ['class' => 'backup-section', 'role' => 'table', 'aria-labelledby' => 'backupcoursedetailsheader']); $html .= $this->output->heading(get_string('backupcoursedetails', 'backup'), 2, 'header', 'backupcoursedetailsheader'); $html .= $this->backup_detail_pair(get_string('coursetitle', 'backup'), format_string($details->course->title)); $html .= $this->backup_detail_pair(get_string('courseid', 'backup'), clean_param($details->course->courseid, PARAM_INT)); // Warning users about front page backups. if ($details->original_course_format === 'site') { $html .= $this->backup_detail_pair(get_string('type_format', 'plugin'), get_string('sitecourseformatwarning', 'backup')); } $html .= html_writer::start_tag('div', array('class' => 'backup-sub-section')); $html .= $this->output->heading(get_string('backupcoursesections', 'backup'), 3, array('class' => 'subheader')); foreach ($details->sections as $key => $section) { $included = $key.'_included'; $userinfo = $key.'_userinfo'; if ($section->settings[$included] && $section->settings[$userinfo]) { $value = get_string('sectionincanduser', 'backup'); } else if ($section->settings[$included]) { $value = get_string('sectioninc', 'backup'); } else { continue; } $html .= $this->backup_detail_pair(get_string('backupcoursesection', 'backup', format_string($section->title)), $value); $table = null; foreach ($details->activities as $activitykey => $activity) { if ($activity->sectionid != $section->sectionid) { continue; } if (empty($table)) { $table = new html_table(); $table->head = array(get_string('module', 'backup'), get_string('title', 'backup'), get_string('userinfo', 'backup')); $table->colclasses = array('modulename', 'moduletitle', 'userinfoincluded'); $table->align = array('left', 'left', 'center'); $table->attributes = array('class' => 'activitytable generaltable'); $table->data = array(); } $name = get_string('pluginname', $activity->modulename); $icon = new image_icon('monologo', '', $activity->modulename); $table->data[] = array( $this->output->render($icon).$name, format_string($activity->title), ($activity->settings[$activitykey.'_userinfo']) ? $yestick : $notick, ); } if (!empty($table)) { $html .= $this->backup_detail_pair(get_string('sectionactivities', 'backup'), html_writer::table($table)); } } $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('div'); } $html .= $this->continue_button($nextstageurl, 'post'); $html .= html_writer::end_tag('div'); return $html; } /** * Displays the general information about a backup file with non-standard format * * @param moodle_url $nextstageurl URL to send user to * @param array $details basic info about the file (format, type) * @return string HTML code to display */ public function backup_details_nonstandard($nextstageurl, array $details) { $html = html_writer::start_tag('div', array('class' => 'backup-restore nonstandardformat')); $html .= html_writer::start_tag('div', array('class' => 'backup-section')); $html .= $this->output->heading(get_string('backupdetails', 'backup'), 2, 'header'); $html .= $this->output->box(get_string('backupdetailsnonstandardinfo', 'backup'), 'noticebox'); $html .= $this->backup_detail_pair( get_string('backupformat', 'backup'), get_string('backupformat'.$details['format'], 'backup')); $html .= $this->backup_detail_pair( get_string('backuptype', 'backup'), get_string('backuptype'.$details['type'], 'backup')); $html .= html_writer::end_tag('div'); $html .= $this->continue_button($nextstageurl, 'post'); $html .= html_writer::end_tag('div'); return $html; } /** * Displays the general information about a backup file with unknown format * * @param moodle_url $nextstageurl URL to send user to * @return string HTML code to display */ public function backup_details_unknown(moodle_url $nextstageurl) { $html = html_writer::start_div('unknownformat'); $html .= $this->output->heading(get_string('errorinvalidformat', 'backup'), 2); $html .= $this->output->notification(get_string('errorinvalidformatinfo', 'backup'), 'notifyproblem'); $html .= $this->continue_button($nextstageurl, 'post'); $html .= html_writer::end_div(); return $html; } /** * Displays a course selector for restore * * @param moodle_url $nextstageurl * @param bool $wholecourse true if we are restoring whole course (as with backup::TYPE_1COURSE), false otherwise * @param restore_category_search $categories * @param restore_course_search $courses * @param int $currentcourse * @return string */ public function course_selector(moodle_url $nextstageurl, $wholecourse = true, ?restore_category_search $categories = null, ?restore_course_search $courses = null, $currentcourse = null) { global $CFG; require_once($CFG->dirroot.'/course/lib.php'); // These variables are used to check if the form using this function was submitted. $target = optional_param('target', false, PARAM_INT); $targetid = optional_param('targetid', null, PARAM_INT); // Check if they submitted the form but did not provide all the data we need. $missingdata = false; if ($target and is_null($targetid)) { $missingdata = true; } $nextstageurl->param('sesskey', sesskey()); $form = html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring(), 'class' => 'mform')); foreach ($nextstageurl->params() as $key => $value) { $form .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $key, 'value' => $value)); } $hasrestoreoption = false; $html = html_writer::start_tag('div', array('class' => 'backup-course-selector backup-restore')); if ($wholecourse && !empty($categories) && ($categories->get_count() > 0 || $categories->get_search())) { // New course. $hasrestoreoption = true; $html .= $form; $html .= html_writer::start_tag('div', array('class' => 'bcs-new-course backup-section')); $html .= $this->output->heading(get_string('restoretonewcourse', 'backup'), 2, array('class' => 'header')); $html .= $this->backup_detail_input(get_string('restoretonewcourse', 'backup'), 'radio', 'target', backup::TARGET_NEW_COURSE, array('checked' => 'checked')); $selectacategoryhtml = $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories)); // Display the category selection as required if the form was submitted but this data was not supplied. if ($missingdata && $target == backup::TARGET_NEW_COURSE) { $html .= html_writer::span(get_string('required'), 'error'); $html .= html_writer::start_tag('fieldset', array('class' => 'error')); $html .= $selectacategoryhtml; $html .= html_writer::end_tag('fieldset'); } else { $html .= $selectacategoryhtml; } $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary'); $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs)); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('form'); } if ($wholecourse && !empty($currentcourse)) { // Current course. $hasrestoreoption = true; $html .= $form; $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'targetid', 'value' => $currentcourse)); $html .= html_writer::start_tag('div', array('class' => 'bcs-current-course backup-section')); $html .= $this->output->heading(get_string('restoretocurrentcourse', 'backup'), 2, array('class' => 'header')); $html .= $this->backup_detail_input(get_string('restoretocurrentcourseadding', 'backup'), 'radio', 'target', backup::TARGET_CURRENT_ADDING, array('checked' => 'checked')); $html .= $this->backup_detail_input(get_string('restoretocurrentcoursedeleting', 'backup'), 'radio', 'target', backup::TARGET_CURRENT_DELETING); $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary'); $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs)); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('form'); } // If we are restoring an activity, then include the current course. if (!$wholecourse) { $courses->invalidate_results(); // Clean list of courses. $courses->set_include_currentcourse(); } if (!empty($courses) && ($courses->get_count() > 0 || $courses->get_search())) { // Existing course. $hasrestoreoption = true; $html .= $form; $html .= html_writer::start_tag('div', array('class' => 'bcs-existing-course backup-section')); $html .= $this->output->heading(get_string('restoretoexistingcourse', 'backup'), 2, array('class' => 'header')); if ($wholecourse) { $html .= $this->backup_detail_input(get_string('restoretoexistingcourseadding', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_ADDING, array('checked' => 'checked')); $html .= $this->backup_detail_input(get_string('restoretoexistingcoursedeleting', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_DELETING); } else { $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'target', 'value' => backup::TARGET_EXISTING_ADDING)); } $selectacoursehtml = $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses)); // Display the course selection as required if the form was submitted but this data was not supplied. if ($missingdata && $target == backup::TARGET_EXISTING_ADDING) { $html .= html_writer::span(get_string('required'), 'error'); $html .= html_writer::start_tag('fieldset', array('class' => 'error')); $html .= $selectacoursehtml; $html .= html_writer::end_tag('fieldset'); } else { $html .= $selectacoursehtml; } $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary'); $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs)); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('form'); } if (!$hasrestoreoption) { echo $this->output->notification(get_string('norestoreoptions', 'backup')); } $html .= html_writer::end_tag('div'); return $html; } /** * Displays the import course selector * * @param moodle_url $nextstageurl * @param import_course_search $courses * @return string */ public function import_course_selector(moodle_url $nextstageurl, ?import_course_search $courses = null) { $html = html_writer::start_tag('div', array('class' => 'import-course-selector backup-restore')); $html .= html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring())); foreach ($nextstageurl->params() as $key => $value) { $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $key, 'value' => $value)); } // We only allow import adding for now. Enforce it here. $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'target', 'value' => backup::TARGET_CURRENT_ADDING)); $html .= html_writer::start_tag('div', array('class' => 'ics-existing-course backup-section')); $html .= $this->output->heading(get_string('importdatafrom'), 2, array('class' => 'header')); $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses)); $attrs = array('type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary'); $html .= html_writer::start_tag('div', array('class' => 'mt-3')); $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', $attrs)); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('div'); $html .= html_writer::end_tag('form'); $html .= html_writer::end_tag('div'); return $html; } /** * Creates a detailed pairing (key + value) * * @staticvar int $count * @param string $label * @param string $value * @return string */ protected function backup_detail_pair($label, $value) { static $count = 0; $count ++; $html = html_writer::start_tag('div', ['class' => 'detail-pair', 'role' => 'row']); $html .= html_writer::tag('div', $label, ['class' => 'detail-pair-label mb-2', 'role' => 'cell']); $html .= html_writer::tag('div', $value, ['class' => 'detail-pair-value ps-2', 'role' => 'cell']); $html .= html_writer::end_tag('div'); return $html; } /** * Creates a unique id string by appending an incremental number to the prefix. * * @param string $prefix To be used as the left part of the id string. * @return string */ protected function make_unique_id(string $prefix): string { static $count = 0; return $prefix . '-' . $count++; } /** * Created a detailed pairing with an input * * @param string $label * @param string $type * @param string $name * @param string $value * @param array $attributes * @param string|null $description * @return string */ protected function backup_detail_input($label, $type, $name, $value, array $attributes = array(), $description = null) { if (!empty($description)) { $description = html_writer::tag('span', $description, array('class' => 'description')); } else { $description = ''; } $id = $this->make_unique_id('detail-pair-value'); return $this->backup_detail_pair( html_writer::label($label, $id), html_writer::empty_tag('input', $attributes + ['id' => $id, 'name' => $name, 'type' => $type, 'value' => $value]) . $description ); } /** * Creates a detailed pairing with a select * * @param string $label * @param string $name * @param array $options * @param string $selected * @param bool $nothing * @param array $attributes * @param string|null $description * @return string */ protected function backup_detail_select($label, $name, $options, $selected = '', $nothing = false, array $attributes = array(), $description = null) { if (!empty ($description)) { $description = html_writer::tag('span', $description, array('class' => 'description')); } else { $description = ''; } return $this->backup_detail_pair($label, html_writer::select($options, $name, $selected, false, $attributes).$description); } /** * Displays precheck notices * * @param array $results * @return string */ public function precheck_notices($results) { $output = html_writer::start_tag('div', array('class' => 'restore-precheck-notices')); if (array_key_exists('errors', $results)) { foreach ($results['errors'] as $error) { $output .= $this->output->notification($error); } } if (array_key_exists('warnings', $results)) { foreach ($results['warnings'] as $warning) { $output .= $this->output->notification($warning, 'notifyproblem'); } } return $output.html_writer::end_tag('div'); } /** * Displays substage buttons * * @param bool $haserrors * @return string */ public function substage_buttons($haserrors) { $output = html_writer::start_tag('div', array('continuebutton')); if (!$haserrors) { $attrs = ['type' => 'submit', 'value' => get_string('continue'), 'class' => 'btn btn-primary me-1']; $output .= html_writer::empty_tag('input', $attrs); } $attrs = array('type' => 'submit', 'name' => 'cancel', 'value' => get_string('cancel'), 'class' => 'btn btn-secondary'); $output .= html_writer::empty_tag('input', $attrs); $output .= html_writer::end_tag('div'); return $output; } /** * Displays a role mapping interface * * @param array $rolemappings * @param array $roles * @return string */ public function role_mappings($rolemappings, $roles) { $roles[0] = get_string('none'); $output = html_writer::start_tag('div', array('class' => 'restore-rolemappings')); $output .= $this->output->heading(get_string('restorerolemappings', 'backup'), 2); foreach ($rolemappings as $id => $mapping) { $label = $mapping->name; $name = 'mapping'.$id; $selected = $mapping->targetroleid; $output .= $this->backup_detail_select($label, $name, $roles, $mapping->targetroleid, false, array(), $mapping->description); } $output .= html_writer::end_tag('div'); return $output; } /** * Displays a continue button, overriding core renderer method of the same in order * to override submission method of the button form * * @param string|moodle_url $url * @param string $method * @return string */ public function continue_button($url, $method = 'post') { if (!($url instanceof moodle_url)) { $url = new moodle_url($url); } if ($method != 'post') { $method = 'get'; } $button = new single_button($url, get_string('continue'), $method, single_button::BUTTON_PRIMARY); $button->class = 'continuebutton'; return $this->render($button); } /** * Print a backup files tree * @param array $options * @return string */ public function backup_files_viewer(?array $options = null) { $files = new backup_files_viewer($options); return $this->render($files); } /** * Generate the status indicator markup for display in the * backup restore file area UI. * * @param int $statuscode The status code of the backup. * @param string $backupid The backup record id. * @return string|boolean $status The status indicator for the operation. */ public function get_status_display($statuscode, $backupid, $restoreid=null, $operation='backup') { if ($statuscode == backup::STATUS_AWAITING || $statuscode == backup::STATUS_EXECUTING || $statuscode == backup::STATUS_REQUIRE_CONV) { // In progress. $progresssetup = array( 'backupid' => $backupid, 'restoreid' => $restoreid, 'operation' => $operation, 'width' => '100' ); $status = $this->render_from_template('core/async_backup_progress', $progresssetup); } else if ($statuscode == backup::STATUS_FINISHED_ERR) { // Error. $icon = $this->output->render(new \pix_icon('i/delete', get_string('failed', 'backup'))); $status = \html_writer::span($icon, 'action-icon'); } else if ($statuscode == backup::STATUS_FINISHED_OK) { // Complete. $icon = $this->output->render(new \pix_icon('i/checked', get_string('successful', 'backup'))); $status = \html_writer::span($icon, 'action-icon'); } return $status; } /** * Displays a backup files viewer * * @global stdClass $USER * @param backup_files_viewer $viewer * @return string */ public function render_backup_files_viewer(backup_files_viewer $viewer) { $files = $viewer->files; $filestodisplay = false; foreach ($files as $file) { if (!$file->is_directory()) { $filestodisplay = true; break; } } $async = \async_helper::is_async_enabled(); switch($viewer->filearea) { case 'activity': $title = get_string('choosefilefromactivitybackup', 'backup'); $description = get_string('choosefilefromactivitybackup_help', 'backup'); $button = get_string('managefiles_activity', 'backup'); $nofilesstring = get_string('restorenofilesbackuparea_activity', 'backup'); break; case 'course': $title = get_string('choosefilefromcoursebackup', 'backup'); $description = get_string('choosefilefromcoursebackup_help', 'backup'); $button = get_string('managefiles_course', 'backup'); $nofilesstring = get_string('restorenofilesbackuparea_course', 'backup'); break; case 'backup': $title = get_string('choosefilefromuserbackup', 'backup'); $description = get_string('choosefilefromuserbackup_help', 'backup'); $button = get_string('managefiles_backup', 'backup'); $nofilesstring = get_string('restorenofilesbackuparea_backup', 'backup'); break; case 'automated': $title = get_string('choosefilefromautomatedbackup', 'backup'); $description = get_string('choosefilefromautomatedbackup_help', 'backup'); $button = get_string('managefiles_automated', 'backup'); $nofilesstring = get_string('restorenofilesbackuparea_automated', 'backup'); break; default: $title = ''; $description = ''; $button = get_string('managefiles', 'backup'); $nofilesstring = get_string('restorenofilesbackuparea', 'backup'); } $html = html_writer::tag('h3', $title, ['class' => 'mt-6']); $html .= html_writer::tag('div', $description, ['class' => 'mb-3']); if ($filestodisplay || $async) { $tablehead = [ get_string('filename', 'backup'), get_string('time'), get_string('size'), get_string('download'), get_string('restore'), ]; if ($async) { $tablehead[] = get_string('status', 'backup'); } $table = new html_table(); $table->attributes['class'] = 'backup-files-table generaltable'; $table->head = $tablehead; $table->width = '100%'; $table->data = []; // First add in progress asynchronous backups. // Only if asynchronous backups are enabled. if ($async) { $tabledata = []; $backups = \async_helper::get_async_backups($viewer->filearea, $viewer->filecontext->instanceid); // For each backup get, new item name, time restore created and progress. foreach ($backups as $backup) { $status = $this->get_status_display($backup->status, $backup->backupid); $timecreated = $backup->timecreated; $tablerow = [$backup->filename, userdate($timecreated), '-', '-', '-', $status]; $tabledata[] = $tablerow; } $table->data = $tabledata; } // Add completed backups. foreach ($files as $file) { if ($file->is_directory()) { continue; } $fileurl = moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), null, $file->get_filepath(), $file->get_filename(), true ); $params = []; $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'] = $viewer->currentcontext->id; $params['itemid'] = $file->get_itemid(); $restoreurl = new moodle_url('/backup/restorefile.php', $params); $restorelink = html_writer::link($restoreurl, get_string('restore')); $downloadlink = html_writer::link($fileurl, get_string('download')); // Conditional display of the restore and download links, initially only for the 'automated' filearea. if ($params['filearea'] == 'automated') { if (!has_capability('moodle/restore:viewautomatedfilearea', $viewer->currentcontext)) { $restorelink = ''; } if (!can_download_from_backup_filearea($params['filearea'], $viewer->currentcontext)) { $downloadlink = ''; } } $tabledata = [ $file->get_filename(), userdate ($file->get_timemodified()), display_size ($file->get_filesize()), $downloadlink, $restorelink, ]; if ($async) { $tabledata[] = $this->get_status_display(backup::STATUS_FINISHED_OK, null); } $table->data[] = $tabledata; } $html .= html_writer::table($table); } else { // There are no files to display. $html .= $this->notification($nofilesstring, 'notifymessage'); } // For automated backups, the ability to manage backup files is controlled by the ability to download them. // All files must be from the same file area in a backup_files_viewer. $canmanagebackups = true; if ($viewer->filearea == 'automated') { if (!can_download_from_backup_filearea($viewer->filearea, $viewer->currentcontext)) { $canmanagebackups = false; } } if ($canmanagebackups) { $html .= $this->output->single_button( new moodle_url('/backup/backupfilesedit.php', array( 'currentcontext' => $viewer->currentcontext->id, 'contextid' => $viewer->filecontext->id, 'filearea' => $viewer->filearea, 'component' => $viewer->component, 'returnurl' => $this->page->url->out()) ), $button, 'post' ); } return $html; } /** * Renders a restore course search object * * @param restore_course_search $component * @return string */ public function render_restore_course_search(restore_course_search $component) { $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1')); $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75')); $table = new html_table(); $table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse')); $table->data = array(); if ($component->get_count() !== 0) { foreach ($component->get_results() as $course) { $row = new html_table_row(); $row->attributes['class'] = 'rcs-course'; if (!$course->visible) { $row->attributes['class'] .= ' dimmed'; } $id = $this->make_unique_id('restore-course'); $attrs = ['type' => 'radio', 'name' => 'targetid', 'value' => $course->id, 'id' => $id]; if ($course->id == $component->get_current_course_id()) { $attrs['checked'] = 'checked'; } $row->cells = [ html_writer::empty_tag('input', $attrs), html_writer::label( format_string($course->shortname, true, ['context' => context_course::instance($course->id)]), $id, true, ['class' => 'd-block'] ), format_string($course->fullname, true, ['context' => context_course::instance($course->id)]) ]; $table->data[] = $row; } if ($component->has_more_results()) { $cell = new html_table_cell(get_string('moreresults', 'backup')); $cell->colspan = 3; $cell->attributes['class'] = 'notifyproblem'; $row = new html_table_row(array($cell)); $row->attributes['class'] = 'rcs-course'; $table->data[] = $row; } } else { $cell = new html_table_cell(get_string('nomatchingcourses', 'backup')); $cell->colspan = 3; $cell->attributes['class'] = 'notifyproblem'; $row = new html_table_row(array($cell)); $row->attributes['class'] = 'rcs-course'; $table->data[] = $row; } $output .= html_writer::table($table); $output .= html_writer::end_tag('div'); $data = [ 'inform' => true, 'extraclasses' => 'rcs-search mb-3 w-25', 'inputname' => restore_course_search::$VAR_SEARCH, 'searchstring' => get_string('searchcourses'), 'buttonattributes' => [ (object) ['key' => 'name', 'value' => 'searchcourses'], (object) ['key' => 'value', 'value' => 1], ], 'query' => $component->get_search(), ]; $output .= $this->output->render_from_template('core/search_input', $data); $output .= html_writer::end_tag('div'); return $output; } /** * Renders an import course search object * * @param import_course_search $component * @return string */ public function render_import_course_search(import_course_search $component) { $output = html_writer::start_tag('div', array('class' => 'import-course-search')); if ($component->get_count() === 0) { $output .= $this->output->notification(get_string('nomatchingcourses', 'backup')); $output .= html_writer::start_tag('div', ['class' => 'ics-search d-flex flex-wrap align-items-center']); $attrs = array( 'type' => 'text', 'name' => restore_course_search::$VAR_SEARCH, 'value' => $component->get_search(), 'aria-label' => get_string('searchcourses'), 'placeholder' => get_string('searchcourses'), 'class' => 'form-control' ); $output .= html_writer::empty_tag('input', $attrs); $attrs = array( 'type' => 'submit', 'name' => 'searchcourses', 'value' => get_string('search'), 'class' => 'btn btn-secondary ms-1' ); $output .= html_writer::empty_tag('input', $attrs); $output .= html_writer::end_tag('div'); $output .= html_writer::end_tag('div'); return $output; } $countstr = ''; if ($component->has_more_results()) { $countstr = get_string('morecoursesearchresults', 'backup', $component->get_count()); } else { $countstr = get_string('totalcoursesearchresults', 'backup', $component->get_count()); } $output .= html_writer::tag('div', $countstr, array('class' => 'ics-totalresults')); $output .= html_writer::start_tag('div', array('class' => 'ics-results')); $table = new html_table(); $table->head = array('', get_string('shortnamecourse'), get_string('fullnamecourse')); $table->data = array(); foreach ($component->get_results() as $course) { $row = new html_table_row(); $row->attributes['class'] = 'ics-course'; if (!$course->visible) { $row->attributes['class'] .= ' dimmed'; } $id = $this->make_unique_id('import-course'); $row->cells = [ html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'importid', 'value' => $course->id, 'id' => $id]), html_writer::label( format_string($course->shortname, true, ['context' => context_course::instance($course->id)]), $id, true, ['class' => 'd-block'] ), format_string($course->fullname, true, ['context' => context_course::instance($course->id)]) ]; $table->data[] = $row; } if ($component->has_more_results()) { $cell = new html_table_cell(get_string('moreresults', 'backup')); $cell->colspan = 3; $cell->attributes['class'] = 'notifyproblem'; $row = new html_table_row(array($cell)); $row->attributes['class'] = 'rcs-course'; $table->data[] = $row; } $output .= html_writer::table($table); $output .= html_writer::end_tag('div'); $output .= html_writer::start_tag('div', ['class' => 'ics-search d-flex flex-wrap align-items-center']); $attrs = array( 'type' => 'text', 'name' => restore_course_search::$VAR_SEARCH, 'value' => $component->get_search(), 'aria-label' => get_string('searchcourses'), 'placeholder' => get_string('searchcourses'), 'class' => 'form-control'); $output .= html_writer::empty_tag('input', $attrs); $attrs = array( 'type' => 'submit', 'name' => 'searchcourses', 'value' => get_string('search'), 'class' => 'btn btn-secondary ms-1' ); $output .= html_writer::empty_tag('input', $attrs); $output .= html_writer::end_tag('div'); $output .= html_writer::end_tag('div'); return $output; } /** * Renders a restore category search object * * @param restore_category_search $component * @return string */ public function render_restore_category_search(restore_category_search $component) { $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1')); $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75')); $table = new html_table(); $table->head = array('', get_string('name'), get_string('description')); $table->data = array(); if ($component->get_count() !== 0) { foreach ($component->get_results() as $category) { $row = new html_table_row(); $row->attributes['class'] = 'rcs-course'; if (!$category->visible) { $row->attributes['class'] .= ' dimmed'; } $context = context_coursecat::instance($category->id); $id = $this->make_unique_id('restore-category'); $row->cells = [ html_writer::empty_tag('input', ['type' => 'radio', 'name' => 'targetid', 'value' => $category->id, 'id' => $id]), html_writer::label( format_string($category->name, true, ['context' => context_coursecat::instance($category->id)]), $id, true, ['class' => 'd-block'] ), format_text(file_rewrite_pluginfile_urls($category->description, 'pluginfile.php', $context->id, 'coursecat', 'description', null), $category->descriptionformat, ['overflowdiv' => true]) ]; $table->data[] = $row; } if ($component->has_more_results()) { $cell = new html_table_cell(get_string('moreresults', 'backup')); $cell->attributes['class'] = 'notifyproblem'; $cell->colspan = 3; $row = new html_table_row(array($cell)); $row->attributes['class'] = 'rcs-course'; $table->data[] = $row; } } else { $cell = new html_table_cell(get_string('nomatchingcourses', 'backup')); $cell->colspan = 3; $cell->attributes['class'] = 'notifyproblem'; $row = new html_table_row(array($cell)); $row->attributes['class'] = 'rcs-course'; $table->data[] = $row; } $output .= html_writer::table($table); $output .= html_writer::end_tag('div'); $data = [ 'inform' => true, 'extraclasses' => 'rcs-search mb-3 w-25', 'inputname' => restore_category_search::$VAR_SEARCH, 'searchstring' => get_string('searchcoursecategories'), 'buttonattributes' => [ (object) ['key' => 'name', 'value' => 'searchcourses'], (object) ['key' => 'value', 'value' => 1], ], 'query' => $component->get_search(), ]; $output .= $this->output->render_from_template('core/search_input', $data); $output .= html_writer::end_tag('div'); return $output; } /** * Get markup to render table for all of a users async * in progress restores. * * @param int $userid The Moodle user id. * @param \context $context The Moodle context for these restores. * @return string $html The table HTML. */ public function restore_progress_viewer($userid, $context) { $tablehead = array(get_string('course'), get_string('time'), get_string('status', 'backup')); $table = new html_table(); $table->attributes['class'] = 'backup-files-table generaltable'; $table->head = $tablehead; $tabledata = array(); // Get all in progress async restores for this user. $restores = \async_helper::get_async_restores($userid); // For each backup get, new item name, time restore created and progress. foreach ($restores as $restore) { $restorename = \async_helper::get_restore_name($context); $timecreated = $restore->timecreated; $status = $this->get_status_display($restore->status, $restore->backupid, $restore->backupid, null, 'restore'); $tablerow = array($restorename, userdate($timecreated), $status); $tabledata[] = $tablerow; } $table->data = $tabledata; $html = html_writer::table($table); return $html; } /** * Get markup to render table for all of a users course copies. * * @param int $userid The Moodle user id. * @param int $courseid The id of the course to get the backups for. * @return string $html The table HTML. */ public function copy_progress_viewer(int $userid, int $courseid): string { $tablehead = array( get_string('copysource', 'backup'), get_string('copydest', 'backup'), get_string('time'), get_string('copyop', 'backup'), get_string('status', 'backup') ); $table = new html_table(); $table->attributes['class'] = 'backup-files-table generaltable'; $table->head = $tablehead; $tabledata = array(); // Get all in progress course copies for this user. $copies = \copy_helper::get_copies($userid, $courseid); foreach ($copies as $copy) { $sourceurl = new \moodle_url('/course/view.php', array('id' => $copy->sourceid)); $tablerow = array( html_writer::link($sourceurl, format_string($copy->source, true, ['context' => context_course::instance($copy->sourceid)])), format_string($copy->destination, true, ['context' => context_course::instance($copy->sourceid)]), userdate($copy->timecreated), get_string($copy->operation), $this->get_status_display($copy->status, $copy->backupid, $copy->restoreid, $copy->operation) ); $tabledata[] = $tablerow; } $table->data = $tabledata; $html = html_writer::table($table); return $html; } } /** * Data structure representing backup files viewer * * @copyright 2010 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ class backup_files_viewer implements renderable { /** * @var array */ public $files; /** * @var context */ public $filecontext; /** * @var string */ public $component; /** * @var string */ public $filearea; /** * @var context */ public $currentcontext; /** * Constructor of backup_files_viewer class * @param array $options */ public function __construct(?array $options = null) { global $CFG, $USER; $fs = get_file_storage(); $this->currentcontext = $options['currentcontext']; $this->filecontext = $options['filecontext']; $this->component = $options['component']; $this->filearea = $options['filearea']; $files = $fs->get_area_files($this->filecontext->id, $this->component, $this->filearea, false, 'timecreated'); $this->files = array_reverse($files); } } util/ui/backup_ui.class.php 0000644 00000016635 15215711721 0011732 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the backup user interface class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This is the backup user interface class * * The backup user interface class manages the user interface and backup for Moodle. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui extends base_ui { /** * The stages of the backup user interface. * The initial stage of the backup - settings are here. */ const STAGE_INITIAL = 1; /** * The stages of the backup user interface. * The schema stage of the backup - here you choose the bits you include. */ const STAGE_SCHEMA = 2; /** * The stages of the backup user interface. * The confirmation stage of the backup. */ const STAGE_CONFIRMATION = 4; /** * The stages of the backup user interface. * The final stage of the backup - where it is being processed. */ const STAGE_FINAL = 8; /** * The stages of the backup user interface. * The backup is now complete. */ const STAGE_COMPLETE = 16; /** * If set to true the current stage is skipped. * @var bool */ protected static $skipcurrentstage = false; /** * Intialises what ever stage is requested. If none are requested we check * params for 'stage' and default to initial * * @param int $stage The desired stage to intialise or null for the default * @param array $params * @return backup_ui_stage_initial|backup_ui_stage_schema|backup_ui_stage_confirmation|backup_ui_stage_final */ protected function initialise_stage($stage = null, ?array $params = null) { if ($stage == null) { $stage = optional_param('stage', self::STAGE_INITIAL, PARAM_INT); } if (self::$skipcurrentstage) { $stage *= 2; } switch ($stage) { case backup_ui::STAGE_INITIAL: $stage = new backup_ui_stage_initial($this, $params); break; case backup_ui::STAGE_SCHEMA: $stage = new backup_ui_stage_schema($this, $params); break; case backup_ui::STAGE_CONFIRMATION: $stage = new backup_ui_stage_confirmation($this, $params); break; case backup_ui::STAGE_FINAL: $stage = new backup_ui_stage_final($this, $params); break; default: $stage = false; break; } return $stage; } /** * Returns the backup id * @return string */ public function get_uniqueid() { return $this->get_backupid(); } /** * Gets the backup id from the controller * @return string */ public function get_backupid() { return $this->controller->get_backupid(); } /** * Executes the backup plan * @throws backup_ui_exception when the steps are wrong. * @return bool */ public function execute() { if ($this->progress >= self::PROGRESS_EXECUTED) { throw new backup_ui_exception('backupuialreadyexecuted'); } if ($this->stage->get_stage() < self::STAGE_FINAL) { throw new backup_ui_exception('backupuifinalisedbeforeexecute'); } $this->progress = self::PROGRESS_EXECUTED; $this->controller->finish_ui(); $this->controller->execute_plan(); $this->stage = new backup_ui_stage_complete($this, $this->stage->get_params(), $this->controller->get_results()); return true; } /** * Loads the backup controller if we are tracking one * @param string $backupid * @return backup_controller|false */ final public static function load_controller($backupid = false) { // Get the backup id optional param. if ($backupid) { try { // Try to load the controller with it. // If it fails at this point it is likely because this is the first load. $controller = backup_controller::load_controller($backupid); return $controller; } catch (Exception $e) { return false; } } return $backupid; } /** * Gets an array of progress bar items that can be displayed through the backup renderer. * @return array Array of items for the progress bar */ public function get_progress_bar() { global $PAGE; $stage = self::STAGE_COMPLETE; $currentstage = $this->stage->get_stage(); $items = array(); while ($stage > 0) { $classes = array('backup_stage'); if (floor($stage / 2) == $currentstage) { $classes[] = 'backup_stage_next'; } else if ($stage == $currentstage) { $classes[] = 'backup_stage_current'; } else if ($stage < $currentstage) { $classes[] = 'backup_stage_complete'; } $item = array('text' => strlen(decbin($stage)).'. '.get_string('currentstage'.$stage, 'backup'), 'class' => join(' ', $classes)); if ($stage < $currentstage && $currentstage < self::STAGE_COMPLETE && (!self::$skipcurrentstage || ($stage * 2) != $currentstage)) { $params = $this->stage->get_params(); if (empty($params)) { $params = array(); } $params = array_merge($params, array('backup' => $this->get_backupid(), 'stage' => $stage)); $item['link'] = new moodle_url($PAGE->url, $params); } array_unshift($items, $item); $stage = floor($stage / 2); } return $items; } /** * Gets the name related to the operation of this UI * @return string */ public function get_name() { return 'backup'; } /** * Gets the id of the first stage this UI is reponsible for * @return int */ public function get_first_stage_id() { return self::STAGE_INITIAL; } /** * If called with default arg the current stage gets skipped. * @static * @param bool $setting Set to true (default) if you want to skip this stage, false otherwise. */ public static function skip_current_stage($setting = true) { self::$skipcurrentstage = $setting; } } /** * Backup user interface exception. Modelled off the backup_exception class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_ui_exception extends base_ui_exception {} util/ui/templates/formselectall.mustache 0000644 00000011743 15215711721 0014533 0 ustar 00 {{! 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/>. }} {{! @template core_backup/formselectall Template that renders the "Select All/None" controls on the backup/restore UI. Example context (json): { "withuserdata": true, "modules": [ { "modname": "assign", "heading": "Assignments" }, { "modname": "book", "heading": "Books" } ] } }} <div class="grouped_settings section_level"> <div id="backup_selectors_included" class="include_setting section_level"> <div class="fitem fitem_fcheckbox backup_selector"> <div class="fitemtitle">{{#str}} select {{/str}}</div> <div class="felement"> <a data-mdl-action="selectall" data-mdl-type="include" href="#" >{{#str}} all {{/str}}</a> / <a data-mdl-action="selectnone" data-mdl-type="include" href="#" >{{#str}} none {{/str}}</a> (<a class="collapsed showmore-container" id="mod_select_links_toggler" data-toggle="collapse" href="#mod_select_links" aria-expanded="false" aria-controls="mod_select_links" >{{! }}<span class="collapsed-content">{{#str}} showtypes, backup {{/str}}</span>{{! }}<span class="expanded-content">{{#str}} hidetypes, backup {{/str}}</span>{{! }}</a>) </div> </div> </div> {{#withuserdata}} <div id="backup_selectors_userdata" class="normal_setting"> <div class="fitem fitem_fcheckbox backup_selector"> <div class="fitemtitle">Select</div> <div class="felement"> <a data-mdl-action="selectall" data-mdl-type="userdata" href="#" >{{#str}} all {{/str}}</a> / <a data-mdl-action="selectnone" data-mdl-type="userdata" href="#" >{{#str}} none {{/str}}</a> </div> </div> </div> {{/withuserdata}} <div id="mod_select_links" class="collapse" style=""> {{#modules}} <div class="grouped_settings section_level"> <div id="backup_selectors_mod_{{modname}}" class="include_setting section_level"> <div class="fitem fitem_fcheckbox backup_selector"> <div class="fitemtitle">{{heading}}</div> <div class="felement"> <a data-mdl-action="selectall" data-mdl-type="include" data-mdl-mod="{{modname}}" href="#" >{{#str}} all {{/str}}</a> / <a data-mdl-action="selectnone" data-mdl-type="include" data-mdl-mod="{{modname}}" href="#" >{{#str}} none {{/str}}</a> </div> </div> </div> {{#withuserdata}} <div class="normal_setting"> <div id="backup_selectors_userdata-mod_{{modname}}" class="fitem fitem_fcheckbox backup_selector"> <div class="fitemtitle">{{heading}}</div> <div class="felement"> <a data-mdl-action="selectall" data-mdl-type="userdata" data-mdl-mod="{{modname}}" href="#" >{{#str}} all {{/str}}</a> / <a data-mdl-action="selectnone" data-mdl-type="userdata" data-mdl-mod="{{modname}}" href="#" >{{#str}} none {{/str}}</a> </div> </div> </div> {{/withuserdata}} </div> {{/modules}} </div> </div> util/ui/backup_moodleform.class.php 0000644 00000011123 15215711721 0013443 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the generic moodleform bridge for the backup user interface * as well as the individual forms that relate to the different stages the user * interface can exist within. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Backup moodleform bridge * * Ahhh the mighty moodleform bridge! Strong enough to take the weight of 682 full * grown african swallows all of whom have been carring coconuts for several days. * EWWWWW!!!!!!!!!!!!!!!!!!!!!!!! * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_moodleform extends base_moodleform { /** * Creates the form * * Overridden for type hinting on the first arg. * * @param backup_ui_stage $uistage * @param moodle_url|string $action * @param mixed $customdata * @param string $method get|post * @param string $target * @param array $attributes * @param bool $editable */ public function __construct(backup_ui_stage $uistage, $action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true) { parent::__construct($uistage, $action, $customdata, $method, $target, $attributes, $editable); } } /** * Initial backup user interface stage moodleform. * * Nothing to override we only need it defined so that moodleform doesn't get confused * between stages. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_initial_form extends backup_moodleform {} /** * Schema backup user interface stage moodleform. * * Nothing to override we only need it defined so that moodleform doesn't get confused * between stages. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_schema_form extends backup_moodleform {} /** * Confirmation backup user interface stage moodleform. * * Nothing to override we only need it defined so that moodleform doesn't get confused * between stages. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_confirmation_form extends backup_moodleform { /** * Adds the last elements, rules, settings etc to the form after data has been set. * * We override this to add a rule and type to the filename setting. * * @throws coding_exception */ public function definition_after_data() { parent::definition_after_data(); $this->_form->addRule('setting_root_filename', get_string('errorfilenamerequired', 'backup'), 'required'); $this->_form->setType('setting_root_filename', PARAM_FILE); } /** * Validates the form. * * Relies on the parent::validation for the bulk of the work. * * @param array $data * @param array $files * @return array * @throws coding_exception */ public function validation($data, $files) { $errors = parent::validation($data, $files); if (!array_key_exists('setting_root_filename', $errors)) { if (trim($data['setting_root_filename']) == '') { $errors['setting_root_filename'] = get_string('errorfilenamerequired', 'backup'); } else if (strlen(trim($data['setting_root_filename'])) > 255) { $errors['setting_root_filename'] = get_string('errorfilenametoolong', 'backup'); } else if (!preg_match('#\.mbz$#i', $data['setting_root_filename'])) { $errors['setting_root_filename'] = get_string('errorfilenamemustbezip', 'backup'); } } return $errors; } } util/ui/import_extensions.php 0000644 00000022074 15215711721 0012447 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains extension of the backup classes that override some methods * and functionality in order to customise the backup UI for the purposes of * import. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Import UI class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui extends backup_ui { /** * The stages of the backup user interface * The precheck/selection stage of the backup - here you choose the initial settings. */ const STAGE_PRECHECK = 0; /** * Customises the backup progress bar * * @global moodle_page $PAGE * @return array[] An array of arrays */ public function get_progress_bar() { global $PAGE; $stage = self::STAGE_COMPLETE; $currentstage = $this->stage->get_stage(); $items = array(); while ($stage > 0) { $classes = array('backup_stage'); if (floor($stage / 2) == $currentstage) { $classes[] = 'backup_stage_next'; } else if ($stage == $currentstage) { $classes[] = 'backup_stage_current'; } else if ($stage < $currentstage) { $classes[] = 'backup_stage_complete'; } $item = array( 'text' => strlen(decbin($stage * 2)).'. '.get_string('importcurrentstage'.$stage, 'backup'), 'class' => join(' ', $classes) ); if ($stage < $currentstage && $currentstage < self::STAGE_COMPLETE && (!self::$skipcurrentstage || $stage * 2 != $currentstage)) { $item['link'] = new moodle_url( $PAGE->url, $this->stage->get_params() + array('backup' => $this->get_backupid(), 'stage' => $stage) ); } array_unshift($items, $item); $stage = floor($stage / 2); } $selectorlink = new moodle_url($PAGE->url, $this->stage->get_params()); $selectorlink->remove_params('importid'); $classes = ["backup_stage"]; if ($currentstage == 0) { $classes[] = "backup_stage_current"; } array_unshift($items, array( 'text' => '1. '.get_string('importcurrentstage0', 'backup'), 'class' => join(' ', $classes), 'link' => $selectorlink)); return $items; } /** * Intialises what ever stage is requested. If none are requested we check * params for 'stage' and default to initial * * @param int|null $stage The desired stage to intialise or null for the default * @param array $params * @return backup_ui_stage_initial|backup_ui_stage_schema|backup_ui_stage_confirmation|backup_ui_stage_final */ protected function initialise_stage($stage = null, ?array $params = null) { if ($stage == null) { $stage = optional_param('stage', self::STAGE_PRECHECK, PARAM_INT); } if (self::$skipcurrentstage) { $stage *= 2; } switch ($stage) { case backup_ui::STAGE_INITIAL: $stage = new import_ui_stage_inital($this, $params); break; case backup_ui::STAGE_SCHEMA: $stage = new import_ui_stage_schema($this, $params); break; case backup_ui::STAGE_CONFIRMATION: $stage = new import_ui_stage_confirmation($this, $params); break; case backup_ui::STAGE_FINAL: $stage = new import_ui_stage_final($this, $params); break; case self::STAGE_PRECHECK: $stage = new import_ui_stage_precheck($this, $params); break; default: $stage = false; break; } return $stage; } } /** * Extends the initial stage * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui_stage_inital extends backup_ui_stage_initial {} /** * Class representing the precheck/selection stage of a import. * * In this stage the user is required to perform initial selections. * That is a choice of which course to import from. * * @package core_backup * @copyright 2019 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui_stage_precheck extends backup_ui_stage { /** * Precheck/selection import stage constructor * @param backup_ui $ui * @param array $params */ public function __construct(backup_ui $ui, ?array $params = null) { $this->stage = import_ui::STAGE_PRECHECK; parent::__construct($ui, $params); } /** * Processes the precheck/selection import stage * * @param base_moodleform|null $form */ public function process(?base_moodleform $form = null) { // Dummy functions. We don't have to do anything here. return; } /** * Gets the next stage for the import. * * @return int */ public function get_next_stage() { return backup_ui::STAGE_INITIAL; } /** * Initialises the backup_moodleform instance for this stage * * @return backup_moodleform|void */ public function initialise_stage_form() { // Dummy functions. We don't have to do anything here. } } /** * Extends the schema stage * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui_stage_schema extends backup_ui_stage_schema {} /** * Extends the confirmation stage. * * This overides the initialise stage form to remove the filenamesetting heading * as it is always hidden. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui_stage_confirmation extends backup_ui_stage_confirmation { /** * Initialises the stages moodleform * @return base_moodleform */ protected function initialise_stage_form() { $form = parent::initialise_stage_form(); $form->remove_element('filenamesetting'); return $form; } /** * Displays the stage * * This function is overriden so that we can manipulate the strings on the * buttons. * * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { $form = $this->initialise_stage_form(); $form->require_definition_after_data(); if ($e = $form->get_element('submitbutton')) { $e->setLabel(get_string('import'.$this->get_ui()->get_name().'stage'.$this->get_stage().'action', 'backup')); } else { $elements = $form->get_element('buttonar')->getElements(); foreach ($elements as &$element) { if ($element->getName() == 'submitbutton') { $element->setValue( get_string('import'.$this->get_ui()->get_name().'stage'.$this->get_stage().'action', 'backup') ); } } } // A nasty hack follows to work around the sad fact that moodle quickforms // do not allow to actually return the HTML content, just to echo it. flush(); ob_start(); $form->display(); $output = ob_get_contents(); ob_end_clean(); return $output; } } /** * Overrides the final stage. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_ui_stage_final extends backup_ui_stage_final {} /** * Extends the restore course search to search for import courses. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class import_course_search extends restore_course_search { /** * Sets up any access restrictions for the courses to be displayed in the search. * * This will typically call $this->require_capability(). */ protected function setup_restrictions() { $this->require_capability('moodle/backup:backuptargetimport'); } } util/ui/base_ui_stage.class.php 0000644 00000010771 15215711721 0012555 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Backup user interface stages * * This file contains the classes required to manage the stages that make up the * backup user interface. * These will be primarily operated a {@link base_ui} instance. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Abstract stage class * * This class should be extended by all backup stages (a requirement of many backup ui functions). * Each stage must then define two abstract methods * - process : To process the stage * - initialise_stage_form : To get a backup_moodleform instance for the stage * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_ui_stage { /** * The current stage * @var int */ protected $stage = 1; /** * The backuck UI object * @var base_ui */ protected $ui; /** * The moodleform for this stage * @var base_moodleform */ protected $stageform = null; /** * Custom form params that will be added as hidden inputs * @var array */ protected $params = null; /** * Constructor * * @param base_ui $ui * @param array $params */ public function __construct(base_ui $ui, ?array $params = null) { $this->ui = $ui; $this->params = $params; } /** * Returns the custom params for this stage * @return array|null */ final public function get_params() { return $this->params; } /** * The current stage * @return int */ final public function get_stage() { return $this->stage; } /** * The next stage * @return int */ public function get_next_stage() { return floor($this->stage * 2); } /** * The previous stage * @return int */ final public function get_prev_stage() { return floor($this->stage / 2); } /** * The name of this stage * @return string */ public function get_name() { return get_string('currentstage' . $this->stage, 'backup'); } /** * The backup id from the backup controller * @return string */ final public function get_uniqueid() { return $this->ui->get_uniqueid(); } /** * Displays the stage. * * By default this involves instantiating the form for the stage and the calling * it to display. * * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { $form = $this->initialise_stage_form(); // A nasty hack follows to work around the sad fact that moodle quickforms // do not allow to actually return the HTML content, just to echo it. flush(); ob_start(); $form->display(); $output = ob_get_contents(); ob_end_clean(); return $output; } /** * Processes the stage. * * This must be overridden by every stage as it will be different for every stage * * @abstract * @param base_moodleform $form */ abstract public function process(?base_moodleform $form = null); /** * Creates an instance of the correct moodleform properly populated and all * dependencies instantiated * * @abstract * @return backup_moodleform */ abstract protected function initialise_stage_form(); /** * Returns the base UI class * @return base_ui */ final public function get_ui() { return $this->ui; } /** * Returns true if this stage is the first stage. * @return bool */ public function is_first_stage() { return $this->stage == 1; } } util/ui/tests/behat/import_course.feature 0000644 00000012346 15215711721 0014642 0 ustar 00 @core @core_backup Feature: Import course's contents into another course In order to move and copy contents between courses As a teacher I need to import a course contents into another course selecting what I want to import Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | Scenario: Import course's contents to another course Given I log in as "teacher1" And the following "activities" exist: | activity | name | course | idnumber | | data | Test database name | C1 | database1 | | forum | Test forum name | C1 | forum1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | comments | Course | C1 | course-view-* | side-pre | | blog_recent | Course | C1 | course-view-* | side-pre | When I import "Course 1" course into "Course 2" course using this options: Then I should see "Test database name" And I should see "Test forum name" And I should see "Comments" in the "Comments" "block" And I should see "Recent blog entries" Scenario: Import process with permission option Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | enrol/manual:enrol | Allow | teacher | Course | C1 | And I log in as "teacher1" When I import "Course 1" course into "Course 2" course using this options: | Initial | Include permission overrides | 1 | And I am on the "Course 1" "permissions" page Then I should see "Non-editing teacher (1)" And I set the field "Advanced role override" to "Non-editing teacher (1)" And I click on "//div[@class='advancedoverride']/div/form/noscript/input" "xpath_element" And "enrol/manual:enrol" capability has "Allow" permission Scenario: Import process without permission option Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | enrol/manual:enrol | Allow | teacher | Course | C1 | And I log in as "teacher1" When I import "Course 1" course into "Course 2" course using this options: | Initial | Include permission overrides | 0 | And I am on the "Course 2" "permissions" page Then I should see "Non-editing teacher (0)" Scenario: Import course badges to another course Given I log in as "teacher1" And the following "core_badges > Badges" exist: | name | course | description | image | status | type | | Published course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | | Unpublished course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | | Unpublished without criteria course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | And the following "core_badges > Criterias" exist: | badge | role | | Published course badge | editingteacher | | Unpublished course badge | editingteacher | When I import "Course 1" course into "Course 2" course using this options: | Initial | Include badges | 1 | And I navigate to "Badges" in current page administration Then I should see "Published course badge" And I should see "Unpublished course badge" And I should see "Unpublished without criteria course badge" # Badges exist and the criteria have been restored too. And I should not see "Criteria for this badge have not been set up yet" in the "Published course badge" "table_row" And I should not see "Criteria for this badge have not been set up yet" in the "Unpublished course badge" "table_row" And I should see "Criteria for this badge have not been set up yet" in the "Unpublished without criteria course badge" "table_row" Scenario: Import process should not include badges by default Given I log in as "teacher1" And the following "core_badges > Badges" exist: | name | course | description | image | status | type | | Published course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | And the following "core_badges > Criterias" exist: | badge | role | | Published course badge | editingteacher | When I import "Course 1" course into "Course 2" course using this options: | Initial | Include badges | 0 | And I navigate to "Badges" in current page administration Then I should not see "Published course badge" util/ui/tests/behat/schema_select_all.feature 0000644 00000037264 15215711721 0015405 0 ustar 00 @core @core_backup Feature: Schema form selectors In order to quickly select schema elements As an admin I need to use the selectors UI to toggle selection of schema elements Background: Given the following "courses" exist: | fullname | shortname | category | numsections | initsections | | Course 1 | C1 | 0 | 2 | 1 | And the following "activities" exist: | activity | course | idnumber | name | intro | section | | assign | C1 | assign1 | Test assign 1 | Assign description | 1 | | data | C1 | data1 | Test data 1 | Database description | 1 | | assign | C1 | assign2 | Test assign 2 | Assign description | 2 | | data | C1 | data2 | Test data 2 | Database description | 2 | And I am on the "C1" "Course" page logged in as "admin" And I navigate to "Course reuse" in current page administration And I follow "Backup" And I click on "Next" "button" in the "page-content" "region" @javascript Scenario: Select all and none should toggle backup schema checkboxes Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test select none. When I click on "None" "link" in the "backup_selectors_included" "region" Then the field "Section 1" matches value "" And the field "Test assign 1" matches value "" And the field "Test data 1" matches value "" And the field "Section 2" matches value "" And the field "Test assign 2" matches value "" And the field "Test data 2" matches value "" And the "Section 1: User data" "checkbox" should be disabled And the "Include Test assign 1 user data" "checkbox" should be disabled And the "Include Test data 1 user data" "checkbox" should be disabled And the "Section 2: User data" "checkbox" should be disabled And the "Include Test assign 2 user data" "checkbox" should be disabled And the "Include Test data 2 user data" "checkbox" should be disabled # Test select all. And I click on "All" "link" in the "backup_selectors_included" "region" And the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled @javascript Scenario: The type options panell allow to select all and none of one activity type Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test select none assignment. When I click on "Show type options" "link" in the "backup_selectors_included" "region" And I click on "None" "link" in the "backup_selectors_mod_assign" "region" Then the field "Section 1" matches value "1" And the field "Test assign 1" matches value "" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be disabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be disabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test select all assignments. And I click on "All" "link" in the "backup_selectors_mod_assign" "region" And the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled @javascript Scenario: Select all or none in user data should toggle backup schema checkboxes Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "1" And the field "Include Test assign 1 user data" matches value "1" And the field "Include Test data 1 user data" matches value "1" And the field "Section 2: User data" matches value "1" And the field "Include Test assign 2 user data" matches value "1" And the field "Include Test data 2 user data" matches value "1" # Test select none. When I click on "None" "link" in the "backup_selectors_userdata" "region" Then the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "" And the field "Include Test assign 1 user data" matches value "" And the field "Include Test data 1 user data" matches value "" And the field "Section 2: User data" matches value "" And the field "Include Test assign 2 user data" matches value "" And the field "Include Test data 2 user data" matches value "" # Test select all. And I click on "All" "link" in the "backup_selectors_userdata" "region" And the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "1" And the field "Include Test assign 1 user data" matches value "1" And the field "Include Test data 1 user data" matches value "1" And the field "Section 2: User data" matches value "1" And the field "Include Test assign 2 user data" matches value "1" And the field "Include Test data 2 user data" matches value "1" @javascript Scenario: The type options panell allow to select all and none user data for an activity type Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "1" And the field "Include Test assign 1 user data" matches value "1" And the field "Include Test data 1 user data" matches value "1" And the field "Section 2: User data" matches value "1" And the field "Include Test assign 2 user data" matches value "1" And the field "Include Test data 2 user data" matches value "1" # Test select none assignment. When I click on "Show type options" "link" in the "backup_selectors_included" "region" And I click on "None" "link" in the "backup_selectors_userdata-mod_assign" "region" Then the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "1" And the field "Include Test assign 1 user data" matches value "" And the field "Include Test data 1 user data" matches value "1" And the field "Section 2: User data" matches value "1" And the field "Include Test assign 2 user data" matches value "" And the field "Include Test data 2 user data" matches value "1" # Test select all assignments. And I click on "All" "link" in the "backup_selectors_userdata-mod_assign" "region" And the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the field "Section 1: User data" matches value "1" And the field "Include Test assign 1 user data" matches value "1" And the field "Include Test data 1 user data" matches value "1" And the field "Section 2: User data" matches value "1" And the field "Include Test assign 2 user data" matches value "1" And the field "Include Test data 2 user data" matches value "1" @javascript Scenario: Select or unselect a section schema disable the activities checkboxes Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" And the field "Section 2" matches value "1" And the field "Test assign 2" matches value "1" And the field "Test data 2" matches value "1" And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test unselect section 1. When I set the field "Section 1" to "" Then the field "Section 1" matches value "" And the "Test assign 1" "checkbox" should be disabled And the "Test data 1" "checkbox" should be disabled And the "Section 2" "checkbox" should be enabled And the "Test assign 2" "checkbox" should be enabled And the "Test data 2" "checkbox" should be enabled And the "Section 1: User data" "checkbox" should be disabled And the "Include Test assign 1 user data" "checkbox" should be disabled And the "Include Test data 1 user data" "checkbox" should be disabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test select section 1. And I set the field "Section 1" to "1" And the field "Section 1" matches value "1" And the "Test assign 1" "checkbox" should be enabled And the "Test data 1" "checkbox" should be enabled And the "Section 2" "checkbox" should be enabled And the "Test assign 2" "checkbox" should be enabled And the "Test data 2" "checkbox" should be enabled And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled @javascript Scenario: Select or unselect a section user data disable the activities checkboxes Given the "Section 1" "checkbox" should be enabled And the "Test assign 1" "checkbox" should be enabled And the "Test data 1" "checkbox" should be enabled And the "Section 2" "checkbox" should be enabled And the "Test assign 2" "checkbox" should be enabled And the "Test data 2" "checkbox" should be enabled And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test unselect section 1. When I set the field "Section 1: User data" to "" Then the "Section 1" "checkbox" should be enabled And the "Test assign 1" "checkbox" should be enabled And the "Test data 1" "checkbox" should be enabled And the "Section 2" "checkbox" should be enabled And the "Test assign 2" "checkbox" should be enabled And the "Test data 2" "checkbox" should be enabled And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be disabled And the "Include Test data 1 user data" "checkbox" should be disabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled # Test select section 1. And I set the field "Section 1: User data" to "1" And the "Section 1" "checkbox" should be enabled And the "Test assign 1" "checkbox" should be enabled And the "Test data 1" "checkbox" should be enabled And the "Section 2" "checkbox" should be enabled And the "Test assign 2" "checkbox" should be enabled And the "Test data 2" "checkbox" should be enabled And the "Section 1: User data" "checkbox" should be enabled And the "Include Test assign 1 user data" "checkbox" should be enabled And the "Include Test data 1 user data" "checkbox" should be enabled And the "Section 2: User data" "checkbox" should be enabled And the "Include Test assign 2 user data" "checkbox" should be enabled And the "Include Test data 2 user data" "checkbox" should be enabled util/ui/tests/behat/backup_xapistate.feature 0000644 00000013630 15215711721 0015274 0 ustar 00 @core @core_backup @core_h5p @mod_h5pactivity @_switch_iframe @javascript Feature: Backup xAPI states In order to save and restore xAPI states As an admin I need to create backups with xAPI states and restore them Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course" exists: | fullname | Course 1 | | shortname | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | packagefilepath | h5p/tests/fixtures/filltheblanks.h5p | And the following config values are set as admin: | enableasyncbackup | 0 | # Save state for the student user. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page And I am on the "Awesome H5P package" "h5pactivity activity" page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia" And I log out Scenario: Content state is backup/restored when user data is included # Backup and restore the course. Given I log in as "admin" And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | User data | 1 | | Schema | Course name | Course 2 | | Schema | Course short name | C2 | # Login as student and confirm xAPI state has been restored. When I am on the "Course 2" course page logged in as student1 And I click on "Awesome H5P package" "link" in the "region-main" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia" Scenario: Content state is not restored when user data is not included in the backup # Backup course without user data and then restore it. When I log in as "admin" And I backup "Course 1" course using this options: | Initial | Include enrolled users | 0 | | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Course 2 | | Schema | Course short name | C2 | # Enrol student to the new course. And the following "course enrolments" exist: | user | course | role | | student1 | C2 | student | # Login as student and confirm xAPI state hasn't been restored. And I am on the "Course 2" course page logged in as student1 And I click on "Awesome H5P package" "link" in the "region-main" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" Scenario: Content state is not restored when user data is included in the backup but xAPI state is not restored # Backup with user data and restore it without user data the course. Given I log in as "admin" And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include user's state in content such as H5P activities | 0 | | Schema | Course name | Course 2 | | Schema | Course short name | C2 | # Login as student and confirm xAPI state hasn't been restored. When I am on the "Course 2" course page logged in as student1 And I click on "Awesome H5P package" "link" in the "region-main" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" Scenario: Content state is not restored when it is not included explicitly in the backup # Backup course with user data but without xAPI state and then restore it. When I log in as "admin" And I backup "Course 1" course using this options: | Initial | Include user's state in content such as H5P activities | 0 | | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Course 2 | | Schema | Course short name | C2 | And I should see "Awesome H5P package" # Login as student and confirm xAPI state hasn't been restored. And I am on the "Course 2" course page logged in as student1 And I click on "Awesome H5P package" "link" in the "region-main" "region" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" util/ui/tests/behat/import_contentbank_content.feature 0000644 00000005571 15215711721 0017404 0 ustar 00 @core @core_backup @core_contentbank @core_h5p @contenttype_h5p @_file_upload @javascript Feature: Import course content bank content In order to import content from a course contentbank As a teacher I need to confirm that errors will not happen Background: Given I log in as "admin" And the following config values are set as admin: | unaddableblocks | | theme_boost| And I am on site homepage And I turn editing mode on And I add the "Navigation" block if not present And I configure the "Navigation" block And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" And I navigate to "H5P > Manage H5P content types" in site administration And I upload "h5p/tests/fixtures/ipsums.h5p" file to "H5P content type" filemanager And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element" And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | teacher1 | ipsums.h5p | /h5p/tests/fixtures/ipsums.h5p | And I log out And I log in as "teacher1" Scenario: Import content bank content to another course Given I am on "Course 2" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should not see "ipsums.h5p" When I import "Course 1" course into "Course 2" course using this options: And I expand "Site pages" node And I click on "Content bank" "link" Then I should see "ipsums.h5p" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should see "ipsums.h5p" Scenario: User could configure not to import content bank Given I am on "Course 2" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should not see "ipsums.h5p" When I import "Course 1" course into "Course 2" course using this options: | Initial | Include content bank content | 0 | And I expand "Site pages" node And I click on "Content bank" "link" Then I should not see "ipsums.h5p" And I am on "Course 1" course homepage And I expand "Site pages" node And I click on "Content bank" "link" And I should see "ipsums.h5p" util/ui/tests/behat/import_groups.feature 0000644 00000004007 15215711721 0014654 0 ustar 00 @core @core_backup Feature: Option to include groups and groupings when importing a course to another course In order to import a course to another course with groups and groupings As a teacher I need an option to include groups and groupings when importing a course to another course Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And the following "groups" exist: | name | description | course | idnumber | | Group 1 | Group description | C1 | GROUP1 | | Group 2 | Group description | C1 | GROUP2 | And the following "groupings" exist: | name | course | idnumber | | Grouping 1 | C1 | GROUPING1 | | Grouping 2 | C1 | GROUPING2 | And I log in as "teacher1" And I am on "Course 1" course homepage Scenario: Include groups and groupings when importing a course to another course Given I import "Course 1" course into "Course 2" course using this options: | Initial | Include groups and groupings | 1 | When I am on the "Course 2" "groups" page Then I should see "Group 1" And I should see "Group 2" And I am on the "Course 2" "groupings" page And I should see "Grouping 1" And I should see "Grouping 2" Scenario: Do not include groups and groupings when importing a course to another course Given I import "Course 1" course into "Course 2" course using this options: | Initial | Include groups and groupings | 0 | When I am on the "Course 2" "groups" page Then I should not see "Group 1" And I should not see "Group 2" And I am on the "Course 2" "groupings" page And I should not see "Grouping 1" And I should not see "Grouping 2" util/ui/tests/behat/behat_backup.php 0000644 00000042140 15215711721 0013507 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Backup and restore actions to help behat feature files writting. * * @package core_backup * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); require_once(__DIR__ . '/../../../../../lib/behat/behat_field_manager.php'); require_once(__DIR__ . '/../../../../../lib/tests/behat/behat_navigation.php'); require_once(__DIR__ . '/../../../../../lib/behat/form_field/behat_form_field.php'); use Behat\Gherkin\Node\TableNode as TableNode, Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, Behat\Mink\Exception\ExpectationException as ExpectationException; /** * Backup-related steps definitions. * * @package core_backup * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_backup extends behat_base { /** * Backups the specified course using the provided options. If you are interested in restoring this backup would be * useful to provide a 'Filename' option. * * @Given /^I backup "(?P<course_fullname_string>(?:[^"]|\\")*)" course using this options:$/ * @param string $backupcourse * @param TableNode $options Backup options or false if no options provided */ public function i_backup_course_using_this_options($backupcourse, $options = false) { // We can not use other steps here as we don't know where the provided data // table elements are used, and we need to catch exceptions contantly. // Navigate to the course backup page. $this->execute("behat_navigation::i_am_on_page_instance", [$backupcourse, 'backup']); // Initial settings. $this->fill_backup_restore_form($this->get_step_options($options, "Initial")); $this->execute("behat_forms::press_button", get_string('backupstage1action', 'backup')); // Schema settings. $this->fill_backup_restore_form($this->get_step_options($options, "Schema")); $this->execute("behat_forms::press_button", get_string('backupstage2action', 'backup')); // Confirmation and review, backup filename can also be specified. $this->fill_backup_restore_form($this->get_step_options($options, "Confirmation")); $this->execute("behat_forms::press_button", get_string('backupstage4action', 'backup')); // Waiting for it to finish. $this->execute("behat_general::wait_until_the_page_is_ready"); // Last backup continue button. $this->execute("behat_general::i_click_on", array(get_string('backupstage16action', 'backup'), 'button')); } /** * Performs a quick (one click) backup of a course. * * Please note that because you can't set settings with this there is no way to know what the filename * that was produced was. It contains a timestamp making it hard to find. * * @Given /^I perform a quick backup of course "(?P<course_fullname_string>(?:[^"]|\\")*)"$/ * @param string $backupcourse */ public function i_perform_a_quick_backup_of_course($backupcourse) { // We can not use other steps here as we don't know where the provided data // table elements are used, and we need to catch exceptions contantly. // Navigate to the course backup page. $this->execute("behat_navigation::i_am_on_page_instance", [$backupcourse, 'backup']); // Initial settings. $this->execute("behat_forms::press_button", get_string('jumptofinalstep', 'backup')); // Waiting for it to finish. $this->execute("behat_general::wait_until_the_page_is_ready"); // Last backup continue button. $this->execute("behat_general::i_click_on", array(get_string('backupstage16action', 'backup'), 'button')); } /** * Imports the specified origin course into the other course using the provided options. * * Keeping it separatelly from backup & restore, it the number of * steps and duplicate code becomes bigger a common method should * be generalized. * * @Given /^I import "(?P<from_course_fullname_string>(?:[^"]|\\")*)" course into "(?P<to_course_fullname_string>(?:[^"]|\\")*)" course using this options:$/ * @param string $fromcourse * @param string $tocourse * @param TableNode $options */ public function i_import_course_into_course($fromcourse, $tocourse, $options = false) { // We can not use other steps here as we don't know where the provided data // table elements are used, and we need to catch exceptions contantly. // Navigate to the course import page. $this->execute("behat_navigation::i_am_on_page_instance", [$tocourse, 'import']); // Select the course. $fromcourse = behat_context_helper::escape($fromcourse); $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ics-results ')]" . "/descendant::tr[contains(., $fromcourse)]" . "/descendant::input[@type='radio']"; $this->execute('behat_forms::i_set_the_field_with_xpath_to', [$xpath, 1]); $this->execute("behat_forms::press_button", get_string('continue')); // Initial settings. $this->fill_backup_restore_form($this->get_step_options($options, "Initial")); $this->execute("behat_forms::press_button", get_string('importbackupstage1action', 'backup')); // Schema settings. $this->fill_backup_restore_form($this->get_step_options($options, "Schema")); $this->execute("behat_forms::press_button", get_string('importbackupstage2action', 'backup')); // Run it. $this->execute("behat_forms::press_button", get_string('importbackupstage4action', 'backup')); // Wait to ensure restore is complete. $this->execute("behat_general::wait_until_the_page_is_ready"); // Continue and redirect to 'to' course. $this->execute("behat_general::i_click_on", array(get_string('continue'), 'button')); } /** * Restores the backup into the specified course and the provided options. * * You should be in the 'Restore' page where the backup is. * * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into "(?P<existing_course_fullname_string>(?:[^"]|\\")*)" course using this options:$/ * @param string $backupfilename * @param string $existingcourse * @param TableNode $options Restore forms options or false if no options provided */ public function i_restore_backup_into_course_using_this_options($backupfilename, $existingcourse, $options = false) { // Confirm restore. $this->select_backup($backupfilename); // The argument should be converted to an xpath literal. $existingcourse = behat_context_helper::escape($existingcourse); // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext). $radionodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' restore-course-search ')]" . "/descendant::tr[contains(., $existingcourse)]" . "/descendant::input[@type='radio']"; $this->execute("behat_general::i_click_on", array($radionodexpath, 'xpath_element')); // Pressing the continue button of the restore into an existing course section. $continuenodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"; $this->execute("behat_general::i_click_on", array($continuenodexpath, 'xpath_element')); // Common restore process using provided key/value options. $this->process_restore($options); } /** * Restores the specified backup into a new course using the provided options. * * You should be in the 'Restore' page where the backup is. * * @Given /^I restore "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into a new course using this options:$/ * @param string $backupfilename * @param TableNode $options Restore forms options or false if no options provided */ public function i_restore_backup_into_a_new_course_using_this_options($backupfilename, $options = false) { // Confirm restore. $this->select_backup($backupfilename); // The first category in the list. $radionodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' restore-course-search ')]" . "/descendant::input[@type='radio']"; $this->execute("behat_general::i_click_on", array($radionodexpath, 'xpath_element')); // Pressing the continue button of the restore into an existing course section. $continuenodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"; $this->execute("behat_general::i_click_on", array($continuenodexpath, 'xpath_element')); // Common restore process using provided key/value options. $this->process_restore($options); } /** * Merges the backup into the current course using the provided restore options. * * You should be in the 'Restore' page where the backup is. * * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course using this options:$/ * @param string $backupfilename * @param TableNode $options Restore forms options or false if no options provided */ public function i_merge_backup_into_the_current_course($backupfilename, $options = false) { // Confirm restore. $this->select_backup($backupfilename); // Merge without deleting radio option. $radionodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='radio'][@name='target'][@value='1']"; $this->execute("behat_general::i_click_on", array($radionodexpath, 'xpath_element')); // Pressing the continue button of the restore merging section. $continuenodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"; $this->execute("behat_general::i_click_on", array($continuenodexpath, 'xpath_element')); // Common restore process using provided key/value options. $this->process_restore($options); } /** * Merges the backup into the current course after deleting this contents, using the provided restore options. * * You should be in the 'Restore' page where the backup is. * * @Given /^I merge "(?P<backup_filename_string>(?:[^"]|\\")*)" backup into the current course after deleting it's contents using this options:$/ * @param string $backupfilename * @param TableNode $options Restore forms options or false if no options provided */ public function i_merge_backup_into_current_course_deleting_its_contents($backupfilename, $options = false) { // Confirm restore. $this->select_backup($backupfilename); // Delete contents radio option. $radionodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='radio'][@name='target'][@value='0']"; $this->execute("behat_general::i_click_on", array($radionodexpath, 'xpath_element')); // Pressing the continue button of the restore merging section. $continuenodexpath = "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"; $this->execute("behat_general::i_click_on", array($continuenodexpath, 'xpath_element')); // Common restore process using provided key/value options. $this->process_restore($options); } /** * Selects the backup to restore. * * @throws ExpectationException * @param string $backupfilename * @return void */ protected function select_backup($backupfilename) { // Using xpath as there are other restore links before this one. $exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession()); // The argument should be converted to an xpath literal. $backupfilename = behat_context_helper::escape($backupfilename); $xpath = "//tr[contains(., $backupfilename)]/descendant::a[contains(., '" . get_string('restore') . "')]"; $restorelink = $this->find('xpath', $xpath, $exception); $restorelink->click(); // Confirm the backup contents. $this->find_button(get_string('continue'))->press(); } /** * Executes the common steps of all restore processes. * * @param TableNode $options The backup and restore options or false if no options provided * @return void */ protected function process_restore($options) { // We can not use other steps here as we don't know where the provided data // table elements are used, and we need to catch exceptions contantly. // Settings. $this->fill_backup_restore_form($this->get_step_options($options, "Settings")); $this->execute("behat_forms::press_button", get_string('restorestage4action', 'backup')); // Schema. $this->fill_backup_restore_form($this->get_step_options($options, "Schema")); $this->execute("behat_forms::press_button", get_string('restorestage8action', 'backup')); // Review, no options here. $this->execute("behat_forms::press_button", get_string('restorestage16action', 'backup')); // Wait till the final button is visible. $this->execute("behat_general::wait_until_the_page_is_ready"); // Last restore continue button, redirected to restore course after this. $this->execute("behat_general::i_click_on", array(get_string('restorestage32action', 'backup'), 'button')); } /** * Tries to fill the current page form elements with the provided options. * * This step is slow as it spins over each provided option, we are * not expected to have lots of provided options, anyways, is better * to be conservative and wait for the elements to appear rather than * to have false failures. * * @param TableNode $options The backup and restore options or false if no options provided * @return void */ protected function fill_backup_restore_form($options) { // Nothing to fill if no options are provided. if (!$options) { return; } // If we find any of the provided options in the current form we should set the value. $datahash = $options->getRowsHash(); foreach ($datahash as $locator => $value) { $field = behat_field_manager::get_form_field_from_label($locator, $this); $field->set_value($value); } } /** * Get the options specific to this step of the backup/restore process. * * @param TableNode $options The options table to filter * @param string $step The name of the step * @return TableNode The filtered options table * @throws ExpectationException */ protected function get_step_options($options, $step) { // Nothing to fill if no options are provided. if (!$options) { return; } $rows = $options->getRows(); $newrows = array(); foreach ($rows as $k => $data) { if (count($data) !== 3) { // Not enough information to guess the page. throw new ExpectationException("The backup/restore step must be specified for all backup options", $this->getSession()); } else if ($data[0] == $step) { unset($data[0]); $newrows[] = $data; } } $pageoptions = new TableNode($newrows); return $pageoptions; } } util/ui/tests/behat/backup_courses.feature 0000644 00000005575 15215711721 0014766 0 ustar 00 @core @core_backup Feature: Backup Moodle courses In order to save and store course contents As an admin I need to create backups of courses Background: Given the following "courses" exist: | fullname | shortname | category | numsections | initsections | | Course 1 | C1 | 0 | 10 | 1 | | Course 2 | C2 | 0 | 2 | 1 | And the following "activities" exist: | activity | course | idnumber | name | intro | section | | assign | C2 | assign1 | Test assign | Assign description | 1 | | data | C2 | data1 | Test data | Database description | 2 | And the following config values are set as admin: | enableasyncbackup | 0 | And I log in as "admin" Scenario: Backup a course providing options When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | Then I should see "Restore" And I click on "Restore" "link" in the "test_backup.mbz" "table_row" And I should see "URL of backup" And I should see "Anonymize user information" @javascript Scenario: Backup a course with default options When I backup "Course 1" course using this options: | Initial | Include calendar events | 0 | | Initial | Include course logs | 1 | | Schema | Section 5 | 0 | | Confirmation | Filename | test_backup.mbz | Then I should see "Restore" And I click on "Restore" "link" in the "test_backup.mbz" "table_row" And I should not see "Section 5" in the "region-main" "region" And I press "Continue" And I click on "Continue" "button" in the ".bcs-current-course" "css_element" And "No" "icon" should exist in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]" "xpath_element" And "Include course logs" "checkbox" should exist And I press "Next" Scenario: Backup a course without blocks When I backup "Course 1" course using this options: | 1 | setting_root_blocks | 0 | Then I should see "Course backup area" Scenario: Backup selecting just one section When I backup "Course 2" course using this options: | Schema | Test data | 0 | | Schema | Section 2 | 0 | | Confirmation | Filename | test_backup.mbz | Then I should see "Course backup area" And I click on "Restore" "link" in the "test_backup.mbz" "table_row" And I should not see "Section 2" in the "region-main" "region" And I press "Continue" And I click on "Continue" "button" in the ".bcs-current-course" "css_element" And I press "Next" And I should see "Test assign" And I should not see "Test data" Scenario: Backup a course using the one click backup button When I perform a quick backup of course "Course 2" Then I should see "Course backup area" And I should see "backup-moodle2-course-" util/ui/tests/behat/duplicate_activities.feature 0000644 00000005214 15215711721 0016142 0 ustar 00 @core @core_backup Feature: Duplicate activities In order to set up my course contents quickly As a teacher I need to duplicate activities inside the same course Scenario: Duplicate an activity Given the following "course" exists: | fullname | Course 1 | | shortname | C1 | | category | 0 | | initsections | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | course | idnumber | section | | data | Test database name | Test database description | C1 | database1 | 1 | And the following config values are set as admin: | backup_import_activities | 0 | backup | And the following "core_badges > Badges" exist: | name | course | description | image | status | type | | My course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | And the following "core_badges > Criterias" exist: | badge | role | | My course badge | editingteacher | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I duplicate "Test database name" activity And I should see "Test database name (copy)" And I wait until section "1" is available And I click on "Edit settings" "link" in the "Test database name" activity And I set the following fields to these values: | Name | Original database name | And I press "Save and return to course" And I click on "Edit settings" "link" in the "Test database name (copy)" activity And I set the following fields to these values: | Name | Duplicated database name | | Description | Duplicated database description | And I press "Save and return to course" Then I should see "Original database name" in the "Section 1" "section" And I should see "Duplicated database name" in the "Section 1" "section" And "Original database name" "link" should appear before "Duplicated database name" "link" # Check that badges are not duplicated. If they are duplicated, they will appear as "Not available". And I navigate to "Badges" in current page administration And the following should not exist in the "reportbuilder-table" table: | Name | Badge status | | My course badge | Not available | util/ui/tests/behat/restore_moodle2_courses_settings.feature 0000644 00000013477 15215711721 0020545 0 ustar 00 @core @core_backup Feature: Restore Moodle 2 course backups with different user data settings In order to decide upon including user data during backup and restore of courses As a teacher and an admin I need to be able to set and override backup and restore settings Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | data | Test database name | n | C1 | data1 | And the following "mod_data > fields" exist: | database | type | name | description | | data1 | text | Test field name | Test field description | And the following "mod_data > templates" exist: | database | name | | data1 | singletemplate | | data1 | listtemplate | | data1 | addtemplate | | data1 | asearchtemplate | | data1 | rsstemplate | And the following "mod_data > entries" exist: | database | user | Test field name | | data1 | student1 | Student entry | And the following config values are set as admin: | enableasyncbackup | 0 | And I log in as "admin" And I backup "Course 1" course using this options: | Initial | Include enrolled users | 1 | | Confirmation | Filename | test_backup.mbz | @javascript Scenario: Restore a backup with user data # "User data" marks the user data field for the section # "-" marks the user data field for the data activity When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | User data | 1 | | Schema | - | 1 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should see "Student entry" @javascript Scenario: Restore a backup without user data for data activity # "User data" marks the user data field for the section # "-" marks the user data field for the data activity When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | User data | 1 | | Schema | - | 0 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should not see "Student entry" @javascript Scenario: Restore a backup without user data for section and data activity # "User data" marks the user data field for the section # "-" marks the user data field for the data activity When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | User data | 0 | | Schema | - | 0 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should not see "Student entry" @javascript Scenario: Restore a backup without user data for section # "User data" marks the user data field for the section # "-" marks the user data field for the data activity When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | - | 1 | | Schema | User data | 0 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should not see "Student entry" @javascript Scenario: Restore a backup with user data with local config for including users set to 0 And I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 0 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should not see "Student entry" @javascript Scenario: Restore a backup with user data with site config for including users set to 0 Given I navigate to "Courses > Backups > General restore defaults" in site administration And I set the field "s_restore_restore_general_users" to "" And I press "Save changes" And I am on the "Course 1" "restore" page # "User data" marks the user data field for the section # "-" marks the user data field for the data activity And I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 1 | | Schema | User data | 1 | | Schema | - | 1 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should see "Student entry" @javascript Scenario: Restore a backup with user data with local and site config config for including users set to 0 Given I navigate to "Courses > Backups > General restore defaults" in site administration And I set the field "s_restore_restore_general_users" to "" And I press "Save changes" And I am on the "Course 1" "restore" page When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include enrolled users | 0 | Then I should see "Test database name" When I click on "Test database name" "link" in the "region-main" "region" Then I should not see "Student entry" util/ui/tests/behat/restore_moodle2_courses.feature 0000644 00000037653 15215711721 0016627 0 ustar 00 @core @core_backup Feature: Restore Moodle 2 course backups In order to continue using my stored course contents As a teacher and an admin I need to restore them inside other Moodle courses or in new courses Background: Given the following "courses" exist: | fullname | shortname | category | format | numsections | coursedisplay | initsections | | Course 1 | C1 | 0 | topics | 15 | 1 | 1 | | Course 2 | C2 | 0 | topics | 5 | 0 | 1 | | Course 3 | C3 | 0 | topics | 2 | 0 | 1 | | Course 4 | C4 | 0 | topics | 20 | 0 | 1 | | Course 5 | C5 | 0 | topics | 15 | 1 | 0 | And the following "activities" exist: | activity | course | idnumber | name | intro | section | externalurl | | assign | C3 | assign1 | Test assign name | Assign description | 1 | | | data | C3 | data1 | Test database name | Database description | 2 | | | forum | C1 | 0001 | Test forum name | | 1 | | | url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org | | forum | C5 | 0005 | Test forum name | | 1 | | | url | C5 | url5 | Test URL name | Test URL description | 3 | http://www.moodle.org | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | activity_modules | Course | C1 | course-view-* | side-pre | | activity_modules | Course | C5 | course-view-* | side-pre | And the following config values are set as admin: | enableasyncbackup | 0 | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on @javascript Scenario: Restore a course in another existing course When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into "Course 2" course using this options: Then I should see "Course 2" And I should see "Activities" in the "Activities" "block" And I should see "Test forum name" @javascript Scenario: Restore a course in a new course When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Course 1 restored in a new course | Then I should see "Course 1 restored in a new course" And I should see "Activities" in the "Activities" "block" And I should see "Test forum name" And I should see "Section 15" And I should not see "Section 16" And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "id_format" matches value "Custom sections" And I press "Cancel" @javascript Scenario: Restore a backup into the same course When I backup "Course 3" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into "Course 2" course using this options: | Schema | Test database name | 0 | | Schema | Section 2 | 0 | Then I should see "Course 2" And I should see "Test assign name" And I should not see "Test database name" @javascript Scenario: Restore a backup into the same course removing it's contents before that When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And the following "activity" exists: | activity | forum | | course | C1 | | section | 1 | | name | Test forum post backup name | And I am on the "Course 1" "restore" page And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options: | Schema | Section 3 | 0 | Then I should see "Course 1" And I should not see "Section 3" And I should not see "Test forum post backup name" And I should see "Activities" in the "Activities" "block" And I should see "Test forum name" @javascript Scenario: Restore a backup into a new course changing the course format afterwards Given I backup "Course 5" course using this options: | Confirmation | Filename | test_backup.mbz | When I restore "test_backup.mbz" backup into a new course using this options: Then I should see "New section" And I should see "Test forum name" And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "id_format" matches value "Custom sections" And I set the following fields to these values: | id_startdate_day | 1 | | id_startdate_month | January | | id_startdate_year | 2020 | | id_format | Weekly sections | | id_enddate_enabled | 0 | And I press "Save and display" And I should see "1 January - 7 January" And I should see "Test forum name" And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "id_format" matches value "Weekly sections" And I set the following fields to these values: | id_format | Social | And I press "Save and display" And I should see "An open forum for chatting about anything you want to" And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "id_format" matches value "Social" And I press "Cancel" @javascript Scenario: Restore a backup in an existing course retaining the backup course settings Given I hide section "3" And I hide section "7" When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into "Course 2" course using this options: | Schema | Overwrite course configuration | Yes | And I navigate to "Settings" in current page administration And I expand all fieldsets Then the field "id_format" matches value "Custom sections" And the field "Course layout" matches value "Show one section per page" And the field "Course short name" matches value "C1_1" And I press "Cancel" And section "3" should be visible And section "7" should be hidden And section "15" should be visible And I should see "Section 15" And I should not see "Section 16" And I should see "Test URL name" in the "Section 3" "section" And I should see "Test forum name" in the "Section 1" "section" @javascript Scenario: Restore a backup in an existing course keeping the target course settings Given I hide section "3" And I hide section "7" When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into "Course 2" course using this options: | Schema | Overwrite course configuration | No | And I navigate to "Settings" in current page administration And I expand all fieldsets Then the field "id_format" matches value "Custom sections" And the field "Course short name" matches value "C2" And the field "Course layout" matches value "Show all sections on one page" And I press "Cancel" And section "3" should be visible And section "7" should be hidden And section "15" should be visible And I should see "Section 15" And I should not see "Section 16" And I should see "Test URL name" in the "Section 3" "section" And I should see "Test forum name" in the "Section 1" "section" @javascript Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings Given I hide section "3" And I hide section "7" When I backup "Course 1" course using this options: | Initial | Include enrolled users | 0 | | Confirmation | Filename | test_backup.mbz | And I am on the "Course 2" "restore" page And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options: | Schema | Overwrite course configuration | Yes | And I navigate to "Settings" in current page administration And I expand all fieldsets Then the field "id_format" matches value "Custom sections" And the field "Course layout" matches value "Show one section per page" And the field "Course short name" matches value "C1_1" And I press "Cancel" And section "3" should be hidden And section "7" should be hidden And section "15" should be visible And I should see "Section 15" And I should not see "Section 16" And I should see "Test URL name" in the "Section 3" "section" And I should see "Test forum name" in the "Section 1" "section" @javascript Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings Given I hide section "3" And I hide section "7" When I backup "Course 1" course using this options: | Initial | Include enrolled users | 0 | | Confirmation | Filename | test_backup.mbz | And I am on the "Course 2" "restore" page And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options: | Schema | Overwrite course configuration | No | And I navigate to "Settings" in current page administration And I expand all fieldsets Then the field "id_format" matches value "Custom sections" And the field "Course short name" matches value "C2" And the field "Course layout" matches value "Show all sections on one page" And I press "Cancel" And section "3" should be hidden And section "7" should be hidden And section "15" should be visible And I should see "Section 15" And I should not see "Section 16" And I should see "Test URL name" in the "Section 3" "section" And I should see "Test forum name" in the "Section 1" "section" @javascript Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections Given I hide section "3" And I hide section "7" When I backup "Course 1" course using this options: | Initial | Include enrolled users | 0 | | Confirmation | Filename | test_backup.mbz | And I am on the "Course 4" "restore" page And I merge "test_backup.mbz" backup into the current course after deleting it's contents using this options: | Schema | Overwrite course configuration | No | And I navigate to "Settings" in current page administration And I expand all fieldsets Then the field "id_format" matches value "Custom sections" And the field "Course short name" matches value "C4" And the field "Course layout" matches value "Show all sections on one page" And I press "Cancel" And section "3" should be hidden And section "7" should be hidden And section "15" should be visible And I should see "Section 15" And I should not see "Section 16" And I should see "Test URL name" in the "Section 3" "section" And I should see "Test forum name" in the "Section 1" "section" @javascript Scenario: Restore a backup with override permission Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | enrol/manual:enrol | Allow | teacher | Course | C1 | And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include permission overrides | 1 | Then I am on the "Course 1 copy 1" "permissions" page And I should see "Non-editing teacher (1)" And I set the field "Advanced role override" to "Non-editing teacher (1)" And "enrol/manual:enrol" capability has "Allow" permission @javascript Scenario: Restore a backup without override permission Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | enrol/manual:enrol | Allow | teacher | Course | C1 | And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include permission overrides | 0 | Then I am on the "Course 1 copy 1" "permissions" page And I should see "Non-editing teacher (0)" @javascript @core_badges Scenario Outline: Restore course badges Given the following "core_badges > Badges" exist: | name | course | description | image | status | type | | Published course badge | C1 | Badge description | badges/tests/behat/badge.png | active | 2 | | Unpublished course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | | Unpublished without criteria course badge | C1 | Badge description | badges/tests/behat/badge.png | 0 | 2 | And the following "core_badges > Criterias" exist: | badge | role | | Published course badge | editingteacher | | Unpublished course badge | editingteacher | And I backup "Course 1" course using this options: | Initial | Include badges | 1 | | Initial | Include activities and resources | <includeactivities> | | Initial | Include enrolled users | 0 | | Initial | Include blocks | 0 | | Initial | Include files | 0 | | Initial | Include filters | 0 | | Initial | Include calendar events | 0 | | Initial | Include question bank | 0 | | Initial | Include groups and groupings | 0 | | Initial | Include competencies | 0 | | Initial | Include custom fields | 0 | | Initial | Include calendar events | 0 | | Initial | Include content bank content | 0 | | Initial | Include legacy course files | 0 | | Confirmation | Filename | test_backup.mbz | When I restore "test_backup.mbz" backup into a new course using this options: | Settings | Include badges | 1 | And I navigate to "Badges" in current page administration Then I should see "Published course badge" And I should see "Unpublished course badge" And I should see "Unpublished without criteria course badge" # If activities were included, the criteria have been restored too; otherwise no criteria have been set up for badges. And I <shouldornotsee> "Criteria for this badge have not been set up yet" in the "Published course badge" "table_row" And I <shouldornotsee> "Criteria for this badge have not been set up yet" in the "Unpublished course badge" "table_row" And I should see "Criteria for this badge have not been set up yet" in the "Unpublished without criteria course badge" "table_row" Examples: | includeactivities | shouldornotsee | | 0 | should see | | 1 | should not see | util/ui/tests/base_setting_ui_test.php 0000644 00000005216 15215711721 0014222 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use base_setting; use base_setting_ui; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/backup/util/settings/tests/settings_test.php'); /** * Tests for base_setting_ui class. * * @package core_backup * @copyright 2021 Université Rennes 2 {@link https://www.univ-rennes2.fr} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class base_setting_ui_test extends \advanced_testcase { /** * Tests set_label(). * * @return void */ public function test_set_label(): void { $this->resetAfterTest(); $bs = new mock_base_setting('test', base_setting::IS_BOOLEAN); $bsui = new base_setting_ui($bs); // Should keep original text string. $bsui->set_label('Section name'); $this->assertEquals('Section name', $bsui->get_label()); // Should keep original HTML string. $bsui->set_label('<b>Section name</b>'); $this->assertEquals('<b>Section name</b>', $bsui->get_label()); // Should be converted to text string. $bsui->set_label(123); $this->assertSame('123', $bsui->get_label()); // Should be converted to non-breaking space (U+00A0) when label is empty. $bsui->set_label(''); $this->assertSame("\u{00A0}", $bsui->get_label()); // Should be converted to non-breaking space (U+00A0) when the trimmed label is empty. $bsui->set_label(" \t\t\n\n\t\t "); $this->assertSame("\u{00A0}", $bsui->get_label()); // Should clean partially the wrong bits. $bsui->set_label('<b onmouseover=alert("test")>label</b>'); $this->assertSame('<b>label</b>', $bsui->get_label()); // Should raise an exception when cleaning ends with 100% empty. $this->expectException(\Exception::class); $this->expectExceptionMessage('error/setting_invalid_ui_label'); $bsui->set_label('<script>alert("test")</script>'); } } util/ui/tests/ui_test.php 0000644 00000002046 15215711721 0011471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; /** * ui tests (all) * * @package core_backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class ui_test extends \basic_testcase { /** * Test backup_ui class */ public function test_backup_ui(): void { } } util/ui/restore_ui_components.php 0000644 00000031074 15215711721 0013303 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains components used by the restore UI * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * A base class that can be used to build a specific search upon * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_search_base implements renderable { /** * The default values for this components params */ const DEFAULT_SEARCH = ''; /** * The param used to convey the current search string * @var string */ static $VAR_SEARCH = 'search'; /** * The current search string * @var string|null */ private $search = null; /** * The URL for this page including required params to return to it * @var moodle_url */ private $url = null; /** * The results of the search * @var array|null */ private $results = null; /** * The total number of results available * @var int */ private $totalcount = null; /** * Array of capabilities required for each item in the search * @var array */ private $requiredcapabilities = array(); /** * Max number of courses to return in a search. * @var int */ private $maxresults = null; /** * Indicates if we have more than maxresults found. * @var boolean */ private $hasmoreresults = false; /** * Constructor * @param array $config Config options */ public function __construct(array $config = array()) { $this->search = optional_param($this->get_varsearch(), self::DEFAULT_SEARCH, PARAM_NOTAGS); $this->search = trim($this->search); $this->maxresults = get_config('backup', 'import_general_maxresults'); foreach ($config as $name => $value) { $method = 'set_'.$name; if (method_exists($this, $method)) { $this->$method($value); } } } /** * The URL for this search * @global moodle_page $PAGE * @return moodle_url The URL for this page */ final public function get_url() { global $PAGE; $params = array( $this->get_varsearch() => $this->get_search() ); return ($this->url !== null) ? new moodle_url($this->url, $params) : new moodle_url($PAGE->url, $params); } /** * The current search string * @return string */ final public function get_search() { return ($this->search !== null) ? $this->search : self::DEFAULT_SEARCH; } /** * The total number of results * @return int */ final public function get_count() { if ($this->totalcount === null) { $this->search(); } return $this->totalcount; } /** * Returns an array of results from the search * @return array */ final public function get_results() { if ($this->results === null) { $this->search(); } return $this->results; } /** * Sets the page URL * @param moodle_url $url */ final public function set_url(moodle_url $url) { $this->url = $url; } /** * Invalidates the results collected so far */ final public function invalidate_results() { $this->results = null; $this->totalcount = null; } /** * Adds a required capability which all results will be checked against * @param string $capability * @param int|null $user */ final public function require_capability($capability, $user = null) { if (!is_int($user)) { $user = null; } $this->requiredcapabilities[] = array( 'capability' => $capability, 'user' => $user ); } /** * Executes the search * * @global moodle_database $DB * @return int The number of results */ final public function search() { global $DB; if (!is_null($this->results)) { return $this->results; } $this->results = array(); $this->totalcount = 0; $contextlevel = $this->get_itemcontextlevel(); list($sql, $params) = $this->get_searchsql(); // Get total number, to avoid some incorrect iterations. $countsql = preg_replace('/ORDER BY.*/', '', $sql); $totalcourses = $DB->count_records_sql("SELECT COUNT(*) FROM ($countsql) sel", $params); if ($totalcourses > 0) { // User to be checked is always the same (usually null, get it from first element). $firstcap = reset($this->requiredcapabilities); $userid = isset($firstcap['user']) ? $firstcap['user'] : null; // Extract caps to check, this saves us a bunch of iterations. $requiredcaps = array(); foreach ($this->requiredcapabilities as $cap) { $requiredcaps[] = $cap['capability']; } // Iterate while we have records and haven't reached $this->maxresults. $resultset = $DB->get_recordset_sql($sql, $params); foreach ($resultset as $result) { context_helper::preload_from_record($result); $classname = context_helper::get_class_for_level($contextlevel); $context = $classname::instance($result->id); if (count($requiredcaps) > 0) { if (!has_all_capabilities($requiredcaps, $context, $userid)) { continue; } } // Check if we are over the limit. if ($this->totalcount + 1 > $this->maxresults) { $this->hasmoreresults = true; break; } // If not, then continue. $this->totalcount++; $this->results[$result->id] = $result; } $resultset->close(); } return $this->totalcount; } /** * Returns true if there are more search results. * @return bool */ final public function has_more_results() { if ($this->results === null) { $this->search(); } return $this->hasmoreresults; } /** * Returns an array containing the SQL for the search and the params * @return array */ abstract protected function get_searchsql(); /** * Gets the context level associated with this components items * @return CONTEXT_* */ abstract protected function get_itemcontextlevel(); /** * Formats the results */ abstract protected function format_results(); /** * Gets the string used to transfer the search string for this compontents requests * @return string */ abstract public function get_varsearch(); } /** * A course search component * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_course_search extends restore_search_base { /** * @var string */ static $VAR_SEARCH = 'search'; /** * The current course id. * @var int */ protected $currentcourseid = null; /** * Determines if the current course is included in the results. * @var bool */ protected $includecurrentcourse; /** * Constructor * @param array $config * @param int $currentcouseid The current course id so it can be ignored */ public function __construct(array $config = array(), $currentcouseid = null) { parent::__construct($config); $this->setup_restrictions(); $this->currentcourseid = $currentcouseid; $this->includecurrentcourse = false; } /** * Sets up any access restrictions for the courses to be displayed in the search. * * This will typically call $this->require_capability(). */ protected function setup_restrictions() { $this->require_capability('moodle/restore:restorecourse'); } /** * Get the search SQL. * @global moodle_database $DB * @return array */ protected function get_searchsql() { global $DB; $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $params = array( 'contextlevel' => CONTEXT_COURSE, 'fullnamesearch' => '%'.$this->get_search().'%', 'shortnamesearch' => '%'.$this->get_search().'%' ); $select = " SELECT c.id, c.fullname, c.shortname, c.visible, c.sortorder "; $from = " FROM {course} c "; $where = " WHERE (".$DB->sql_like('c.fullname', ':fullnamesearch', false)." OR ". $DB->sql_like('c.shortname', ':shortnamesearch', false).")"; $orderby = " ORDER BY c.sortorder"; if ($this->currentcourseid !== null && !$this->includecurrentcourse) { $where .= " AND c.id <> :currentcourseid"; $params['currentcourseid'] = $this->currentcourseid; } return array($select.$ctxselect.$from.$ctxjoin.$where.$orderby, $params); } /** * Gets the context level for the search result items. * @return CONTEXT_|int */ protected function get_itemcontextlevel() { return CONTEXT_COURSE; } /** * Formats results. */ protected function format_results() {} /** * Returns the name the search variable should use * @return string */ public function get_varsearch() { return self::$VAR_SEARCH; } /** * Returns true if the current course should be included in the results. */ public function set_include_currentcourse() { $this->includecurrentcourse = true; } /** * Get the current course id * * @return int */ public function get_current_course_id(): int { return $this->currentcourseid; } } /** * A category search component * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_category_search extends restore_search_base { /** * The search variable to use. * @var string */ static $VAR_SEARCH = 'catsearch'; /** * Constructor * @param array $config */ public function __construct(array $config = array()) { parent::__construct($config); $this->require_capability('moodle/course:create'); } /** * Returns the search SQL. * @global moodle_database $DB * @return array */ protected function get_searchsql() { global $DB; $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $params = array( 'contextlevel' => CONTEXT_COURSECAT, 'namesearch' => '%'.$this->get_search().'%', ); $select = " SELECT c.id, c.name, c.visible, c.sortorder, c.description, c.descriptionformat "; $from = " FROM {course_categories} c "; $where = " WHERE ".$DB->sql_like('c.name', ':namesearch', false); $orderby = " ORDER BY c.sortorder"; return array($select.$ctxselect.$from.$ctxjoin.$where.$orderby, $params); } /** * Returns the context level of the search results. * @return CONTEXT_COURSECAT */ protected function get_itemcontextlevel() { return CONTEXT_COURSECAT; } /** * Formats the results. */ protected function format_results() {} /** * Returns the name to use for the search variable. * @return string */ public function get_varsearch() { return self::$VAR_SEARCH; } } util/ui/base_moodleform.class.php 0000644 00000042230 15215711721 0013113 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the generic moodleform bridge for the backup user interface * as well as the individual forms that relate to the different stages the user * interface can exist within. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); /** * Base moodleform bridge * * Ahhh the mighty moodleform bridge! Strong enough to take the weight of 682 full * grown african swallows all of whom have been carring coconuts for several days. * EWWWWW!!!!!!!!!!!!!!!!!!!!!!!! * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_moodleform extends moodleform { /** * The stage this form belongs to * @var base_ui_stage */ protected $uistage = null; /** * Group stack to control open and closed div groups. * @var array */ protected array $groupstack = []; /** * Creates the form * * @param base_ui_stage $uistage * @param moodle_url|string $action * @param mixed $customdata * @param string $method get|post * @param string $target * @param array $attributes * @param bool $editable */ public function __construct(base_ui_stage $uistage, $action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true) { $this->uistage = $uistage; // Add a class to the attributes to prevent the default collapsible behaviour. if (!$attributes) { $attributes = array(); } $attributes['class'] = 'unresponsive'; if (!isset($attributes['enctype'])) { $attributes['enctype'] = 'application/x-www-form-urlencoded'; // Enforce compatibility with our max_input_vars hack. } parent::__construct($action, $customdata, $method, $target, $attributes, $editable); } /** * The standard form definition... obviously not much here */ public function definition() { $ui = $this->uistage->get_ui(); $mform = $this->_form; $mform->setDisableShortforms(); $stage = $mform->addElement('hidden', 'stage', $this->uistage->get_stage()); $mform->setType('stage', PARAM_INT); $stage = $mform->addElement('hidden', $ui->get_name(), $ui->get_uniqueid()); $mform->setType($ui->get_name(), PARAM_ALPHANUM); $params = $this->uistage->get_params(); if (is_array($params) && count($params) > 0) { foreach ($params as $name => $value) { // TODO: Horrible hack, but current backup ui structure does not allow // to make this easy (only changing params to objects that would be // possible. MDL-38735. $intparams = array( 'contextid', 'importid', 'target'); $stage = $mform->addElement('hidden', $name, $value); if (in_array($name, $intparams)) { $mform->setType($name, PARAM_INT); } else { // Adding setType() to avoid missing setType() warnings. // MDL-39126: support $mform->setType() for additional backup parameters. $mform->setType($name, PARAM_RAW); } } } } /** * Definition applied after the data is organised.. why's it here? because I want * to add elements on the fly. * @global moodle_page $PAGE */ public function definition_after_data() { $buttonarray = array(); if (!$this->uistage->is_first_stage()) { $buttonarray[] = $this->_form->createElement('submit', 'previous', get_string('previousstage', 'backup')); } else if ($this->uistage instanceof backup_ui_stage) { // Only display the button on the first stage of backup, they only place where it has an effect. $buttonarray[] = $this->_form->createElement('submit', 'oneclickbackup', get_string('jumptofinalstep', 'backup'), array('class' => 'oneclickbackup')); } $cancelparams = [ 'data-modal' => 'confirmation', 'data-modal-content-str' => json_encode([ 'confirmcancelquestion', 'backup', ]), 'data-modal-yes-button-str' => json_encode([ 'yes', 'moodle', ]), ]; if ($this->uistage->get_ui() instanceof import_ui) { $cancelparams['data-modal-title-str'] = json_encode([ 'confirmcancelimport', 'backup', ]); } else if ($this->uistage->get_ui() instanceof restore_ui) { $cancelparams['data-modal-title-str'] = json_encode([ 'confirmcancelrestore', 'backup', ]); } else { $cancelparams['data-modal-title-str'] = json_encode([ 'confirmcancel', 'backup', ]); } $buttonarray[] = $this->_form->createElement('cancel', 'cancel', get_string('cancel'), $cancelparams); $buttonarray[] = $this->_form->createElement( 'submit', 'submitbutton', get_string($this->uistage->get_ui()->get_name().'stage'.$this->uistage->get_stage().'action', 'backup'), array('class' => 'proceedbutton') ); $this->_form->addGroup($buttonarray, 'buttonar', '', array(' '), false); $this->_form->closeHeaderBefore('buttonar'); $this->_definition_finalized = true; } /** * Closes any open divs. */ public function close_task_divs() { while (!empty($this->groupstack)) { $this->_form->addElement('html', html_writer::end_tag('div')); array_pop($this->groupstack); } } /** * Adds the backup_setting as a element to the form * @param backup_setting $setting * @param base_task $task * @return bool */ public function add_setting(backup_setting $setting, ?base_task $task = null) { return $this->add_settings(array(array($setting, $task))); } /** * Adds multiple backup_settings as elements to the form * @param array $settingstasks Consists of array($setting, $task) elements * @return bool */ public function add_settings(array $settingstasks) { global $OUTPUT; // Determine highest setting level, which is displayed in this stage. This is relevant for considering only // locks of dependency settings for parent settings, which are not displayed in this stage. $highestlevel = backup_setting::ACTIVITY_LEVEL; foreach ($settingstasks as $st) { list($setting, $task) = $st; if ($setting->get_level() < $highestlevel) { $highestlevel = $setting->get_level(); } } $defaults = array(); foreach ($settingstasks as $st) { list($setting, $task) = $st; // If the setting cant be changed or isn't visible then add it as a fixed setting. if (!$setting->get_ui()->is_changeable($highestlevel) || $setting->get_visibility() != backup_setting::VISIBLE) { $this->add_fixed_setting($setting, $task); continue; } // First add the formatting for this setting. $this->add_html_formatting($setting); // Then call the add method with the get_element_properties array. call_user_func_array(array($this->_form, 'addElement'), array_values($setting->get_ui()->get_element_properties($task, $OUTPUT))); $this->_form->setType($setting->get_ui_name(), $setting->get_param_validation()); $defaults[$setting->get_ui_name()] = $setting->get_value(); if ($setting->has_help()) { list($identifier, $component) = $setting->get_help(); $this->_form->addHelpButton($setting->get_ui_name(), $identifier, $component); } $this->pop_group(); } $this->_form->setDefaults($defaults); return true; } /** * Adds a heading to the form * @param string $name * @param string $text */ public function add_heading($name , $text) { $this->_form->addElement('header', $name, $text); } /** * Adds HTML formatting for the given backup setting, needed to group/segment * correctly. * @param backup_setting $setting */ protected function add_html_formatting(backup_setting $setting) { $isincludesetting = (strpos($setting->get_name(), '_include') !== false); if ($isincludesetting && $setting->get_level() != backup_setting::ROOT_LEVEL) { switch ($setting->get_level()) { case backup_setting::COURSE_LEVEL: $this->pop_groups_to('course'); $this->push_group_start('course', 'grouped_settings course_level'); $this->push_group_start(null, 'include_setting course_level'); break; case backup_setting::SECTION_LEVEL: $this->pop_groups_to('course'); $this->push_group_start('section', 'grouped_settings section_level'); $this->push_group_start(null, 'include_setting section_level'); break; case backup_setting::ACTIVITY_LEVEL: $this->pop_groups_to('section'); $this->push_group_start('activity', 'grouped_settings activity_level'); $this->push_group_start(null, 'include_setting activity_level'); break; case backup_setting::SUBSECTION_LEVEL: $this->pop_groups_to('section'); $this->push_group_start('subsection', 'grouped_settings subsection_level'); $this->push_group_start(null, 'normal_setting'); break; case backup_setting::SUBACTIVITY_LEVEL: $this->pop_groups_to('subsection'); $this->push_group_start('subactivity', 'grouped_settings activity_level'); $this->push_group_start(null, 'include_setting activity_level'); break; default: $this->push_group_start(null, 'normal_setting'); break; } } else if ($setting->get_level() == backup_setting::ROOT_LEVEL) { $this->push_group_start('root', 'root_setting'); } else { $this->push_group_start(null, 'normal_setting'); } } /** * Adds a fixed or static setting to the form * @param backup_setting $setting * @param base_task $task */ public function add_fixed_setting(backup_setting $setting, base_task $task) { global $OUTPUT; $settingui = $setting->get_ui(); if ($setting->get_visibility() == backup_setting::VISIBLE) { $this->add_html_formatting($setting); switch ($setting->get_status()) { case backup_setting::LOCKED_BY_PERMISSION: $icon = ' '.$OUTPUT->pix_icon('i/permissionlock', get_string('lockedbypermission', 'backup'), 'moodle', array('class' => 'smallicon lockedicon permissionlock')); break; case backup_setting::LOCKED_BY_CONFIG: $icon = ' '.$OUTPUT->pix_icon('i/configlock', get_string('lockedbyconfig', 'backup'), 'moodle', array('class' => 'smallicon lockedicon configlock')); break; case backup_setting::LOCKED_BY_HIERARCHY: $icon = ' '.$OUTPUT->pix_icon('i/hierarchylock', get_string('lockedbyhierarchy', 'backup'), 'moodle', array('class' => 'smallicon lockedicon configlock')); break; default: $icon = ''; break; } $context = context_course::instance($task->get_courseid()); $label = format_string($settingui->get_label($task), true, array('context' => $context)); $labelicon = $settingui->get_icon(); if (!empty($labelicon)) { $label .= $OUTPUT->render($labelicon); } $this->_form->addElement('static', 'static_'.$settingui->get_name(), $label, $settingui->get_static_value().$icon); $this->pop_group(); } $this->_form->addElement('hidden', $settingui->get_name(), $settingui->get_value()); $this->_form->setType($settingui->get_name(), $settingui->get_param_validation()); } /** * Pushes a group start to the form. * * This method will create a new group div in the form and add it to the group stack. * The name can be used to close all stacked groups up to a certain group. * * @param string|null $name The name of the group, if any. * @param string $classes The classes to add to the div. */ protected function push_group_start(?string $name, string $classes) { $mform = $this->_form; $this->groupstack[] = $name; $mform->addElement('html', html_writer::start_tag('div', ['class' => $classes])); } /** * Pops groups from the stack until the given group name is reached. * * @param string $name The name of the group to pop to. */ protected function pop_groups_to(string $name) { if (empty($this->groupstack)) { return; } while (!empty($this->groupstack) && end($this->groupstack) !== $name) { $this->pop_group(); } } /** * Pops a group from the stack and closes the div. * * @return string|null The name of the group that was popped, or null if the stack is empty. */ protected function pop_group(): ?string { if (empty($this->groupstack)) { return null; } $mform = $this->_form; $mform->addElement('html', html_writer::end_tag('div')); return array_pop($this->groupstack); } /** * Adds dependencies to the form recursively * * @param backup_setting $setting */ public function add_dependencies(backup_setting $setting) { $mform = $this->_form; // Apply all dependencies for backup. foreach ($setting->get_my_dependency_properties() as $key => $dependency) { call_user_func_array(array($this->_form, 'disabledIf'), array_values($dependency)); } } /** * Returns true if the form was cancelled, false otherwise * @return bool */ public function is_cancelled() { return (optional_param('cancel', false, PARAM_BOOL) || parent::is_cancelled()); } /** * Removes an element from the form if it exists * @param string $elementname * @return bool */ public function remove_element($elementname) { if ($this->_form->elementExists($elementname)) { return $this->_form->removeElement($elementname); } else { return false; } } /** * Gets an element from the form if it exists * * @param string $elementname * @return HTML_QuickForm_input|MoodleQuickForm_group */ public function get_element($elementname) { if ($this->_form->elementExists($elementname)) { return $this->_form->getElement($elementname); } else { return false; } } /** * Displays the form */ public function display() { global $PAGE, $COURSE; $this->require_definition_after_data(); // Get list of module types on course. $modinfo = get_fast_modinfo($COURSE); $modnames = array_map('strval', $modinfo->get_used_module_names(true)); core_collator::asort($modnames); $PAGE->requires->js_call_amd('core_backup/schema_backup_form', 'init', [$modnames]); $PAGE->requires->strings_for_js(array('select', 'all', 'none'), 'moodle'); $PAGE->requires->strings_for_js(array('showtypes', 'hidetypes'), 'backup'); parent::display(); } /** * Ensures the the definition after data is loaded */ public function require_definition_after_data() { if (!$this->_definition_finalized) { $this->definition_after_data(); } } } util/ui/amd/src/async_backup.js 0000644 00000055134 15215711721 0012500 0 ustar 00 // 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/>. /** * This module updates the UI during an asynchronous * backup or restore process. * * @module core_backup/async_backup * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.7 */ define(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'], function($, ajax, Str, notification, Templates) { /** * Module level constants. * * Using var instead of const as ES6 isn't fully supported yet. */ var STATUS_EXECUTING = 800; var STATUS_FINISHED_ERR = 900; var STATUS_FINISHED_OK = 1000; /** * Module level variables. */ var Asyncbackup = {}; var checkdelayoriginal = 15000; // This is the default time to use. var checkdelay = 15000; // How often we should check for progress updates. var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value var backupid; // The backup id to get the progress for. var contextid; // The course this backup progress is for. var restoreurl; // The URL to view course restores. var typeid; // The type of operation backup or restore. var backupintervalid; // The id of the setInterval function. var allbackupintervalid; // The id of the setInterval function. var allcopyintervalid; // The id of the setInterval function. var timeout = 2000; // Timeout for ajax requests. /** * Helper function to update UI components. * * @param {string} backupid The id to match elements on. * @param {string} type The type of operation, backup or restore. * @param {number} percentage The completion percentage to apply. */ function updateElement(backupid, type, percentage) { var percentagewidth = Math.round(percentage) + '%'; var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0]; var percentagetext = percentage.toFixed(2) + '%'; // Set progress bar percentage indicators elementbar.setAttribute('aria-valuenow', percentagewidth); elementbar.style.width = percentagewidth; elementbar.innerHTML = percentagetext; } /** * Updates the interval we use to check for backup progress. * * @param {Number} intervalid The id of the interval * @param {Function} callback The function to use in setInterval * @param {Number} value The specified interval (in milliseconds) * @returns {Number} */ function updateInterval(intervalid, callback, value) { clearInterval(intervalid); return setInterval(callback, value); } /** * Update backup table row when an async backup completes. * * @param {string} backupid The id to match elements on. */ function updateBackupTableRow(backupid) { var statuscell = $('#' + backupid + '_bar').parent().parent(); var tablerow = statuscell.parent(); var cellsiblings = statuscell.siblings(); var timecell = cellsiblings[1]; var timevalue = $(timecell).text(); var filenamecell = cellsiblings[0]; var filename = $(filenamecell).text(); ajax.call([{ // Get the table data via webservice. methodname: 'core_backup_get_async_backup_links_backup', args: { 'filename': filename, 'contextid': contextid, 'backupid': backupid }, }])[0].done(function(response) { // We have the data now update the UI. var context = { filename: filename, time: timevalue, size: response.filesize, fileurl: response.fileurl, restoreurl: response.restoreurl }; Templates.render('core/async_backup_progress_row', context).then(function(html, js) { Templates.replaceNodeContents(tablerow, html, js); return; }).fail(function() { notification.exception(new Error('Failed to load table row')); return; }); }); } /** * Update restore table row when an async restore completes. * * @param {string} backupid The id to match elements on. */ function updateRestoreTableRow(backupid) { var statuscell = $('#' + backupid + '_bar').parent().parent(); var tablerow = statuscell.parent(); var cellsiblings = statuscell.siblings(); var coursecell = cellsiblings[0]; var timecell = cellsiblings[1]; var timevalue = $(timecell).text(); ajax.call([{ // Get the table data via webservice. methodname: 'core_backup_get_async_backup_links_restore', args: { 'backupid': backupid, 'contextid': contextid }, }])[0].done(function(response) { // We have the data now update the UI. var resourcename = $(coursecell).text(); var context = { resourcename: resourcename, restoreurl: response.restoreurl, time: timevalue }; Templates.render('core/async_restore_progress_row', context).then(function(html, js) { Templates.replaceNodeContents(tablerow, html, js); return; }).fail(function() { notification.exception(new Error('Failed to load table row')); return; }); }); } /** * Update copy table row when an course copy completes. * * @param {string} backupid The id to match elements on. */ function updateCopyTableRow(backupid) { var elementbar = document.querySelectorAll("[data-restoreid=" + CSS.escape(backupid) + "]")[0]; var restorecourse = elementbar.closest('tr').children[1]; var coursename = restorecourse.innerHTML; var courselink = document.createElement('a'); var elementbarparent = elementbar.closest('td'); var operation = elementbarparent.previousElementSibling; // Replace the prgress bar. Str.get_string('complete').then(function(content) { operation.innerHTML = content; return; }).catch(function() { notification.exception(new Error('Failed to load string: complete')); return; }); Templates.render('core/async_copy_complete_cell', {}).then(function(html, js) { Templates.replaceNodeContents(elementbarparent, html, js); return; }).fail(function() { notification.exception(new Error('Failed to load table cell')); return; }); // Update the destination course name to a link to that course. ajax.call([{ methodname: 'core_backup_get_async_backup_links_restore', args: { 'backupid': backupid, 'contextid': 0 }, }])[0].done(function(response) { courselink.setAttribute('href', response.restoreurl); courselink.innerHTML = coursename; restorecourse.innerHTML = null; restorecourse.appendChild(courselink); return; }).fail(function() { notification.exception(new Error('Failed to update table row')); return; }); } /** * Update the Moodle user interface with the progress of * the backup process. * * @param {object} progress The progress and status of the process. */ function updateProgress(progress) { var percentage = progress.progress * 100; var type = 'backup'; var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0]; var elementstatus = $('#' + backupid + '_status'); var elementdetail = $('#' + backupid + '_detail'); var elementbutton = $('#' + backupid + '_button'); var stringRequests; if (progress.status == STATUS_EXECUTING) { // Process is in progress. // Add in progress class color to bar. elementbar.classList.add('bg-success'); updateElement(backupid, type, percentage); // Change heading. var strProcessing = 'async' + typeid + 'processing'; Str.get_string(strProcessing, 'backup').then(function(title) { elementstatus.text(title); return; }).catch(function() { notification.exception(new Error('Failed to load string: backup ' + strProcessing)); }); } else if (progress.status == STATUS_FINISHED_ERR) { // Process completed with error. // Add in fail class color to bar. elementbar.classList.add('bg-danger'); // Remove in progress class color to bar. elementbar.classList.remove('bg-success'); updateElement(backupid, type, 100); // Change heading and text. var strStatus = 'async' + typeid + 'error'; var strStatusDetail = 'async' + typeid + 'errordetail'; stringRequests = [ {key: strStatus, component: 'backup'}, {key: strStatusDetail, component: 'backup'} ]; Str.get_strings(stringRequests).then(function(strings) { elementstatus.text(strings[0]); elementdetail.text(strings[1]); return; }) .catch(function() { notification.exception(new Error('Failed to load string')); return; }); $('.backup_progress').children('span').removeClass('backup_stage_current'); $('.backup_progress').children('span').last().addClass('backup_stage_current'); // Stop checking when we either have an error or a completion. clearInterval(backupintervalid); } else if (progress.status == STATUS_FINISHED_OK) { // Process completed successfully. // Add in progress class color to bar elementbar.classList.add('bg-success'); updateElement(backupid, type, 100); // Change heading and text var strComplete = 'async' + typeid + 'complete'; Str.get_string(strComplete, 'backup').then(function(title) { elementstatus.text(title); return; }).catch(function() { notification.exception(new Error('Failed to load string: backup ' + strComplete)); }); if (typeid == 'restore') { ajax.call([{ // Get the table data via webservice. methodname: 'core_backup_get_async_backup_links_restore', args: { 'backupid': backupid, 'contextid': contextid }, }])[0].done(function(response) { var strDetail = 'async' + typeid + 'completedetail'; var strButton = 'async' + typeid + 'completebutton'; var stringRequests = [ {key: strDetail, component: 'backup', param: response.restoreurl}, {key: strButton, component: 'backup'} ]; Str.get_strings(stringRequests).then(function(strings) { elementdetail.html(strings[0]); elementbutton.text(strings[1]); elementbutton.attr('href', response.restoreurl); return; }) .catch(function() { notification.exception(new Error('Failed to load string')); return; }); }); } else { var strDetail = 'async' + typeid + 'completedetail'; var strButton = 'async' + typeid + 'completebutton'; stringRequests = [ {key: strDetail, component: 'backup', param: restoreurl}, {key: strButton, component: 'backup'} ]; Str.get_strings(stringRequests).then(function(strings) { elementdetail.html(strings[0]); elementbutton.text(strings[1]); elementbutton.attr('href', restoreurl); return; }) .catch(function() { notification.exception(new Error('Failed to load string')); return; }); } $('.backup_progress').children('span').removeClass('backup_stage_current'); $('.backup_progress').children('span').last().addClass('backup_stage_current'); // Stop checking when we either have an error or a completion. clearInterval(backupintervalid); } } /** * Update the Moodle user interface with the progress of * all the pending processes for backup and restore operations. * * @param {object} progress The progress and status of the process. */ function updateProgressAll(progress) { progress.forEach(function(element) { var percentage = element.progress * 100; var backupid = element.backupid; var type = element.operation; var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0]; if (element.status == STATUS_EXECUTING) { // Process is in element. // Add in element class color to bar elementbar.classList.add('bg-success'); updateElement(backupid, type, percentage); } else if (element.status == STATUS_FINISHED_ERR) { // Process completed with error. // Add in fail class color to bar elementbar.classList.add('bg-danger'); elementbar.classList.add('complete'); // Remove in element class color to bar elementbar.classList.remove('bg-success'); updateElement(backupid, type, 100); } else if (element.status == STATUS_FINISHED_OK) { // Process completed successfully. // Add in element class color to bar elementbar.classList.add('bg-success'); elementbar.classList.add('complete'); updateElement(backupid, type, 100); // We have a successful backup. Update the UI with download and file details. if (type == 'backup') { updateBackupTableRow(backupid); } else { updateRestoreTableRow(backupid); } } }); } /** * Update the Moodle user interface with the progress of * all the pending processes for copy operations. * * @param {object} progress The progress and status of the process. */ function updateProgressCopy(progress) { progress.forEach(function(element) { var percentage = element.progress * 100; var backupid = element.backupid; var type = element.operation; var elementbar = document.querySelectorAll("[data-" + type + "id=" + CSS.escape(backupid) + "]")[0]; if (type == 'restore') { let restorecell = elementbar.closest('tr').children[3]; Str.get_string('restore').then(function(content) { restorecell.innerHTML = content; return; }).catch(function() { notification.exception(new Error('Failed to load string: restore')); }); } if (element.status == STATUS_EXECUTING) { // Process is in element. // Add in element class color to bar elementbar.classList.add('bg-success'); updateElement(backupid, type, percentage); } else if (element.status == STATUS_FINISHED_ERR) { // Process completed with error. // Add in fail class color to bar elementbar.classList.add('bg-danger'); elementbar.classList.add('complete'); // Remove in element class color to bar elementbar.classList.remove('bg-success'); updateElement(backupid, type, 100); } else if ((element.status == STATUS_FINISHED_OK) && (type == 'restore')) { // Process completed successfully. // Add in element class color to bar elementbar.classList.add('bg-success'); elementbar.classList.add('complete'); updateElement(backupid, type, 100); // We have a successful copy. Update the UI link to copied course. updateCopyTableRow(backupid); } }); } /** * Get the progress of the backup process via ajax. */ function getBackupProgress() { ajax.call([{ // Get the backup progress via webservice. methodname: 'core_backup_get_async_backup_progress', args: { 'backupids': [backupid], 'contextid': contextid }, }], true, true, false, timeout)[0].done(function(response) { // We have the progress now update the UI. updateProgress(response[0]); checkdelay = checkdelayoriginal; backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal); }).fail(function() { checkdelay = checkdelay * checkdelaymultipler; backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay); }); } /** * Get the progress of all backup processes via ajax. */ function getAllBackupProgress() { var backupids = []; var progressbars = $('.progress').find('.progress-bar').not('.complete'); progressbars.each(function() { backupids.push((this.id).substring(0, 32)); }); if (backupids.length > 0) { ajax.call([{ // Get the backup progress via webservice. methodname: 'core_backup_get_async_backup_progress', args: { 'backupids': backupids, 'contextid': contextid }, }], true, true, false, timeout)[0].done(function(response) { updateProgressAll(response); checkdelay = checkdelayoriginal; allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal); }).fail(function() { checkdelay = checkdelay * checkdelaymultipler; allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay); }); } else { clearInterval(allbackupintervalid); // No more progress bars to update, stop checking. } } /** * Get the progress of all copy processes via ajax. */ function getAllCopyProgress() { var copyids = []; var progressbars = $('.progress').find('.progress-bar[data-operation][data-backupid][data-restoreid]').not('.complete'); progressbars.each(function() { let progressvars = { 'backupid': this.dataset.backupid, 'restoreid': this.dataset.restoreid, 'operation': this.dataset.operation, }; copyids.push(progressvars); }); if (copyids.length > 0) { ajax.call([{ // Get the copy progress via webservice. methodname: 'core_backup_get_copy_progress', args: { 'copies': copyids }, }], true, true, false, timeout)[0].done(function(response) { updateProgressCopy(response); checkdelay = checkdelayoriginal; allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelayoriginal); }).fail(function() { checkdelay = checkdelay * checkdelaymultipler; allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelay); }); } else { clearInterval(allcopyintervalid); // No more progress bars to update, stop checking. } } /** * Get status updates for all backups. * * @public * @param {number} context The context id. */ Asyncbackup.asyncBackupAllStatus = function(context) { contextid = context; allbackupintervalid = setInterval(getAllBackupProgress, checkdelay); }; /** * Get status updates for all course copies. * * @public */ Asyncbackup.asyncCopyAllStatus = function() { allcopyintervalid = setInterval(getAllCopyProgress, checkdelay); }; /** * Get status updates for backup. * * @public * @param {string} backup The backup record id. * @param {number} context The context id. * @param {string} restore The restore link. * @param {string} type The operation type (backup or restore). */ Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) { backupid = backup; contextid = context; restoreurl = restore; if (type == 'backup') { typeid = 'backup'; } else { typeid = 'restore'; } // Remove the links from the progress bar, no going back now. $('.backup_progress').children('a').removeAttr('href'); // Periodically check for progress updates and update the UI as required. backupintervalid = setInterval(getBackupProgress, checkdelay); }; return Asyncbackup; }); util/ui/amd/src/schema_backup_form.js 0000644 00000015425 15215711721 0013645 0 ustar 00 // 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/>. /** * Schema selector javascript controls. * * This module controls: * - The select all feature. * - Disabling activities checkboxes when the section is not selected. * * @module core_backup/schema_backup_form * @copyright 2024 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Notification from 'core/notification'; import * as Templates from 'core/templates'; const Selectors = { action: '[data-mdl-action]', checkboxes: '#id_coursesettings input[type="checkbox"]', firstSection: 'fieldset#id_coursesettings .fcontainer .grouped_settings.section_level', modCheckboxes: (modName) => `setting_activity_${modName}_`, }; const Suffixes = { userData: '_userdata', userInfo: '_userinfo', included: '_included', }; /** * Adds select all/none links to the top of the backup/restore/import schema page. */ export default class BackupFormController { /** * Static module init method. * @param {Array<string>} modNames - The names of the modules. * @returns {BackupFormController} */ static init(modNames) { return new BackupFormController(modNames); } /** * Creates a new instance of the SchemaBackupForm class. * @param {Array<string>} modNames - The names of the modules. */ constructor(modNames) { this.modNames = modNames; this.scanFormUserData(); this.addSelectorsToPage(); } /** * Detect the user data attribute from the form. * * @private */ scanFormUserData() { this.withuserdata = false; this.userDataSuffix = Suffixes.userData; const checkboxes = document.querySelectorAll(Selectors.checkboxes); if (!checkboxes) { return; } // Depending on the form, user data inclusion is called userinfo or userdata. for (const checkbox of checkboxes) { const name = checkbox.name; if (name.endsWith(Suffixes.userData)) { this.withuserdata = true; break; } else if (name.endsWith(Suffixes.userInfo)) { this.withuserdata = true; this.userDataSuffix = Suffixes.userInfo; break; } } } /** * Initializes all related events. * * @private * @param {HTMLElement} element - The element to attach the events to. */ initEvents(element) { element.addEventListener('click', (event) => { const action = event.target.closest(Selectors.action); if (!action) { return; } event.preventDefault(); const suffix = (action.dataset?.mdlType == 'userdata') ? this.userDataSuffix : Suffixes.included; this.changeSelection( action.dataset.mdlAction == 'selectall', suffix, action.dataset?.mdlMod ?? null ); }); } /** * Changes the selection according to the params. * * @private * @param {boolean} checked - The checked state for the checkboxes. * @param {string} suffix - The checkboxes suffix * @param {string} [modName] - The module name. */ changeSelection(checked, suffix, modName) { const prefix = modName ? Selectors.modCheckboxes(modName) : null; let formId; const checkboxes = document.querySelectorAll(Selectors.checkboxes); for (const checkbox of checkboxes) { formId = formId ?? checkbox.closest('form').getAttribute('id'); if (prefix && !checkbox.name.startsWith(prefix)) { continue; } if (checkbox.name.endsWith(suffix)) { checkbox.checked = checked; } } // At this point, we really need to persuade the form we are part of to // update all of its disabledIf rules. However, as far as I can see, // given the way that lib/form/form.js is written, that is impossible. if (formId && M.form) { M.form.updateFormState(formId); } } /** * Generates the full selectors element to add to the page. * * @private * @returns {HTMLElement} The selectors element. */ generateSelectorsElement() { const links = document.createElement('div'); links.id = 'backup_selectors'; this.initEvents(links); this.renderSelectorsTemplate(links); return links; } /** * Load the select all template. * * @private * @param {HTMLElement} element the container */ renderSelectorsTemplate(element) { const data = { modules: this.getModulesTemplateData(), withuserdata: (this.withuserdata) ? true : undefined, }; Templates.renderForPromise( 'core_backup/formselectall', data ).then(({html, js}) => { return Templates.replaceNodeContents(element, html, js); }).catch(Notification.exception); } /** * Generate the modules template data. * * @private * @returns {Array} of modules data. */ getModulesTemplateData() { const modules = []; for (const modName in this.modNames) { if (!this.modNames.hasOwnProperty(modName)) { continue; } modules.push({ modname: modName, heading: this.modNames[modName], }); } return modules; } /** * Adds select all/none functionality to the backup form. * * @private */ addSelectorsToPage() { const firstSection = document.querySelector(Selectors.firstSection); if (!firstSection) { // This is not a relevant page. return; } if (!firstSection.querySelector(Selectors.checkboxes)) { // No checkboxes. return; } // Add global select all/none options. const selector = this.generateSelectorsElement(); firstSection.parentNode.insertBefore(selector, firstSection); } } util/ui/amd/build/schema_backup_form.min.js 0000644 00000011173 15215711721 0014733 0 ustar 00 define("core_backup/schema_backup_form",["exports","core/notification","core/templates"],(function(_exports,_notification,Templates){var obj; /** * Schema selector javascript controls. * * This module controls: * - The select all feature. * - Disabling activities checkboxes when the section is not selected. * * @module core_backup/schema_backup_form * @copyright 2024 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj},Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Templates);const Selectors_action="[data-mdl-action]",Selectors_checkboxes='#id_coursesettings input[type="checkbox"]',Selectors_firstSection="fieldset#id_coursesettings .fcontainer .grouped_settings.section_level",Selectors_modCheckboxes=modName=>"setting_activity_".concat(modName,"_"),Suffixes_userData="_userdata",Suffixes_userInfo="_userinfo",Suffixes_included="_included";class BackupFormController{static init(modNames){return new BackupFormController(modNames)}constructor(modNames){this.modNames=modNames,this.scanFormUserData(),this.addSelectorsToPage()}scanFormUserData(){this.withuserdata=!1,this.userDataSuffix=Suffixes_userData;const checkboxes=document.querySelectorAll(Selectors_checkboxes);if(checkboxes)for(const checkbox of checkboxes){const name=checkbox.name;if(name.endsWith(Suffixes_userData)){this.withuserdata=!0;break}if(name.endsWith(Suffixes_userInfo)){this.withuserdata=!0,this.userDataSuffix=Suffixes_userInfo;break}}}initEvents(element){element.addEventListener("click",(event=>{var _action$dataset,_action$dataset$mdlMo,_action$dataset2;const action=event.target.closest(Selectors_action);if(!action)return;event.preventDefault();const suffix="userdata"==(null===(_action$dataset=action.dataset)||void 0===_action$dataset?void 0:_action$dataset.mdlType)?this.userDataSuffix:Suffixes_included;this.changeSelection("selectall"==action.dataset.mdlAction,suffix,null!==(_action$dataset$mdlMo=null===(_action$dataset2=action.dataset)||void 0===_action$dataset2?void 0:_action$dataset2.mdlMod)&&void 0!==_action$dataset$mdlMo?_action$dataset$mdlMo:null)}))}changeSelection(checked,suffix,modName){const prefix=modName?Selectors_modCheckboxes(modName):null;let formId;const checkboxes=document.querySelectorAll(Selectors_checkboxes);for(const checkbox of checkboxes){var _formId;formId=null!==(_formId=formId)&&void 0!==_formId?_formId:checkbox.closest("form").getAttribute("id"),prefix&&!checkbox.name.startsWith(prefix)||checkbox.name.endsWith(suffix)&&(checkbox.checked=checked)}formId&&M.form&&M.form.updateFormState(formId)}generateSelectorsElement(){const links=document.createElement("div");return links.id="backup_selectors",this.initEvents(links),this.renderSelectorsTemplate(links),links}renderSelectorsTemplate(element){const data={modules:this.getModulesTemplateData(),withuserdata:!!this.withuserdata||void 0};Templates.renderForPromise("core_backup/formselectall",data).then((_ref=>{let{html:html,js:js}=_ref;return Templates.replaceNodeContents(element,html,js)})).catch(_notification.default.exception)}getModulesTemplateData(){const modules=[];for(const modName in this.modNames)this.modNames.hasOwnProperty(modName)&&modules.push({modname:modName,heading:this.modNames[modName]});return modules}addSelectorsToPage(){const firstSection=document.querySelector(Selectors_firstSection);if(!firstSection)return;if(!firstSection.querySelector(Selectors_checkboxes))return;const selector=this.generateSelectorsElement();firstSection.parentNode.insertBefore(selector,firstSection)}}return _exports.default=BackupFormController,_exports.default})); //# sourceMappingURL=schema_backup_form.min.js.map util/ui/amd/build/async_backup.min.js.map 0000644 00000076413 15215711721 0014351 0 ustar 00 {"version":3,"file":"async_backup.min.js","sources":["../src/async_backup.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * This module updates the UI during an asynchronous\n * backup or restore process.\n *\n * @module core_backup/async_backup\n * @copyright 2018 Matt Porritt <mattp@catalyst-au.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.7\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification', 'core/templates'],\n function($, ajax, Str, notification, Templates) {\n\n /**\n * Module level constants.\n *\n * Using var instead of const as ES6 isn't fully supported yet.\n */\n var STATUS_EXECUTING = 800;\n var STATUS_FINISHED_ERR = 900;\n var STATUS_FINISHED_OK = 1000;\n\n /**\n * Module level variables.\n */\n var Asyncbackup = {};\n var checkdelayoriginal = 15000; // This is the default time to use.\n var checkdelay = 15000; // How often we should check for progress updates.\n var checkdelaymultipler = 1.5; // If a request fails this multiplier will be used to increase the checkdelay value\n var backupid; // The backup id to get the progress for.\n var contextid; // The course this backup progress is for.\n var restoreurl; // The URL to view course restores.\n var typeid; // The type of operation backup or restore.\n var backupintervalid; // The id of the setInterval function.\n var allbackupintervalid; // The id of the setInterval function.\n var allcopyintervalid; // The id of the setInterval function.\n var timeout = 2000; // Timeout for ajax requests.\n\n /**\n * Helper function to update UI components.\n *\n * @param {string} backupid The id to match elements on.\n * @param {string} type The type of operation, backup or restore.\n * @param {number} percentage The completion percentage to apply.\n */\n function updateElement(backupid, type, percentage) {\n var percentagewidth = Math.round(percentage) + '%';\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n var percentagetext = percentage.toFixed(2) + '%';\n\n // Set progress bar percentage indicators\n elementbar.setAttribute('aria-valuenow', percentagewidth);\n elementbar.style.width = percentagewidth;\n elementbar.innerHTML = percentagetext;\n }\n\n /**\n * Updates the interval we use to check for backup progress.\n *\n * @param {Number} intervalid The id of the interval\n * @param {Function} callback The function to use in setInterval\n * @param {Number} value The specified interval (in milliseconds)\n * @returns {Number}\n */\n function updateInterval(intervalid, callback, value) {\n clearInterval(intervalid);\n return setInterval(callback, value);\n }\n\n /**\n * Update backup table row when an async backup completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateBackupTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n var filenamecell = cellsiblings[0];\n var filename = $(filenamecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_backup',\n args: {\n 'filename': filename,\n 'contextid': contextid,\n 'backupid': backupid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var context = {\n filename: filename,\n time: timevalue,\n size: response.filesize,\n fileurl: response.fileurl,\n restoreurl: response.restoreurl\n };\n\n Templates.render('core/async_backup_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update restore table row when an async restore completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateRestoreTableRow(backupid) {\n var statuscell = $('#' + backupid + '_bar').parent().parent();\n var tablerow = statuscell.parent();\n var cellsiblings = statuscell.siblings();\n var coursecell = cellsiblings[0];\n var timecell = cellsiblings[1];\n var timevalue = $(timecell).text();\n\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n // We have the data now update the UI.\n var resourcename = $(coursecell).text();\n var context = {\n resourcename: resourcename,\n restoreurl: response.restoreurl,\n time: timevalue\n };\n\n Templates.render('core/async_restore_progress_row', context).then(function(html, js) {\n Templates.replaceNodeContents(tablerow, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table row'));\n return;\n });\n });\n }\n\n /**\n * Update copy table row when an course copy completes.\n *\n * @param {string} backupid The id to match elements on.\n */\n function updateCopyTableRow(backupid) {\n var elementbar = document.querySelectorAll(\"[data-restoreid=\" + CSS.escape(backupid) + \"]\")[0];\n var restorecourse = elementbar.closest('tr').children[1];\n var coursename = restorecourse.innerHTML;\n var courselink = document.createElement('a');\n var elementbarparent = elementbar.closest('td');\n var operation = elementbarparent.previousElementSibling;\n\n // Replace the prgress bar.\n Str.get_string('complete').then(function(content) {\n operation.innerHTML = content;\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: complete'));\n return;\n });\n\n Templates.render('core/async_copy_complete_cell', {}).then(function(html, js) {\n Templates.replaceNodeContents(elementbarparent, html, js);\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to load table cell'));\n return;\n });\n\n // Update the destination course name to a link to that course.\n ajax.call([{\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': 0\n },\n }])[0].done(function(response) {\n courselink.setAttribute('href', response.restoreurl);\n courselink.innerHTML = coursename;\n restorecourse.innerHTML = null;\n restorecourse.appendChild(courselink);\n\n return;\n }).fail(function() {\n notification.exception(new Error('Failed to update table row'));\n return;\n });\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * the backup process.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgress(progress) {\n var percentage = progress.progress * 100;\n var type = 'backup';\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n var elementstatus = $('#' + backupid + '_status');\n var elementdetail = $('#' + backupid + '_detail');\n var elementbutton = $('#' + backupid + '_button');\n var stringRequests;\n\n if (progress.status == STATUS_EXECUTING) {\n // Process is in progress.\n // Add in progress class color to bar.\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n // Change heading.\n var strProcessing = 'async' + typeid + 'processing';\n Str.get_string(strProcessing, 'backup').then(function(title) {\n elementstatus.text(title);\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strProcessing));\n });\n\n } else if (progress.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar.\n elementbar.classList.add('bg-danger');\n\n // Remove in progress class color to bar.\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n // Change heading and text.\n var strStatus = 'async' + typeid + 'error';\n var strStatusDetail = 'async' + typeid + 'errordetail';\n stringRequests = [\n {key: strStatus, component: 'backup'},\n {key: strStatusDetail, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementstatus.text(strings[0]);\n elementdetail.text(strings[1]);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n\n } else if (progress.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in progress class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, 100);\n\n // Change heading and text\n var strComplete = 'async' + typeid + 'complete';\n Str.get_string(strComplete, 'backup').then(function(title) {\n elementstatus.text(title);\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: backup ' + strComplete));\n });\n\n if (typeid == 'restore') {\n ajax.call([{\n // Get the table data via webservice.\n methodname: 'core_backup_get_async_backup_links_restore',\n args: {\n 'backupid': backupid,\n 'contextid': contextid\n },\n }])[0].done(function(response) {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n var stringRequests = [\n {key: strDetail, component: 'backup', param: response.restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', response.restoreurl);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n });\n } else {\n var strDetail = 'async' + typeid + 'completedetail';\n var strButton = 'async' + typeid + 'completebutton';\n stringRequests = [\n {key: strDetail, component: 'backup', param: restoreurl},\n {key: strButton, component: 'backup'}\n ];\n Str.get_strings(stringRequests).then(function(strings) {\n elementdetail.html(strings[0]);\n elementbutton.text(strings[1]);\n elementbutton.attr('href', restoreurl);\n\n return;\n })\n .catch(function() {\n notification.exception(new Error('Failed to load string'));\n return;\n });\n\n }\n\n $('.backup_progress').children('span').removeClass('backup_stage_current');\n $('.backup_progress').children('span').last().addClass('backup_stage_current');\n\n // Stop checking when we either have an error or a completion.\n clearInterval(backupintervalid);\n }\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * all the pending processes for backup and restore operations.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgressAll(progress) {\n progress.forEach(function(element) {\n var percentage = element.progress * 100;\n var backupid = element.backupid;\n var type = element.operation;\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n\n if (element.status == STATUS_EXECUTING) {\n // Process is in element.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n } else if (element.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.classList.add('bg-danger');\n elementbar.classList.add('complete');\n\n // Remove in element class color to bar\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n } else if (element.status == STATUS_FINISHED_OK) {\n // Process completed successfully.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n elementbar.classList.add('complete');\n\n updateElement(backupid, type, 100);\n\n // We have a successful backup. Update the UI with download and file details.\n if (type == 'backup') {\n updateBackupTableRow(backupid);\n } else {\n updateRestoreTableRow(backupid);\n }\n\n }\n\n });\n }\n\n /**\n * Update the Moodle user interface with the progress of\n * all the pending processes for copy operations.\n *\n * @param {object} progress The progress and status of the process.\n */\n function updateProgressCopy(progress) {\n progress.forEach(function(element) {\n var percentage = element.progress * 100;\n var backupid = element.backupid;\n var type = element.operation;\n var elementbar = document.querySelectorAll(\"[data-\" + type + \"id=\" + CSS.escape(backupid) + \"]\")[0];\n\n if (type == 'restore') {\n let restorecell = elementbar.closest('tr').children[3];\n Str.get_string('restore').then(function(content) {\n restorecell.innerHTML = content;\n return;\n }).catch(function() {\n notification.exception(new Error('Failed to load string: restore'));\n });\n }\n\n if (element.status == STATUS_EXECUTING) {\n // Process is in element.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n\n updateElement(backupid, type, percentage);\n\n } else if (element.status == STATUS_FINISHED_ERR) {\n // Process completed with error.\n\n // Add in fail class color to bar\n elementbar.classList.add('bg-danger');\n elementbar.classList.add('complete');\n\n // Remove in element class color to bar\n elementbar.classList.remove('bg-success');\n\n updateElement(backupid, type, 100);\n\n } else if ((element.status == STATUS_FINISHED_OK) && (type == 'restore')) {\n // Process completed successfully.\n\n // Add in element class color to bar\n elementbar.classList.add('bg-success');\n elementbar.classList.add('complete');\n\n updateElement(backupid, type, 100);\n\n // We have a successful copy. Update the UI link to copied course.\n updateCopyTableRow(backupid);\n }\n\n });\n }\n\n /**\n * Get the progress of the backup process via ajax.\n */\n function getBackupProgress() {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': [backupid],\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n // We have the progress now update the UI.\n updateProgress(response[0]);\n checkdelay = checkdelayoriginal;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n backupintervalid = updateInterval(backupintervalid, getBackupProgress, checkdelay);\n });\n }\n\n /**\n * Get the progress of all backup processes via ajax.\n */\n function getAllBackupProgress() {\n var backupids = [];\n var progressbars = $('.progress').find('.progress-bar').not('.complete');\n\n progressbars.each(function() {\n backupids.push((this.id).substring(0, 32));\n });\n\n if (backupids.length > 0) {\n ajax.call([{\n // Get the backup progress via webservice.\n methodname: 'core_backup_get_async_backup_progress',\n args: {\n 'backupids': backupids,\n 'contextid': contextid\n },\n }], true, true, false, timeout)[0].done(function(response) {\n updateProgressAll(response);\n checkdelay = checkdelayoriginal;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n allbackupintervalid = updateInterval(allbackupintervalid, getAllBackupProgress, checkdelay);\n });\n } else {\n clearInterval(allbackupintervalid); // No more progress bars to update, stop checking.\n }\n }\n\n /**\n * Get the progress of all copy processes via ajax.\n */\n function getAllCopyProgress() {\n var copyids = [];\n var progressbars = $('.progress').find('.progress-bar[data-operation][data-backupid][data-restoreid]').not('.complete');\n\n progressbars.each(function() {\n let progressvars = {\n 'backupid': this.dataset.backupid,\n 'restoreid': this.dataset.restoreid,\n 'operation': this.dataset.operation,\n };\n copyids.push(progressvars);\n });\n\n if (copyids.length > 0) {\n ajax.call([{\n // Get the copy progress via webservice.\n methodname: 'core_backup_get_copy_progress',\n args: {\n 'copies': copyids\n },\n }], true, true, false, timeout)[0].done(function(response) {\n updateProgressCopy(response);\n checkdelay = checkdelayoriginal;\n allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelayoriginal);\n }).fail(function() {\n checkdelay = checkdelay * checkdelaymultipler;\n allcopyintervalid = updateInterval(allcopyintervalid, getAllCopyProgress, checkdelay);\n });\n } else {\n clearInterval(allcopyintervalid); // No more progress bars to update, stop checking.\n }\n }\n\n /**\n * Get status updates for all backups.\n *\n * @public\n * @param {number} context The context id.\n */\n Asyncbackup.asyncBackupAllStatus = function(context) {\n contextid = context;\n allbackupintervalid = setInterval(getAllBackupProgress, checkdelay);\n };\n\n /**\n * Get status updates for all course copies.\n *\n * @public\n */\n Asyncbackup.asyncCopyAllStatus = function() {\n allcopyintervalid = setInterval(getAllCopyProgress, checkdelay);\n };\n\n /**\n * Get status updates for backup.\n *\n * @public\n * @param {string} backup The backup record id.\n * @param {number} context The context id.\n * @param {string} restore The restore link.\n * @param {string} type The operation type (backup or restore).\n */\n Asyncbackup.asyncBackupStatus = function(backup, context, restore, type) {\n backupid = backup;\n contextid = context;\n restoreurl = restore;\n\n if (type == 'backup') {\n typeid = 'backup';\n } else {\n typeid = 'restore';\n }\n\n // Remove the links from the progress bar, no going back now.\n $('.backup_progress').children('a').removeAttr('href');\n\n // Periodically check for progress updates and update the UI as required.\n backupintervalid = setInterval(getBackupProgress, checkdelay);\n\n };\n\n return Asyncbackup;\n});\n"],"names":["define","$","ajax","Str","notification","Templates","backupid","contextid","restoreurl","typeid","backupintervalid","allbackupintervalid","allcopyintervalid","Asyncbackup","checkdelay","updateElement","type","percentage","percentagewidth","Math","round","elementbar","document","querySelectorAll","CSS","escape","percentagetext","toFixed","setAttribute","style","width","innerHTML","updateInterval","intervalid","callback","value","clearInterval","setInterval","updateProgressAll","progress","forEach","element","operation","status","classList","add","remove","statuscell","parent","tablerow","cellsiblings","siblings","timecell","timevalue","text","filenamecell","filename","call","methodname","args","done","response","context","time","size","filesize","fileurl","render","then","html","js","replaceNodeContents","fail","exception","Error","updateBackupTableRow","coursecell","resourcename","updateRestoreTableRow","updateProgressCopy","restorecell","closest","children","get_string","content","catch","restorecourse","coursename","courselink","createElement","elementbarparent","previousElementSibling","appendChild","updateCopyTableRow","getBackupProgress","stringRequests","elementstatus","elementdetail","elementbutton","strProcessing","title","key","component","get_strings","strings","removeClass","last","addClass","strComplete","strButton","param","attr","updateProgress","getAllBackupProgress","backupids","find","not","each","push","this","id","substring","length","getAllCopyProgress","copyids","progressvars","dataset","restoreid","asyncBackupAllStatus","asyncCopyAllStatus","asyncBackupStatus","backup","restore","removeAttr"],"mappings":";;;;;;;;;AAwBAA,kCAAO,CAAC,SAAU,YAAa,WAAY,oBAAqB,mBACxD,SAASC,EAAGC,KAAMC,IAAKC,aAAcC,eAkBrCC,SACAC,UACAC,WACAC,OACAC,iBACAC,oBACAC,kBAVAC,YAAc,GAEdC,WAAa,cAkBRC,cAAcT,SAAUU,KAAMC,gBAC/BC,gBAAkBC,KAAKC,MAAMH,YAAc,IAC3CI,WAAaC,SAASC,iBAAiB,SAAWP,KAAO,MAAQQ,IAAIC,OAAOnB,UAAY,KAAK,GAC7FoB,eAAiBT,WAAWU,QAAQ,GAAK,IAG7CN,WAAWO,aAAa,gBAAiBV,iBACzCG,WAAWQ,MAAMC,MAAQZ,gBACzBG,WAAWU,UAAYL,wBAWlBM,eAAeC,WAAYC,SAAUC,cAC1CC,cAAcH,YACPI,YAAYH,SAAUC,gBAwRxBG,kBAAkBC,UACvBA,SAASC,SAAQ,SAASC,aAClBxB,WAAgC,IAAnBwB,QAAQF,SACrBjC,SAAWmC,QAAQnC,SACnBU,KAAOyB,QAAQC,UACfrB,WAAaC,SAASC,iBAAiB,SAAWP,KAAO,MAAQQ,IAAIC,OAAOnB,UAAY,KAAK,GA7UlF,KA+UXmC,QAAQE,QAIRtB,WAAWuB,UAAUC,IAAI,cAEzB9B,cAAcT,SAAUU,KAAMC,aApVhB,KAsVPwB,QAAQE,QAIftB,WAAWuB,UAAUC,IAAI,aACzBxB,WAAWuB,UAAUC,IAAI,YAGzBxB,WAAWuB,UAAUE,OAAO,cAE5B/B,cAAcT,SAAUU,KAAM,MA/VjB,KAiWNyB,QAAQE,SAIftB,WAAWuB,UAAUC,IAAI,cACzBxB,WAAWuB,UAAUC,IAAI,YAEzB9B,cAAcT,SAAUU,KAAM,KAGlB,UAARA,cArTcV,cACtByC,WAAa9C,EAAE,IAAMK,SAAW,QAAQ0C,SAASA,SACjDC,SAAWF,WAAWC,SACtBE,aAAeH,WAAWI,WAC1BC,SAAWF,aAAa,GACxBG,UAAYpD,EAAEmD,UAAUE,OACxBC,aAAeL,aAAa,GAC5BM,SAAWvD,EAAEsD,cAAcD,OAE/BpD,KAAKuD,KAAK,CAAC,CAEPC,WAAY,4CACZC,KAAM,UACUH,mBACCjD,mBACDD,aAEhB,GAAGsD,MAAK,SAASC,cAEbC,QAAU,CACNN,SAAUA,SACVO,KAAMV,UACNW,KAAMH,SAASI,SACfC,QAASL,SAASK,QAClB1D,WAAYqD,SAASrD,YAG7BH,UAAU8D,OAAO,iCAAkCL,SAASM,MAAK,SAASC,KAAMC,IAC5EjE,UAAUkE,oBAAoBtB,SAAUoB,KAAMC,OAE/CE,MAAK,WACJpE,aAAaqE,UAAU,IAAIC,MAAM,mCAuR7BC,CAAqBrE,mBA5QNA,cACvByC,WAAa9C,EAAE,IAAMK,SAAW,QAAQ0C,SAASA,SACjDC,SAAWF,WAAWC,SACtBE,aAAeH,WAAWI,WAC1ByB,WAAa1B,aAAa,GAC1BE,SAAWF,aAAa,GACxBG,UAAYpD,EAAEmD,UAAUE,OAE5BpD,KAAKuD,KAAK,CAAC,CAEPC,WAAY,6CACZC,KAAM,UACUrD,mBACCC,cAEjB,GAAGqD,MAAK,SAASC,cAGbC,QAAU,CACNe,aAFW5E,EAAE2E,YAAYtB,OAGzB9C,WAAYqD,SAASrD,WACrBuD,KAAMV,WAGdhD,UAAU8D,OAAO,kCAAmCL,SAASM,MAAK,SAASC,KAAMC,IAC7EjE,UAAUkE,oBAAoBtB,SAAUoB,KAAMC,OAE/CE,MAAK,WACJpE,aAAaqE,UAAU,IAAIC,MAAM,mCAkP7BI,CAAsBxE,uBAc7ByE,mBAAmBxC,UACxBA,SAASC,SAAQ,SAASC,aAClBxB,WAAgC,IAAnBwB,QAAQF,SACrBjC,SAAWmC,QAAQnC,SACnBU,KAAOyB,QAAQC,UACfrB,WAAaC,SAASC,iBAAiB,SAAWP,KAAO,MAAQQ,IAAIC,OAAOnB,UAAY,KAAK,MAErF,WAARU,KAAmB,KACdgE,YAAc3D,WAAW4D,QAAQ,MAAMC,SAAS,GACpD/E,IAAIgF,WAAW,WAAWf,MAAK,SAASgB,SACpCJ,YAAYjD,UAAYqD,WAEzBC,OAAM,WACLjF,aAAaqE,UAAU,IAAIC,MAAM,sCA3Y3B,KA+YXjC,QAAQE,QAIRtB,WAAWuB,UAAUC,IAAI,cAEzB9B,cAAcT,SAAUU,KAAMC,aApZhB,KAsZPwB,QAAQE,QAIftB,WAAWuB,UAAUC,IAAI,aACzBxB,WAAWuB,UAAUC,IAAI,YAGzBxB,WAAWuB,UAAUE,OAAO,cAE5B/B,cAAcT,SAAUU,KAAM,MA/ZjB,KAiaLyB,QAAQE,QAA0C,WAAR3B,OAIlDK,WAAWuB,UAAUC,IAAI,cACzBxB,WAAWuB,UAAUC,IAAI,YAEzB9B,cAAcT,SAAUU,KAAM,cAjSdV,cACpBe,WAAaC,SAASC,iBAAiB,mBAAqBC,IAAIC,OAAOnB,UAAY,KAAK,GACxFgF,cAAgBjE,WAAW4D,QAAQ,MAAMC,SAAS,GAClDK,WAAaD,cAAcvD,UAC3ByD,WAAalE,SAASmE,cAAc,KACpCC,iBAAmBrE,WAAW4D,QAAQ,MACtCvC,UAAYgD,iBAAiBC,uBAGjCxF,IAAIgF,WAAW,YAAYf,MAAK,SAASgB,SACrC1C,UAAUX,UAAYqD,WAEvBC,OAAM,WACLjF,aAAaqE,UAAU,IAAIC,MAAM,uCAIrCrE,UAAU8D,OAAO,gCAAiC,IAAIC,MAAK,SAASC,KAAMC,IACtEjE,UAAUkE,oBAAoBmB,iBAAkBrB,KAAMC,OAEvDE,MAAK,WACJpE,aAAaqE,UAAU,IAAIC,MAAM,iCAKrCxE,KAAKuD,KAAK,CAAC,CACPC,WAAY,6CACZC,KAAM,UACUrD,mBACC,MAEjB,GAAGsD,MAAK,SAASC,UACjB2B,WAAW5D,aAAa,OAAQiC,SAASrD,YACzCgF,WAAWzD,UAAYwD,WACvBD,cAAcvD,UAAY,KAC1BuD,cAAcM,YAAYJ,eAG3BhB,MAAK,WACJpE,aAAaqE,UAAU,IAAIC,MAAM,kCA4P7BmB,CAAmBvF,uBAStBwF,oBACL5F,KAAKuD,KAAK,CAAC,CAEPC,WAAY,wCACZC,KAAM,WACW,CAACrD,oBACDC,cAEjB,GAAM,GAAM,EA5aN,KA4asB,GAAGqD,MAAK,SAASC,oBAlQ7BtB,cAOhBwD,eANA9E,WAAiC,IAApBsB,SAASA,SACtBvB,KAAO,SACPK,WAAaC,SAASC,iBAAiB,kBAA0BC,IAAIC,OAAOnB,UAAY,KAAK,GAC7F0F,cAAgB/F,EAAE,IAAMK,SAAW,WACnC2F,cAAgBhG,EAAE,IAAMK,SAAW,WACnC4F,cAAgBjG,EAAE,IAAMK,SAAW,cAlMpB,KAqMfiC,SAASI,OAA4B,CAGrCtB,WAAWuB,UAAUC,IAAI,cAEzB9B,cAAcT,SAAUU,KAAMC,gBAG1BkF,cAAgB,QAAU1F,OAAS,aACvCN,IAAIgF,WAAWgB,cAAe,UAAU/B,MAAK,SAASgC,OAClDJ,cAAc1C,KAAK8C,UAEpBf,OAAM,WACLjF,aAAaqE,UAAU,IAAIC,MAAM,iCAAmCyB,wBAGrE,GApNe,KAoNX5D,SAASI,OAIhBtB,WAAWuB,UAAUC,IAAI,aAGzBxB,WAAWuB,UAAUE,OAAO,cAE5B/B,cAAcT,SAAUU,KAAM,KAK9B+E,eAAiB,CACb,CAACM,IAHW,QAAU5F,OAAS,QAGd6F,UAAW,UAC5B,CAACD,IAHiB,QAAU5F,OAAS,cAGd6F,UAAW,WAEtCnG,IAAIoG,YAAYR,gBAAgB3B,MAAK,SAASoC,SAC1CR,cAAc1C,KAAKkD,QAAQ,IAC3BP,cAAc3C,KAAKkD,QAAQ,OAI9BnB,OAAM,WACHjF,aAAaqE,UAAU,IAAIC,MAAM,6BAIrCzE,EAAE,oBAAoBiF,SAAS,QAAQuB,YAAY,wBACnDxG,EAAE,oBAAoBiF,SAAS,QAAQwB,OAAOC,SAAS,wBAGvDvE,cAAc1B,uBAEX,GAtPc,KAsPV6B,SAASI,OAA8B,CAI9CtB,WAAWuB,UAAUC,IAAI,cAEzB9B,cAAcT,SAAUU,KAAM,SAG1B4F,YAAc,QAAUnG,OAAS,WACrCN,IAAIgF,WAAWyB,YAAa,UAAUxC,MAAK,SAASgC,OAChDJ,cAAc1C,KAAK8C,UAEpBf,OAAM,WACLjF,aAAaqE,UAAU,IAAIC,MAAM,iCAAmCkC,iBAG1D,WAAVnG,OACAP,KAAKuD,KAAK,CAAC,CAEPC,WAAY,6CACZC,KAAM,UACUrD,mBACCC,cAEjB,GAAGqD,MAAK,SAASC,cAEbgD,UAAY,QAAUpG,OAAS,iBAC/BsF,eAAiB,CACjB,CAACM,IAHW,QAAU5F,OAAS,iBAGd6F,UAAW,SAAUQ,MAAOjD,SAASrD,YACtD,CAAC6F,IAAKQ,UAAWP,UAAW,WAEhCnG,IAAIoG,YAAYR,gBAAgB3B,MAAK,SAASoC,SAC1CP,cAAc5B,KAAKmC,QAAQ,IAC3BN,cAAc5C,KAAKkD,QAAQ,IAC3BN,cAAca,KAAK,OAAQlD,SAASrD,eAIvC6E,OAAM,WACHjF,aAAaqE,UAAU,IAAIC,MAAM,iCAQzCqB,eAAiB,CACb,CAACM,IAHW,QAAU5F,OAAS,iBAGd6F,UAAW,SAAUQ,MAAOtG,YAC7C,CAAC6F,IAHW,QAAU5F,OAAS,iBAGd6F,UAAW,WAEhCnG,IAAIoG,YAAYR,gBAAgB3B,MAAK,SAASoC,SAC1CP,cAAc5B,KAAKmC,QAAQ,IAC3BN,cAAc5C,KAAKkD,QAAQ,IAC3BN,cAAca,KAAK,OAAQvG,eAI9B6E,OAAM,WACHjF,aAAaqE,UAAU,IAAIC,MAAM,8BAMzCzE,EAAE,oBAAoBiF,SAAS,QAAQuB,YAAY,wBACnDxG,EAAE,oBAAoBiF,SAAS,QAAQwB,OAAOC,SAAS,wBAGvDvE,cAAc1B,mBAkIdsG,CAAenD,SAAS,IACxB/C,WAzbiB,KA0bjBJ,iBAAmBsB,eAAetB,iBAAkBoF,kBA1bnC,SA2blBtB,MAAK,WAEJ9D,iBAAmBsB,eAAetB,iBAAkBoF,kBADpDhF,YA1bkB,iBAkcjBmG,2BACDC,UAAY,GACGjH,EAAE,aAAakH,KAAK,iBAAiBC,IAAI,aAE/CC,MAAK,WACdH,UAAUI,KAAMC,KAAKC,GAAIC,UAAU,EAAG,QAGtCP,UAAUQ,OAAS,EACnBxH,KAAKuD,KAAK,CAAC,CAEPC,WAAY,wCACZC,KAAM,WACWuD,oBACA3G,cAEjB,GAAM,GAAM,EA1cV,KA0c0B,GAAGqD,MAAK,SAASC,UAC7CvB,kBAAkBuB,UAClB/C,WAtda,KAudbH,oBAAsBqB,eAAerB,oBAAqBsG,qBAvd7C,SAwddzC,MAAK,WAEJ7D,oBAAsBqB,eAAerB,oBAAqBsG,qBAD1DnG,YAvdc,QA2dlBsB,cAAczB,8BAObgH,yBACDC,QAAU,GACK3H,EAAE,aAAakH,KAAK,gEAAgEC,IAAI,aAE9FC,MAAK,eACVQ,aAAe,UACCN,KAAKO,QAAQxH,mBACZiH,KAAKO,QAAQC,oBACbR,KAAKO,QAAQpF,WAElCkF,QAAQN,KAAKO,iBAGbD,QAAQF,OAAS,EACjBxH,KAAKuD,KAAK,CAAC,CAEPC,WAAY,gCACZC,KAAM,QACQiE,YAEd,GAAM,GAAM,EA9eV,KA8e0B,GAAGhE,MAAK,SAASC,UAC7CkB,mBAAmBlB,UACnB/C,WA1fa,KA2fbF,kBAAoBoB,eAAepB,kBAAmB+G,mBA3fzC,SA4fdnD,MAAK,WAEJ5D,kBAAoBoB,eAAepB,kBAAmB+G,mBADtD7G,YA3fc,QA+flBsB,cAAcxB,0BAUtBC,YAAYmH,qBAAuB,SAASlE,SACxCvD,UAAYuD,QACZnD,oBAAsB0B,YAAY4E,qBAAsBnG,aAQ5DD,YAAYoH,mBAAqB,WAC7BrH,kBAAoByB,YAAYsF,mBAAoB7G,aAYxDD,YAAYqH,kBAAoB,SAASC,OAAQrE,QAASsE,QAASpH,MAC/DV,SAAW6H,OACX5H,UAAYuD,QACZtD,WAAa4H,QAGT3H,OADQ,UAARO,KACS,SAEA,UAIbf,EAAE,oBAAoBiF,SAAS,KAAKmD,WAAW,QAG/C3H,iBAAmB2B,YAAYyD,kBAAmBhF,aAI7CD"} util/ui/amd/build/async_backup.min.js 0000644 00000024017 15215711721 0013566 0 ustar 00 /** * This module updates the UI during an asynchronous * backup or restore process. * * @module core_backup/async_backup * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.7 */ define("core_backup/async_backup",["jquery","core/ajax","core/str","core/notification","core/templates"],(function($,ajax,Str,notification,Templates){var backupid,contextid,restoreurl,typeid,backupintervalid,allbackupintervalid,allcopyintervalid,Asyncbackup={},checkdelay=15e3;function updateElement(backupid,type,percentage){var percentagewidth=Math.round(percentage)+"%",elementbar=document.querySelectorAll("[data-"+type+"id="+CSS.escape(backupid)+"]")[0],percentagetext=percentage.toFixed(2)+"%";elementbar.setAttribute("aria-valuenow",percentagewidth),elementbar.style.width=percentagewidth,elementbar.innerHTML=percentagetext}function updateInterval(intervalid,callback,value){return clearInterval(intervalid),setInterval(callback,value)}function updateProgressAll(progress){progress.forEach((function(element){var percentage=100*element.progress,backupid=element.backupid,type=element.operation,elementbar=document.querySelectorAll("[data-"+type+"id="+CSS.escape(backupid)+"]")[0];800==element.status?(elementbar.classList.add("bg-success"),updateElement(backupid,type,percentage)):900==element.status?(elementbar.classList.add("bg-danger"),elementbar.classList.add("complete"),elementbar.classList.remove("bg-success"),updateElement(backupid,type,100)):1e3==element.status&&(elementbar.classList.add("bg-success"),elementbar.classList.add("complete"),updateElement(backupid,type,100),"backup"==type?function(backupid){var statuscell=$("#"+backupid+"_bar").parent().parent(),tablerow=statuscell.parent(),cellsiblings=statuscell.siblings(),timecell=cellsiblings[1],timevalue=$(timecell).text(),filenamecell=cellsiblings[0],filename=$(filenamecell).text();ajax.call([{methodname:"core_backup_get_async_backup_links_backup",args:{filename:filename,contextid:contextid,backupid:backupid}}])[0].done((function(response){var context={filename:filename,time:timevalue,size:response.filesize,fileurl:response.fileurl,restoreurl:response.restoreurl};Templates.render("core/async_backup_progress_row",context).then((function(html,js){Templates.replaceNodeContents(tablerow,html,js)})).fail((function(){notification.exception(new Error("Failed to load table row"))}))}))}(backupid):function(backupid){var statuscell=$("#"+backupid+"_bar").parent().parent(),tablerow=statuscell.parent(),cellsiblings=statuscell.siblings(),coursecell=cellsiblings[0],timecell=cellsiblings[1],timevalue=$(timecell).text();ajax.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:backupid,contextid:contextid}}])[0].done((function(response){var context={resourcename:$(coursecell).text(),restoreurl:response.restoreurl,time:timevalue};Templates.render("core/async_restore_progress_row",context).then((function(html,js){Templates.replaceNodeContents(tablerow,html,js)})).fail((function(){notification.exception(new Error("Failed to load table row"))}))}))}(backupid))}))}function updateProgressCopy(progress){progress.forEach((function(element){var percentage=100*element.progress,backupid=element.backupid,type=element.operation,elementbar=document.querySelectorAll("[data-"+type+"id="+CSS.escape(backupid)+"]")[0];if("restore"==type){let restorecell=elementbar.closest("tr").children[3];Str.get_string("restore").then((function(content){restorecell.innerHTML=content})).catch((function(){notification.exception(new Error("Failed to load string: restore"))}))}800==element.status?(elementbar.classList.add("bg-success"),updateElement(backupid,type,percentage)):900==element.status?(elementbar.classList.add("bg-danger"),elementbar.classList.add("complete"),elementbar.classList.remove("bg-success"),updateElement(backupid,type,100)):1e3==element.status&&"restore"==type&&(elementbar.classList.add("bg-success"),elementbar.classList.add("complete"),updateElement(backupid,type,100),function(backupid){var elementbar=document.querySelectorAll("[data-restoreid="+CSS.escape(backupid)+"]")[0],restorecourse=elementbar.closest("tr").children[1],coursename=restorecourse.innerHTML,courselink=document.createElement("a"),elementbarparent=elementbar.closest("td"),operation=elementbarparent.previousElementSibling;Str.get_string("complete").then((function(content){operation.innerHTML=content})).catch((function(){notification.exception(new Error("Failed to load string: complete"))})),Templates.render("core/async_copy_complete_cell",{}).then((function(html,js){Templates.replaceNodeContents(elementbarparent,html,js)})).fail((function(){notification.exception(new Error("Failed to load table cell"))})),ajax.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:backupid,contextid:0}}])[0].done((function(response){courselink.setAttribute("href",response.restoreurl),courselink.innerHTML=coursename,restorecourse.innerHTML=null,restorecourse.appendChild(courselink)})).fail((function(){notification.exception(new Error("Failed to update table row"))}))}(backupid))}))}function getBackupProgress(){ajax.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:[backupid],contextid:contextid}}],!0,!0,!1,2e3)[0].done((function(response){!function(progress){var stringRequests,percentage=100*progress.progress,type="backup",elementbar=document.querySelectorAll("[data-backupid="+CSS.escape(backupid)+"]")[0],elementstatus=$("#"+backupid+"_status"),elementdetail=$("#"+backupid+"_detail"),elementbutton=$("#"+backupid+"_button");if(800==progress.status){elementbar.classList.add("bg-success"),updateElement(backupid,type,percentage);var strProcessing="async"+typeid+"processing";Str.get_string(strProcessing,"backup").then((function(title){elementstatus.text(title)})).catch((function(){notification.exception(new Error("Failed to load string: backup "+strProcessing))}))}else if(900==progress.status)elementbar.classList.add("bg-danger"),elementbar.classList.remove("bg-success"),updateElement(backupid,type,100),stringRequests=[{key:"async"+typeid+"error",component:"backup"},{key:"async"+typeid+"errordetail",component:"backup"}],Str.get_strings(stringRequests).then((function(strings){elementstatus.text(strings[0]),elementdetail.text(strings[1])})).catch((function(){notification.exception(new Error("Failed to load string"))})),$(".backup_progress").children("span").removeClass("backup_stage_current"),$(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(backupintervalid);else if(1e3==progress.status){elementbar.classList.add("bg-success"),updateElement(backupid,type,100);var strComplete="async"+typeid+"complete";Str.get_string(strComplete,"backup").then((function(title){elementstatus.text(title)})).catch((function(){notification.exception(new Error("Failed to load string: backup "+strComplete))})),"restore"==typeid?ajax.call([{methodname:"core_backup_get_async_backup_links_restore",args:{backupid:backupid,contextid:contextid}}])[0].done((function(response){var strButton="async"+typeid+"completebutton",stringRequests=[{key:"async"+typeid+"completedetail",component:"backup",param:response.restoreurl},{key:strButton,component:"backup"}];Str.get_strings(stringRequests).then((function(strings){elementdetail.html(strings[0]),elementbutton.text(strings[1]),elementbutton.attr("href",response.restoreurl)})).catch((function(){notification.exception(new Error("Failed to load string"))}))})):(stringRequests=[{key:"async"+typeid+"completedetail",component:"backup",param:restoreurl},{key:"async"+typeid+"completebutton",component:"backup"}],Str.get_strings(stringRequests).then((function(strings){elementdetail.html(strings[0]),elementbutton.text(strings[1]),elementbutton.attr("href",restoreurl)})).catch((function(){notification.exception(new Error("Failed to load string"))}))),$(".backup_progress").children("span").removeClass("backup_stage_current"),$(".backup_progress").children("span").last().addClass("backup_stage_current"),clearInterval(backupintervalid)}}(response[0]),checkdelay=15e3,backupintervalid=updateInterval(backupintervalid,getBackupProgress,15e3)})).fail((function(){backupintervalid=updateInterval(backupintervalid,getBackupProgress,checkdelay*=1.5)}))}function getAllBackupProgress(){var backupids=[];$(".progress").find(".progress-bar").not(".complete").each((function(){backupids.push(this.id.substring(0,32))})),backupids.length>0?ajax.call([{methodname:"core_backup_get_async_backup_progress",args:{backupids:backupids,contextid:contextid}}],!0,!0,!1,2e3)[0].done((function(response){updateProgressAll(response),checkdelay=15e3,allbackupintervalid=updateInterval(allbackupintervalid,getAllBackupProgress,15e3)})).fail((function(){allbackupintervalid=updateInterval(allbackupintervalid,getAllBackupProgress,checkdelay*=1.5)})):clearInterval(allbackupintervalid)}function getAllCopyProgress(){var copyids=[];$(".progress").find(".progress-bar[data-operation][data-backupid][data-restoreid]").not(".complete").each((function(){let progressvars={backupid:this.dataset.backupid,restoreid:this.dataset.restoreid,operation:this.dataset.operation};copyids.push(progressvars)})),copyids.length>0?ajax.call([{methodname:"core_backup_get_copy_progress",args:{copies:copyids}}],!0,!0,!1,2e3)[0].done((function(response){updateProgressCopy(response),checkdelay=15e3,allcopyintervalid=updateInterval(allcopyintervalid,getAllCopyProgress,15e3)})).fail((function(){allcopyintervalid=updateInterval(allcopyintervalid,getAllCopyProgress,checkdelay*=1.5)})):clearInterval(allcopyintervalid)}return Asyncbackup.asyncBackupAllStatus=function(context){contextid=context,allbackupintervalid=setInterval(getAllBackupProgress,checkdelay)},Asyncbackup.asyncCopyAllStatus=function(){allcopyintervalid=setInterval(getAllCopyProgress,checkdelay)},Asyncbackup.asyncBackupStatus=function(backup,context,restore,type){backupid=backup,contextid=context,restoreurl=restore,typeid="backup"==type?"backup":"restore",$(".backup_progress").children("a").removeAttr("href"),backupintervalid=setInterval(getBackupProgress,checkdelay)},Asyncbackup})); //# sourceMappingURL=async_backup.min.js.map util/ui/amd/build/schema_backup_form.min.js.map 0000644 00000022602 15215711721 0015506 0 ustar 00 {"version":3,"file":"schema_backup_form.min.js","sources":["../src/schema_backup_form.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Schema selector javascript controls.\n *\n * This module controls:\n * - The select all feature.\n * - Disabling activities checkboxes when the section is not selected.\n *\n * @module core_backup/schema_backup_form\n * @copyright 2024 Ferran Recio <ferran@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport * as Templates from 'core/templates';\n\nconst Selectors = {\n action: '[data-mdl-action]',\n checkboxes: '#id_coursesettings input[type=\"checkbox\"]',\n firstSection: 'fieldset#id_coursesettings .fcontainer .grouped_settings.section_level',\n modCheckboxes: (modName) => `setting_activity_${modName}_`,\n};\n\nconst Suffixes = {\n userData: '_userdata',\n userInfo: '_userinfo',\n included: '_included',\n};\n\n/**\n * Adds select all/none links to the top of the backup/restore/import schema page.\n */\nexport default class BackupFormController {\n\n /**\n * Static module init method.\n * @param {Array<string>} modNames - The names of the modules.\n * @returns {BackupFormController}\n */\n static init(modNames) {\n return new BackupFormController(modNames);\n }\n\n /**\n * Creates a new instance of the SchemaBackupForm class.\n * @param {Array<string>} modNames - The names of the modules.\n */\n constructor(modNames) {\n this.modNames = modNames;\n this.scanFormUserData();\n this.addSelectorsToPage();\n }\n\n /**\n * Detect the user data attribute from the form.\n *\n * @private\n */\n scanFormUserData() {\n this.withuserdata = false;\n this.userDataSuffix = Suffixes.userData;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n if (!checkboxes) {\n return;\n }\n // Depending on the form, user data inclusion is called userinfo or userdata.\n for (const checkbox of checkboxes) {\n const name = checkbox.name;\n if (name.endsWith(Suffixes.userData)) {\n this.withuserdata = true;\n break;\n } else if (name.endsWith(Suffixes.userInfo)) {\n this.withuserdata = true;\n this.userDataSuffix = Suffixes.userInfo;\n break;\n }\n }\n }\n\n /**\n * Initializes all related events.\n *\n * @private\n * @param {HTMLElement} element - The element to attach the events to.\n */\n initEvents(element) {\n element.addEventListener('click', (event) => {\n const action = event.target.closest(Selectors.action);\n if (!action) {\n return;\n }\n event.preventDefault();\n\n const suffix = (action.dataset?.mdlType == 'userdata') ? this.userDataSuffix : Suffixes.included;\n\n this.changeSelection(\n action.dataset.mdlAction == 'selectall',\n suffix,\n action.dataset?.mdlMod ?? null\n );\n });\n }\n\n /**\n * Changes the selection according to the params.\n *\n * @private\n * @param {boolean} checked - The checked state for the checkboxes.\n * @param {string} suffix - The checkboxes suffix\n * @param {string} [modName] - The module name.\n */\n changeSelection(checked, suffix, modName) {\n const prefix = modName ? Selectors.modCheckboxes(modName) : null;\n\n let formId;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n for (const checkbox of checkboxes) {\n formId = formId ?? checkbox.closest('form').getAttribute('id');\n\n if (prefix && !checkbox.name.startsWith(prefix)) {\n continue;\n }\n if (checkbox.name.endsWith(suffix)) {\n checkbox.checked = checked;\n }\n }\n\n // At this point, we really need to persuade the form we are part of to\n // update all of its disabledIf rules. However, as far as I can see,\n // given the way that lib/form/form.js is written, that is impossible.\n if (formId && M.form) {\n M.form.updateFormState(formId);\n }\n }\n\n /**\n * Generates the full selectors element to add to the page.\n *\n * @private\n * @returns {HTMLElement} The selectors element.\n */\n generateSelectorsElement() {\n const links = document.createElement('div');\n links.id = 'backup_selectors';\n this.initEvents(links);\n this.renderSelectorsTemplate(links);\n return links;\n }\n\n /**\n * Load the select all template.\n *\n * @private\n * @param {HTMLElement} element the container\n */\n renderSelectorsTemplate(element) {\n const data = {\n modules: this.getModulesTemplateData(),\n withuserdata: (this.withuserdata) ? true : undefined,\n };\n Templates.renderForPromise(\n 'core_backup/formselectall',\n data\n ).then(({html, js}) => {\n return Templates.replaceNodeContents(element, html, js);\n }).catch(Notification.exception);\n }\n\n /**\n * Generate the modules template data.\n *\n * @private\n * @returns {Array} of modules data.\n */\n getModulesTemplateData() {\n const modules = [];\n for (const modName in this.modNames) {\n if (!this.modNames.hasOwnProperty(modName)) {\n continue;\n }\n modules.push({\n modname: modName,\n heading: this.modNames[modName],\n });\n }\n return modules;\n }\n\n /**\n * Adds select all/none functionality to the backup form.\n *\n * @private\n */\n addSelectorsToPage() {\n const firstSection = document.querySelector(Selectors.firstSection);\n if (!firstSection) {\n // This is not a relevant page.\n return;\n }\n if (!firstSection.querySelector(Selectors.checkboxes)) {\n // No checkboxes.\n return;\n }\n\n // Add global select all/none options.\n const selector = this.generateSelectorsElement();\n firstSection.parentNode.insertBefore(selector, firstSection);\n }\n}\n"],"names":["Selectors","modName","Suffixes","BackupFormController","modNames","constructor","scanFormUserData","addSelectorsToPage","withuserdata","userDataSuffix","checkboxes","document","querySelectorAll","checkbox","name","endsWith","initEvents","element","addEventListener","event","action","target","closest","preventDefault","suffix","dataset","mdlType","this","changeSelection","mdlAction","_action$dataset2","mdlMod","checked","prefix","formId","getAttribute","startsWith","M","form","updateFormState","generateSelectorsElement","links","createElement","id","renderSelectorsTemplate","data","modules","getModulesTemplateData","undefined","Templates","renderForPromise","then","_ref","html","js","replaceNodeContents","catch","Notification","exception","hasOwnProperty","push","modname","heading","firstSection","querySelector","selector","parentNode","insertBefore"],"mappings":";;;;;;;;;;;olCA8BMA,iBACM,oBADNA,qBAEU,4CAFVA,uBAGY,yEAHZA,wBAIcC,oCAAgCA,aAG9CC,kBACQ,YADRA,kBAEQ,YAFRA,kBAGQ,kBAMOC,iCAOLC,iBACD,IAAID,qBAAqBC,UAOpCC,YAAYD,eACHA,SAAWA,cACXE,wBACAC,qBAQTD,wBACSE,cAAe,OACfC,eAAiBP,wBAEhBQ,WAAaC,SAASC,iBAAiBZ,yBACxCU,eAIA,MAAMG,YAAYH,WAAY,OACzBI,KAAOD,SAASC,QAClBA,KAAKC,SAASb,mBAAoB,MAC7BM,cAAe,QAEjB,GAAIM,KAAKC,SAASb,mBAAoB,MACpCM,cAAe,OACfC,eAAiBP,0BAYlCc,WAAWC,SACPA,QAAQC,iBAAiB,SAAUC,yEACzBC,OAASD,MAAME,OAAOC,QAAQtB,sBAC/BoB,cAGLD,MAAMI,uBAEAC,OAAqC,qCAA3BJ,OAAOK,0DAASC,SAAyBC,KAAKlB,eAAiBP,uBAE1E0B,gBAC2B,aAA5BR,OAAOK,QAAQI,UACfL,8DACAJ,OAAOK,2CAAPK,iBAAgBC,8DAAU,SAatCH,gBAAgBI,QAASR,OAAQvB,eACvBgC,OAAShC,QAAUD,wBAAwBC,SAAW,SAExDiC,aAEExB,WAAaC,SAASC,iBAAiBZ,0BACxC,MAAMa,YAAYH,WAAY,aAC/BwB,uBAASA,kCAAUrB,SAASS,QAAQ,QAAQa,aAAa,MAErDF,SAAWpB,SAASC,KAAKsB,WAAWH,SAGpCpB,SAASC,KAAKC,SAASS,UACvBX,SAASmB,QAAUA,SAOvBE,QAAUG,EAAEC,MACZD,EAAEC,KAAKC,gBAAgBL,QAU/BM,iCACUC,MAAQ9B,SAAS+B,cAAc,cACrCD,MAAME,GAAK,wBACN3B,WAAWyB,YACXG,wBAAwBH,OACtBA,MASXG,wBAAwB3B,eACd4B,KAAO,CACTC,QAASnB,KAAKoB,yBACdvC,eAAemB,KAAKnB,mBAAuBwC,GAE/CC,UAAUC,iBACN,4BACAL,MACFM,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACJL,UAAUM,oBAAoBtC,QAASoC,KAAMC,OACrDE,MAAMC,sBAAaC,WAS1BX,+BACUD,QAAU,OACX,MAAM7C,WAAW0B,KAAKvB,SAClBuB,KAAKvB,SAASuD,eAAe1D,UAGlC6C,QAAQc,KAAK,CACTC,QAAS5D,QACT6D,QAASnC,KAAKvB,SAASH,kBAGxB6C,QAQXvC,2BACUwD,aAAepD,SAASqD,cAAchE,4BACvC+D,wBAIAA,aAAaC,cAAchE,mCAM1BiE,SAAWtC,KAAKa,2BACtBuB,aAAaG,WAAWC,aAAaF,SAAUF"} util/ui/restore_ui.class.php 0000644 00000032051 15215711721 0012136 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the restore user interface class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This is the restore user interface class * * The restore user interface class manages the user interface and restore for * Moodle. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui extends base_ui { /** * The stages of the restore user interface. * Confirm the backup you are going to restore. */ const STAGE_CONFIRM = 1; /** * The stages of the restore user interface. * Select the destination for the restore. */ const STAGE_DESTINATION = 2; /** * The stages of the restore user interface. * Alter the setting for the restore. */ const STAGE_SETTINGS = 4; /** * The stages of the restore user interface. * Alter and review the schema that you are going to restore. */ const STAGE_SCHEMA = 8; /** * The stages of the restore user interface. * The final review before the restore is run. */ const STAGE_REVIEW = 16; /** * The stages of the restore user interface. * The restore is in process right now. */ const STAGE_PROCESS = 32; /** * The stages of the restore user interface. * The process is complete. */ const STAGE_COMPLETE = 64; /** * The current UI stage. * @var restore_ui_stage */ protected $stage = null; /** * @var \core\progress\base Progress indicator (where there is no controller) */ protected $progressreporter = null; /** * String mappings to the above stages * @var array */ public static $stages = array( restore_ui::STAGE_CONFIRM => 'confirm', restore_ui::STAGE_DESTINATION => 'destination', restore_ui::STAGE_SETTINGS => 'settings', restore_ui::STAGE_SCHEMA => 'schema', restore_ui::STAGE_REVIEW => 'review', restore_ui::STAGE_PROCESS => 'process', restore_ui::STAGE_COMPLETE => 'complete' ); /** * Intialises what ever stage is requested. If none are requested we check * params for 'stage' and default to initial * * @throws restore_ui_exception for an invalid stage * @param int|null $stage The desired stage to intialise or null for the default * @param array $params * @return restore_ui_stage_initial|restore_ui_stage_schema|restore_ui_stage_confirmation|restore_ui_stage_final */ protected function initialise_stage($stage = null, ?array $params = null) { if ($stage == null) { $stage = optional_param('stage', self::STAGE_CONFIRM, PARAM_INT); } $class = 'restore_ui_stage_'.self::$stages[$stage]; if (!class_exists($class)) { throw new restore_ui_exception('unknownuistage'); } $stage = new $class($this, $params); return $stage; } /** * This processes the current stage of the restore * @throws restore_ui_exception if the progress is wrong. * @return bool */ public function process() { if ($this->progress >= self::PROGRESS_PROCESSED) { throw new restore_ui_exception('restoreuialreadyprocessed'); } $this->progress = self::PROGRESS_PROCESSED; if (optional_param('previous', false, PARAM_BOOL) && $this->stage->get_stage() > self::STAGE_CONFIRM) { $this->stage = $this->initialise_stage($this->stage->get_prev_stage(), $this->stage->get_params()); return false; } // Process the stage. $processoutcome = $this->stage->process(); if ($processoutcome !== false && !($this->get_stage() == self::STAGE_PROCESS && optional_param('substage', false, PARAM_BOOL))) { $this->stage = $this->initialise_stage($this->stage->get_next_stage(), $this->stage->get_params()); } // Process UI event after to check changes are valid. $this->controller->process_ui_event(); return $processoutcome; } /** * Returns true if the stage is independent (not requiring a restore controller) * @return bool */ public function is_independent() { return false; } /** * Gets the unique ID associated with this UI * @return string */ public function get_uniqueid() { return $this->get_restoreid(); } /** * Gets the restore id from the controller * @return string */ public function get_restoreid() { return $this->controller->get_restoreid(); } /** * Gets the progress reporter object in use for this restore UI. * * IMPORTANT: This progress reporter is used only for UI progress that is * outside the restore controller. The restore controller has its own * progress reporter which is used for progress during the main restore. * Use the restore controller's progress reporter to report progress during * a restore operation, not this one. * * This extra reporter is necessary because on some restore UI screens, * there are long-running tasks even though there is no restore controller * in use. * * @return \core\progress\none */ public function get_progress_reporter() { if (!$this->progressreporter) { $this->progressreporter = new \core\progress\none(); } return $this->progressreporter; } /** * Sets the progress reporter that will be returned by get_progress_reporter. * * @param \core\progress\base $progressreporter Progress reporter */ public function set_progress_reporter(\core\progress\base $progressreporter) { $this->progressreporter = $progressreporter; } /** * Executes the restore plan * @throws restore_ui_exception if the progress or stage is wrong. * @return bool */ public function execute() { if ($this->progress >= self::PROGRESS_EXECUTED) { throw new restore_ui_exception('restoreuialreadyexecuted'); } if ($this->stage->get_stage() < self::STAGE_PROCESS) { throw new restore_ui_exception('restoreuifinalisedbeforeexecute'); } $this->controller->execute_plan(); $this->progress = self::PROGRESS_EXECUTED; $this->stage = new restore_ui_stage_complete($this, $this->stage->get_params(), $this->controller->get_results()); return true; } /** * Delete course which is created by restore process */ public function cleanup() { global $DB; $courseid = $this->controller->get_courseid(); if ($this->is_temporary_course_created($courseid) && $course = $DB->get_record('course', array('id' => $courseid))) { $course->deletesource = 'restore'; delete_course($course, false); } } /** * Checks if the course is not restored fully and current controller has created it. * @param int $courseid id of the course which needs to be checked * @return bool */ protected function is_temporary_course_created($courseid) { global $DB; // Check if current controller instance has created new course. if ($this->controller->get_target() == backup::TARGET_NEW_COURSE) { $results = $DB->record_exists_sql("SELECT bc.itemid FROM {backup_controllers} bc, {course} c WHERE bc.operation = 'restore' AND bc.type = 'course' AND bc.itemid = c.id AND bc.itemid = ?", array($courseid) ); return $results; } return false; } /** * Returns true if enforce_dependencies changed any settings * @return bool */ public function enforce_changed_dependencies() { return ($this->dependencychanges > 0); } /** * Loads the restore controller if we are tracking one * @param string|bool $restoreid * @return string */ final public static function load_controller($restoreid = false) { // Get the restore id optional param. if ($restoreid) { try { // Try to load the controller with it. // If it fails at this point it is likely because this is the first load. $controller = restore_controller::load_controller($restoreid); return $controller; } catch (Exception $e) { return false; } } return $restoreid; } /** * Initialised the requested independent stage * * @throws restore_ui_exception * @param int $stage One of self::STAGE_* * @param int $contextid * @return restore_ui_stage_confirm|restore_ui_stage_destination */ final public static function engage_independent_stage($stage, $contextid) { if (!($stage & self::STAGE_CONFIRM + self::STAGE_DESTINATION)) { throw new restore_ui_exception('dependentstagerequested'); } $class = 'restore_ui_stage_'.self::$stages[$stage]; if (!class_exists($class)) { throw new restore_ui_exception('unknownuistage'); } return new $class($contextid); } /** * Cancels the current restore and redirects the user back to the relevant place */ public function cancel_process() { // Delete temporary restore course if exists. if ($this->controller->get_target() == backup::TARGET_NEW_COURSE) { $this->cleanup(); } parent::cancel_process(); } /** * Gets an array of progress bar items that can be displayed through the restore renderer. * @return array Array of items for the progress bar */ public function get_progress_bar() { global $PAGE; $stage = self::STAGE_COMPLETE; $currentstage = $this->stage->get_stage(); $items = array(); while ($stage > 0) { $classes = array('backup_stage'); if (floor($stage / 2) == $currentstage) { $classes[] = 'backup_stage_next'; } else if ($stage == $currentstage) { $classes[] = 'backup_stage_current'; } else if ($stage < $currentstage) { $classes[] = 'backup_stage_complete'; } $item = array('text' => strlen(decbin($stage)).'. '.get_string('restorestage'.$stage, 'backup'), 'class' => join(' ', $classes)); if ($stage < $currentstage && $currentstage < self::STAGE_COMPLETE && $stage > self::STAGE_DESTINATION) { $item['link'] = new moodle_url($PAGE->url, array('restore' => $this->get_restoreid(), 'stage' => $stage)); } array_unshift($items, $item); $stage = floor($stage / 2); } return $items; } /** * Gets the name of this UI * @return string */ public function get_name() { return 'restore'; } /** * Gets the first stage for this UI * @return int STAGE_CONFIRM */ public function get_first_stage_id() { return self::STAGE_CONFIRM; } /** * Returns true if this stage has substages of which at least one needs to be displayed * @return bool */ public function requires_substage() { return ($this->stage->has_sub_stages() && !$this->stage->process()); } /** * Displays this stage * * @throws base_ui_exception if the progress is wrong. * @param core_backup_renderer $renderer * @return string HTML code to echo */ public function display(core_backup_renderer $renderer) { if ($this->progress < self::PROGRESS_SAVED) { throw new base_ui_exception('backupsavebeforedisplay'); } return $this->stage->display($renderer); } } /** * Restore user interface exception. Modelled off the restore_exception class * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_ui_exception extends base_ui_exception {} util/ui/restore_moodleform.class.php 0000644 00000005024 15215711721 0013664 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This file contains the forms used by the restore stages * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * An abstract moodleform class specially designed for the restore forms. * * @abstract Marked abstract here because some idiot forgot to mark it abstract in code! * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_moodleform extends base_moodleform { /** * Constructor. * * Overridden just for the purpose of typehinting the first arg. * * @param restore_ui_stage $uistage * @param null $action * @param null $customdata * @param string $method * @param string $target * @param null $attributes * @param bool $editable */ public function __construct(restore_ui_stage $uistage, $action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true) { parent::__construct($uistage, $action, $customdata, $method, $target, $attributes, $editable); } } /** * Restore settings form. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_settings_form extends restore_moodleform {} /** * Restore schema review form. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_schema_form extends restore_moodleform {} /** * Restore complete process review form. * * @package core_backup * @copyright 2010 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_review_form extends restore_moodleform {}; util/xml/output/xml_output.class.php 0000644 00000013055 15215711721 0013724 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This abstract class outputs XML contents provided by @xml_writer * * Contains the common functionalities for all the xml_output_xxx classes * and the interface for them. Mainly it's in charge of: * - Initialize the corresponding stream/handler (file, DB connection...) * - Finalize the stream/handler * - Provide one common buffer for all output implementations * - Receive XML contents from the @xml_writer and output them * - Some basic throughtput stats * * TODO: Finish phpdocs */ abstract class xml_output { const DEFAULT_BUFFER_SIZE = 4096; // Use a default buffer size of 4K protected $inittime; // Initial microtime protected $sentbytes; // Bytes sent to output protected $usebuffer; // Boolean to specify if output supports buffer (true) or no (false) protected $buffersize;// Size, in bytes, of the buffer. protected $currentbuffer; // Buffer contents protected $currentbuffersize;// Current buffer size protected $running; // To know if output is running /** @var string|float finish microtime. */ protected $finishtime; public function __construct($usebuffer = true) { $this->inittime = microtime(true); $this->finishtime = $this->inittime; $this->sentbytes = 0; $this->usebuffer = $usebuffer; $this->buffersize = $this->usebuffer ? self::DEFAULT_BUFFER_SIZE : 0; $this->running = null; } public function set_buffersize($buffersize) { if ($this->running) { throw new xml_output_exception('xml_output_already_started'); } if (!$this->usebuffer) { throw new xml_output_exception('xml_output_buffer_nosupport'); } // TODO: check it is integer > 0 $this->buffersize = $buffersize; } public function start() { if ($this->running === true) { throw new xml_output_exception('xml_output_already_started'); } if ($this->running === false) { throw new xml_output_exception('xml_output_already_stopped'); } $this->inittime = microtime(true); $this->sentbytes = 0; $this->running = true; $this->currentbuffer = ''; $this->currentbuffersize = 0; $this->init(); } public function stop() { if (!$this->running) { throw new xml_output_exception('xml_output_not_started'); } $this->finishtime = microtime(true); if ($this->usebuffer && $this->currentbuffersize > 0) { // Have pending contents in buffer $this->send($this->currentbuffer); // Send them $this->currentbuffer = ''; $this->currentbuffersize = 0; } $this->running = false; $this->finish(); } /** * Get contents from @xml_writer and buffer/output them */ public function write($content) { if (!$this->running) { throw new xml_output_exception('xml_output_not_started'); } $lenc = strlen($content ?? ''); // Get length in bytes. if ($lenc == 0) { // 0 length contents, nothing to do return; } // Buffer handling if available $tooutput = true; // By default, perform output if ($this->usebuffer) { // Buffer $this->currentbuffer .= $content; $this->currentbuffersize += $lenc; if ($this->currentbuffersize < $this->buffersize) { $tooutput = false; // Still within the buffer, don't output } else { $content = $this->currentbuffer; // Prepare for output $lenc = $this->currentbuffersize; $this->currentbuffer = ''; $this->currentbuffersize = 0; } } // Output if ($tooutput) { $this->send($content); // Efectively send the contents $this->sentbytes += $lenc; } } public function debug_info() { if ($this->running !== false) { throw new xml_output_exception('xml_output_not_stopped'); } return array('memory' => memory_get_peak_usage(true), 'time' => $this->finishtime - $this->inittime, 'sent' => $this->sentbytes); } // Implementable API starts here abstract protected function init(); abstract protected function finish(); abstract protected function send($content); } /* * Exception class used by all the @xml_output stuff */ class xml_output_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); } } util/xml/output/file_xml_output.class.php 0000644 00000004640 15215711721 0014723 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This class implements one @xml_output able to send contents to one OS file * * Buffering enabled by default (can be disabled) * * TODO: Finish phpdocs */ class file_xml_output extends xml_output { protected $fullpath; // Full path to OS file where contents will be stored protected $fhandle; // File handle where all write operations happen public function __construct($fullpath, $usebuffer = true) { $this->fullpath = $fullpath; parent::__construct($usebuffer); } // Private API starts here protected function init() { if (!file_exists(dirname($this->fullpath))) { throw new xml_output_exception('directory_not_exists', dirname($this->fullpath)); } if (file_exists($this->fullpath)) { throw new xml_output_exception('file_already_exists', $this->fullpath); } if (!is_writable(dirname($this->fullpath))) { throw new xml_output_exception('directory_not_writable', dirname($this->fullpath)); } // Open the OS file for writing if (! $this->fhandle = fopen($this->fullpath, 'w')) { throw new xml_output_exception('error_opening_file'); } } protected function finish() { if (false === fclose($this->fhandle)) { throw new xml_output_exception('error_closing_file'); } } protected function send($content) { if (false === fwrite($this->fhandle, $content)) { throw new xml_output_exception('error_writing_file'); } } } util/xml/output/tests/output_test.php 0000644 00000025504 15215711721 0014143 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * xml_output tests (base, memory and file). * * @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 file_xml_output; use memory_xml_output; use xml_output; use xml_output_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php'); /** * xml_output tests (base, memory and file) * * @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 */ final class output_test extends \advanced_testcase { /* * test memory_xml_output */ function test_memory_xml_output(): void { // Instantiate xml_output $xo = new memory_xml_output(); $this->assertTrue($xo instanceof xml_output); // Try to write some contents before starting it $xo = new memory_xml_output(); try { $xo->write('test'); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_started'); } // Try to set buffer size if unsupported $xo = new memory_xml_output(); try { $xo->set_buffersize(8192); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_buffer_nosupport'); } // Try to set buffer after start $xo = new memory_xml_output(); $xo->start(); try { $xo->set_buffersize(8192); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_already_started'); } // Try to stop output before starting it $xo = new memory_xml_output(); try { $xo->stop(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_started'); } // Try to debug_info() before starting $xo = new memory_xml_output(); try { $xo->debug_info(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_stopped'); } // Start output twice $xo = new memory_xml_output(); $xo->start(); try { $xo->start(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_already_started'); } // Try to debug_info() before stoping $xo = new memory_xml_output(); $xo->start(); try { $xo->debug_info(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_stopped'); } // Stop output twice $xo = new memory_xml_output(); $xo->start(); $xo->stop(); try { $xo->stop(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_started'); } // Try to re-start after stop $xo = new memory_xml_output(); $xo->start(); $xo->stop(); try { $xo->start(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_already_stopped'); } // Try to get contents before stopping $xo = new memory_xml_output(); $xo->start(); try { $xo->get_allcontents(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'xml_output_not_stopped'); } // Write some contents and check them $xo = new memory_xml_output(); $xo->start(); $xo->write('first test'); $xo->stop(); $this->assertEquals('first test', $xo->get_allcontents()); // Write 3 times and check them $xo = new memory_xml_output(); $xo->start(); $xo->write('first test'); $xo->write(', sencond test'); $xo->write(', third test'); $xo->stop(); $this->assertEquals('first test, sencond test, third test', $xo->get_allcontents()); // Write some line feeds, tabs and friends $string = "\n\r\tcrazy test\n\r\t"; $xo = new memory_xml_output(); $xo->start(); $xo->write($string); $xo->stop(); $this->assertEquals($string, $xo->get_allcontents()); // Write some UTF-8 chars $string = 'áéíóú'; $xo = new memory_xml_output(); $xo->start(); $xo->write($string); $xo->stop(); $this->assertEquals($string, $xo->get_allcontents()); // Write some empty content $xo = new memory_xml_output(); $xo->start(); $xo->write('Hello '); $xo->write(null); $xo->write(false); $xo->write(''); $xo->write('World'); $xo->write(null); $xo->stop(); $this->assertEquals('Hello World', $xo->get_allcontents()); // Get debug info $xo = new memory_xml_output(); $xo->start(); $xo->write('01234'); $xo->write('56789'); $xo->stop(); $this->assertEquals('0123456789', $xo->get_allcontents()); $debug = $xo->debug_info(); $this->assertTrue(is_array($debug)); $this->assertTrue(array_key_exists('sent', $debug)); $this->assertEquals($debug['sent'], 10); } /* * test file_xml_output */ function test_file_xml_output(): void { global $CFG; $this->resetAfterTest(); $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; // Remove the test dir and any content @remove_dir(dirname($file)); // Recreate test dir if (!check_dir_exists(dirname($file), true, true)) { throw new \moodle_exception('error_creating_temp_dir', 'error', dirname($file)); } // Instantiate xml_output $xo = new file_xml_output($file); $this->assertTrue($xo instanceof xml_output); // Try to init file in (near) impossible path $file = $CFG->tempdir . '/test_azby/test_file_xml_output.txt'; $xo = new file_xml_output($file); try { $xo->start(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'directory_not_exists'); } // Try to init file already existing $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; file_put_contents($file, 'createdtobedeleted'); // create file manually $xo = new file_xml_output($file); try { $xo->start(); $this->assertTrue(false, 'xml_output_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_output_exception); $this->assertEquals($e->errorcode, 'file_already_exists'); } unlink($file); // delete file // Send some output and check $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; $xo = new file_xml_output($file); $xo->start(); $xo->write('first text'); $xo->stop(); $this->assertEquals('first text', file_get_contents($file)); unlink($file); // delete file // With buffer of 4 bytes, send 3 contents of 3 bytes each // so we force both buffering and last write on stop $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; $xo = new file_xml_output($file); $xo->set_buffersize(5); $xo->start(); $xo->write('123'); $xo->write('456'); $xo->write('789'); $xo->stop(); $this->assertEquals('123456789', file_get_contents($file)); unlink($file); // delete file // Write some line feeds, tabs and friends $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; $string = "\n\r\tcrazy test\n\r\t"; $xo = new file_xml_output($file); $xo->start(); $xo->write($string); $xo->stop(); $this->assertEquals($string, file_get_contents($file)); unlink($file); // delete file // Write some UTF-8 chars $file = $CFG->tempdir . '/test/test_file_xml_output.txt'; $string = 'áéíóú'; $xo = new file_xml_output($file); $xo->start(); $xo->write($string); $xo->stop(); $this->assertEquals($string, file_get_contents($file)); unlink($file); // delete file // Remove the test dir and any content @remove_dir(dirname($file)); } } util/xml/output/memory_xml_output.class.php 0000644 00000003561 15215711721 0015315 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This class implements one @xml_output able to store and return output in memory * * Although possible to use, has been defined as not supporting buffering for * testing purposes. get_allcontents() will return the contents after ending. * * TODO: Finish phpdocs */ class memory_xml_output extends xml_output{ protected $allcontents; // Here we'll store all the written contents public function __construct() { $this->allcontents = ''; parent::__construct(false); // disable buffering } public function get_allcontents() { if ($this->running !== false) { throw new xml_output_exception('xml_output_not_stopped'); } return $this->allcontents; } // Private API starts here protected function init() { // Easy :-) } protected function finish() { // Trivial :-) } protected function send($content) { // Accumulate contents $this->allcontents .= $content; } } util/xml/xml_writer.class.php 0000644 00000024452 15215711721 0012343 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @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 one (more or less complete) UTF-8 XML writer * * General purpose class used to output UTF-8 XML contents easily. Can be customized * using implementations of @xml_output (to define where to send the xml) and * and @xml_contenttransformer (to perform any transformation in contents before * outputting the XML). * * Has support for attributes, basic w3c xml schemas declaration, * and performs some content cleaning to avoid potential incorret UTF-8 * mess and has complete exception support. * * TODO: Provide UTF-8 safe strtoupper() function if using casefolding and non-ascii tags/attrs names * TODO: Finish phpdocs */ class xml_writer { protected $output; // @xml_output that defines how to output XML protected $contenttransformer; // @xml_contenttransformer to modify contents before output protected $prologue; // Complete string prologue we want to use protected $xmlschema; // URI to nonamespaceschema to be added to main tag protected $casefolding; // To define if xml tags must be uppercase (true) or not (false) protected $level; // current number of open tags, useful for indent text protected $opentags; // open tags accumulator, to check for errors protected $lastwastext;// to know when we are writing after text content protected $nullcontent;// to know if we are going to write one tag with null content protected $running; // To know if writer is running public function __construct($output, $contenttransformer = null, $casefolding = false) { if (!$output instanceof xml_output) { throw new xml_writer_exception('invalid_xml_output'); } if (!is_null($contenttransformer) && !$contenttransformer instanceof xml_contenttransformer) { throw new xml_writer_exception('invalid_xml_contenttransformer'); } $this->output = $output; $this->contenttransformer = $contenttransformer; $this->prologue = null; $this->xmlschema = null; $this->casefolding = $casefolding; $this->level = 0; $this->opentags = array(); $this->lastwastext = false; $this->nullcontent = false; $this->running = null; } /** * Initializes the XML writer, preparing it to accept instructions, also * invoking the underlying @xml_output init method to be ready for operation */ public function start() { if ($this->running === true) { throw new xml_writer_exception('xml_writer_already_started'); } if ($this->running === false) { throw new xml_writer_exception('xml_writer_already_stopped'); } $this->output->start(); // Initialize whatever we need in output if (!is_null($this->prologue)) { // Output prologue $this->write($this->prologue); } else { $this->write($this->get_default_prologue()); } $this->running = true; } /** * Finishes the XML writer, not accepting instructions any more, also * invoking the underlying @xml_output finish method to close/flush everything as needed */ public function stop() { if (is_null($this->running)) { throw new xml_writer_exception('xml_writer_not_started'); } if ($this->running === false) { throw new xml_writer_exception('xml_writer_already_stopped'); } if ($this->level > 0) { // Cannot stop if not at level 0, remaining open tags throw new xml_writer_exception('xml_writer_open_tags_remaining'); } $this->output->stop(); $this->running = false; } /** * Set the URI location for the *nonamespace* schema to be used by the (whole) XML document */ public function set_nonamespace_schema($uri) { if ($this->running) { throw new xml_writer_exception('xml_writer_already_started'); } $this->xmlschema = $uri; } /** * Define the complete prologue to be used, replacing the simple, default one */ public function set_prologue($prologue) { if ($this->running) { throw new xml_writer_exception('xml_writer_already_started'); } $this->prologue = $prologue; } /** * Outputs one XML start tag with optional attributes (name => value array) */ public function begin_tag($tag, $attributes = null) { // TODO: chek the tag name is valid $pre = $this->level ? "\n" . str_repeat(' ', $this->level * 2) : ''; // Indent $tag = $this->casefolding ? strtoupper($tag) : $tag; // Follow casefolding $end = $this->nullcontent ? ' /' : ''; // Tag without content, close it // Build attributes output $attrstring = ''; if (!empty($attributes) && is_array($attributes)) { // TODO: check the attr name is valid foreach ($attributes as $name => $value) { $name = $this->casefolding ? strtoupper($name) : $name; // Follow casefolding $attrstring .= ' ' . $name . '="'. $this->xml_safe_attr_content($value) . '"'; } } // Optional xml schema definition (level 0 only) $schemastring = ''; if ($this->level == 0 && !empty($this->xmlschema)) { $schemastring .= "\n " . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' . "\n " . 'xsi:noNamespaceSchemaLocation="' . $this->xml_safe_attr_content($this->xmlschema) . '"'; } // Send to xml_output $this->write($pre . '<' . $tag . $attrstring . $schemastring . $end . '>'); // Acumulate the tag and inc level if (!$this->nullcontent) { array_push($this->opentags, $tag); $this->level++; } $this->lastwastext = false; } /** * Outputs one XML end tag */ public function end_tag($tag) { // TODO: check the tag name is valid if ($this->level == 0) { // Nothing to end, already at level 0 throw new xml_writer_exception('xml_writer_end_tag_no_match'); } $pre = $this->lastwastext ? '' : "\n" . str_repeat(' ', ($this->level - 1) * 2); // Indent $tag = $this->casefolding ? strtoupper($tag) : $tag; // Follow casefolding $lastopentag = array_pop($this->opentags); if ($tag != $lastopentag) { $a = new stdclass(); $a->lastopen = $lastopentag; $a->tag = $tag; throw new xml_writer_exception('xml_writer_end_tag_no_match', $a); } // Send to xml_output $this->write($pre . '</' . $tag . '>'); $this->level--; $this->lastwastext = false; } /** * Outputs one tag completely (open, contents and close) */ public function full_tag($tag, $content = null, $attributes = null) { $content = $this->text_content($content); // First of all, apply transformations $this->nullcontent = is_null($content) ? true : false; // Is it null content $this->begin_tag($tag, $attributes); if (!$this->nullcontent) { $this->write($content); $this->lastwastext = true; $this->end_tag($tag); } } // Protected API starts here /** * Send some XML formatted chunk to output. */ protected function write($output) { $this->output->write($output); } /** * Get default prologue contents for this writer if there isn't a custom one */ protected function get_default_prologue() { return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; } /** * Clean attribute content and encode needed chars * (&, <, >, ") - single quotes not needed in this class * as far as we are enclosing with " */ protected function xml_safe_attr_content($content) { return htmlspecialchars($this->xml_safe_utf8($content), ENT_COMPAT); } /** * Clean text content and encode needed chars * (&, <, >) */ protected function xml_safe_text_content($content) { return htmlspecialchars($this->xml_safe_utf8($content), ENT_NOQUOTES); } /** * Perform some UTF-8 cleaning, stripping the control chars (\x00-\x1f) * but tabs (\x09), newlines (\xa) and returns (\xd). The delete control * char (\x7f) is also included. All them are forbiden in XML 1.0 specs. * The expression below seems to be UTF-8 safe too because it simply * ignores the rest of characters. Also normalize linefeeds and return chars. */ protected function xml_safe_utf8($content) { $content = core_text::trim_ctrl_chars($content ?? ''); $content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line return fix_utf8($content); } /** * Returns text contents processed by the corresponding @xml_contenttransformer */ protected function text_content($content) { if (!is_null($this->contenttransformer)) { // Apply content transformation $content = $this->contenttransformer->process($content); } return is_null($content) ? null : $this->xml_safe_text_content($content); // Safe UTF-8 and encode } } /* * Exception class used by all the @xml_writer stuff */ class xml_writer_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, $debuginfo); } } util/xml/contenttransformer/xml_contenttransformer.class.php 0000644 00000002615 15215711721 0020716 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @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 to extend in order to transform @xml_writer text contents * * Implementations of this class will provide @xml_writer with the ability of * transform xml text contents before being sent to output. Useful for various * things like link transformations in the backup process and others. * * Just define the process() method, program the desired transformations and done! * * TODO: Finish phpdocs */ abstract class xml_contenttransformer { abstract public function process($content); } util/xml/parser/progressive_parser.class.php 0000644 00000026004 15215711721 0015362 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @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 one SAX progressive push parser. * * SAX parser able to process XML content from files/variables. It supports * attributes and case folding and works only with UTF-8 content. It's one * progressive push parser because, intead of loading big crunchs of information * in memory, it "publishes" (pushes) small information in a "propietary array format" througt * the corresponding @progressive_parser_processor, that will be the responsibe for * returning information into handy formats to higher levels. * * Note that, while this progressive parser is able to process any XML file, it is * 100% progressive so it publishes the information in the original order it's parsed (that's * the expected behaviour) so information belonging to the same path can be returned in * different chunks if there are inner levels/paths in the middle. Be warned! * * The "propietary array format" that the parser publishes to the @progressive_parser_processor * is this: * array ( * 'path' => path where the tags belong to, * 'level'=> level (1-based) of the tags * 'tags => array ( * 'name' => name of the tag, * 'attrs'=> array( name of the attr => value of the attr), * 'cdata => cdata of the tag * ) * ) * * TODO: Finish phpdocs */ class progressive_parser { protected $xml_parser; // PHP's low level XML SAX parser protected $file; // full path to file being progressively parsed | => mutually exclusive protected $contents; // contents being progressively parsed | /** * @var progressive_parser_processor to be used to publish processed information */ protected $processor; protected $level; // level of the current tag protected $path; // path of the current tag protected $accum; // accumulated char data of the current tag protected $attrs; // attributes of the current tag protected $topush; // array containing current level information being parsed to be "pushed" protected $prevlevel; // level of the previous tag processed - to detect pushing places protected $currtag; // name/value/attributes of the tag being processed /** * @var \core\progress\base Progress tracker called for each action */ protected $progress; public function __construct($case_folding = false) { $this->xml_parser = xml_parser_create('UTF-8'); xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, $case_folding); xml_set_object($this->xml_parser, $this); xml_set_element_handler($this->xml_parser, array($this, 'start_tag'), array($this, 'end_tag')); xml_set_character_data_handler($this->xml_parser, array($this, 'char_data')); $this->file = null; $this->contents = null; $this->level = 0; $this->path = ''; $this->accum = ''; $this->attrs = array(); $this->topush = array(); $this->prevlevel = 0; $this->currtag = array(); } /* * Sets the XML file to be processed by the parser */ public function set_file($file) { if (!file_exists($file) || (!is_readable($file))) { throw new progressive_parser_exception('invalid_file_to_parse'); } $this->file = $file; $this->contents = null; } /* * Sets the XML contents to be processed by the parser */ public function set_contents($contents) { if (empty($contents)) { throw new progressive_parser_exception('invalid_contents_to_parse'); } $this->contents = $contents; $this->file = null; } /* * Define the @progressive_parser_processor in charge of processing the parsed chunks */ public function set_processor($processor) { if (!$processor instanceof progressive_parser_processor) { throw new progressive_parser_exception('invalid_parser_processor'); } $this->processor = $processor; } /** * Sets the progress tracker for the parser. If set, the tracker will be * called to report indeterminate progress for each chunk of XML. * * The caller should have already called start_progress on the progress tracker. * * @param \core\progress\base $progress Progress tracker */ public function set_progress(\core\progress\base $progress) { $this->progress = $progress; } /* * Process the XML, delegating found chunks to the @progressive_parser_processor */ public function process() { if (empty($this->processor)) { throw new progressive_parser_exception('undefined_parser_processor'); } if (empty($this->file) && empty($this->contents)) { throw new progressive_parser_exception('undefined_xml_to_parse'); } if (is_null($this->xml_parser)) { throw new progressive_parser_exception('progressive_parser_already_used'); } if ($this->file) { $fh = fopen($this->file, 'r'); while ($buffer = fread($fh, 8192)) { $this->parse($buffer, feof($fh)); } fclose($fh); } else { $this->parse($this->contents, true); } xml_parser_free($this->xml_parser); $this->xml_parser = null; } /** * Provides one cross-platform dirname function for * handling parser paths, see MDL-24381 */ public static function dirname($path) { return str_replace('\\', '/', dirname($path)); } // Protected API starts here protected function parse($data, $eof) { if (!xml_parse($this->xml_parser, $data, $eof)) { throw new progressive_parser_exception( 'xml_parsing_error', null, sprintf('XML error: %s at line %d, column %d', xml_error_string(xml_get_error_code($this->xml_parser)), xml_get_current_line_number($this->xml_parser), xml_get_current_column_number($this->xml_parser))); } } protected function publish($data) { $this->processor->receive_chunk($data); if (!empty($this->progress)) { // Report indeterminate progress. $this->progress->progress(); } } /** * Inform to the processor that we have started parsing one path */ protected function inform_start($path) { $this->processor->before_path($path); } /** * Inform to the processor that we have finished parsing one path */ protected function inform_end($path) { $this->processor->after_path($path); } protected function postprocess_cdata($data) { return $this->processor->process_cdata($data); } protected function start_tag($parser, $tag, $attributes) { // Normal update of parser internals $this->level++; $this->path .= '/' . $tag; $this->accum = ''; $this->attrs = !empty($attributes) ? $attributes : array(); // Inform processor we are about to start one tag $this->inform_start($this->path); // Entering a new inner level, publish all the information available if ($this->level > $this->prevlevel) { if (!empty($this->currtag) && (!empty($this->currtag['attrs']) || !empty($this->currtag['cdata']))) { // We always add the last not-empty repetition. Empty ones are ignored. if (isset($this->topush['tags'][$this->currtag['name']]) && trim($this->currtag['cdata']) === '') { // Do nothing, the tag already exists and the repetition is empty } else { $this->topush['tags'][$this->currtag['name']] = $this->currtag; } } if (!empty($this->topush['tags'])) { $this->publish($this->topush); } $this->currtag = array(); $this->topush = array(); } // If not set, build to push common header if (empty($this->topush)) { $this->topush['path'] = progressive_parser::dirname($this->path); $this->topush['level'] = $this->level; $this->topush['tags'] = array(); } // Handling a new tag, create it $this->currtag['name'] = $tag; // And add attributes if present if ($this->attrs) { $this->currtag['attrs'] = $this->attrs; } // For the records $this->prevlevel = $this->level; } protected function end_tag($parser, $tag) { // Ending rencently started tag, add value to current tag if ($this->level == $this->prevlevel) { $this->currtag['cdata'] = $this->postprocess_cdata($this->accum); // We always add the last not-empty repetition. Empty ones are ignored. if (isset($this->topush['tags'][$this->currtag['name']]) && trim($this->currtag['cdata']) === '') { // Do nothing, the tag already exists and the repetition is empty } else { $this->topush['tags'][$this->currtag['name']] = $this->currtag; } $this->currtag = array(); } // Leaving one level, publish all the information available if ($this->level < $this->prevlevel) { if (!empty($this->topush['tags'])) { $this->publish($this->topush); } $this->currtag = array(); $this->topush = array(); } // For the records $this->prevlevel = $this->level; // Inform processor we have finished one tag $this->inform_end($this->path); // Normal update of parser internals $this->level--; $this->path = progressive_parser::dirname($this->path); } protected function char_data($parser, $data) { $this->accum .= $data; } } /* * Exception class used by all the @progressive_parser stuff */ class progressive_parser_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, $debuginfo); } } util/xml/parser/tests/fixtures/test2.xml 0000644 00000000363 15215711721 0014417 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <firsttag> <secondtag name="secondtag" level="2" path="/firsttag/secondtag">secondvalue</secondtag> <secondtag name="secondtag" level="2" path="/firsttag/secondtag">secondvalue</wrongtag> </firsttag> util/xml/parser/tests/fixtures/test6.xml 0000644 00000002760 15215711721 0014426 0 ustar 00 <test> <MOODLE_BACKUP> <COURSE> <FORMATDATA> <WEEKS> <WEEK> <SECTION>1</SECTION> <HIDENUMBER>1</HIDENUMBER> <HIDEDATE>0</HIDEDATE> <SHOWTO></SHOWTO> <OFFLINEMATERIAL>0</OFFLINEMATERIAL> </WEEK> <WEEK> <SECTION>2</SECTION> <HIDENUMBER>0</HIDENUMBER> <HIDEDATE>0</HIDEDATE> <RESETNUMBER>0</RESETNUMBER> <SHOWTO></SHOWTO> <OFFLINEMATERIAL>0</OFFLINEMATERIAL> </WEEK> </WEEKS> <IMPORTED> </IMPORTED> </FORMATDATA> <GROUPEDNOTOBSERVED> <NOBODYOBSERVERSTHIS> <NOTOBSERVED>Muhehe</NOTOBSERVED> </NOBODYOBSERVERSTHIS> </GROUPEDNOTOBSERVED> <EMPTYGROUPED> </EMPTYGROUPED> <SECONDGROUPED> <SUBS> <SUB> <PROP>Unit tests rock!</PROP> </SUB> </SUBS> </SECONDGROUPED> </COURSE> </MOODLE_BACKUP> <moodle2> <grouped id="this is not parsed at the moment because there are no final elements"> <subs> <sub id="34"> <prop>Oh yeah</prop> </sub> </subs> </grouped> <groupednonemptywithattr id="78"> <prop>Go baby go</prop> <subs> <sub id="89"> <prop>http://moodle.org</prop> </sub> </subs> </groupednonemptywithattr> <groupedemptywithattr attr="ay?"> </groupedemptywithattr> </moodle2> </test> util/xml/parser/tests/fixtures/test3.xml 0000644 00000002351 15215711721 0014417 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <toptag name="toptag" level="1" path="/toptag"> <secondtag name="secondtag" level="2" path="/toptag/secondtag" value="secondvalue">secondvalue</secondtag> <thirdtag name="thirdtag" level="2" path="/toptag/thirdtag"> <onevalue name="onevalue" level="3" path="/toptag/thirdtag/onevalue">onevalue</onevalue> <onevalue name="onevalue" level="3" value="anothervalue">anothervalue</onevalue> <onevalue name="onevalue" level="3" value="yetanothervalue">yetanothervalue</onevalue> <twovalue name="twovalue" level="3" path="/toptag/thirdtag/twovalue">twovalue</twovalue> <forthtag name="forthtag" level="3" path="/toptag/thirdtag/forthtag"> <innervalue>innervalue</innervalue> <innertag> <superinnertag name="superinnertag" level="5"> <superinnervalue name="superinnervalue" level="6">superinnervalue</superinnervalue> </superinnertag> </innertag> </forthtag> <fifthtag level='3'> <sixthtag level='4'> <seventh level='5'>seventh</seventh> </sixthtag> </fifthtag> <finalvalue name="finalvalue" level="3" path="/toptag/thirdtag/finalvalue">finalvalue</finalvalue> <finalvalue /> <finalvalue/> </thirdtag> </toptag> util/xml/parser/tests/fixtures/test4.xml 0000644 00000007555 15215711721 0014433 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <activity id="1" moduleid="5" modulename="glossary" contextid="26"> <glossary id="1"> <name>One glossary</name> <intro><p>One simple glossary to test backup &amp; restore. Here it's the standard image:</p> <p><img src="@@PLUGINFILE@@/88_31.png" alt="pwd by moodle" width="88" height="31" /></p></intro> <allowduplicatedentries>0</allowduplicatedentries> <displayformat>dictionary</displayformat> <mainglossary>0</mainglossary> <showspecial>1</showspecial> <showalphabet>1</showalphabet> <showall>1</showall> <allowcomments>0</allowcomments> <allowprintview>1</allowprintview> <usedynalink>1</usedynalink> <defaultapproval>1</defaultapproval> <globalglossary>0</globalglossary> <entbypage>10</entbypage> <editalways>0</editalways> <rsstype>0</rsstype> <rssarticles>0</rssarticles> <assessed>1</assessed> <assesstimestart>0</assesstimestart> <assesstimefinish>0</assesstimefinish> <scale>10</scale> <timecreated>1275638215</timecreated> <timemodified>1275639747</timemodified> <entries> <entry id="1"> <userid>2</userid> <concept>dog</concept> <definition><p>Traditional enemies of cats</p></definition> <definitionformat>1</definitionformat> <definitiontrust>0</definitiontrust> <attachment></attachment> <timecreated>1275638279</timecreated> <timemodified>1275638279</timemodified> <teacherentry>1</teacherentry> <sourceglossaryid>0</sourceglossaryid> <usedynalink>1</usedynalink> <casesensitive>0</casesensitive> <fullmatch>0</fullmatch> <approved>1</approved> <aliases> <alias id="1"> <alias_text>dogs</alias_text> </alias> </aliases> <ratings> <rating id="2"> <scaleid>10</scaleid> <value>6</value> <userid>5</userid> <timecreated>1275639785</timecreated> <timemodified>1275639797</timemodified> </rating> </ratings> </entry> <entry id="2"> <userid>2</userid> <concept>cat</concept> <definition><p>traditional enemies of dogs</p></definition> <definitionformat>1</definitionformat> <definitiontrust>0</definitiontrust> <attachment></attachment> <timecreated>1275638304</timecreated> <timemodified>1275638304</timemodified> <teacherentry>1</teacherentry> <sourceglossaryid>0</sourceglossaryid> <usedynalink>1</usedynalink> <casesensitive>0</casesensitive> <fullmatch>0</fullmatch> <approved>1</approved> <aliases> <alias id="2"> <alias_text>cats</alias_text> </alias> <alias id="3"> <alias_text>felines</alias_text> </alias> </aliases> <ratings> <rating id="1"> <scaleid>10</scaleid> <value>5</value> <userid>5</userid> <timecreated>1275639779</timecreated> <timemodified>1275639779</timemodified> </rating> </ratings> </entry> </entries> <categories> </categories> <onetest> <name>1</name> <value>1</value> </onetest> <onetest> <name>2</name> <value>2</value> </onetest> <othertest> <name>3</name> <value>3</value> <name>4</name><!-- Only last will be processed. We don't allow repeated final tags in our parser --> <value>4</value> <value>5</value><!-- Only last will be processed. We don't allow repeated final tags in our parser --> <value> </value><!-- If one tag already is set and the repeated is empty, the original value is kept --> </othertest> </glossary> </activity> util/xml/parser/tests/fixtures/test1.xml 0000644 00000000364 15215711721 0014417 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <firsttag> <secondtag name="secondtag" level="2" path="/firsttag/secondtag">secondvalue</secondtag> <secondtag name="secondtag" level="2" path="/firsttag/secondtag">secondvalue</secondtag> </firsttag> util/xml/parser/tests/fixtures/test5.xml 0000644 00000000711 15215711721 0014417 0 ustar 00 <MOODLE_BACKUP> <COURSE ID="100"> <SECTIONS> <SECTION> <ID>200</ID> <MODS> <MOD> <ID>300</ID> <ROLES_OVERRIDES> </ROLES_OVERRIDES> </MOD> <MOD /> <MOD /> <MOD ID="400" /> <MOD> <ID>500</ID> </MOD> <MOD /> <MOD /> </MODS> </SECTION> </SECTIONS> </COURSE> </MOODLE_BACKUP> util/xml/parser/tests/parser_test.php 0000644 00000124565 15215711721 0014042 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Test progressive_parser and progressive_parser_processor tests. * * @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 grouped_parser_processor; use progressive_parser; use progressive_parser_exception; use progressive_parser_processor; use simplified_parser_processor; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/processors/simplified_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); /** * Test progressive_parser and progressive_parser_processor tests. * * @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 */ final class parser_test extends \advanced_testcase { /* * test progressive_parser public methods */ function test_parser_public_api(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); $this->assertTrue($pp instanceof progressive_parser); $pr = new mock_parser_processor(); $this->assertTrue($pr instanceof progressive_parser_processor); // Try to process without processor try { $pp->process(); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'undefined_parser_processor'); } // Assign processor to parser $pp->set_processor($pr); // Try to process without file and contents try { $pp->process(); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'undefined_xml_to_parse'); } // Assign *invalid* processor to parser try { $pp->set_processor(new \stdClass()); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'invalid_parser_processor'); } // Set file from fixtures (test1.xml) and process it $pp = new progressive_parser(); $pr = new mock_parser_processor(); $pp->set_processor($pr); $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test1.xml'); $pp->process(); $serfromfile = serialize($pr->get_chunks()); // Get serialized results (to compare later) // Set *unexisting* file from fixtures try { $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test0.xml'); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'invalid_file_to_parse'); } // Set contents from fixtures (test1.xml) and process it $pp = new progressive_parser(); $pr = new mock_parser_processor(); $pp->set_processor($pr); $pp->set_contents(file_get_contents($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test1.xml')); $pp->process(); $serfrommemory = serialize($pr->get_chunks()); // Get serialized results (to compare later) // Set *empty* contents try { $pp->set_contents(''); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'invalid_contents_to_parse'); } // Check that both results from file processing and content processing are equal $this->assertEquals($serfromfile, $serfrommemory); // Check case_folding is working ok $pp = new progressive_parser(true); $pr = new mock_parser_processor(); $pp->set_processor($pr); $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test1.xml'); $pp->process(); $chunks = $pr->get_chunks(); $this->assertTrue($chunks[0]['path'] === '/FIRSTTAG'); $this->assertTrue($chunks[0]['tags']['SECONDTAG']['name'] === 'SECONDTAG'); $this->assertTrue($chunks[0]['tags']['SECONDTAG']['attrs']['NAME'] === 'secondtag'); // Check invalid XML exception is working ok $pp = new progressive_parser(true); $pr = new mock_parser_processor(); $pp->set_processor($pr); $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test2.xml'); try { $pp->process(); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'xml_parsing_error'); } // Check double process throws exception $pp = new progressive_parser(true); $pr = new mock_parser_processor(); $pp->set_processor($pr); $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test1.xml'); $pp->process(); try { // Second process, will throw exception $pp->process(); $this->assertTrue(false); } catch (\Exception $e) { $this->assertTrue($e instanceof progressive_parser_exception); $this->assertEquals($e->errorcode, 'progressive_parser_already_used'); } } /* * test progressive_parser parsing results using testing_parser_processor and test1.xml * auto-described file from fixtures */ function test_parser_results(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); // Instantiate processor, passing the unit test as param $pr = new mock_auto_parser_processor($this); $this->assertTrue($pr instanceof progressive_parser_processor); // Assign processor to parser $pp->set_processor($pr); // Set file from fixtures $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test3.xml'); // Process the file, the autotest processor will perform a bunch of automatic tests $pp->process(); // Get processor debug info $debug = $pr->debug_info(); $this->assertTrue(is_array($debug)); $this->assertTrue(array_key_exists('chunks', $debug)); // Check the number of chunks is correct for the file $this->assertEquals($debug['chunks'], 10); } /* * test progressive_parser parsing results using simplified_parser_processor and test4.xml * (one simple glossary backup file example) */ function test_simplified_parser_results(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); // Instantiate simplified_parser_processor declaring the interesting paths $pr = new mock_simplified_parser_processor(array( '/activity', '/activity/glossary', '/activity/glossary/entries/entry', '/activity/glossary/entries/entry/aliases/alias', '/activity/glossary/entries/entry/ratings/rating', '/activity/glossary/categories/category', '/activity/glossary/onetest', '/activity/glossary/othertest')); $this->assertTrue($pr instanceof progressive_parser_processor); // Assign processor to parser $pp->set_processor($pr); // Set file from fixtures $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test4.xml'); // Process the file $pp->process(); // Get processor debug info $debug = $pr->debug_info(); $this->assertTrue(is_array($debug)); $this->assertTrue(array_key_exists('chunks', $debug)); // Check the number of chunks is correct for the file $this->assertEquals($debug['chunks'], 12); // Get all the simplified chunks and perform various validations $chunks = $pr->get_chunks(); // Check we have received the correct number of chunks $this->assertEquals(count($chunks), 12); // chunk[0] (/activity) tests $this->assertEquals(count($chunks[0]), 3); $this->assertEquals($chunks[0]['path'], '/activity'); $this->assertEquals($chunks[0]['level'],'2'); $tags = $chunks[0]['tags']; $this->assertEquals(count($tags), 4); $this->assertEquals($tags['id'], 1); $this->assertEquals($tags['moduleid'], 5); $this->assertEquals($tags['modulename'], 'glossary'); $this->assertEquals($tags['contextid'], 26); $this->assertEquals($chunks[0]['level'],'2'); // chunk[1] (/activity/glossary) tests $this->assertEquals(count($chunks[1]), 3); $this->assertEquals($chunks[1]['path'], '/activity/glossary'); $this->assertEquals($chunks[1]['level'],'3'); $tags = $chunks[1]['tags']; $this->assertEquals(count($tags), 24); $this->assertEquals($tags['id'], 1); $this->assertEquals($tags['intro'], '<p>One simple glossary to test backup & restore. Here it\'s the standard image:</p>'. "\n". '<p><img src="@@PLUGINFILE@@/88_31.png" alt="pwd by moodle" width="88" height="31" /></p>'); $this->assertEquals($tags['timemodified'], 1275639747); $this->assertTrue(!isset($tags['categories'])); // chunk[5] (second /activity/glossary/entries/entry) tests $this->assertEquals(count($chunks[5]), 3); $this->assertEquals($chunks[5]['path'], '/activity/glossary/entries/entry'); $this->assertEquals($chunks[5]['level'],'5'); $tags = $chunks[5]['tags']; $this->assertEquals(count($tags), 15); $this->assertEquals($tags['id'], 2); $this->assertEquals($tags['concept'], 'cat'); $this->assertTrue(!isset($tags['aliases'])); $this->assertTrue(!isset($tags['entries'])); // chunk[6] (second /activity/glossary/entries/entry/aliases/alias) tests $this->assertEquals(count($chunks[6]), 3); $this->assertEquals($chunks[6]['path'], '/activity/glossary/entries/entry/aliases/alias'); $this->assertEquals($chunks[6]['level'],'7'); $tags = $chunks[6]['tags']; $this->assertEquals(count($tags), 2); $this->assertEquals($tags['id'], 2); $this->assertEquals($tags['alias_text'], 'cats'); // chunk[7] (second /activity/glossary/entries/entry/aliases/alias) tests $this->assertEquals(count($chunks[7]), 3); $this->assertEquals($chunks[7]['path'], '/activity/glossary/entries/entry/aliases/alias'); $this->assertEquals($chunks[7]['level'],'7'); $tags = $chunks[7]['tags']; $this->assertEquals(count($tags), 2); $this->assertEquals($tags['id'], 3); $this->assertEquals($tags['alias_text'], 'felines'); // chunk[8] (second /activity/glossary/entries/entry/ratings/rating) tests $this->assertEquals(count($chunks[8]), 3); $this->assertEquals($chunks[8]['path'], '/activity/glossary/entries/entry/ratings/rating'); $this->assertEquals($chunks[8]['level'],'7'); $tags = $chunks[8]['tags']; $this->assertEquals(count($tags), 6); $this->assertEquals($tags['id'], 1); $this->assertEquals($tags['timemodified'], '1275639779'); // chunk[9] (first /activity/glossary/onetest) tests $this->assertEquals(count($chunks[9]), 3); $this->assertEquals($chunks[9]['path'], '/activity/glossary/onetest'); $this->assertEquals($chunks[9]['level'],'4'); $tags = $chunks[9]['tags']; $this->assertEquals(count($tags), 2); $this->assertEquals($tags['name'], 1); $this->assertEquals($tags['value'], 1); // chunk[10] (second /activity/glossary/onetest) tests $this->assertEquals(count($chunks[10]), 3); $this->assertEquals($chunks[10]['path'], '/activity/glossary/onetest'); $this->assertEquals($chunks[10]['level'],'4'); $tags = $chunks[10]['tags']; $this->assertEquals(count($tags), 2); $this->assertEquals($tags['name'], 2); $this->assertEquals($tags['value'], 2); // chunk[11] (first /activity/glossary/othertest) tests // note we don't allow repeated "final" element, so we only return the last one $this->assertEquals(count($chunks[11]), 3); $this->assertEquals($chunks[11]['path'], '/activity/glossary/othertest'); $this->assertEquals($chunks[11]['level'],'4'); $tags = $chunks[11]['tags']; $this->assertEquals(count($tags), 2); $this->assertEquals($tags['name'], 4); $this->assertEquals($tags['value'], 5); // Now check start notifications $snotifs = $pr->get_start_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 12); // Check first, sixth and last notifications $this->assertEquals($snotifs[0], '/activity'); $this->assertEquals($snotifs[5], '/activity/glossary/entries/entry'); $this->assertEquals($snotifs[11], '/activity/glossary/othertest'); // Now check end notifications $enotifs = $pr->get_end_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 12); // Check first, sixth and last notifications $this->assertEquals($enotifs[0], '/activity/glossary/entries/entry/aliases/alias'); $this->assertEquals($enotifs[5], '/activity/glossary/entries/entry/ratings/rating'); $this->assertEquals($enotifs[11], '/activity'); // Check start and end notifications are balanced sort($snotifs); sort($enotifs); $this->assertEquals($snotifs, $enotifs); // Now verify that the start/process/end order is correct $allnotifs = $pr->get_all_notifications(); $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks)); // The count // Check integrity of the notifications $errcount = $this->helper_check_notifications_order_integrity($allnotifs); $this->assertEquals($errcount, 0); // No errors found, plz } /** * test how the simplified processor and the order of start/process/end events happens * with one real fragment of one backup 1.9 file, where some problems * were found by David, hence we honor him in the name of the test ;-) */ function test_simplified_david_backup19_file_fragment(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); // Instantiate grouped_parser_processor $pr = new mock_simplified_parser_processor(); // Add interesting paths $pr->add_path('/MOODLE_BACKUP/COURSE'); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION'); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'); $this->assertTrue($pr instanceof progressive_parser_processor); // Assign processor to parser $pp->set_processor($pr); // Set file from fixtures $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test5.xml'); // Process the file $pp->process(); // Get all the simplified chunks and perform various validations $chunks = $pr->get_chunks(); $this->assertEquals(count($chunks), 3); // Only 3, because 7 (COURSE, ROLES_OVERRIDES and 5 MOD) are empty, aka no chunk // Now check start notifications $snotifs = $pr->get_start_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 10); // Start tags are dispatched for empties (ROLES_OVERRIDES) // Check first and last notifications $this->assertEquals($snotifs[0], '/MOODLE_BACKUP/COURSE'); $this->assertEquals($snotifs[1], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION'); $this->assertEquals($snotifs[2], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($snotifs[3], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'); $this->assertEquals($snotifs[7], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($snotifs[8], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($snotifs[9], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); // Now check end notifications $enotifs = $pr->get_end_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 10); // End tags are dispatched for empties (ROLES_OVERRIDES) // Check first, and last notifications $this->assertEquals($enotifs[0], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'); $this->assertEquals($enotifs[1], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($enotifs[2], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($enotifs[3], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($enotifs[7], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $this->assertEquals($enotifs[8], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION'); $this->assertEquals($enotifs[9], '/MOODLE_BACKUP/COURSE'); // Check start and end notifications are balanced sort($snotifs); sort($enotifs); $this->assertEquals($snotifs, $enotifs); // Now verify that the start/process/end order is correct $allnotifs = $pr->get_all_notifications(); $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks)); // The count // Check integrity of the notifications $errcount = $this->helper_check_notifications_order_integrity($allnotifs); $this->assertEquals($errcount, 0); // No errors found, plz } /* * test progressive_parser parsing results using grouped_parser_processor and test4.xml * (one simple glossary backup file example) */ function test_grouped_parser_results(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); // Instantiate grouped_parser_processor $pr = new mock_grouped_parser_processor(); // Add interesting paths $pr->add_path('/activity'); $pr->add_path('/activity/glossary', true); $pr->add_path('/activity/glossary/entries/entry'); $pr->add_path('/activity/glossary/entries/entry/aliases/alias'); $pr->add_path('/activity/glossary/entries/entry/ratings/rating'); $pr->add_path('/activity/glossary/categories/category'); $pr->add_path('/activity/glossary/onetest'); $pr->add_path('/activity/glossary/othertest'); $this->assertTrue($pr instanceof progressive_parser_processor); // Assign processor to parser $pp->set_processor($pr); // Set file from fixtures $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test4.xml'); // Process the file $pp->process(); // Get processor debug info $debug = $pr->debug_info(); $this->assertTrue(is_array($debug)); $this->assertTrue(array_key_exists('chunks', $debug)); // Check the number of chunks is correct for the file $this->assertEquals($debug['chunks'], 2); // Get all the simplified chunks and perform various validations $chunks = $pr->get_chunks(); // Check we have received the correct number of chunks $this->assertEquals(count($chunks), 2); // chunk[0] (/activity) tests $this->assertEquals(count($chunks[0]), 3); $this->assertEquals($chunks[0]['path'], '/activity'); $this->assertEquals($chunks[0]['level'],'2'); $tags = $chunks[0]['tags']; $this->assertEquals(count($tags), 4); $this->assertEquals($tags['id'], 1); $this->assertEquals($tags['moduleid'], 5); $this->assertEquals($tags['modulename'], 'glossary'); $this->assertEquals($tags['contextid'], 26); $this->assertEquals($chunks[0]['level'],'2'); // chunk[1] (grouped /activity/glossary tests) $this->assertEquals(count($chunks[1]), 3); $this->assertEquals($chunks[1]['path'], '/activity/glossary'); $this->assertEquals($chunks[1]['level'],'3'); $tags = $chunks[1]['tags']; $this->assertEquals(count($tags), 27); $this->assertEquals($tags['id'], 1); $this->assertEquals($tags['intro'], '<p>One simple glossary to test backup & restore. Here it\'s the standard image:</p>'. "\n". '<p><img src="@@PLUGINFILE@@/88_31.png" alt="pwd by moodle" width="88" height="31" /></p>'); $this->assertEquals($tags['timemodified'], 1275639747); $this->assertTrue(!isset($tags['categories'])); $this->assertTrue(isset($tags['entries'])); $this->assertTrue(isset($tags['onetest'])); $this->assertTrue(isset($tags['othertest'])); // Various tests under the entries $entries = $chunks[1]['tags']['entries']['entry']; $this->assertEquals(count($entries), 2); // First entry $entry1 = $entries[0]; $this->assertEquals(count($entry1), 17); $this->assertEquals($entry1['id'], 1); $this->assertEquals($entry1['userid'], 2); $this->assertEquals($entry1['concept'], 'dog'); $this->assertEquals($entry1['definition'], '<p>Traditional enemies of cats</p>'); $this->assertTrue(isset($entry1['aliases'])); $this->assertTrue(isset($entry1['ratings'])); // aliases of first entry $aliases = $entry1['aliases']['alias']; $this->assertEquals(count($aliases), 1); // first alias $alias1 = $aliases[0]; $this->assertEquals(count($alias1), 2); $this->assertEquals($alias1['id'], 1); $this->assertEquals($alias1['alias_text'], 'dogs'); // ratings of first entry $ratings = $entry1['ratings']['rating']; $this->assertEquals(count($ratings), 1); // first rating $rating1 = $ratings[0]; $this->assertEquals(count($rating1), 6); $this->assertEquals($rating1['id'], 2); $this->assertEquals($rating1['value'], 6); $this->assertEquals($rating1['timemodified'], '1275639797'); // Second entry $entry2 = $entries[1]; $this->assertEquals(count($entry2), 17); $this->assertEquals($entry2['id'], 2); $this->assertEquals($entry2['userid'], 2); $this->assertEquals($entry2['concept'], 'cat'); $this->assertEquals($entry2['definition'], '<p>traditional enemies of dogs</p>'); $this->assertTrue(isset($entry2['aliases'])); $this->assertTrue(isset($entry2['ratings'])); // aliases of first entry $aliases = $entry2['aliases']['alias']; $this->assertEquals(count($aliases), 2); // first alias $alias1 = $aliases[0]; $this->assertEquals(count($alias1), 2); $this->assertEquals($alias1['id'], 2); $this->assertEquals($alias1['alias_text'], 'cats'); // second alias $alias2 = $aliases[1]; $this->assertEquals(count($alias2), 2); $this->assertEquals($alias2['id'], 3); $this->assertEquals($alias2['alias_text'], 'felines'); // ratings of first entry $ratings = $entry2['ratings']['rating']; $this->assertEquals(count($ratings), 1); // first rating $rating1 = $ratings[0]; $this->assertEquals(count($rating1), 6); $this->assertEquals($rating1['id'], 1); $this->assertEquals($rating1['value'], 5); $this->assertEquals($rating1['scaleid'], 10); // Onetest test (only 1 level nested) $onetest = $tags['onetest']; $this->assertEquals(count($onetest), 2); $this->assertEquals(count($onetest[0]), 2); $this->assertEquals($onetest[0]['name'], 1); $this->assertEquals($onetest[0]['value'], 1); $this->assertEquals(count($onetest[1]), 2); $this->assertEquals($onetest[1]['name'], 2); $this->assertEquals($onetest[1]['value'], 2); // Other test (0 level nested, only last one is retrieved) $othertest = $tags['othertest']; $this->assertEquals(count($othertest), 1); $this->assertEquals(count($othertest[0]), 2); $this->assertEquals($othertest[0]['name'], 4); $this->assertEquals($othertest[0]['value'], 5); // Now check start notifications $snotifs = $pr->get_start_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 2); // Check first and last notifications $this->assertEquals($snotifs[0], '/activity'); $this->assertEquals($snotifs[1], '/activity/glossary'); // Now check end notifications $enotifs = $pr->get_end_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 2); // Check first, and last notifications $this->assertEquals($enotifs[0], '/activity/glossary'); $this->assertEquals($enotifs[1], '/activity'); // Check start and end notifications are balanced sort($snotifs); sort($enotifs); $this->assertEquals($snotifs, $enotifs); // Now verify that the start/process/end order is correct $allnotifs = $pr->get_all_notifications(); $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks)); // The count // Check integrity of the notifications $errcount = $this->helper_check_notifications_order_integrity($allnotifs); $this->assertEquals($errcount, 0); // No errors found, plz } /** * test how the grouped processor and the order of start/process/end events happens * with one real fragment of one backup 1.9 file, where some problems * were found by David, hence we honor him in the name of the test ;-) */ function test_grouped_david_backup19_file_fragment(): void { global $CFG; // Instantiate progressive_parser $pp = new progressive_parser(); // Instantiate grouped_parser_processor $pr = new mock_grouped_parser_processor(); // Add interesting paths $pr->add_path('/MOODLE_BACKUP/COURSE'); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION', true); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD'); $pr->add_path('/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'); $this->assertTrue($pr instanceof progressive_parser_processor); // Assign processor to parser $pp->set_processor($pr); // Set file from fixtures $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test5.xml'); // Process the file $pp->process(); // Get all the simplified chunks and perform various validations $chunks = $pr->get_chunks(); $this->assertEquals(count($chunks), 1); // Only 1, the SECTION one // Now check start notifications $snotifs = $pr->get_start_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 2); // Check first and last notifications $this->assertEquals($snotifs[0], '/MOODLE_BACKUP/COURSE'); $this->assertEquals($snotifs[1], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION'); // Now check end notifications $enotifs = $pr->get_end_notifications(); // Check we have received the correct number of notifications $this->assertEquals(count($snotifs), 2); // End tags are dispatched for empties (ROLES_OVERRIDES) // Check first, and last notifications $this->assertEquals($enotifs[0], '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION'); $this->assertEquals($enotifs[1], '/MOODLE_BACKUP/COURSE'); // Check start and end notifications are balanced sort($snotifs); sort($enotifs); $this->assertEquals($snotifs, $enotifs); // Now verify that the start/process/end order is correct $allnotifs = $pr->get_all_notifications(); $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks)); // The count // Check integrity of the notifications $errcount = $this->helper_check_notifications_order_integrity($allnotifs); $this->assertEquals($errcount, 0); // No errors found, plz } /** */ function test_grouped_at_empty_node(): void { global $CFG; // Instantiate progressive_parser. $pp = new progressive_parser(); // Instantiate grouped_parser_processor. $pr = new mock_grouped_parser_processor(); $this->assertTrue($pr instanceof progressive_parser_processor); // Add interesting paths - moodle1 style. $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA', true); $pr->add_path('/test/MOODLE_BACKUP/COURSE/FORMATDATA/WEEKS/WEEK'); $pr->add_path('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', true); $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', true); $pr->add_path('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED/SUBS/SUB'); // Add interesting paths - moodle2 style. $pr->add_path('/test/moodle2/grouped', true); $pr->add_path('/test/moodle2/grouped/subs/sub'); $pr->add_path('/test/moodle2/groupedemptywithattr', true); $pr->add_path('/test/moodle2/groupednonemptywithattr', true); $pr->add_path('/test/moodle2/groupednonemptywithattr/subs/sub'); // Assign processor to parser. $pp->set_processor($pr); // Set file from fixtures. $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/tests/fixtures/test6.xml'); // Process the file. $pp->process(); // Get all the simplified chunks and perform various validations. $chunks = $pr->get_chunks(); $this->assertEquals(count($chunks), 6); // All grouped elements. // Check some random data. $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $chunks[0]['path']); $this->assertEquals(2, $chunks[0]['tags']['WEEKS']['WEEK'][1]['SECTION']); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $chunks[1]['path']); $this->assertEquals(array(), $chunks[1]['tags']); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $chunks[2]['path']); $this->assertEquals('Unit tests rock!', $chunks[2]['tags']['SUBS']['SUB'][0]['PROP']); $this->assertEquals('/test/moodle2/grouped', $chunks[3]['path']); $this->assertFalse(isset($chunks[3]['tags']['id'])); // No final elements, this should be fixed one day. $this->assertEquals(34, $chunks[3]['tags']['subs']['sub'][0]['id']); // We have final element so this is parsed. $this->assertEquals('Oh yeah', $chunks[3]['tags']['subs']['sub'][0]['prop']); $this->assertEquals('/test/moodle2/groupednonemptywithattr', $chunks[4]['path']); $this->assertEquals(78, $chunks[4]['tags']['id']); // We have final element so this is parsed. $this->assertEquals('Go baby go', $chunks[4]['tags']['prop']); $this->assertEquals(89, $chunks[4]['tags']['subs']['sub'][0]['id']); $this->assertEquals('http://moodle.org', $chunks[4]['tags']['subs']['sub'][0]['prop']); $this->assertEquals('/test/moodle2/groupedemptywithattr', $chunks[5]['path']); $this->assertFalse(isset($chunks[5]['tags']['attr'])); // No final elements, this should be fixed one day. // Now check start notifications. $snotifs = $pr->get_start_notifications(); // Check we have received the correct number of notifications. $this->assertEquals(count($snotifs), 6); // Check the order of notifications (in order they appear in test6.xml). $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $snotifs[0]); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $snotifs[1]); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $snotifs[2]); $this->assertEquals('/test/moodle2/grouped', $snotifs[3]); $this->assertEquals('/test/moodle2/groupednonemptywithattr', $snotifs[4]); $this->assertEquals('/test/moodle2/groupedemptywithattr', $snotifs[5]); // Now check end notifications. $enotifs = $pr->get_end_notifications(); // Check we have received the correct number of notifications. $this->assertEquals(count($enotifs), 6); // Check the order of notifications (in order they appear in test6.xml). $this->assertEquals('/test/MOODLE_BACKUP/COURSE/FORMATDATA', $enotifs[0]); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/EMPTYGROUPED', $enotifs[1]); $this->assertEquals('/test/MOODLE_BACKUP/COURSE/SECONDGROUPED', $enotifs[2]); $this->assertEquals('/test/moodle2/grouped', $enotifs[3]); $this->assertEquals('/test/moodle2/groupednonemptywithattr', $enotifs[4]); $this->assertEquals('/test/moodle2/groupedemptywithattr', $enotifs[5]); // Now verify that the start/process/end order is correct. $allnotifs = $pr->get_all_notifications(); $this->assertEquals(count($allnotifs), count($snotifs) + count($enotifs) + count($chunks)); // Check integrity of the notifications. $errcount = $this->helper_check_notifications_order_integrity($allnotifs); $this->assertEquals(0, $errcount); } /** * Helper function that given one array of ordered start/process/end notifications will * check it of integrity like: * - process only happens if start is the previous notification * - end only happens if dispatch is the previous notification * - start only happen with level > than last one and if there is no already started like that * * @param array $notifications ordered array of notifications with format [start|process|end]:path * @return int number of integrity problems found (errors) */ function helper_check_notifications_order_integrity($notifications) { $numerrors = 0; $notifpile = array('pilebase' => 'start'); $lastnotif = 'start:pilebase'; foreach ($notifications as $notif) { $lastpiletype = end($notifpile); $lastpilepath = key($notifpile); $lastpilelevel = strlen(preg_replace('/[^\/]/', '', $lastpilepath)); $lastnotiftype = preg_replace('/:.*/', '', $lastnotif); $lastnotifpath = preg_replace('/.*:/', '', $lastnotif); $lastnotiflevel = strlen(preg_replace('/[^\/]/', '', $lastnotifpath)); $notiftype = preg_replace('/:.*/', '', $notif); $notifpath = preg_replace('/.*:/', '', $notif); $notiflevel = strlen(preg_replace('/[^\/]/', '', $notifpath)); switch ($notiftype) { case 'process': if ($lastnotifpath != $notifpath or $lastnotiftype != 'start') { $numerrors++; // Only start for same path from last notification is allowed before process } $notifpile[$notifpath] = 'process'; // Update the status in the pile break; case 'end': if ($lastpilepath != $notifpath or ($lastpiletype != 'process' and $lastpiletype != 'start')) { $numerrors++; // Only process and start for same path from last pile is allowed before end } unset($notifpile[$notifpath]); // Delete from the pile break; case 'start': if (array_key_exists($notifpath, $notifpile) or $notiflevel <= $lastpilelevel) { $numerrors++; // Only non existing in pile and with level > last pile is allowed on start } $notifpile[$notifpath] = 'start'; // Add to the pile break; default: $numerrors++; // Incorrect type of notification => error } // Update lastnotif $lastnotif = $notif; } return $numerrors; } } /* * helper processor able to perform various auto-cheks based on attributes while processing * the test1.xml file available in the fixtures dir. It performs these checks: * - name equal to "name" attribute of the tag (if present) * - level equal to "level" attribute of the tag (if present) * - path + tagname equal to "path" attribute of the tag (if present) * - cdata, if not empty is: * - equal to "value" attribute of the tag (if present) * - else, equal to tag name * * We pass the whole advanced_testcase object to the processor in order to be * able to perform the tests in the straight in the process */ class mock_auto_parser_processor extends progressive_parser_processor { private $utc = null; // To store the unit test case public function __construct($unit_test_case) { parent::__construct(); $this->utc = $unit_test_case; } public function process_chunk($data) { // Perform auto-checks based in the rules above if (isset($data['tags'])) { foreach ($data['tags'] as $tag) { if (isset($tag['attrs']['name'])) { // name tests $this->utc->assertEquals($tag['name'], $tag['attrs']['name']); } if (isset($tag['attrs']['level'])) { // level tests $this->utc->assertEquals($data['level'], $tag['attrs']['level']); } if (isset($tag['attrs']['path'])) { // path tests $this->utc->assertEquals(rtrim($data['path'], '/') . '/' . $tag['name'], $tag['attrs']['path']); } if (!empty($tag['cdata'])) { // cdata tests if (isset($tag['attrs']['value'])) { $this->utc->assertEquals($tag['cdata'], $tag['attrs']['value']); } else { $this->utc->assertEquals($tag['cdata'], $tag['name']); } } } } } } /* * helper processor that accumulates all the chunks, resturning them with the get_chunks() method */ class mock_parser_processor extends progressive_parser_processor { private $chunksarr = array(); // To accumulate the found chunks public function process_chunk($data) { $this->chunksarr[] = $data; } public function get_chunks() { return $this->chunksarr; } } /* * helper processor that accumulates simplified chunks, returning them with the get_chunks() method */ class mock_simplified_parser_processor extends simplified_parser_processor { private $chunksarr = array(); // To accumulate the found chunks private $startarr = array(); // To accumulate all the notified path starts private $endarr = array(); // To accumulate all the notified path ends private $allnotif = array(); // To accumulate all the notified and dispatched events in an ordered way public function dispatch_chunk($data) { $this->chunksarr[] = $data; $this->allnotif[] = 'process:' . $data['path']; } public function notify_path_start($path) { $this->startarr[] = $path; $this->allnotif[] = 'start:' . $path; } public function notify_path_end($path) { $this->endarr[] = $path; $this->allnotif[] = 'end:' . $path; } public function get_chunks() { return $this->chunksarr; } public function get_start_notifications() { return $this->startarr; } public function get_end_notifications() { return $this->endarr; } public function get_all_notifications() { return $this->allnotif; } } /* * helper processor that accumulates grouped chunks, returning them with the get_chunks() method */ class mock_grouped_parser_processor extends grouped_parser_processor { private $chunksarr = array(); // To accumulate the found chunks private $startarr = array(); // To accumulate all the notified path starts private $endarr = array(); // To accumulate all the notified path ends private $allnotif = array(); // To accumulate all the notified and dispatched events in an ordered way public function dispatch_chunk($data) { $this->chunksarr[] = $data; $this->allnotif[] = 'process:' . $data['path']; } public function notify_path_start($path) { $this->startarr[] = $path; $this->allnotif[] = 'start:' . $path; } public function notify_path_end($path) { $this->endarr[] = $path; $this->allnotif[] = 'end:' . $path; } public function get_chunks() { return $this->chunksarr; } public function get_start_notifications() { return $this->startarr; } public function get_end_notifications() { return $this->endarr; } public function get_all_notifications() { return $this->allnotif; } } util/xml/parser/processors/grouped_parser_processor.class.php 0000644 00000030227 15215711721 0020762 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @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/simplified_parser_processor.class.php'); /** * Abstract xml parser processor able to group chunks as configured * and dispatch them to other arbitrary methods * * This @progressive_parser_processor handles the requested paths, * allowing to group information under any of them, dispatching them * to the methods specified * * Note memory increases as you group more and more paths, so use it for * well-known structures being smaller enough (never to group MBs into one * in-memory structure) * * TODO: Complete phpdocs */ abstract class grouped_parser_processor extends simplified_parser_processor { protected $groupedpaths; // Paths we are requesting grouped protected $currentdata; // Where we'll be acummulating data // We create a array that stores each of the paths in a tree fashion // like the filesystem. Each element stores all the child elements that are // part of a full path that builds the grouped parent path we are storing. // eg Array keys are stored as follows; // root => a => b // => b // => c => d // => e => f. // Grouped paths here are; /a/b, /b, /c/d, /c/e/f. // There are no nested parent paths, that is an enforced rule so // we store an empty array to designate that the particular XML path element // is in fact a grouped path. // eg; $this->groupedparentprefixtree['a']['b'] = array(); /** @var array Search tree storing the grouped paths. */ protected $groupedparentprefixtree; /** * Keep cache of parent directory paths for XML parsing. * @var array */ protected $parentcache = array(); /** * Remaining space for parent directory paths. * @var integer */ protected $parentcacheavailablesize = 2048; public function __construct(array $paths = array()) { $this->groupedpaths = array(); $this->currentdata = null; parent::__construct($paths); } public function add_path($path, $grouped = false) { if ($grouped) { // Check there is no parent in the branch being grouped if ($found = $this->grouped_parent_exists($path)) { $a = new stdclass(); $a->path = $path; $a->parent = $found; throw new progressive_parser_exception('xml_grouped_parent_found', $a); } // Check there is no child in the branch being grouped if ($found = $this->grouped_child_exists($path)) { $a = new stdclass(); $a->path = $path; $a->child = $found; throw new progressive_parser_exception('xml_grouped_child_found', $a); } $this->groupedpaths[$path] = true; // We check earlier in the function if there is a parent that is above the path // to be added so we can be sure no parent exists in the tree. $patharray = explode('/', $path); $currentpos = &$this->groupedparentprefixtree; foreach ($patharray as $item) { if (!isset($currentpos[$item])) { $currentpos[$item] = array(); } // Update the current array position using a reference to allow in-place updates to the array. $currentpos = &$currentpos[$item]; } } parent::add_path($path); } /** * The parser fires this each time one path is going to be parsed * * @param string $path xml path which parsing has started */ public function before_path($path) { if ($this->path_is_grouped($path) and !isset($this->currentdata[$path])) { // If the grouped element itself does not contain any final tags, // we would not get any chunk data for it. So we add an artificial // empty data chunk here that will be eventually replaced with // real data later in {@link self::postprocess_chunk()}. $this->currentdata[$path] = array( 'path' => $path, 'level' => substr_count($path, '/') + 1, 'tags' => array(), ); } if (!$this->grouped_parent_exists($path)) { parent::before_path($path); } } /** * The parser fires this each time one path has been parsed * * @param string $path xml path which parsing has ended */ public function after_path($path) { // Have finished one grouped path, dispatch it if ($this->path_is_grouped($path)) { // Any accumulated information must be in // currentdata, properly built $data = $this->currentdata[$path]; unset($this->currentdata[$path]); // Always, before dispatching any chunk, send all pending start notifications. $this->process_pending_startend_notifications($path, 'start'); // TODO: If running under DEBUG_DEVELOPER notice about >1MB grouped chunks // And, finally, dispatch it. $this->dispatch_chunk($data); } // Normal notification of path end // Only if path is selected and not child of grouped if (!$this->grouped_parent_exists($path)) { parent::after_path($path); } } // Protected API starts here /** * Override this method so grouping will be happening here * also deciding between accumulating/dispatching */ protected function postprocess_chunk($data) { $path = $data['path']; // If the chunk is a grouped one, simply put it into currentdata if ($this->path_is_grouped($path)) { $this->currentdata[$path] = $data; // If the chunk is child of grouped one, add it to currentdata } else if ($grouped = $this->grouped_parent_exists($path)) { $this->build_currentdata($grouped, $data); $this->chunks--; // not counted, as it's accumulated // No grouped nor child of grouped, dispatch it } else { $this->dispatch_chunk($data); } } protected function path_is_grouped($path) { return isset($this->groupedpaths[$path]); } /** * Function that will look for any grouped * parent for the given path, returning it if found, * false if not */ protected function grouped_parent_exists($path) { // Search the tree structure to find out if one of the paths // above the $path is a grouped path. $patharray = explode('/', $this->get_parent_path($path)); $groupedpath = ''; $currentpos = &$this->groupedparentprefixtree; foreach ($patharray as $item) { // When the item isn't set in the array we know // there is no parent grouped path. if (!isset($currentpos[$item])) { return false; } // When we aren't at the start of the path, continue to build // a string representation of the path that is traversed. We will // return the grouped path to the caller if we find one. if ($item != '') { $groupedpath .= '/'.$item; } if ($currentpos[$item] == array()) { return $groupedpath; } $currentpos = &$currentpos[$item]; } return false; } /** * Get the parent path using a local cache for performance. * * @param $path string The pathname you wish to obtain the parent name for. * @return string The parent pathname. */ protected function get_parent_path($path) { if (!isset($this->parentcache[$path])) { $this->parentcache[$path] = progressive_parser::dirname($path); $this->parentcacheavailablesize--; if ($this->parentcacheavailablesize < 0) { // Older first is cheaper than LRU. We use 10% as items are grouped together and the large quiz // restore from MDL-40585 used only 600 parent paths. This is an XML heirarchy, so common paths // are grouped near each other. eg; /question_bank/question_category/question/element. After keeping // question_bank paths in the cache when we move to another area and the question_bank cache is not // useful any longer. $this->parentcache = array_slice($this->parentcache, 200, null, true); $this->parentcacheavailablesize += 200; } } return $this->parentcache[$path]; } /** * Function that will look for any grouped * child for the given path, returning it if found, * false if not */ protected function grouped_child_exists($path) { $childpath = $path . '/'; foreach ($this->groupedpaths as $groupedpath => $set) { if (strpos($groupedpath, $childpath) === 0) { return $groupedpath; } } return false; } /** * This function will accumulate the chunk into the specified * grouped element for later dispatching once it is complete */ protected function build_currentdata($grouped, $data) { // Check the grouped already exists into currentdata if (!is_array($this->currentdata) or !array_key_exists($grouped, $this->currentdata)) { $a = new stdclass(); $a->grouped = $grouped; $a->child = $data['path']; throw new progressive_parser_exception('xml_cannot_add_to_grouped', $a); } $this->add_missing_sub($grouped, $data['path'], $data['tags']); } /** * Add non-existing subarray elements */ protected function add_missing_sub($grouped, $path, $tags) { // Remember tag being processed $processedtag = basename($path); $info =& $this->currentdata[$grouped]['tags']; $hierarchyarr = explode('/', str_replace($grouped . '/', '', $path)); $previouselement = ''; $currentpath = ''; foreach ($hierarchyarr as $index => $element) { $currentpath = $currentpath . '/' . $element; // If element is already set and it's not // the processed one (with tags) fast move the $info // pointer and continue if ($element !== $processedtag && isset($info[$element])) { $previouselement = $element; $info =& $info[$element]; continue; } // If previous element already has occurrences // we move $info pointer there (only if last is // numeric occurrence) if (!empty($previouselement) && is_array($info) && count($info) > 0) { end($info); $key = key($info); if ((int) $key === $key) { $info =& $info[$key]; } } // Create element if not defined if (!isset($info[$element])) { // First into last element if present $info[$element] = array(); } // If element is the current one, add information if ($element === $processedtag) { $info[$element][] = $tags; } $previouselement = $element; $info =& $info[$element]; } } } util/xml/parser/processors/selective_like_parser_processor.class.php 0000644 00000003436 15215711721 0022306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Selective progressive_parser_processor that will send chunks straight * to output but only for chunks matching (in a left padded way - like) some defined paths */ class selective_like_parser_processor extends progressive_parser_processor { protected $paths; // array of paths we are interested on public function __construct(array $paths) { parent::__construct(); $this->paths = '=>' . implode('=>', $paths); } public function process_chunk($data) { if ($this->path_is_selected($data['path'])) { print_r($data); // Simply output chunk, for testing purposes } else { $this->chunks--; // Chunk skipped } } // Protected API starts here protected function path_is_selected($path) { return strpos('@=>' . $path, $this->paths); } } util/xml/parser/processors/null_parser_processor.class.php 0000644 00000002334 15215711721 0020265 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Null progressive_parser_processor that won't process chunks at all. * Useful for comparing memory use/execution time. */ class null_parser_processor extends progressive_parser_processor { public function process_chunk($data) { } } util/xml/parser/processors/simple_parser_processor.class.php 0000644 00000002463 15215711721 0020607 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Simple progressive_parser_processor that will send chunks straight * to output. Useful for testing, compare memory use/execution time. */ class simple_parser_processor extends progressive_parser_processor { public function process_chunk($data) { print_r($data); // Simply output chunk, for testing purposes } } util/xml/parser/processors/findpaths_parser_processor.class.php 0000644 00000004070 15215711721 0021272 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Find paths progressive_parser_processor that will search for all the paths present in * the chunks being returned. Useful to know the overal structure of the XML file. */ class findpaths_parser_processor extends progressive_parser_processor { protected $foundpaths; // array of paths foudn in the chunks received from the parser public function __construct() { parent::__construct(); $this->foundpaths = array(); } public function process_chunk($data) { if (isset($data['tags'])) { foreach ($data['tags'] as $tag) { $tagpath = $data['path'] . '/' . $tag['name']; if (!array_key_exists($tagpath, $this->foundpaths)) { $this->foundpaths[$tagpath] = 1; } else { $this->foundpaths[$tagpath]++; } } } } public function debug_info() { $debug = array(); foreach($this->foundpaths as $path => $chunks) { $debug['paths'][$path] = $chunks; } return array_merge($debug, parent::debug_info()); } } util/xml/parser/processors/progressive_parser_processor.class.php 0000644 00000005741 15215711721 0021670 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-xml * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This abstract class implements one progressive_parser_processor * * Processor that will receive chunks of data from the @progressive_parser * and will perform all sort of operations with them (join, split, invoke * other methods, output, whatever... * * You will need to extend this class to get the expected functionality * by implementing the @process_chunk() method to handle different * chunks of information and, optionally, the @process_cdata() to * process each cdata piece individually before being "published" to * the chunk processor. * * The "propietary array format" that the parser publishes to the @progressive_parser_procesor * is this: * array ( * 'path' => path where the tags belong to, * 'level'=> level (1-based) of the tags * 'tags => array ( * 'name' => name of the tag, * 'attrs'=> array( name of the attr => value of the attr), * 'cdata => cdata of the tag * ) * ) * * TODO: Finish phpdocs */ abstract class progressive_parser_processor { protected $inittime; // Initial microtime protected $chunks; // Number of chunks processed public function __construct() { $this->inittime= microtime(true); $this->chunks = 0; } /** * Receive one chunk of information from the parser */ abstract public function process_chunk($data); /** * The parser fires this each time one path is going to be parsed */ public function before_path($path) { } /** * The parser fires this each time one path has been parsed */ public function after_path($path) { } /** * Perform custom transformations in the processed cdata */ public function process_cdata($cdata) { return $cdata; } public function debug_info() { return array('memory' => memory_get_peak_usage(true), 'time' => microtime(true) - $this->inittime, 'chunks' => $this->chunks); } public function receive_chunk($data) { $this->chunks++; $this->process_chunk($data); } } util/xml/parser/processors/simplified_parser_processor.class.php 0000644 00000023415 15215711721 0021443 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Abstract xml parser processor to to simplify and dispatch parsed chunks * * This @progressive_parser_processor handles the requested paths, * performing some conversions from the original "propietary array format" * used by the @progressive_parser to a simplified structure to be used * easily. Found attributes are converted automatically to tags and cdata * to simpler values. * * Note: final tag attributes are discarded completely! * * TODO: Complete phpdocs */ abstract class simplified_parser_processor extends progressive_parser_processor { protected $paths; // array of paths we are interested on protected $parentpaths; // array of parent paths of the $paths protected $parentsinfo; // array of parent attributes to be added as child tags protected $startendinfo;// array (stack) of startend information public function __construct(array $paths = array()) { parent::__construct(); $this->paths = array(); $this->parentpaths = array(); $this->parentsinfo = array(); $this->startendinfo = array(); // Add paths and parentpaths. We are looking for attributes there foreach ($paths as $key => $path) { $this->add_path($path); } } public function add_path($path) { $this->paths[] = $path; $this->parentpaths[] = progressive_parser::dirname($path); } /** * Get the already simplified chunk and dispatch it */ abstract protected function dispatch_chunk($data); /** * Get one selected path and notify about start */ abstract protected function notify_path_start($path); /** * Get one selected path and notify about end */ abstract protected function notify_path_end($path); /** * Get one chunk of parsed data and make it simpler * adding attributes as tags and delegating to * dispatch_chunk() the procesing of the resulting chunk */ public function process_chunk($data) { // Precalculate some vars for readability $path = $data['path']; $parentpath = progressive_parser::dirname($path); $tag = basename($path); // If the path is a registered parent one, store all its tags // so, we'll be able to find attributes later when processing // (child) registered paths (to get attributes if present) if ($this->path_is_selected_parent($path)) { // if path is parent if (isset($data['tags'])) { // and has tags, save them $this->parentsinfo[$path] = $data['tags']; } } // If the path is a registered one, let's process it if ($this->path_is_selected($path)) { // Send all the pending notify_path_start/end() notifications $this->process_pending_startend_notifications($path, 'start'); // First of all, look for attributes available at parentsinfo // in order to get them available as normal tags if (isset($this->parentsinfo[$parentpath][$tag]['attrs'])) { $data['tags'] = array_merge($this->parentsinfo[$parentpath][$tag]['attrs'], $data['tags']); unset($this->parentsinfo[$parentpath][$tag]['attrs']); } // Now, let's simplify the tags array, ignoring tag attributtes and // reconverting to simpler name => value array. At the same time, // check for all the tag values being whitespace-string values, if all them // are whitespace strings, we aren't going to postprocess/dispatch the chunk $alltagswhitespace = true; foreach ($data['tags'] as $key => $value) { // If the value is already a single value, do nothing // surely was added above from parentsinfo attributes, // so we'll process the chunk always if (!is_array($value)) { $alltagswhitespace = false; continue; } // If the path including the tag name matches another selected path // (registered or parent) and is null or begins with linefeed, we know it's part // of another chunk, delete it, another chunk will contain that info if ($this->path_is_selected($path . '/' . $key) || $this->path_is_selected_parent($path . '/' . $key)) { if (!isset($value['cdata']) || substr($value['cdata'], 0, 1) === "\n") { unset($data['tags'][$key]); continue; } } // Convert to simple name => value array $data['tags'][$key] = isset($value['cdata']) ? $value['cdata'] : null; // Check $alltagswhitespace continues being true if ($alltagswhitespace && strlen($data['tags'][$key]) !== 0 && trim($data['tags'][$key]) !== '') { $alltagswhitespace = false; // Found non-whitespace value } } // Arrived here, if the chunk has tags and not all tags are whitespace, // send it to postprocess filter that will decide about dispatching. Else // skip the chunk completely if (!empty($data['tags']) && !$alltagswhitespace) { return $this->postprocess_chunk($data); } else { $this->chunks--; // Chunk skipped } } else { $this->chunks--; // Chunk skipped } return true; } /** * The parser fires this each time one path is going to be parsed * * @param string $path xml path which parsing has started */ public function before_path($path) { if ($this->path_is_selected($path)) { $this->startendinfo[] = array('path' => $path, 'action' => 'start'); } } /** * The parser fires this each time one path has been parsed * * @param string $path xml path which parsing has ended */ public function after_path($path) { $toprocess = false; // If the path being closed matches (same or parent) the first path in the stack // we process pending startend notifications until one matching end is found if ($element = reset($this->startendinfo)) { $elepath = $element['path']; $eleaction = $element['action']; if (strpos($elepath, $path) === 0) { $toprocess = true; } // Also, if the stack of startend notifications is empty, we can process current end // path safely } else { $toprocess = true; } if ($this->path_is_selected($path)) { $this->startendinfo[] = array('path' => $path, 'action' => 'end'); } // Send all the pending startend notifications if decided to do so if ($toprocess) { $this->process_pending_startend_notifications($path, 'end'); } } // Protected API starts here /** * Adjust start/end til finding one match start/end path (included) * * This will trigger all the pending {@see notify_path_start} and * {@see notify_path_end} calls for one given path and action * * @param string path the path to look for as limit * @param string action the action to look for as limit */ protected function process_pending_startend_notifications($path, $action) { // Iterate until one matching path and action is found (or the array is empty) $elecount = count($this->startendinfo); $elematch = false; while ($elecount > 0 && !$elematch) { $element = array_shift($this->startendinfo); $elecount--; $elepath = $element['path']; $eleaction = $element['action']; if ($elepath == $path && $eleaction == $action) { $elematch = true; } if ($eleaction == 'start') { $this->notify_path_start($elepath); } else { $this->notify_path_end($elepath); } } } protected function postprocess_chunk($data) { $this->dispatch_chunk($data); } protected function path_is_selected($path) { return in_array($path, $this->paths); } protected function path_is_selected_parent($path) { return in_array($path, $this->parentpaths); } /** * Returns the first selected parent if available or false */ protected function selected_parent_exists($path) { $parentpath = progressive_parser::dirname($path); while ($parentpath != '/') { if ($this->path_is_selected($parentpath)) { return $parentpath; } $parentpath = progressive_parser::dirname($parentpath); } return false; } } util/xml/parser/processors/selective_exact_parser_processor.class.php 0000644 00000003367 15215711721 0022471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage xml * @copyright 2003 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/progressive_parser_processor.class.php'); /** * Selective progressive_parser_processor that will send chunks straight * to output but only for chunks matching (in an exact way) some defined paths */ class selective_exact_parser_processor extends progressive_parser_processor { protected $paths; // array of paths we are interested on public function __construct(array $paths) { parent::__construct(); $this->paths = $paths; } public function process_chunk($data) { if ($this->path_is_selected($data['path'])) { print_r($data); // Simply output chunk, for testing purposes } else { $this->chunks--; // Chunk skipped } } // Protected API starts here protected function path_is_selected($path) { return in_array($path, $this->paths); } } util/xml/tests/fixtures/test1.xml 0000644 00000002327 15215711721 0013124 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <toptag name="toptag" level="1" path="/toptag"> <secondtag name="secondtag" level="2" path="/toptag/secondtag" value="secondvalue">secondvalue</secondtag> <thirdtag name="thirdtag" level="2" path="/toptag/thirdtag"> <onevalue name="onevalue" level="3" path="/toptag/thirdtag/onevalue">onevalue</onevalue> <onevalue name="onevalue" level="3" value="anothervalue">anothervalue</onevalue> <onevalue name="onevalue" level="3" value="yetanothervalue">yetanothervalue</onevalue> <twovalue name="twovalue" level="3" path="/toptag/thirdtag/twovalue">twovalue</twovalue> <forthtag name="forthtag" level="3" path="/toptag/thirdtag/forthtag"> <innervalue>innervalue</innervalue> <innertag> <superinnertag name="superinnertag" level="5"> <superinnervalue name="superinnervalue" level="6">superinnervalue</superinnervalue> </superinnertag> </innertag> </forthtag> <fifthtag level="3"> <sixthtag level="4"> <seventh level="5">seventh</seventh> </sixthtag> </fifthtag> <finalvalue name="finalvalue" level="3" path="/toptag/thirdtag/finalvalue">finalvalue</finalvalue> <finalvalue /> </thirdtag> </toptag> util/xml/tests/writer_test.php 0000644 00000034240 15215711721 0012554 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Test xml_writer tests. * * @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 memory_xml_output; use phpunit_util; use xml_contenttransformer; use xml_output; use xml_writer; use xml_writer_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.php'); require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php'); /** * Test xml_writer tests. * * @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 */ final class writer_test extends \basic_testcase { /** * test xml_writer public methods */ function test_xml_writer_public_api(): void { global $CFG; // Instantiate xml_output $xo = new memory_xml_output(); $this->assertTrue($xo instanceof xml_output); // Instantiate xml_writer with null xml_output try { $xw = new mock_xml_writer(null); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'invalid_xml_output'); } // Instantiate xml_writer with wrong xml_output object try { $xw = new mock_xml_writer(new \stdClass()); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'invalid_xml_output'); } // Instantiate xml_writer with wrong xml_contenttransformer object try { $xw = new mock_xml_writer($xo, new \stdClass()); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'invalid_xml_contenttransformer'); } // Instantiate xml_writer and start it twice $xw = new mock_xml_writer($xo); $xw->start(); try { $xw->start(); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_already_started'); } // Instantiate xml_writer and stop it twice $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->stop(); try { $xw->stop(); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_already_stopped'); } // Stop writer without starting it $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); try { $xw->stop(); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_not_started'); } // Start writer after stopping it $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->stop(); try { $xw->start(); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_already_stopped'); } // Try to set prologue/schema after start $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); try { $xw->set_nonamespace_schema('http://moodle.org'); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_already_started'); } try { $xw->set_prologue('sweet prologue'); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_already_started'); } // Instantiate properly with memory_xml_output, start and stop. // Must get default UTF-8 prologue $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->stop(); $this->assertEquals($xo->get_allcontents(), $xw->get_default_prologue()); // Instantiate, set prologue and schema, put 1 full tag and get results $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->set_prologue('CLEARLY WRONG PROLOGUE'); $xw->set_nonamespace_schema('http://moodle.org/littleschema'); $xw->start(); $xw->full_tag('TEST', 'Hello World!', array('id' => 1)); $xw->stop(); $result = $xo->get_allcontents(); // Perform various checks $this->assertEquals(strpos($result, 'WRONG'), 8); $this->assertEquals(strpos($result, '<TEST id="1"'), 22); $this->assertEquals(strpos($result, 'xmlns:xsi='), 39); $this->assertEquals(strpos($result, 'http://moodle.org/littleschema'), 128); $this->assertEquals(strpos($result, 'Hello World'), 160); $this->assertFalse(strpos($result, $xw->get_default_prologue())); // Try to close one tag in wrong order $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->begin_tag('first'); $xw->begin_tag('second'); try { $xw->end_tag('first'); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_end_tag_no_match'); } // Try to close one tag before starting any tag $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); try { $xw->end_tag('first'); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_end_tag_no_match'); } // Full tag without contents (null and empty string) $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('tagname', null, array('attrname' => 'attrvalue')); $xw->full_tag('tagname2', '', array('attrname' => 'attrvalue')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<tagname attrname="attrvalue" /><tagname2 attrname="attrvalue"></tagname2>'); // Test case-folding is working $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo, null, true); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('tagname', 'textcontent', array('attrname' => 'attrvalue')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<TAGNAME ATTRNAME="attrvalue">textcontent</TAGNAME>'); // Test UTF-8 chars in tag and attribute names, attr values and contents $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('áéíóú', 'ÁÉÍÓÚ', array('àèìòù' => 'ÀÈÌÒÙ')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<áéíóú àèìòù="ÀÈÌÒÙ">ÁÉÍÓÚ</áéíóú>'); // Try non-safe content in attributes $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('tagname', 'textcontent', array('attrname' => 'attr' . chr(27) . '\'"value')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<tagname attrname="attr\'"value">textcontent</tagname>'); // Try non-safe content in text $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('tagname', "text\r\ncontent\rwith" . chr(27), array('attrname' => 'attrvalue')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<tagname attrname="attrvalue">text' . "\ncontent\n" . 'with</tagname>'); // Try to stop the writer without clossing all the open tags $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->begin_tag('first'); try { $xw->stop(); $this->assertTrue(false, 'xml_writer_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof xml_writer_exception); $this->assertEquals($e->errorcode, 'xml_writer_open_tags_remaining'); } // Test simple transformer $xo = new memory_xml_output(); $xt = new mock_xml_contenttransformer(); $xw = new mock_xml_writer($xo, $xt); $xw->set_prologue(''); // empty prologue for easier matching $xw->start(); $xw->full_tag('tagname', null, array('attrname' => 'attrvalue')); $xw->full_tag('tagname2', 'somecontent', array('attrname' => 'attrvalue')); $xw->stop(); $result = $xo->get_allcontents(); $this->assertEquals($result, '<tagname attrname="attrvalue" /><tagname2 attrname="attrvalue">testsomecontent</tagname2>'); // Build a complex XML file and test results against stored file in fixtures $xo = new memory_xml_output(); $xw = new mock_xml_writer($xo); $xw->start(); $xw->begin_tag('toptag', array('name' => 'toptag', 'level' => 1, 'path' => '/toptag')); $xw->full_tag('secondtag', 'secondvalue', array('name' => 'secondtag', 'level' => 2, 'path' => '/toptag/secondtag', 'value' => 'secondvalue')); $xw->begin_tag('thirdtag', array('name' => 'thirdtag', 'level' => 2, 'path' => '/toptag/thirdtag')); $xw->full_tag('onevalue', 'onevalue', array('name' => 'onevalue', 'level' => 3, 'path' => '/toptag/thirdtag/onevalue')); $xw->full_tag('onevalue', 'anothervalue', array('name' => 'onevalue', 'level' => 3, 'value' => 'anothervalue')); $xw->full_tag('onevalue', 'yetanothervalue', array('name' => 'onevalue', 'level' => 3, 'value' => 'yetanothervalue')); $xw->full_tag('twovalue', 'twovalue', array('name' => 'twovalue', 'level' => 3, 'path' => '/toptag/thirdtag/twovalue')); $xw->begin_tag('forthtag', array('name' => 'forthtag', 'level' => 3, 'path' => '/toptag/thirdtag/forthtag')); $xw->full_tag('innervalue', 'innervalue'); $xw->begin_tag('innertag'); $xw->begin_tag('superinnertag', array('name' => 'superinnertag', 'level' => 5)); $xw->full_tag('superinnervalue', 'superinnervalue', array('name' => 'superinnervalue', 'level' => 6)); $xw->end_tag('superinnertag'); $xw->end_tag('innertag'); $xw->end_tag('forthtag'); $xw->begin_tag('fifthtag', array('level' => 3)); $xw->begin_tag('sixthtag', array('level' => 4)); $xw->full_tag('seventh', 'seventh', array('level' => 5)); $xw->end_tag('sixthtag'); $xw->end_tag('fifthtag'); $xw->full_tag('finalvalue', 'finalvalue', array('name' => 'finalvalue', 'level' => 3, 'path' => '/toptag/thirdtag/finalvalue')); $xw->full_tag('finalvalue'); $xw->end_tag('thirdtag'); $xw->end_tag('toptag'); $xw->stop(); $result = $xo->get_allcontents(); $fcontents = file_get_contents($CFG->dirroot . '/backup/util/xml/tests/fixtures/test1.xml'); // Normalise carriage return characters. $fcontents = phpunit_util::normalise_line_endings($fcontents); $this->assertEquals(trim($result), trim($fcontents)); } } /* * helper extended xml_writer class that makes some methods public for testing */ class mock_xml_writer extends xml_writer { public function get_default_prologue() { return parent::get_default_prologue(); } } /* * helper extended xml_contenttransformer prepending "test" to all the notnull contents */ class mock_xml_contenttransformer extends xml_contenttransformer { public function process($content) { return is_null($content) ? null : 'test' . $content; } } util/dbops/backup_plan_dbops.class.php 0000644 00000026107 15215711721 0014123 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB support to the @backup_plan class * * This class contains various static methods available for all the DB operations * performed by the @backup_plan (and builder) classes * * TODO: Finish phpdocs */ abstract class backup_plan_dbops extends backup_dbops { /** * Given one course module id, return one array with all the block intances that belong to it */ public static function get_blockids_from_moduleid($moduleid) { global $DB; // Get the context of the module $contextid = context_module::instance($moduleid)->id; // Get all the block instances which parentcontextid is the module contextid $blockids = array(); $instances = $DB->get_records('block_instances', array('parentcontextid' => $contextid), '', 'id'); foreach ($instances as $instance) { $blockids[] = $instance->id; } return $blockids; } /** * Given one course id, return one array with all the block intances that belong to it */ public static function get_blockids_from_courseid($courseid) { global $DB; // Get the context of the course $contextid = context_course::instance($courseid)->id; // Get all the block instances which parentcontextid is the course contextid $blockids = array(); $instances = $DB->get_records('block_instances', array('parentcontextid' => $contextid), '', 'id'); foreach ($instances as $instance) { $blockids[] = $instance->id; } return $blockids; } /** * Given one section id, return one array with all the course modules that belong to it */ public static function get_modules_from_sectionid($sectionid) { global $DB; // Get the course and sequence of the section $secrec = $DB->get_record('course_sections', array('id' => $sectionid), 'course, sequence'); $courseid = $secrec->course; // Get the section->sequence contents (it roots the activities order) // Get all course modules belonging to requested section $modulesarr = array(); $modules = $DB->get_records_sql(" SELECT cm.id, m.name AS modname FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module WHERE cm.course = ? AND cm.section = ? AND cm.deletioninprogress <> 1", array($courseid, $sectionid)); foreach (explode(',', (string) $secrec->sequence) as $moduleid) { if (isset($modules[$moduleid])) { $module = array('id' => $modules[$moduleid]->id, 'modname' => $modules[$moduleid]->modname); $modulesarr[] = (object)$module; unset($modules[$moduleid]); } } if (!empty($modules)) { // This shouldn't happen, but one borked sequence can lead to it. Add the rest foreach ($modules as $module) { $module = array('id' => $module->id, 'modname' => $module->modname); $modulesarr[] = (object)$module; } } return $modulesarr; } /** * Given one course id, return one array with all the course_sections belonging to it */ public static function get_sections_from_courseid($courseid) { global $DB; // Get all sections belonging to requested course $sectionsarr = array(); $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section'); foreach ($sections as $section) { $sectionsarr[] = $section->id; } return $sectionsarr; } /** * Given one section id, returns the full section record. * * @param int $sectionid * @return stdClass */ public static function get_section_from_id($sectionid): stdClass { global $DB; return $DB->get_record('course_sections', ['id' => $sectionid]); } /** * Given one course id, return its format in DB */ public static function get_courseformat_from_courseid($courseid) { global $DB; return $DB->get_field('course', 'format', array('id' => $courseid)); } /** * Given a course id, returns its theme. This can either be the course * theme or (if not specified in course) the category, site theme. * * User, session, and inherited-from-mnet themes cannot have backed-up * per course data. This is course-related data so it must be in a course * theme specified as part of the course structure * @param int $courseid * @return string Name of course theme * @see moodle_page#resolve_theme() */ public static function get_theme_from_courseid($courseid) { global $DB, $CFG; // Course theme first if (!empty($CFG->allowcoursethemes)) { $theme = $DB->get_field('course', 'theme', array('id' => $courseid)); if ($theme) { return $theme; } } // Category themes in reverse order if (!empty($CFG->allowcategorythemes)) { $catid = $DB->get_field('course', 'category', array('id' => $courseid)); while($catid) { $category = $DB->get_record('course_categories', array('id'=>$catid), 'theme,parent', MUST_EXIST); if ($category->theme) { return $category->theme; } $catid = $category->parent; } } // Finally use site theme return $CFG->theme; } /** * Return the wwwroot of the $CFG->mnet_localhost_id host * caching it along the request */ public static function get_mnet_localhost_wwwroot() { global $CFG, $DB; static $wwwroot = null; if (is_null($wwwroot)) { $wwwroot = $DB->get_field('mnet_host', 'wwwroot', array('id' => $CFG->mnet_localhost_id)); } return $wwwroot; } /** * Returns the default backup filename, based in passed params. * * Default format is (see MDL-22145) * backup word - format - type - name - date - info . mbz * where name is variable (course shortname, section name/id, activity modulename + cmid) * and info can be (nu = no user info, an = anonymized). The last param $useidasname, * defaulting to false, allows to replace the course shortname by the course id (used * by automated backups, to avoid non-ascii chars in OS filesystem) * * @param string $format One of backup::FORMAT_ * @param string $type One of backup::TYPE_ * @param int $courseid/$sectionid/$cmid * @param bool $users Should be true is users were included in the backup * @param bool $anonymised Should be true is user information was anonymized. * @param bool $useidonly only use the ID in the file name * @return string The filename to use */ public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidonly = false, $files = true) { global $DB; // Calculate backup word $backupword = str_replace(' ', '_', core_text::strtolower(get_string('backupfilename'))); $backupword = trim(clean_filename($backupword), '_'); // Not $useidonly, lets fetch the name $shortname = ''; if (!$useidonly) { // Calculate proper name element (based on type) switch ($type) { case backup::TYPE_1COURSE: $shortname = $DB->get_field('course', 'shortname', array('id' => $id)); $context = context_course::instance($id); $shortname = format_string($shortname, true, array('context' => $context)); break; case backup::TYPE_1SECTION: if (!$shortname = $DB->get_field('course_sections', 'name', array('id' => $id))) { $shortname = $DB->get_field('course_sections', 'section', array('id' => $id)); } break; case backup::TYPE_1ACTIVITY: $cm = get_coursemodule_from_id(null, $id); $shortname = $cm->modname . $id; break; } $shortname = str_replace(' ', '_', $shortname); $shortname = core_text::strtolower(trim(clean_filename($shortname), '_')); } // The name will always contain the ID, but we append the course short name if requested. $name = $id; if (!$useidonly && $shortname != '') { $name .= '-' . $shortname; } // Calculate date $backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig')); $date = userdate(time(), $backupdateformat, 99, false); $date = core_text::strtolower(trim(clean_filename($date), '_')); // Calculate info $info = ''; if (!$users) { $info = '-nu'; } else if ($anonymised) { $info = '-an'; } // Indicate if backup doesn't contain files. if (!$files) { $info .= '-nf'; } return $backupword . '-' . $format . '-' . $type . '-' . $name . '-' . $date . $info . '.mbz'; } /** * Returns a flag indicating the need to backup gradebook elements like calculated grade items and category visibility * If all activity related grade items are being backed up we can also backup calculated grade items and categories */ public static function require_gradebook_backup($courseid, $backupid) { global $DB; $sql = "SELECT count(id) FROM {grade_items} WHERE courseid=:courseid AND itemtype = 'mod' AND id NOT IN ( SELECT bi.itemid FROM {backup_ids_temp} bi WHERE bi.itemname = 'grade_itemfinal' AND bi.backupid = :backupid)"; $params = array('courseid'=>$courseid, 'backupid'=>$backupid); $count = $DB->count_records_sql($sql, $params); //if there are 0 activity grade items not already included in the backup return $count == 0; } } util/dbops/backup_question_dbops.class.php 0000644 00000012004 15215711721 0015027 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB support to the questions backup stuff * * This class contains various static methods available for all the DB operations * performed by questions stuff * * TODO: Finish phpdocs */ abstract class backup_question_dbops extends backup_dbops { /** * Calculates all the question_categories to be included * in backup, based in a given context (course/module) and * the already annotated questions present in backup_ids_temp */ public static function calculate_question_categories($backupid, $contextid) { global $DB; // First step, annotate all the categories for the given context (course/module) // i.e. the whole context questions bank $DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid) SELECT ?, 'question_category', id FROM {question_categories} WHERE contextid = ?", array($backupid, $contextid)); // Now, based in the annotated questions, annotate all the categories they // belong to (whole context question banks too) // First, get all the contexts we are going to save their question bank (no matter // where they are in the contexts hierarchy, transversals... whatever) $contexts = $DB->get_fieldset_sql("SELECT DISTINCT qc2.contextid FROM {question_categories} qc2 JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc2.id JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id JOIN {question} q ON q.id = qv.questionid JOIN {backup_ids_temp} bi ON bi.itemid = q.id WHERE bi.backupid = ? AND bi.itemname = 'question' AND qc2.contextid != ?", array($backupid, $contextid)); // Calculate and get the set reference records. $setreferencecontexts = $DB->get_fieldset_sql(" SELECT DISTINCT qc.contextid FROM {question_categories} qc JOIN {question_set_references} qsr ON qsr.questionscontextid = qc.contextid WHERE qsr.usingcontextid = ?", [$contextid]); foreach ($setreferencecontexts as $setreferencecontext) { if (!in_array($setreferencecontext, $contexts) && (int)$setreferencecontext !== $contextid) { $contexts [] = $setreferencecontext; } } // Calculate the get the reference records. $referencecontexts = $DB->get_fieldset_sql(" SELECT DISTINCT qc.contextid FROM {question_categories} qc JOIN {question_bank_entries} qbe ON qbe.questioncategoryid = qc.id JOIN {question_references} qr ON qr.questionbankentryid = qbe.id WHERE qr.usingcontextid =?", [$contextid]); foreach ($referencecontexts as $referencecontext) { if (!in_array($referencecontext, $contexts) && (int)$referencecontext !== $contextid) { $contexts [] = $referencecontext; } } // And now, simply insert all the question categories (complete question bank) // for those contexts if we have found any if ($contexts) { list($contextssql, $contextparams) = $DB->get_in_or_equal($contexts); $params = array_merge(array($backupid), $contextparams); $DB->execute("INSERT INTO {backup_ids_temp} (backupid, itemname, itemid) SELECT ?, 'question_category', id FROM {question_categories} WHERE contextid $contextssql", $params); } } /** * Delete all the annotated questions present in backup_ids_temp */ public static function delete_temp_questions($backupid) { global $DB; $DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'question')); } } util/dbops/restore_dbops.class.php 0000644 00000302012 15215711721 0013317 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB operations * * TODO: Finish phpdocs */ abstract class restore_dbops { /** * Keep cache of backup records. * @var array * @todo MDL-25290 static should be replaced with MUC code. */ private static $backupidscache = array(); /** * Keep track of backup ids which are cached. * @var array * @todo MDL-25290 static should be replaced with MUC code. */ private static $backupidsexist = array(); /** * Count is expensive, so manually keeping track of * backupidscache, to avoid memory issues. * @var int * @todo MDL-25290 static should be replaced with MUC code. */ private static $backupidscachesize = 2048; /** * Count is expensive, so manually keeping track of * backupidsexist, to avoid memory issues. * @var int * @todo MDL-25290 static should be replaced with MUC code. */ private static $backupidsexistsize = 10240; /** * Slice backupids cache to add more data. * @var int * @todo MDL-25290 static should be replaced with MUC code. */ private static $backupidsslice = 512; /** * Return one array containing all the tasks that have been included * in the restore process. Note that these tasks aren't built (they * haven't steps nor ids data available) */ public static function get_included_tasks($restoreid) { $rc = restore_controller_dbops::load_controller($restoreid); $tasks = $rc->get_plan()->get_tasks(); $includedtasks = array(); foreach ($tasks as $key => $task) { // Calculate if the task is being included $included = false; // blocks, based in blocks setting and parent activity/course if ($task instanceof restore_block_task) { if (!$task->get_setting_value('blocks')) { // Blocks not included, continue continue; } $parent = basename(dirname(dirname($task->get_taskbasepath()))); if ($parent == 'course') { // Parent is course, always included if present $included = true; } else { // Look for activity_included setting $included = $task->get_setting_value($parent . '_included'); } // ativities, based on included setting } else if ($task instanceof restore_activity_task) { $included = $task->get_setting_value('included'); // sections, based on included setting } else if ($task instanceof restore_section_task) { $included = $task->get_setting_value('included'); // course always included if present } else if ($task instanceof restore_course_task) { $included = true; } // If included, add it if ($included) { $includedtasks[] = clone($task); // A clone is enough. In fact we only need the basepath. } } $rc->destroy(); // Always need to destroy. return $includedtasks; } /** * Load one inforef.xml file to backup_ids table for future reference * * @param string $restoreid Restore id * @param string $inforeffile File path * @param \core\progress\base $progress Progress tracker */ public static function load_inforef_to_tempids($restoreid, $inforeffile, ?\core\progress\base $progress = null) { if (!file_exists($inforeffile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_inforef_xml_file', $inforeffile); } // Set up progress tracking (indeterminate). if (!$progress) { $progress = new \core\progress\none(); } $progress->start_progress('Loading inforef.xml file'); // Let's parse, custom processor will do its work, sending info to DB $xmlparser = new progressive_parser(); $xmlparser->set_file($inforeffile); $xmlprocessor = new restore_inforef_parser_processor($restoreid); $xmlparser->set_processor($xmlprocessor); $xmlparser->set_progress($progress); $xmlparser->process(); // Finish progress $progress->end_progress(); } /** * Load the needed role.xml file to backup_ids table for future reference */ public static function load_roles_to_tempids($restoreid, $rolesfile) { if (!file_exists($rolesfile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_roles_xml_file', $rolesfile); } // Let's parse, custom processor will do its work, sending info to DB $xmlparser = new progressive_parser(); $xmlparser->set_file($rolesfile); $xmlprocessor = new restore_roles_parser_processor($restoreid); $xmlparser->set_processor($xmlprocessor); $xmlparser->process(); } /** * Precheck the loaded roles, return empty array if everything is ok, and * array with 'errors', 'warnings' elements (suitable to be used by restore_prechecks) * with any problem found. At the same time, store all the mapping into backup_ids_temp * and also put the information into $rolemappings (controller->info), so it can be reworked later by * post-precheck stages while at the same time accept modified info in the same object coming from UI */ public static function precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { global $DB; $problems = array(); // To store warnings/errors // Get loaded roles from backup_ids $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info'); foreach ($rs as $recrole) { // If the rolemappings->modified flag is set, that means that we are coming from // manually modified mappings (by UI), so accept those mappings an put them to backup_ids if ($rolemappings->modified) { $target = $rolemappings->mappings[$recrole->itemid]->targetroleid; self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $target); // Else, we haven't any info coming from UI, let's calculate the mappings, matching // in multiple ways and checking permissions. Note mapping to 0 means "skip" } else { $role = (object)backup_controller_dbops::decode_backup_temp_info($recrole->info); $match = self::get_best_assignable_role($role, $courseid, $userid, $samesite); // Send match to backup_ids self::set_backup_ids_record($restoreid, 'role', $recrole->itemid, $match); // Build the rolemappings element for controller unset($role->id); unset($role->nameincourse); $role->targetroleid = $match; $rolemappings->mappings[$recrole->itemid] = $role; // Prepare warning if no match found if (!$match) { $problems['warnings'][] = get_string('cannotfindassignablerole', 'backup', $role->name); } } } $rs->close(); return $problems; } /** * Return cached backup id's * * @param int $restoreid id of backup * @param string $itemname name of the item * @param int $itemid id of item * @return stdClass|false record from 'backup_ids_temp' table * @todo MDL-25290 replace static backupids* with MUC code */ protected static function get_backup_ids_cached($restoreid, $itemname, $itemid) { global $DB; $key = "$itemid $itemname $restoreid"; // If record exists in cache then return. if (isset(self::$backupidsexist[$key]) && isset(self::$backupidscache[$key])) { // Return a copy of cached data, to avoid any alterations in cached data. return clone self::$backupidscache[$key]; } // Clean cache, if it's full. if (self::$backupidscachesize <= 0) { // Remove some records, to keep memory in limit. self::$backupidscache = array_slice(self::$backupidscache, self::$backupidsslice, null, true); self::$backupidscachesize = self::$backupidscachesize + self::$backupidsslice; } if (self::$backupidsexistsize <= 0) { self::$backupidsexist = array_slice(self::$backupidsexist, self::$backupidsslice, null, true); self::$backupidsexistsize = self::$backupidsexistsize + self::$backupidsslice; } // Retrive record from database. $record = array( 'backupid' => $restoreid, 'itemname' => $itemname, 'itemid' => $itemid ); if ($dbrec = $DB->get_record('backup_ids_temp', $record)) { self::$backupidsexist[$key] = $dbrec->id; self::$backupidscache[$key] = $dbrec; self::$backupidscachesize--; self::$backupidsexistsize--; return $dbrec; } else { return false; } } /** * Cache backup ids' * * @param int $restoreid id of backup * @param string $itemname name of the item * @param int $itemid id of item * @param array $extrarecord extra record which needs to be updated * @return void * @todo MDL-25290 replace static BACKUP_IDS_* with MUC code */ protected static function set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord) { global $DB; $key = "$itemid $itemname $restoreid"; $record = array( 'backupid' => $restoreid, 'itemname' => $itemname, 'itemid' => $itemid, ); // If record is not cached then add one. if (!isset(self::$backupidsexist[$key])) { // If we have this record in db, then just update this. if ($existingrecord = $DB->get_record('backup_ids_temp', $record)) { self::$backupidsexist[$key] = $existingrecord->id; self::$backupidsexistsize--; self::update_backup_cached_record($record, $extrarecord, $key, $existingrecord); } else { // Add new record to cache and db. $recorddefault = array ( 'newitemid' => 0, 'parentitemid' => null, 'info' => null); $record = array_merge($record, $recorddefault, $extrarecord); $record['id'] = $DB->insert_record('backup_ids_temp', $record); self::$backupidsexist[$key] = $record['id']; self::$backupidsexistsize--; if (self::$backupidscachesize > 0) { // Cache new records if we haven't got many yet. self::$backupidscache[$key] = (object) $record; self::$backupidscachesize--; } } } else { self::update_backup_cached_record($record, $extrarecord, $key); } } /** * Updates existing backup record * * @param array $record record which needs to be updated * @param array $extrarecord extra record which needs to be updated * @param string $key unique key which is used to identify cached record * @param stdClass $existingrecord (optional) existing record */ protected static function update_backup_cached_record($record, $extrarecord, $key, $existingrecord = null) { global $DB; // Update only if extrarecord is not empty. if (!empty($extrarecord)) { $extrarecord['id'] = self::$backupidsexist[$key]; $DB->update_record('backup_ids_temp', $extrarecord); // Update existing cache or add new record to cache. if (isset(self::$backupidscache[$key])) { $record = array_merge((array)self::$backupidscache[$key], $extrarecord); self::$backupidscache[$key] = (object) $record; } else if (self::$backupidscachesize > 0) { if ($existingrecord) { self::$backupidscache[$key] = $existingrecord; } else { // Retrive record from database and cache updated records. self::$backupidscache[$key] = $DB->get_record('backup_ids_temp', $record); } $record = array_merge((array)self::$backupidscache[$key], $extrarecord); self::$backupidscache[$key] = (object) $record; self::$backupidscachesize--; } } } /** * Reset the ids caches completely * * Any destructive operation (partial delete, truncate, drop or recreate) performed * with the backup_ids table must cause the backup_ids caches to be * invalidated by calling this method. See MDL-33630. * * Note that right now, the only operation of that type is the recreation * (drop & restore) of the table that may happen once the prechecks have ended. All * the rest of operations are always routed via {@link set_backup_ids_record()}, 1 by 1, * keeping the caches on sync. * * @todo MDL-25290 static should be replaced with MUC code. */ public static function reset_backup_ids_cached() { // Reset the ids cache. $cachetoadd = count(self::$backupidscache); self::$backupidscache = array(); self::$backupidscachesize = self::$backupidscachesize + $cachetoadd; // Reset the exists cache. $existstoadd = count(self::$backupidsexist); self::$backupidsexist = array(); self::$backupidsexistsize = self::$backupidsexistsize + $existstoadd; } /** * Given one role, as loaded from XML, perform the best possible matching against the assignable * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid) * returning the id of the best matching role or 0 if no match is found */ protected static function get_best_assignable_role($role, $courseid, $userid, $samesite) { global $CFG, $DB; // Gather various information about roles $coursectx = context_course::instance($courseid); $assignablerolesshortname = get_assignable_roles($coursectx, ROLENAME_SHORT, false, $userid); // Note: under 1.9 we had one function restore_samerole() that performed one complete // matching of roles (all caps) and if match was found the mapping was availabe bypassing // any assignable_roles() security. IMO that was wrong and we must not allow such // mappings anymore. So we have left that matching strategy out in 2.0 // Empty assignable roles, mean no match possible if (empty($assignablerolesshortname)) { return 0; } // Match by shortname if ($match = array_search($role->shortname, $assignablerolesshortname)) { return $match; } // Match by archetype list($in_sql, $in_params) = $DB->get_in_or_equal(array_keys($assignablerolesshortname)); $params = array_merge(array($role->archetype), $in_params); if ($rec = $DB->get_record_select('role', "archetype = ? AND id $in_sql", $params, 'id', IGNORE_MULTIPLE)) { return $rec->id; } // Match editingteacher to teacher (happens a lot, from 1.9) if ($role->shortname == 'editingteacher' && in_array('teacher', $assignablerolesshortname)) { return array_search('teacher', $assignablerolesshortname); } // No match, return 0 return 0; } /** * Process the loaded roles, looking for their best mapping or skipping * Any error will cause exception. Note this is one wrapper over * precheck_included_roles, that contains all the logic, but returns * errors/warnings instead and is executed as part of the restore prechecks */ public static function process_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings) { global $DB; // Just let precheck_included_roles() to do all the hard work $problems = self::precheck_included_roles($restoreid, $courseid, $userid, $samesite, $rolemappings); // With problems of type error, throw exception, shouldn't happen if prechecks executed if (array_key_exists('errors', $problems)) { throw new restore_dbops_exception('restore_problems_processing_roles', null, implode(', ', $problems['errors'])); } } /** * Load the needed users.xml file to backup_ids table for future reference * * @param string $restoreid Restore id * @param string $usersfile File path * @param \core\progress\base $progress Progress tracker */ public static function load_users_to_tempids($restoreid, $usersfile, ?\core\progress\base $progress = null) { if (!file_exists($usersfile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_users_xml_file', $usersfile); } // Set up progress tracking (indeterminate). if (!$progress) { $progress = new \core\progress\none(); } $progress->start_progress('Loading users into temporary table'); // Let's parse, custom processor will do its work, sending info to DB $xmlparser = new progressive_parser(); $xmlparser->set_file($usersfile); $xmlprocessor = new restore_users_parser_processor($restoreid); $xmlparser->set_processor($xmlprocessor); $xmlparser->set_progress($progress); $xmlparser->process(); // Finish progress. $progress->end_progress(); } /** * Load the needed questions.xml file to backup_ids table for future reference */ public static function load_categories_and_questions_to_tempids($restoreid, $questionsfile) { if (!file_exists($questionsfile)) { // Shouldn't happen ever, but... throw new backup_helper_exception('missing_questions_xml_file', $questionsfile); } // Let's parse, custom processor will do its work, sending info to DB $xmlparser = new progressive_parser(); $xmlparser->set_file($questionsfile); $xmlprocessor = new restore_questions_parser_processor($restoreid); $xmlparser->set_processor($xmlprocessor); $xmlparser->process(); } /** * Check all the included categories and questions, deciding the action to perform * for each one (mapping / creation) and returning one array of problems in case * something is wrong. * * There are some basic rules that the method below will always try to enforce: * * Rule1: Targets will be, always, calculated for *whole* question banks (a.k.a. contexid source), * so, given 2 question categories belonging to the same bank, their target bank will be * always the same. If not, we can be incurring into "fragmentation", leading to random/cloze * problems (qtypes having "child" questions). * * Rule2: The 'moodle/question:managecategory' and 'moodle/question:add' capabilities will be * checked before creating any category/question respectively and, if the cap is not allowed * into upper contexts (system, coursecat)) but in lower ones (course), the *whole* question bank * will be created there. * * Rule3: Coursecat question banks not existing in the target site will be created as course * (lower ctx) question banks, never as "guessed" coursecat question banks base on depth or so. * * Rule4: System question banks will be created at system context if user has perms to do so. Else they * will created as course (lower ctx) question banks (similary to rule3). In other words, course ctx * if always a fallback for system and coursecat question banks. * * Also, there are some notes to clarify the scope of this method: * * Note1: This method won't create any question category nor question at all. It simply will calculate * which actions (create/map) must be performed for each element and where, validating that all those * actions are doable by the user executing the restore operation. Any problem found will be * returned in the problems array, causing the restore process to stop with error. * * Note2: To decide if one question bank (all its question categories and questions) is going to be remapped, * then all the categories and questions must exist in the same target bank. If able to do so, missing * qcats and qs will be created (rule2). But if, at the end, something is missing, the whole question bank * will be recreated at course ctx (rule1), no matter if that duplicates some categories/questions. * * Note3: We'll be using the newitemid column in the temp_ids table to store the action to be performed * with each question category and question. newitemid = 0 means the qcat/q needs to be created and * any other value means the qcat/q is mapped. Also, for qcats, parentitemid will contain the target * context where the categories have to be created (but for module contexts where we'll keep the old * one until the activity is created) * * Note4: All these "actions" will be "executed" later by {@link restore_create_categories_and_questions} */ public static function precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite) { $problems = array(); // TODO: Check all qs, looking their qtypes are restorable // Precheck all qcats and qs looking for target contexts / warnings / errors list($syserr, $syswarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_SYSTEM); list($caterr, $catwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSECAT); list($couerr, $couwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_COURSE); list($moderr, $modwarn) = self::prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, CONTEXT_MODULE); // Acummulate and handle errors and warnings $errors = array_merge($syserr, $caterr, $couerr, $moderr); $warnings = array_merge($syswarn, $catwarn, $couwarn, $modwarn); if (!empty($errors)) { $problems['errors'] = $errors; } if (!empty($warnings)) { $problems['warnings'] = $warnings; } return $problems; } /** * This function will process all the question banks present in restore * at some contextlevel (from CONTEXT_SYSTEM to CONTEXT_MODULE), finding * the target contexts where each bank will be restored and returning * warnings/errors as needed. * * Some contextlevels (system, coursecat), will delegate process to * course level if any problem is found (lack of permissions, non-matching * target context...). Other contextlevels (course, module) will * cause return error if some problem is found. * * At the end, if no errors were found, all the categories in backup_temp_ids * will be pointing (parentitemid) to the target context where they must be * created later in the restore process. * * Note: at the time these prechecks are executed, activities haven't been * created yet so, for CONTEXT_MODULE banks, we keep the old contextid * in the parentitemid field. Once the activity (and its context) has been * created, we'll update that context in the required qcats * * Caller {@link precheck_categories_and_questions} will, simply, execute * this function for all the contextlevels, acting as a simple controller * of warnings and errors. * * The function returns 2 arrays, one containing errors and another containing * warnings. Both empty if no errors/warnings are found. * * @param int $restoreid The restore ID * @param int $courseid The ID of the course * @param int $userid The id of the user doing the restore * @param bool $samesite True if restore is to same site * @param int $contextlevel (CONTEXT_SYSTEM, etc.) * @return array A separate list of all error and warnings detected */ public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) { global $DB, $CFG; // To return any errors and warnings found $errors = array(); $warnings = array(); // Specify which fallbacks must be performed $fallbacks = array( CONTEXT_SYSTEM => CONTEXT_COURSE, CONTEXT_COURSECAT => CONTEXT_COURSE); /** @var restore_controller $rc */ $rc = restore_controller_dbops::load_controller($restoreid); $plan = $rc->get_plan(); $after35 = $plan->backup_release_compare('3.5', '>=') && $plan->backup_version_compare(20180205, '>'); $rc->destroy(); // Always need to destroy. // For any contextlevel, follow this process logic: // // 0) Iterate over each context (qbank) // 1) Iterate over each qcat in the context, matching by stamp for the found target context // 2a) No match, check if user can create qcat and q // 3a) User can, mark the qcat and all dependent qs to be created in that target context // 3b) User cannot, check if we are in some contextlevel with fallback // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop // 4b) No fallback, error. End qcat loop. // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version // 5a) No match, check if user can add q // 6a) User can, mark the q to be created // 6b) User cannot, check if we are in some contextlevel with fallback // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop // 7b) No fallback, error. End qcat loop // 5b) Random question, must always create new. // 5c) Match, mark q to be mapped // 8) Check if backup is from Moodle >= 3.5 and error if more than one top-level category in the context. // Get all the contexts (question banks) in restore for the given contextlevel $contexts = self::restore_get_question_banks($restoreid, $contextlevel); // 0) Iterate over each context (qbank) foreach ($contexts as $contextid => $contextlevel) { // Init some perms $canmanagecategory = false; $canadd = false; // Top-level category counter. $topcats = 0; // get categories in context (bank) $categories = self::restore_get_question_categories($restoreid, $contextid, $contextlevel); // cache permissions if $targetcontext is found if ($targetcontext = self::restore_find_best_target_context($categories, $courseid, $contextlevel)) { $canmanagecategory = has_capability('moodle/question:managecategory', $targetcontext, $userid); $canadd = has_capability('moodle/question:add', $targetcontext, $userid); } // 1) Iterate over each qcat in the context, matching by stamp for the found target context foreach ($categories as $category) { if ($category->parent == 0) { $topcats++; } $matchcat = false; if ($targetcontext) { $matchcat = $DB->get_record('question_categories', array( 'contextid' => $targetcontext->id, 'stamp' => $category->stamp)); } // 2a) No match, check if user can create qcat and q if (!$matchcat) { // 3a) User can, mark the qcat and all dependent qs to be created in that target context if ($canmanagecategory && $canadd) { // Set parentitemid to targetcontext, BUT for CONTEXT_MODULE categories, where // we keep the source contextid unmodified (for easier matching later when the // activities are created) $parentitemid = $targetcontext->id; if ($contextlevel == CONTEXT_MODULE) { $parentitemid = null; // null means "not modify" a.k.a. leave original contextid } self::set_backup_ids_record($restoreid, 'question_category', $category->id, 0, $parentitemid); // Nothing else to mark, newitemid = 0 means create // 3b) User cannot, check if we are in some contextlevel with fallback } else { // 4a) There is fallback, move ALL the qcats to fallback, warn. End qcat loop if (array_key_exists($contextlevel, $fallbacks)) { foreach ($categories as $movedcat) { $movedcat->contextlevel = $fallbacks[$contextlevel]; self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); // Warn about the performed fallback $warnings[] = get_string('qcategory2coursefallback', 'backup', $movedcat); } // 4b) No fallback, error. End qcat loop. } else { $errors[] = get_string('qcategorycannotberestored', 'backup', $category); } break; // out from qcat loop (both 4a and 4b), we have decided about ALL categories in context (bank) } // 2b) Match, mark qcat to be mapped and iterate over each q, matching by stamp and version } else { self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id); $questions = self::restore_get_questions($restoreid, $category->id); $transformer = self::get_backup_xml_transformer($courseid); // Collect all the questions for this category into memory so we only talk to the DB once. $recordset = $DB->get_recordset_sql( "SELECT q.* FROM {question} q JOIN {question_versions} qv ON qv.questionid = q.id JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid WHERE qc.id = ?", [$matchcat->id], ); // Compute a hash of question and answer fields to differentiate between identical stamp-version questions. $questioncache = []; foreach ($recordset as $question) { $question->export_process = true; // Include all question options required for export. get_question_options($question); unset($question->export_process); // Remove some additional properties from get_question_options() that isn't included in backups // before we produce the identity hash. unset($question->categoryobject); unset($question->questioncategoryid); $cachekey = restore_questions_parser_processor::generate_question_identity_hash($question, $transformer); $questioncache[$cachekey] = $question->id; } $recordset->close(); foreach ($questions as $question) { if (isset($questioncache[$question->questionhash])) { $matchqid = $questioncache[$question->questionhash]; } else { $matchqid = false; } // 5a) No match, check if user can add q if (!$matchqid) { // 6a) User can, mark the q to be created if ($canadd) { // Nothing to mark, newitemid means create // 6b) User cannot, check if we are in some contextlevel with fallback } else { // 7a) There is fallback, move ALL the qcats to fallback, warn. End qcat loo if (array_key_exists($contextlevel, $fallbacks)) { foreach ($categories as $movedcat) { $movedcat->contextlevel = $fallbacks[$contextlevel]; self::set_backup_ids_record($restoreid, 'question_category', $movedcat->id, 0, $contextid, $movedcat); // Warn about the performed fallback $warnings[] = get_string('question2coursefallback', 'backup', $movedcat); } // 7b) No fallback, error. End qcat loop } else { $errors[] = get_string('questioncannotberestored', 'backup', $question); } break 2; // out from qcat loop (both 7a and 7b), we have decided about ALL categories in context (bank) } // 5b) Random questions must always be newly created. } else if ($question->qtype == 'random') { // Nothing to mark, newitemid means create // 5c) Match, mark q to be mapped. } else { self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid); } } } } // 8) Check if backup is made on Moodle >= 3.5 and there are more than one top-level category in the context. if ($after35 && $topcats > 1) { $errors[] = get_string('restoremultipletopcats', 'question', $contextid); } } return array($errors, $warnings); } /** * Return one array of contextid => contextlevel pairs * of question banks to be checked for one given restore operation * ordered from CONTEXT_SYSTEM downto CONTEXT_MODULE * If contextlevel is specified, then only banks corresponding to * that level are returned */ public static function restore_get_question_banks($restoreid, $contextlevel = null) { global $DB; $results = array(); $qcats = $DB->get_recordset_sql("SELECT itemid, parentitemid AS contextid, info FROM {backup_ids_temp} WHERE backupid = ? AND itemname = 'question_category'", array($restoreid)); foreach ($qcats as $qcat) { // If this qcat context haven't been acummulated yet, do that if (!isset($results[$qcat->contextid])) { $info = backup_controller_dbops::decode_backup_temp_info($qcat->info); // Filter by contextlevel if necessary if (is_null($contextlevel) || $contextlevel == $info->contextlevel) { $results[$qcat->contextid] = $info->contextlevel; } } } $qcats->close(); // Sort by value (contextlevel from CONTEXT_SYSTEM downto CONTEXT_MODULE) asort($results); return $results; } /** * Return one array of question_category records for * a given restore operation and one restore context (question bank) * * @param string $restoreid Unique identifier of the restore operation being performed. * @param int $contextid Context id we want question categories to be returned. * @param int $contextlevel Context level we want to restrict the returned categories. * @return array Question categories for the given context id and level. */ public static function restore_get_question_categories($restoreid, $contextid, $contextlevel) { global $DB; $results = array(); $qcats = $DB->get_recordset_sql("SELECT itemid, info FROM {backup_ids_temp} WHERE backupid = ? AND itemname = 'question_category' AND parentitemid = ?", array($restoreid, $contextid)); foreach ($qcats as $qcat) { $result = backup_controller_dbops::decode_backup_temp_info($qcat->info); // Filter out found categories that belong to another context level. // (this can happen when a higher level category becomes remapped to // a context id that, by coincidence, matches a context id of another // category at lower level). See MDL-72950 for more info. if ($result->contextlevel == $contextlevel) { $results[$qcat->itemid] = $result; } } $qcats->close(); return $results; } /** * Calculates the best context found to restore one collection of qcats, * al them belonging to the same context (question bank), returning the * target context found (object) or false */ public static function restore_find_best_target_context($categories, $courseid, $contextlevel) { global $DB; $targetcontext = false; // Depending of $contextlevel, we perform different actions switch ($contextlevel) { // For system is easy, the best context is the system context case CONTEXT_SYSTEM: $targetcontext = context_system::instance(); break; // For coursecat, we are going to look for stamps in all the // course categories between CONTEXT_SYSTEM and CONTEXT_COURSE // (i.e. in all the course categories in the path) // // And only will return one "best" target context if all the // matches belong to ONE and ONLY ONE context. If multiple // matches are found, that means that there is some annoying // qbank "fragmentation" in the categories, so we'll fallback // to create the qbank at course level case CONTEXT_COURSECAT: // Build the array of stamps we are going to match $stamps = array(); foreach ($categories as $category) { $stamps[] = $category->stamp; } $contexts = array(); // Build the array of contexts we are going to look $systemctx = context_system::instance(); $coursectx = context_course::instance($courseid); $parentctxs = $coursectx->get_parent_context_ids(); foreach ($parentctxs as $parentctx) { // Exclude system context if ($parentctx == $systemctx->id) { continue; } $contexts[] = $parentctx; } if (!empty($stamps) && !empty($contexts)) { // Prepare the query list($stamp_sql, $stamp_params) = $DB->get_in_or_equal($stamps); list($context_sql, $context_params) = $DB->get_in_or_equal($contexts); $sql = "SELECT DISTINCT contextid FROM {question_categories} WHERE stamp $stamp_sql AND contextid $context_sql"; $params = array_merge($stamp_params, $context_params); $matchingcontexts = $DB->get_records_sql($sql, $params); // Only if ONE and ONLY ONE context is found, use it as valid target if (count($matchingcontexts) == 1) { $targetcontext = context::instance_by_id(reset($matchingcontexts)->contextid); } } break; // For course is easy, the best context is the course context case CONTEXT_COURSE: $targetcontext = context_course::instance($courseid); break; // For module is easy, there is not best context, as far as the // activity hasn't been created yet. So we return context course // for them, so permission checks and friends will work. Note this // case is handled by {@link prechek_precheck_qbanks_by_level} // in an special way case CONTEXT_MODULE: $targetcontext = context_course::instance($courseid); break; } return $targetcontext; } /** * Return one array of question records for * a given restore operation and one question category */ public static function restore_get_questions($restoreid, $qcatid) { global $DB; $results = array(); $qs = $DB->get_recordset_sql("SELECT itemid, info FROM {backup_ids_temp} WHERE backupid = ? AND itemname = 'question' AND parentitemid = ?", array($restoreid, $qcatid)); foreach ($qs as $q) { $results[$q->itemid] = backup_controller_dbops::decode_backup_temp_info($q->info); } $qs->close(); return $results; } /** * Given one component/filearea/context and * optionally one source itemname to match itemids * put the corresponding files in the pool * * If you specify a progress reporter, it will get called once per file with * indeterminate progress. * * @param string $basepath the full path to the root of unzipped backup file * @param string $restoreid the restore job's identification * @param string $component * @param string $filearea * @param int $oldcontextid * @param int $dfltuserid default $file->user if the old one can't be mapped * @param string|null $itemname * @param int|null $olditemid * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping) * @param bool $skipparentitemidctxmatch * @param \core\progress\base $progress Optional progress reporter * @return array of result object */ public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false, ?\core\progress\base $progress = null) { global $DB, $CFG; $backupinfo = backup_general_helper::get_backup_information(basename($basepath)); $includesfiles = $backupinfo->include_files; $results = array(); if ($forcenewcontextid) { // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings, // with questions originally at system/coursecat context in source being restored to course context in target). So we need // to be able to force the new contextid $newcontextid = $forcenewcontextid; } else { // Get new context, must exist or this will fail $newcontextrecord = self::get_backup_ids_record($restoreid, 'context', $oldcontextid); if (!$newcontextrecord || !$newcontextrecord->newitemid) { throw new restore_dbops_exception('unknown_context_mapping', $oldcontextid); } $newcontextid = $newcontextrecord->newitemid; } // Sometimes it's possible to have not the oldcontextids stored into backup_ids_temp->parentitemid // columns (because we have used them to store other information). This happens usually with // all the question related backup_ids_temp records. In that case, it's safe to ignore that // matching as far as we are always restoring for well known oldcontexts and olditemids $parentitemctxmatchsql = ' AND i.parentitemid = f.contextid '; if ($skipparentitemidctxmatch) { $parentitemctxmatchsql = ''; } // Important: remember how files have been loaded to backup_files_temp // - info: contains the whole original object (times, names...) // (all them being original ids as loaded from xml) // itemname = null, we are going to match only by context, no need to use itemid (all them are 0) if ($itemname == null) { $sql = "SELECT id AS bftid, contextid, component, filearea, itemid, itemid AS newitemid, info FROM {backup_files_temp} WHERE backupid = ? AND contextid = ? AND component = ? AND filearea = ?"; $params = array($restoreid, $oldcontextid, $component, $filearea); // itemname not null, going to join with backup_ids to perform the old-new mapping of itemids } else { $sql = "SELECT f.id AS bftid, f.contextid, f.component, f.filearea, f.itemid, i.newitemid, f.info FROM {backup_files_temp} f JOIN {backup_ids_temp} i ON i.backupid = f.backupid $parentitemctxmatchsql AND i.itemid = f.itemid WHERE f.backupid = ? AND f.contextid = ? AND f.component = ? AND f.filearea = ? AND i.itemname = ?"; $params = array($restoreid, $oldcontextid, $component, $filearea, $itemname); if ($olditemid !== null) { // Just process ONE olditemid intead of the whole itemname $sql .= ' AND i.itemid = ?'; $params[] = $olditemid; } } $fs = get_file_storage(); // Get moodle file storage $basepath = $basepath . '/files/';// Get backup file pool base // Report progress before query. if ($progress) { $progress->progress(); } $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $rec) { // Report progress each time around loop. if ($progress) { $progress->progress(); } $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info); // ignore root dirs (they are created automatically) if ($file->filepath == '/' && $file->filename == '.') { continue; } // set the best possible user $mappeduser = self::get_backup_ids_record($restoreid, 'user', $file->userid); $mappeduserid = !empty($mappeduser) ? $mappeduser->newitemid : $dfltuserid; // dir found (and not root one), let's create it if ($file->filename == '.') { $fs->create_directory($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $mappeduserid); continue; } // Updated the times of the new record. // The file record should reflect when the file entered the system, // and when this record was created. $time = time(); // The file record to restore. $file_record = array( 'contextid' => $newcontextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $rec->newitemid, 'filepath' => $file->filepath, 'filename' => $file->filename, 'timecreated' => $time, 'timemodified' => $time, 'userid' => $mappeduserid, 'source' => $file->source, 'author' => $file->author, 'license' => $file->license, 'sortorder' => $file->sortorder ); if (empty($file->repositoryid)) { // If contenthash is empty then gracefully skip adding file. if (empty($file->contenthash)) { $result = new stdClass(); $result->code = 'file_missing_in_backup'; $result->message = sprintf('missing file (%s) contenthash in backup for component %s', $file->filename, $component); $result->level = backup::LOG_WARNING; $results[] = $result; continue; } // this is a regular file, it must be present in the backup pool $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash); // Some file types do not include the files as they should already be // present. We still need to create entries into the files table. if ($includesfiles) { // The file is not found in the backup. if (!file_exists($backuppath)) { $results[] = self::get_missing_file_result($file); continue; } // create the file in the filepool if it does not exist yet if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) { // If no license found, use default. if ($file->license == null){ $file->license = $CFG->sitedefaultlicense; } $fs->create_file_from_pathname($file_record, $backuppath); } } else { // This backup does not include the files - they should be available in moodle filestorage already. // Create the file in the filepool if it does not exist yet. if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) { // Even if a file has been deleted since the backup was made, the file metadata may remain in the // files table, and the file will not yet have been moved to the trashdir. e.g. a draft file version. // Try to recover from file table first. if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash), '', '*', 0, 1)) { // Only grab one of the foundfiles - the file content should be the same for all entries. $foundfile = reset($foundfiles); $fs->create_file_from_storedfile($file_record, $foundfile->id); } else { $filesystem = $fs->get_file_system(); $restorefile = $file; $restorefile->contextid = $newcontextid; $restorefile->itemid = $rec->newitemid; $storedfile = new stored_file($fs, $restorefile); // Ok, let's try recover this file. // 1. We check if the file can be fetched locally without attempting to fetch // from the trash. // 2. We check if we can get the remote filepath for the specified stored file. // 3. We check if the file can be fetched from the trash. // 4. All failed, say we couldn't find it. if ($filesystem->is_file_readable_locally_by_storedfile($storedfile)) { $localpath = $filesystem->get_local_path_from_storedfile($storedfile); $fs->create_file_from_pathname($file, $localpath); } else if ($filesystem->is_file_readable_remotely_by_storedfile($storedfile)) { $remotepath = $filesystem->get_remote_path_from_storedfile($storedfile); $fs->create_file_from_pathname($file, $remotepath); } else if ($filesystem->is_file_readable_locally_by_storedfile($storedfile, true)) { $localpath = $filesystem->get_local_path_from_storedfile($storedfile, true); $fs->create_file_from_pathname($file, $localpath); } else { // A matching file was not found. $results[] = self::get_missing_file_result($file); continue; } } } } // store the the new contextid and the new itemid in case we need to remap // references to this file later $DB->update_record('backup_files_temp', array( 'id' => $rec->bftid, 'newcontextid' => $newcontextid, 'newitemid' => $rec->newitemid), true); } else { // this is an alias - we can't create it yet so we stash it in a temp // table and will let the final task to deal with it if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) { $info = new stdClass(); // oldfile holds the raw information stored in MBZ (including reference-related info) $info->oldfile = $file; // newfile holds the info for the new file_record with the context, user and itemid mapped $info->newfile = (object) $file_record; restore_dbops::set_backup_ids_record($restoreid, 'file_aliases_queue', $file->id, 0, null, $info); } } } $rs->close(); return $results; } /** * Returns suitable entry to include in log when there is a missing file. * * @param stdClass $file File definition * @return stdClass Log entry */ protected static function get_missing_file_result($file) { $result = new stdClass(); $result->code = 'file_missing_in_backup'; $result->message = 'Missing file in backup: ' . $file->filepath . $file->filename . ' (old context ' . $file->contextid . ', component ' . $file->component . ', filearea ' . $file->filearea . ', old itemid ' . $file->itemid . ')'; $result->level = backup::LOG_WARNING; return $result; } /** * Given one restoreid, create in DB all the users present * in backup_ids having newitemid = 0, as far as * precheck_included_users() have left them there * ready to be created. Also, annotate their newids * once created for later reference. * * This function will start and end a new progress section in the progress * object. * * @param string $basepath Base path of unzipped backup * @param string $restoreid Restore ID * @param int $userid Default userid for files * @param \core\progress\base $progress Object used for progress tracking * @param int $courseid Course ID */ public static function create_included_users($basepath, $restoreid, $userid, \core\progress\base $progress, int $courseid = 0) { global $CFG, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); $progress->start_progress('Creating included users'); $authcache = array(); // Cache to get some bits from authentication plugins $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search later $themes = get_list_of_themes(); // Get themes for quick search later // Iterate over all the included users with newitemid = 0, have to create them $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'user', 'newitemid' => 0), '', 'itemid, parentitemid, info'); foreach ($rs as $recuser) { $progress->progress(); $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info); // if user lang doesn't exist here, use site default if (!array_key_exists($user->lang, $languages)) { $user->lang = get_newuser_language(); } // if user theme isn't available on target site or they are disabled, reset theme if (!empty($user->theme)) { if (empty($CFG->allowuserthemes) || !in_array($user->theme, $themes)) { $user->theme = ''; } } // if user to be created has mnet auth and its mnethostid is $CFG->mnet_localhost_id // that's 100% impossible as own server cannot be accesed over mnet. Change auth to email/manual if ($user->auth == 'mnet' && $user->mnethostid == $CFG->mnet_localhost_id) { // Respect registerauth if ($CFG->registerauth == 'email') { $user->auth = 'email'; } else { $user->auth = 'manual'; } } unset($user->mnethosturl); // Not needed anymore // Disable pictures based on global setting if (!empty($CFG->disableuserimages)) { $user->picture = 0; } // We need to analyse the AUTH field to recode it: // - if the auth isn't enabled in target site, $CFG->registerauth will decide // - finally, if the auth resulting isn't enabled, default to 'manual' if (!is_enabled_auth($user->auth)) { if ($CFG->registerauth == 'email') { $user->auth = 'email'; } else { $user->auth = 'manual'; } } if (!is_enabled_auth($user->auth)) { // Final auth check verify, default to manual if not enabled $user->auth = 'manual'; } // Now that we know the auth method, for users to be created without pass // if password handling is internal and reset password is available // we set the password to "restored" (plain text), so the login process // will know how to handle that situation in order to allow the user to // recover the password. MDL-20846 if (empty($user->password)) { // Only if restore comes without password if (!array_key_exists($user->auth, $authcache)) { // Not in cache $userauth = new stdClass(); $authplugin = get_auth_plugin($user->auth); $userauth->preventpassindb = $authplugin->prevent_local_passwords(); $userauth->isinternal = $authplugin->is_internal(); $userauth->canresetpwd = $authplugin->can_reset_password(); $authcache[$user->auth] = $userauth; } else { $userauth = $authcache[$user->auth]; // Get from cache } // Most external plugins do not store passwords locally if (!empty($userauth->preventpassindb)) { $user->password = AUTH_PASSWORD_NOT_CACHED; // If Moodle is responsible for storing/validating pwd and reset functionality is available, mark } else if ($userauth->isinternal and $userauth->canresetpwd) { $user->password = 'restored'; } } else if (self::password_should_be_discarded($user->password)) { // Password is not empty and it is MD5 hashed. Generate a new random password for the user. // We don't want MD5 hashes in the database and users won't be able to log in with the associated password anyway. $user->password = hash_internal_user_password(base64_encode(random_bytes(24))); } // Creating new user, we must reset the policyagreed always $user->policyagreed = 0; // Set time created if empty if (empty($user->timecreated)) { $user->timecreated = time(); } // Done, let's create the user and annotate its id $newuserid = $DB->insert_record('user', $user); self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $newuserid); // Let's create the user context and annotate it (we need it for sure at least for files) // but for deleted users that don't have a context anymore (MDL-30192). We are done for them // and nothing else (custom fields, prefs, tags, files...) will be created. if (empty($user->deleted)) { $newuserctxid = $user->deleted ? 0 : context_user::instance($newuserid)->id; self::set_backup_ids_record($restoreid, 'context', $recuser->parentitemid, $newuserctxid); // Process custom fields if (isset($user->custom_fields)) { // if present in backup foreach($user->custom_fields['custom_field'] as $udata) { $udata = (object)$udata; // If the profile field has data and the profile shortname-datatype is defined in server if ($udata->field_data) { $field = profile_get_custom_field_data_by_shortname($udata->field_name); if ($field && $field->datatype === $udata->field_type) { // Insert the user_custom_profile_field. $rec = new stdClass(); $rec->userid = $newuserid; $rec->fieldid = $field->id; $rec->data = $udata->field_data; $DB->insert_record('user_info_data', $rec); } } } } // Trigger event that user was created. \core\event\user_created::create_from_user_id_on_restore($newuserid, $restoreid, $courseid)->trigger(); // Process tags if (core_tag_tag::is_enabled('core', 'user') && isset($user->tags)) { // If enabled in server and present in backup. $tags = array(); foreach($user->tags['tag'] as $usertag) { $usertag = (object)$usertag; $tags[] = $usertag->rawname; } core_tag_tag::set_item_tags('core', 'user', $newuserid, context_user::instance($newuserid), $tags); } // Process preferences if (isset($user->preferences)) { // if present in backup foreach($user->preferences['preference'] as $preference) { $preference = (object)$preference; // Prepare the record and insert it $preference->userid = $newuserid; // Translate _loggedin / _loggedoff message user preferences to _enabled. (MDL-67853) // This code cannot be removed. if (preg_match('/message_provider_.*/', $preference->name)) { $nameparts = explode('_', $preference->name); $name = array_pop($nameparts); if ($name == 'loggedin' || $name == 'loggedoff') { $preference->name = implode('_', $nameparts).'_enabled'; $existingpreference = $DB->get_record('user_preferences', ['name' => $preference->name , 'userid' => $newuserid]); // Merge both values. if ($existingpreference) { $values = []; if (!empty($existingpreference->value) && $existingpreference->value != 'none') { $values = explode(',', $existingpreference->value); } if (!empty($preference->value) && $preference->value != 'none') { $values = array_merge(explode(',', $preference->value), $values); $values = array_unique($values); } $existingpreference->value = empty($values) ? 'none' : implode(',', $values); $DB->update_record('user_preferences', $existingpreference); continue; } } } // End translating loggedin / loggedoff message user preferences. $DB->insert_record('user_preferences', $preference); } } // Special handling for htmleditor which was converted to a preference. if (isset($user->htmleditor)) { if ($user->htmleditor == 0) { $preference = new stdClass(); $preference->userid = $newuserid; $preference->name = 'htmleditor'; $preference->value = 'textarea'; $DB->insert_record('user_preferences', $preference); } } // Create user files in pool (profile, icon, private) by context restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'icon', $recuser->parentitemid, $userid, null, null, null, false, $progress); restore_dbops::send_files_to_pool($basepath, $restoreid, 'user', 'profile', $recuser->parentitemid, $userid, null, null, null, false, $progress); } } $rs->close(); $progress->end_progress(); } /** * Given one user object (from backup file), perform all the neccesary * checks is order to decide how that user will be handled on restore. * * Note the function requires $user->mnethostid to be already calculated * so it's caller responsibility to set it * * This function is used both by @restore_precheck_users() and * @restore_create_users() to get consistent results in both places * * It returns: * - one user object (from DB), if match has been found and user will be remapped * - boolean true if the user needs to be created * - boolean false if some conflict happened and the user cannot be handled * * Each test is responsible for returning its results and interrupt * execution. At the end, boolean true (user needs to be created) will be * returned if no test has interrupted that. * * Here it's the logic applied, keep it updated: * * If restoring users from same site backup: * 1A - Normal check: If match by id and username and mnethost => ok, return target user * 1B - If restoring an 'anonymous' user (created via the 'Anonymize user information' option) try to find a * match by username only => ok, return target user MDL-31484 * 1C - Handle users deleted in DB and "alive" in backup file: * If match by id and mnethost and user is deleted in DB and * (match by username LIKE 'backup_email.%' or by non empty email = md5(username)) => ok, return target user * 1D - Handle users deleted in backup file and "alive" in DB: * If match by id and mnethost and user is deleted in backup file * and match by email = email_without_time(backup_email) => ok, return target user * 1E - Conflict: If match by username and mnethost and doesn't match by id => conflict, return false * 1F - None of the above, return true => User needs to be created * * if restoring from another site backup (cannot match by id here, replace it by email/firstaccess combination): * 2A - Normal check: * 2A1 - If match by username and mnethost and (email or non-zero firstaccess) => ok, return target user * 2A2 - Exceptional handling (MDL-21912): Match "admin" username. Then, if import_general_duplicate_admin_allowed is * enabled, attempt to map the admin user to the user 'admin_[oldsiteid]' if it exists. If not, * the user 'admin_[oldsiteid]' will be created in precheck_included users * 2B - Handle users deleted in DB and "alive" in backup file: * 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and * (username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user * 2B2 - If match by mnethost and user is deleted in DB and * username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user * (to cover situations were md5(username) wasn't implemented on delete we requiere both) * 2C - Handle users deleted in backup file and "alive" in DB: * If match mnethost and user is deleted in backup file * and by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user * 2D - Conflict: If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false * 2E - None of the above, return true => User needs to be created * * Note: for DB deleted users email is stored in username field, hence we * are looking there for emails. See delete_user() * Note: for DB deleted users md5(username) is stored *sometimes* in the email field, * hence we are looking there for usernames if not empty. See delete_user() */ protected static function precheck_user($user, $samesite, $siteid = null) { global $CFG, $DB; // Handle checks from same site backups if ($samesite && empty($CFG->forcedifferentsitecheckingusersonrestore)) { // 1A - If match by id and username and mnethost => ok, return target user if ($rec = $DB->get_record('user', array('id'=>$user->id, 'username'=>$user->username, 'mnethostid'=>$user->mnethostid))) { return $rec; // Matching user found, return it } // 1B - If restoring an 'anonymous' user (created via the 'Anonymize user information' option) try to find a // match by username only => ok, return target user MDL-31484 // This avoids username / id mis-match problems when restoring subsequent anonymized backups. if (backup_anonymizer_helper::is_anonymous_user($user)) { if ($rec = $DB->get_record('user', array('username' => $user->username))) { return $rec; // Matching anonymous user found - return it } } // 1C - Handle users deleted in DB and "alive" in backup file // Note: for DB deleted users email is stored in username field, hence we // are looking there for emails. See delete_user() // Note: for DB deleted users md5(username) is stored *sometimes* in the email field, // hence we are looking there for usernames if not empty. See delete_user() // If match by id and mnethost and user is deleted in DB and // match by username LIKE 'substring(backup_email).%' where the substr length matches the retained data in the // username field (100 - (timestamp + 1) characters), or by non empty email = md5(username) => ok, return target user. $usernamelookup = core_text::substr($user->email, 0, 89) . '.%'; if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE id = ? AND mnethostid = ? AND deleted = 1 AND ( UPPER(username) LIKE UPPER(?) OR ( ".$DB->sql_isnotempty('user', 'email', false, false)." AND email = ? ) )", array($user->id, $user->mnethostid, $usernamelookup, md5($user->username)))) { return $rec; // Matching user, deleted in DB found, return it } // 1D - Handle users deleted in backup file and "alive" in DB // If match by id and mnethost and user is deleted in backup file // and match by substring(email) = email_without_time(backup_email) where the substr length matches the retained data // in the username field (100 - (timestamp + 1) characters) => ok, return target user. if ($user->deleted) { // Note: for DB deleted users email is stored in username field, hence we // are looking there for emails. See delete_user() // Trim time() from email $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username); if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE id = ? AND mnethostid = ? AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)", array($user->id, $user->mnethostid, $trimemail))) { return $rec; // Matching user, deleted in backup file found, return it } } // 1E - If match by username and mnethost and doesn't match by id => conflict, return false if ($rec = $DB->get_record('user', array('username'=>$user->username, 'mnethostid'=>$user->mnethostid))) { if ($user->id != $rec->id) { return false; // Conflict, username already exists and belongs to another id } } // Handle checks from different site backups } else { // 2A1 - If match by username and mnethost and // (email or non-zero firstaccess) => ok, return target user if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE username = ? AND mnethostid = ? AND ( UPPER(email) = UPPER(?) OR ( firstaccess != 0 AND firstaccess = ? ) )", array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) { return $rec; // Matching user found, return it } // 2A2 - If we're allowing conflicting admins, attempt to map user to admin_[oldsiteid]. if (get_config('backup', 'import_general_duplicate_admin_allowed') && $user->username === 'admin' && $siteid && $user->mnethostid == $CFG->mnet_localhost_id) { if ($rec = $DB->get_record('user', array('username' => 'admin_' . $siteid))) { return $rec; } } // 2B - Handle users deleted in DB and "alive" in backup file // Note: for DB deleted users email is stored in username field, hence we // are looking there for emails. See delete_user() // Note: for DB deleted users md5(username) is stored *sometimes* in the email field, // hence we are looking there for usernames if not empty. See delete_user() // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and // (by username LIKE 'substring(backup_email).%' or non-zero firstaccess) => ok, return target user. $usernamelookup = core_text::substr($user->email, 0, 89) . '.%'; if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE mnethostid = ? AND deleted = 1 AND ".$DB->sql_isnotempty('user', 'email', false, false)." AND email = ? AND ( UPPER(username) LIKE UPPER(?) OR ( firstaccess != 0 AND firstaccess = ? ) )", array($user->mnethostid, md5($user->username), $usernamelookup, $user->firstaccess))) { return $rec; // Matching user found, return it } // 2B2 - If match by mnethost and user is deleted in DB and // username LIKE 'substring(backup_email).%' and non-zero firstaccess) => ok, return target user // (this covers situations where md5(username) wasn't being stored so we require both // the email & non-zero firstaccess to match) $usernamelookup = core_text::substr($user->email, 0, 89) . '.%'; if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE mnethostid = ? AND deleted = 1 AND UPPER(username) LIKE UPPER(?) AND firstaccess != 0 AND firstaccess = ?", array($user->mnethostid, $usernamelookup, $user->firstaccess))) { return $rec; // Matching user found, return it } // 2C - Handle users deleted in backup file and "alive" in DB // If match mnethost and user is deleted in backup file // and match by substring(email) = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user. if ($user->deleted) { // Note: for DB deleted users email is stored in username field, hence we // are looking there for emails. See delete_user() // Trim time() from email $trimemail = preg_replace('/(.*?)\.[0-9]+.?$/', '\\1', $user->username); if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE mnethostid = ? AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?) AND firstaccess != 0 AND firstaccess = ?", array($user->mnethostid, $trimemail, $user->firstaccess))) { return $rec; // Matching user, deleted in backup file found, return it } } // 2D - If match by username and mnethost and not by (email or non-zero firstaccess) => conflict, return false if ($rec = $DB->get_record_sql("SELECT * FROM {user} u WHERE username = ? AND mnethostid = ? AND NOT ( UPPER(email) = UPPER(?) OR ( firstaccess != 0 AND firstaccess = ? ) )", array($user->username, $user->mnethostid, $user->email, $user->firstaccess))) { return false; // Conflict, username/mnethostid already exist and belong to another user (by email/firstaccess) } } // Arrived here, return true as the user will need to be created and no // conflicts have been found in the logic above. This covers: // 1E - else => user needs to be created, return true // 2E - else => user needs to be created, return true return true; } /** * Check all the included users, deciding the action to perform * for each one (mapping / creation) and returning one array * of problems in case something is wrong (lack of permissions, * conficts) * * @param string $restoreid Restore id * @param int $courseid Course id * @param int $userid User id * @param bool $samesite True if restore is to same site * @param \core\progress\base $progress Progress reporter */ public static function precheck_included_users($restoreid, $courseid, $userid, $samesite, \core\progress\base $progress) { global $CFG, $DB; // To return any problem found $problems = array(); // We are going to map mnethostid, so load all the available ones $mnethosts = $DB->get_records('mnet_host', array(), 'wwwroot', 'wwwroot, id'); // Calculate the context we are going to use for capability checking $context = context_course::instance($courseid); // TODO: Some day we must kill this dependency and change the process // to pass info around without loading a controller copy. // When conflicting users are detected we may need original site info. $rc = restore_controller_dbops::load_controller($restoreid); $restoreinfo = $rc->get_info(); $rc->destroy(); // Always need to destroy. // Calculate if we have perms to create users, by checking: // to 'moodle/restore:createuser' and 'moodle/restore:userinfo' // and also observe $CFG->disableusercreationonrestore $cancreateuser = false; if (has_capability('moodle/restore:createuser', $context, $userid) and has_capability('moodle/restore:userinfo', $context, $userid) and empty($CFG->disableusercreationonrestore)) { // Can create users $cancreateuser = true; } // Prepare for reporting progress. $conditions = array('backupid' => $restoreid, 'itemname' => 'user'); $max = $DB->count_records('backup_ids_temp', $conditions); $done = 0; $progress->start_progress('Checking users', $max); // Iterate over all the included users $rs = $DB->get_recordset('backup_ids_temp', $conditions, '', 'itemid, info'); foreach ($rs as $recuser) { $user = (object)backup_controller_dbops::decode_backup_temp_info($recuser->info); // Find the correct mnethostid for user before performing any further check if (empty($user->mnethosturl) || $user->mnethosturl === $CFG->wwwroot) { $user->mnethostid = $CFG->mnet_localhost_id; } else { // fast url-to-id lookups if (isset($mnethosts[$user->mnethosturl])) { $user->mnethostid = $mnethosts[$user->mnethosturl]->id; } else { $user->mnethostid = $CFG->mnet_localhost_id; } } // Now, precheck that user and, based on returned results, annotate action/problem $usercheck = self::precheck_user($user, $samesite, $restoreinfo->original_site_identifier_hash); if (is_object($usercheck)) { // No problem, we have found one user in DB to be mapped to // Annotate it, for later process. Set newitemid to mapping user->id self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, $usercheck->id); } else if ($usercheck === false) { // Found conflict, report it as problem if (!get_config('backup', 'import_general_duplicate_admin_allowed')) { $problems[] = get_string('restoreuserconflict', '', $user->username); } else if ($user->username == 'admin') { if (!$cancreateuser) { $problems[] = get_string('restorecannotcreateuser', '', $user->username); } if ($user->mnethostid != $CFG->mnet_localhost_id) { $problems[] = get_string('restoremnethostidmismatch', '', $user->username); } if (!$problems) { // Duplicate admin allowed, append original site idenfitier to username. $user->username .= '_' . $restoreinfo->original_site_identifier_hash; self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user); } } } else if ($usercheck === true) { // User needs to be created, check if we are able if ($cancreateuser) { // Can create user, set newitemid to 0 so will be created later self::set_backup_ids_record($restoreid, 'user', $recuser->itemid, 0, null, (array)$user); } else { // Cannot create user, report it as problem $problems[] = get_string('restorecannotcreateuser', '', $user->username); } } else { // Shouldn't arrive here ever, something is for sure wrong. Exception throw new restore_dbops_exception('restore_error_processing_user', $user->username); } $done++; $progress->progress($done); } $rs->close(); $progress->end_progress(); return $problems; } /** * Process the needed users in order to decide * which action to perform with them (create/map) * * Just wrap over precheck_included_users(), returning * exception if any problem is found * * @param string $restoreid Restore id * @param int $courseid Course id * @param int $userid User id * @param bool $samesite True if restore is to same site * @param \core\progress\base $progress Optional progress tracker */ public static function process_included_users($restoreid, $courseid, $userid, $samesite, ?\core\progress\base $progress = null) { global $DB; // Just let precheck_included_users() to do all the hard work $problems = self::precheck_included_users($restoreid, $courseid, $userid, $samesite, $progress); // With problems, throw exception, shouldn't happen if prechecks were originally // executed, so be radical here. if (!empty($problems)) { throw new restore_dbops_exception('restore_problems_processing_users', null, implode(', ', $problems)); } } /** * Process the needed question categories and questions * to check all them, deciding about the action to perform * (create/map) and target. * * Just wrap over precheck_categories_and_questions(), returning * exception if any problem is found */ public static function process_categories_and_questions($restoreid, $courseid, $userid, $samesite) { global $DB; // Just let precheck_included_users() to do all the hard work $problems = self::precheck_categories_and_questions($restoreid, $courseid, $userid, $samesite); // With problems of type error, throw exception, shouldn't happen if prechecks were originally // executed, so be radical here. if (array_key_exists('errors', $problems)) { throw new restore_dbops_exception('restore_problems_processing_questions', null, implode(', ', $problems['errors'])); } } public static function set_backup_files_record($restoreid, $filerec) { global $DB; // Store external files info in `info` field $filerec->info = backup_controller_dbops::encode_backup_temp_info($filerec); // Encode the whole record into info. $filerec->backupid = $restoreid; $DB->insert_record('backup_files_temp', $filerec); } public static function set_backup_ids_record($restoreid, $itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) { // Build conditionally the extra record info $extrarecord = array(); if ($newitemid != 0) { $extrarecord['newitemid'] = $newitemid; } if ($parentitemid != null) { $extrarecord['parentitemid'] = $parentitemid; } if ($info != null) { $extrarecord['info'] = backup_controller_dbops::encode_backup_temp_info($info); } self::set_backup_ids_cached($restoreid, $itemname, $itemid, $extrarecord); } public static function get_backup_ids_record($restoreid, $itemname, $itemid) { $dbrec = self::get_backup_ids_cached($restoreid, $itemname, $itemid); // We must test if info is a string, as the cache stores info in object form. if ($dbrec && isset($dbrec->info) && is_string($dbrec->info)) { $dbrec->info = backup_controller_dbops::decode_backup_temp_info($dbrec->info); } return $dbrec; } /** * Given on courseid, fullname and shortname, calculate the correct fullname/shortname to avoid dupes */ public static function calculate_course_names($courseid, $fullname, $shortname) { global $CFG, $DB; $counter = 0; // Iterate while fullname or shortname exist. do { if ($counter) { $suffixfull = ' ' . get_string('copyasnoun') . ' ' . $counter; $suffixshort = '_' . $counter; } else { $suffixfull = ''; $suffixshort = ''; } // Ensure we don't overflow maximum length of name fields, in multi-byte safe manner. $currentfullname = core_text::substr($fullname, 0, 254 - strlen($suffixfull)) . $suffixfull; $currentshortname = core_text::substr($shortname, 0, 100 - strlen($suffixshort)) . $suffixshort; $coursefull = $DB->get_record_select('course', 'fullname = ? AND id != ?', array($currentfullname, $courseid), '*', IGNORE_MULTIPLE); $courseshort = $DB->get_record_select('course', 'shortname = ? AND id != ?', array($currentshortname, $courseid)); $counter++; } while ($coursefull || $courseshort); // Return results return array($currentfullname, $currentshortname); } /** * For the target course context, put as many custom role names as possible */ public static function set_course_role_names($restoreid, $courseid) { global $DB; // Get the course context $coursectx = context_course::instance($courseid); // Get all the mapped roles we have $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $restoreid, 'itemname' => 'role'), '', 'itemid, info, newitemid'); foreach ($rs as $recrole) { $info = backup_controller_dbops::decode_backup_temp_info($recrole->info); // If it's one mapped role and we have one name for it if (!empty($recrole->newitemid) && !empty($info['nameincourse'])) { // If role name doesn't exist, add it $rolename = new stdclass(); $rolename->roleid = $recrole->newitemid; $rolename->contextid = $coursectx->id; if (!$DB->record_exists('role_names', (array)$rolename)) { $rolename->name = $info['nameincourse']; $DB->insert_record('role_names', $rolename); } } } $rs->close(); } /** * Creates a skeleton record within the database using the passed parameters * and returns the new course id. * * @global moodle_database $DB * @param string $fullname * @param string $shortname * @param int $categoryid * @return int The new course id */ public static function create_new_course($fullname, $shortname, $categoryid) { global $DB; $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST); $course = new stdClass; $course->fullname = $fullname; $course->shortname = $shortname; $course->category = $category->id; $course->sortorder = 0; $course->timecreated = time(); $course->timemodified = $course->timecreated; // forcing skeleton courses to be hidden instead of going by $category->visible , until MDL-27790 is resolved. $course->visible = 0; $courseid = $DB->insert_record('course', $course); $category->coursecount++; $DB->update_record('course_categories', $category); return $courseid; } /** * Deletes all of the content associated with the given course (courseid) * @param int $courseid * @param array $options * @return bool True for success */ public static function delete_course_content($courseid, ?array $options = null) { return remove_course_contents($courseid, false, $options); } /** * Checks if password stored in backup is a MD5 hash. * Returns true if it is, false otherwise. * * @param string $password The password to check. * @return bool */ private static function password_should_be_discarded(#[\SensitiveParameter] string $password): bool { return (bool) preg_match('/^[0-9a-f]{32}$/', $password); } /** * Load required classes and return a backup XML transformer for the specified course. * * These classes may not have been loaded if we're only doing a restore in the current process, * so make sure we have them here. * * @param int $courseid * @return backup_xml_transformer */ protected static function get_backup_xml_transformer(int $courseid): backup_xml_transformer { global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); return new backup_xml_transformer($courseid); } } /* * Exception class used by all the @dbops stuff */ class restore_dbops_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); } } util/dbops/restore_controller_dbops.class.php 0000644 00000035021 15215711721 0015565 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB support to the @restore_controller * * This class contains various static methods available for all the DB operations * performed by the restore_controller class * * TODO: Finish phpdocs */ abstract class restore_controller_dbops extends restore_dbops { /** * Send one restore controller to DB * * @param restore_controller $controller controller to send to DB * @param string $checksum hash of the controller to be checked * @param bool $includeobj to decide if the object itself must be updated (true) or no (false) * @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false) * @return int id of the controller record in the DB * @throws backup_controller_exception|restore_dbops_exception */ public static function save_controller($controller, $checksum, $includeobj = true, $cleanobj = false) { global $DB; // Check we are going to save one backup_controller if (! $controller instanceof restore_controller) { throw new backup_controller_exception('restore_controller_expected'); } // Check checksum is ok. Only if we are including object info. Sounds silly but it isn't ;-). if ($includeobj and !$controller->is_checksum_correct($checksum)) { throw new restore_dbops_exception('restore_controller_dbops_saving_checksum_mismatch'); } // Cannot request to $includeobj and $cleanobj at the same time. if ($includeobj and $cleanobj) { throw new restore_dbops_exception('restore_controller_dbops_saving_cannot_include_and_delete'); } // Get all the columns $rec = new stdclass(); $rec->backupid = $controller->get_restoreid(); $rec->operation = $controller->get_operation(); $rec->type = $controller->get_type(); $rec->itemid = $controller->get_courseid(); $rec->format = $controller->get_format(); $rec->interactive = $controller->get_interactive(); $rec->purpose = $controller->get_mode(); $rec->userid = $controller->get_userid(); $rec->status = $controller->get_status(); $rec->execution = $controller->get_execution(); $rec->executiontime= $controller->get_executiontime(); $rec->checksum = $checksum; // Serialize information if ($includeobj) { $rec->controller = base64_encode(serialize($controller)); } else if ($cleanobj) { $rec->controller = ''; } // Send it to DB if ($recexists = $DB->get_record('backup_controllers', array('backupid' => $rec->backupid))) { $rec->id = $recexists->id; $rec->timemodified = time(); $DB->update_record('backup_controllers', $rec); } else { $rec->timecreated = time(); $rec->timemodified = 0; $rec->id = $DB->insert_record('backup_controllers', $rec); } return $rec->id; } public static function load_controller($restoreid) { global $DB; if (! $controllerrec = $DB->get_record('backup_controllers', array('backupid' => $restoreid))) { throw new backup_dbops_exception('restore_controller_dbops_nonexisting'); } $controller = unserialize(base64_decode($controllerrec->controller)); if (!is_object($controller)) { // The controller field of the table did not contain a serialized object. // It is made empty after it has been used successfully, it is likely that // the user has pressed the browser back button at some point. throw new backup_dbops_exception('restore_controller_dbops_loading_invalid_controller'); } // Check checksum is ok. Sounds silly but it isn't ;-) if (!$controller->is_checksum_correct($controllerrec->checksum)) { throw new backup_dbops_exception('restore_controller_dbops_loading_checksum_mismatch'); } return $controller; } public static function create_restore_temp_tables($restoreid) { global $CFG, $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services if ($dbman->table_exists('backup_ids_temp')) { // Table exists, from restore prechecks // TODO: Improve this by inserting/selecting some record to see there is restoreid match // TODO: If not match, exception, table corresponds to another backup/restore operation return true; } backup_controller_dbops::create_backup_ids_temp_table($restoreid); backup_controller_dbops::create_backup_files_temp_table($restoreid); return false; } public static function drop_restore_temp_tables($backupid) { global $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services $targettablenames = array('backup_ids_temp', 'backup_files_temp'); foreach ($targettablenames as $targettablename) { $table = new xmldb_table($targettablename); $dbman->drop_table($table); // And drop it } // Invalidate the backup_ids caches. restore_dbops::reset_backup_ids_cached(); } /** * Sets the default values for the settings in a restore operation * * @param restore_controller $controller */ public static function apply_config_defaults(restore_controller $controller) { $settings = array( 'restore_general_users' => 'users', 'restore_general_enrolments' => 'enrolments', 'restore_general_role_assignments' => 'role_assignments', 'restore_general_permissions' => 'permissions', 'restore_general_activities' => 'activities', 'restore_general_blocks' => 'blocks', 'restore_general_filters' => 'filters', 'restore_general_comments' => 'comments', 'restore_general_badges' => 'badges', 'restore_general_calendarevents' => 'calendarevents', 'restore_general_userscompletion' => 'userscompletion', 'restore_general_logs' => 'logs', 'restore_general_histories' => 'grade_histories', 'restore_general_questionbank' => 'questionbank', 'restore_general_groups' => 'groups', 'restore_general_competencies' => 'competencies', 'restore_general_customfield' => 'customfield', 'restore_general_contentbankcontent' => 'contentbankcontent', 'restore_general_xapistate' => 'xapistate', 'restore_general_legacyfiles' => 'legacyfiles' ); self::apply_admin_config_defaults($controller, $settings, true); $target = $controller->get_target(); if ($target == backup::TARGET_EXISTING_ADDING || $target == backup::TARGET_CURRENT_ADDING) { $settings = array( 'restore_merge_overwrite_conf' => 'overwrite_conf', 'restore_merge_course_fullname' => 'course_fullname', 'restore_merge_course_shortname' => 'course_shortname', 'restore_merge_course_startdate' => 'course_startdate', ); self::apply_admin_config_defaults($controller, $settings, true); } if ($target == backup::TARGET_EXISTING_DELETING || $target == backup::TARGET_CURRENT_DELETING) { $settings = array( 'restore_replace_overwrite_conf' => 'overwrite_conf', 'restore_replace_course_fullname' => 'course_fullname', 'restore_replace_course_shortname' => 'course_shortname', 'restore_replace_course_startdate' => 'course_startdate', 'restore_replace_keep_roles_and_enrolments' => 'keep_roles_and_enrolments', 'restore_replace_keep_groups_and_groupings' => 'keep_groups_and_groupings', ); self::apply_admin_config_defaults($controller, $settings, true); } if ($controller->get_mode() == backup::MODE_IMPORT && (!$controller->get_interactive()) && $controller->get_type() == backup::TYPE_1ACTIVITY) { // This is duplicate - there is no concept of defaults - these settings must be on. $settings = array( 'activities', 'blocks', 'filters', 'questionbank' ); self::force_enable_settings($controller, $settings); }; // Add some dependencies. $plan = $controller->get_plan(); if ($plan->setting_exists('overwrite_conf')) { /** @var restore_course_overwrite_conf_setting $overwriteconf */ $overwriteconf = $plan->get_setting('overwrite_conf'); if ($overwriteconf->get_visibility()) { foreach (['course_fullname', 'course_shortname', 'course_startdate'] as $settingname) { if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); $overwriteconf->add_dependency($setting, setting_dependency::DISABLED_FALSE, array('defaultvalue' => $setting->get_value())); } } } } } /** * Returns the default value to be used for a setting from the admin restore config * * @param string $config * @param backup_setting $setting * @return mixed */ private static function get_setting_default($config, $setting) { $value = get_config('restore', $config); if (in_array($setting->get_name(), ['course_fullname', 'course_shortname', 'course_startdate']) && $setting->get_ui() instanceof backup_setting_ui_defaultcustom) { // Special case - admin config settings course_fullname, etc. are boolean and the restore settings are strings. $value = (bool)$value; if ($value) { $attributes = $setting->get_ui()->get_attributes(); $value = $attributes['customvalue']; } } if ($setting->get_ui() instanceof backup_setting_ui_select) { // Make sure the value is a valid option in the select element, otherwise just pick the first from the options list. // Example: enrolments dropdown may not have the "enrol_withusers" option because users info can not be restored. $options = array_keys($setting->get_ui()->get_values()); if (!in_array($value, $options)) { $value = reset($options); } } return $value; } /** * Turn these settings on. No defaults from admin settings. * * @param restore_controller $controller * @param array $settings a map from admin config names to setting names (Config name => Setting name) */ private static function force_enable_settings(restore_controller $controller, array $settings) { $plan = $controller->get_plan(); foreach ($settings as $config => $settingname) { $value = true; if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); // We do not allow this setting to be locked for a duplicate function. if ($setting->get_status() !== base_setting::NOT_LOCKED) { $setting->set_status(base_setting::NOT_LOCKED); } $setting->set_value($value); $setting->set_status(base_setting::LOCKED_BY_CONFIG); } else { $controller->log('Unknown setting: ' . $settingname, BACKUP::LOG_DEBUG); } } } /** * Sets the controller settings default values from the admin config. * * @param restore_controller $controller * @param array $settings a map from admin config names to setting names (Config name => Setting name) * @param boolean $uselocks whether "locked" admin settings should be honoured */ private static function apply_admin_config_defaults(restore_controller $controller, array $settings, $uselocks) { $plan = $controller->get_plan(); foreach ($settings as $config => $settingname) { if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); $value = self::get_setting_default($config, $setting); $locked = (get_config('restore',$config . '_locked') == true); // Use the original value when this is an import and the setting is unlocked. if ($controller->get_mode() == backup::MODE_IMPORT && $controller->get_interactive()) { if (!$uselocks || !$locked) { $value = $setting->get_value(); } } // We can only update the setting if it isn't already locked by config or permission. if ($setting->get_status() != base_setting::LOCKED_BY_CONFIG && $setting->get_status() != base_setting::LOCKED_BY_PERMISSION && $setting->get_ui()->is_changeable()) { $setting->set_value($value); if ($uselocks && $locked) { $setting->set_status(base_setting::LOCKED_BY_CONFIG); } } } else { $controller->log('Unknown setting: ' . $settingname, BACKUP::LOG_DEBUG); } } } } util/dbops/tests/restore_dbops_test.php 0000644 00000036574 15215711721 0014435 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use restore_controller_dbops; use restore_dbops; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * @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 */ final class restore_dbops_test extends \advanced_testcase { /** * Verify the xxx_ids_cached (in-memory backup_ids cache) stuff works as expected. * * Note that those private implementations are tested here by using the public * backup_ids API and later performing low-level tests. */ public function test_backup_ids_cached(): void { global $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services. $this->resetAfterTest(true); // Playing with temp tables, better reset once finished. // Some variables and objects for testing. $restoreid = 'testrestoreid'; $mapping = new \stdClass(); $mapping->itemname = 'user'; $mapping->itemid = 1; $mapping->newitemid = 2; $mapping->parentitemid = 3; $mapping->info = 'info'; // Create the backup_ids temp tables used by restore. restore_controller_dbops::create_restore_temp_tables($restoreid); // Send one mapping using the public api with defaults. restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); // Get that mapping and verify everything is returned as expected. $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); $this->assertSame($mapping->itemname, $result->itemname); $this->assertSame($mapping->itemid, $result->itemid); $this->assertSame(0, $result->newitemid); $this->assertSame(null, $result->parentitemid); $this->assertSame(null, $result->info); // Drop the backup_xxx_temp temptables manually, so memory cache won't be invalidated. $dbman->drop_table(new \xmldb_table('backup_ids_temp')); $dbman->drop_table(new \xmldb_table('backup_files_temp')); // Verify the mapping continues returning the same info, // now from cache (the table does not exist). $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); $this->assertSame($mapping->itemname, $result->itemname); $this->assertSame($mapping->itemid, $result->itemid); $this->assertSame(0, $result->newitemid); $this->assertSame(null, $result->parentitemid); $this->assertSame(null, $result->info); // Recreate the temp table, just to drop it using the restore API in // order to check that, then, the cache becomes invalid for the same request. restore_controller_dbops::create_restore_temp_tables($restoreid); restore_controller_dbops::drop_restore_temp_tables($restoreid); // No cached info anymore, so the mapping request will arrive to // DB leading to error (temp table does not exist). try { $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); $this->fail('Expecting an exception, none occurred'); } catch (\Exception $e) { $this->assertTrue($e instanceof \dml_exception); $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage()); } // Create the backup_ids temp tables once more. restore_controller_dbops::create_restore_temp_tables($restoreid); // Send one mapping using the public api with complete values. restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid, $mapping->newitemid, $mapping->parentitemid, $mapping->info); // Get that mapping and verify everything is returned as expected. $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); $this->assertSame($mapping->itemname, $result->itemname); $this->assertSame($mapping->itemid, $result->itemid); $this->assertSame($mapping->newitemid, $result->newitemid); $this->assertSame($mapping->parentitemid, $result->parentitemid); $this->assertSame($mapping->info, $result->info); // Finally, drop the temp tables properly and get the DB error again (memory caches empty). restore_controller_dbops::drop_restore_temp_tables($restoreid); try { $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid); $this->fail('Expecting an exception, none occurred'); } catch (\Exception $e) { $this->assertTrue($e instanceof \dml_exception); $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage()); } } /** * Data provider for {@link test_precheck_user()} */ public static function precheck_user_provider(): array { $emailmultiplier = [ 'shortmail' => 'normalusername@example.com', 'longmail' => str_repeat('a', 100) // It's not validated, hence any string is ok. ]; $providercases = []; foreach ($emailmultiplier as $emailk => $email) { // Get the related cases. $cases = self::precheck_user_cases($email); // Rename them (keys). foreach ($cases as $key => $case) { $providercases[$key . ' - ' . $emailk] = $case; } } return $providercases; } /** * Get all the cases implemented in {@link restore_dbops::precheck_users()} * * @param string $email */ private static function precheck_user_cases($email) { global $CFG; $baseuserarr = [ 'username' => 'normalusername', 'email' => $email, 'mnethostid' => $CFG->mnet_localhost_id, 'firstaccess' => 123456789, 'deleted' => 0, 'forceemailcleanup' => false, // Hack to force the DB record to have empty mail. 'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed. return [ // Cases with samesite = true. 'samesite match existing (1A)' => [ 'dbuser' => $baseuserarr, 'backupuser' => $baseuserarr, 'samesite' => true, 'outcome' => 'match' ], 'samesite match existing anon (1B)' => [ 'dbuser' => array_merge($baseuserarr, [ 'username' => 'anon01']), 'backupuser' => array_merge($baseuserarr, [ 'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01', 'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']), 'samesite' => true, 'outcome' => 'match' ], 'samesite match existing deleted in db, alive in backup, by db username (1C)' => [ 'dbuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'backupuser' => array_merge($baseuserarr, [ 'username' => 'this_wont_match']), 'samesite' => true, 'outcome' => 'match' ], 'samesite match existing deleted in db, alive in backup, by db email (1C)' => [ 'dbuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'backupuser' => array_merge($baseuserarr, [ 'email' => 'this_wont_match']), 'samesite' => true, 'outcome' => 'match' ], 'samesite match existing alive in db, deleted in backup (1D)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'samesite' => true, 'outcome' => 'match' ], 'samesite conflict (1E)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, ['id' => -1]), 'samesite' => true, 'outcome' => false ], 'samesite create user (1F)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'username' => 'newusername']), 'samesite' => false, 'outcome' => true ], // Cases with samesite = false. 'no samesite match existing, by db email (2A1)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'firstaccess' => 0]), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing, by db firstaccess (2A1)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'email' => 'this_wont_match@example.con']), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing anon (2A1 too)' => [ 'dbuser' => array_merge($baseuserarr, [ 'username' => 'anon01']), 'backupuser' => array_merge($baseuserarr, [ 'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01', 'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match dupe admin (2A2)' => [ 'dbuser' => array_merge($baseuserarr, [ 'username' => 'admin_old_site_id', 'forceduplicateadminallowed' => true]), 'backupuser' => array_merge($baseuserarr, [ 'username' => 'admin']), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [ 'dbuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'backupuser' => array_merge($baseuserarr, [ 'firstaccess' => 0]), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [ 'dbuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'backupuser' => array_merge($baseuserarr, [ 'mail' => 'this_wont_match']), 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing deleted in db, alive in backup (2B2)' => [ 'dbuser' => array_merge($baseuserarr, [ 'deleted' => 1, 'forceemailcleanup' => true]), 'backupuser' => $baseuserarr, 'samesite' => false, 'outcome' => 'match' ], 'no samesite match existing alive in db, deleted in backup (2C)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'deleted' => 1]), 'samesite' => false, 'outcome' => 'match' ], 'no samesite conflict (2D)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'email' => 'anotheruser@example.com', 'firstaccess' => 0]), 'samesite' => false, 'outcome' => false ], 'no samesite create user (2E)' => [ 'dbuser' => $baseuserarr, 'backupuser' => array_merge($baseuserarr, [ 'username' => 'newusername']), 'samesite' => false, 'outcome' => true ], ]; } /** * Test restore precheck_user method * * @dataProvider precheck_user_provider * @covers \restore_dbops::precheck_user() * * @param array $dbuser * @param array $backupuser * @param bool $samesite * @param mixed $outcome **/ public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome): void { global $DB; $this->resetAfterTest(); $dbuser = (object)$dbuser; $backupuser = (object)$backupuser; $siteid = null; // If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back). if ($backupuser->deleted) { $backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0])); delete_user($backupuser); $backupuser = $DB->get_record('user', ['id' => $backupuser->id]); $DB->delete_records('user', ['id' => $backupuser->id]); unset($backupuser->id); } // Create the db user, normally. $dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0])); $backupuser->id = $backupuser->id ?? $dbuser->id; // We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records. if ($dbuser->forceduplicateadminallowed) { set_config('import_general_duplicate_admin_allowed', true, 'backup'); $siteid = 'old_site_id'; } // If the DB user must be deleted, do it and fetch it back. if ($dbuser->deleted) { delete_user($dbuser); // We may want to clean the mail field (old behavior, not containing the current md5(username). if ($dbuser->forceemailcleanup) { $DB->set_field('user', 'email', '', ['id' => $dbuser->id]); } } // Get the dbuser record, because we may have changed it above. $dbuser = $DB->get_record('user', ['id' => $dbuser->id]); $method = (new \ReflectionClass('restore_dbops'))->getMethod('precheck_user'); $result = $method->invoke(null, $backupuser, $samesite, $siteid); if (is_bool($result)) { $this->assertSame($outcome, $result); } else { $outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser, // Just check ids, it means the expected match has been found in database. $this->assertSame($outcome->id, $result->id); } } } util/dbops/tests/backup_dbops_test.php 0000644 00000022005 15215711721 0014177 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core_backup * @category test * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup; use backup; use backup_controller; use backup_controller_dbops; use backup_controller_exception; use backup_dbops_exception; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * @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 */ final class backup_dbops_test extends \advanced_testcase { protected $moduleid; // course_modules id used for testing protected $sectionid; // course_sections id used for testing protected $courseid; // course id used for testing protected $userid; // user record used for testing protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3)); $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid)); $this->moduleid = $page->cmid; $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id)); $this->courseid = $coursemodule->course; $this->userid = 2; // admin $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; unset($CFG->backup_file_logger_extra); $CFG->backup_file_logger_level_extra = backup::LOG_NONE; } /* * test backup_ops class */ function test_backup_dbops(): void { // Nothing to do here, abstract class + exception, will be tested by the rest } /* * test backup_controller_dbops class */ function test_backup_controller_dbops(): void { global $DB; $dbman = $DB->get_manager(); // Going to use some database_manager services for testing // Instantiate non interactive backup_controller $bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $this->assertTrue($bc instanceof backup_controller); // Calculate checksum $checksum = $bc->calculate_checksum(); $this->assertEquals(strlen($checksum), 32); // is one md5 // save controller $recid = backup_controller_dbops::save_controller($bc, $checksum); $this->assertNotEmpty($recid); // save it again (should cause update to happen) $recid2 = backup_controller_dbops::save_controller($bc, $checksum); $this->assertNotEmpty($recid2); $this->assertEquals($recid, $recid2); // Same record in both save operations // Try incorrect checksum $bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $checksum = $bc->calculate_checksum(); try { $recid = backup_controller_dbops::save_controller($bc, 'lalala'); $this->assertTrue(false, 'backup_dbops_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_dbops_exception); $this->assertEquals($e->errorcode, 'backup_controller_dbops_saving_checksum_mismatch'); } // Try to save non backup_controller object $bc = new \stdClass(); try { $recid = backup_controller_dbops::save_controller($bc, 'lalala'); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_controller_expected'); } // save and load controller (by backupid). Then compare $bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $checksum = $bc->calculate_checksum(); // Calculate checksum $backupid = $bc->get_backupid(); $this->assertEquals(strlen($backupid), 32); // is one md5 $recid = backup_controller_dbops::save_controller($bc, $checksum); // save controller $newbc = backup_controller_dbops::load_controller($backupid); // load controller $this->assertTrue($newbc instanceof backup_controller); $newchecksum = $newbc->calculate_checksum(); $this->assertEquals($newchecksum, $checksum); // try to load non-existing controller try { $bc = backup_controller_dbops::load_controller('1234567890'); $this->assertTrue(false, 'backup_dbops_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_dbops_exception); $this->assertEquals($e->errorcode, 'backup_controller_dbops_nonexisting'); } // backup_ids_temp table tests // If, for any reason table exists, drop it if ($dbman->table_exists('backup_ids_temp')) { $dbman->drop_table(new xmldb_table('backup_ids_temp')); } // Check backup_ids_temp table doesn't exist $this->assertFalse($dbman->table_exists('backup_ids_temp')); // Create and check it exists backup_controller_dbops::create_backup_ids_temp_table('testingid'); $this->assertTrue($dbman->table_exists('backup_ids_temp')); // Drop and check it doesn't exists anymore backup_controller_dbops::drop_backup_ids_temp_table('testingid'); $this->assertFalse($dbman->table_exists('backup_ids_temp')); // Test encoding/decoding of backup_ids_temp,backup_files_temp encode/decode functions. // We need to handle both objects and data elements. $object = new \stdClass(); $object->item1 = 10; $object->item2 = 'a String'; $testarray = array($object, 10, null, 'string', array('a' => 'b', 1 => 1)); foreach ($testarray as $item) { $encoded = backup_controller_dbops::encode_backup_temp_info($item); $decoded = backup_controller_dbops::decode_backup_temp_info($encoded); $this->assertEquals($item, $decoded); } } /** * Check backup_includes_files */ function test_backup_controller_dbops_includes_files(): void { global $DB; $dbman = $DB->get_manager(); // Going to use some database_manager services for testing // A MODE_GENERAL controller - this should include files $bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 1); // A MODE_IMPORT controller - should not include files $bc = new mock_backup_controller4dbops(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $this->userid); $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0); // A MODE_SAMESITE controller - should not include files $bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $this->userid); $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0); } } class mock_backup_controller4dbops extends backup_controller { /** * Change standard behavior so the checksum is also stored and not onlt calculated */ public function calculate_checksum() { $this->checksum = parent::calculate_checksum(); return $this->checksum; } } util/dbops/tests/backup_structure_dbops_test.php 0000644 00000007655 15215711721 0016335 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup_structure_dbops; /** * Tests for backup_structure_dbops * * @package core_backup * @category test * @copyright 2023 Andrew Lyons <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \backup_structure_dbops */ final class backup_structure_dbops_test extends \advanced_testcase { public static function setUpBeforeClass(): void { global $CFG; parent::setUpBeforeClass(); require_once("{$CFG->dirroot}/backup/util/includes/backup_includes.php"); } /** * Tests for convert_params_to_values. * * @dataProvider convert_params_to_values_provider * @param array $params * @param mixed $processor * @param array $expected */ public function test_convert_params_to_values( array $params, $processor, array $expected, ): void { if (is_callable($processor)) { $newprocessor = $this->createMock(\backup_structure_processor::class); $newprocessor->method('get_var')->willReturnCallback($processor); $processor = $newprocessor; } $result = backup_structure_dbops::convert_params_to_values($params, $processor); $this->assertEqualsCanonicalizing($expected, $result); } /** * Data provider for convert_params_to_values_provider. */ public static function convert_params_to_values_provider(): array { return [ 'String value is not processed' => [ ['/0/1/2/345'], null, ['/0/1/2/345'], ], 'Positive integer' => [ [123, 456], null, [123, 456], ], 'Negative integer' => [ [-42], function () { return 'Life, the Universe, and Everything'; }, ['Life, the Universe, and Everything'], ], 'Mix of strings, and ints with a processor' => [ ['foo', 123, 'bar', -42], function () { return 'Life, the Universe, and Everything'; }, ['foo', 123, 'bar', 'Life, the Universe, and Everything'], ], ]; } /** * Tests for convert_params_to_values with an atom. */ public function test_convert_params_to_values_with_atom(): void { $atom = $this->createMock(\base_atom::class); $atom->method('is_set')->willReturn(true); $atom->method('get_value')->willReturn('Some atomised value'); $result = backup_structure_dbops::convert_params_to_values([$atom], null); $this->assertEqualsCanonicalizing(['Some atomised value'], $result); } /** * Tests for convert_params_to_values with an atom without any value. */ public function test_convert_params_to_values_with_atom_no_value(): void { $atom = $this->createMock(\base_atom::class); $atom->method('is_set')->willReturn(false); $atom->method('get_name')->willReturn('Atomisd name'); $this->expectException(\base_element_struct_exception::class); backup_structure_dbops::convert_params_to_values([$atom], null); } } util/dbops/backup_controller_dbops.class.php 0000644 00000102266 15215711721 0015355 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB support to the @backup_controller * * This class contains various static methods available for all the DB operations * performed by the backup_controller class * * TODO: Finish phpdocs */ abstract class backup_controller_dbops extends backup_dbops { /** * @var string Backup id for cached backup_includes_files result. */ protected static $includesfilescachebackupid; /** * @var int Cached backup_includes_files result */ protected static $includesfilescache; /** * Send one backup controller to DB * * @param backup_controller $controller controller to send to DB * @param string $checksum hash of the controller to be checked * @param bool $includeobj to decide if the object itself must be updated (true) or no (false) * @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false) * @return int id of the controller record in the DB * @throws backup_controller_exception|backup_dbops_exception */ public static function save_controller($controller, $checksum, $includeobj = true, $cleanobj = false) { global $DB; // Check we are going to save one backup_controller if (! $controller instanceof backup_controller) { throw new backup_controller_exception('backup_controller_expected'); } // Check checksum is ok. Only if we are including object info. Sounds silly but it isn't ;-). if ($includeobj and !$controller->is_checksum_correct($checksum)) { throw new backup_dbops_exception('backup_controller_dbops_saving_checksum_mismatch'); } // Cannot request to $includeobj and $cleanobj at the same time. if ($includeobj and $cleanobj) { throw new backup_dbops_exception('backup_controller_dbops_saving_cannot_include_and_delete'); } // Get all the columns $rec = new stdclass(); $rec->backupid = $controller->get_backupid(); $rec->operation = $controller->get_operation(); $rec->type = $controller->get_type(); $rec->itemid = $controller->get_id(); $rec->format = $controller->get_format(); $rec->interactive = $controller->get_interactive(); $rec->purpose = $controller->get_mode(); $rec->userid = $controller->get_userid(); $rec->status = $controller->get_status(); $rec->execution = $controller->get_execution(); $rec->executiontime= $controller->get_executiontime(); $rec->checksum = $checksum; // Serialize information if ($includeobj) { $rec->controller = base64_encode(serialize($controller)); } else if ($cleanobj) { $rec->controller = ''; } // Send it to DB if ($recexists = $DB->get_record('backup_controllers', array('backupid' => $rec->backupid))) { $rec->id = $recexists->id; $rec->timemodified = time(); $DB->update_record('backup_controllers', $rec); } else { $rec->timecreated = time(); $rec->timemodified = 0; $rec->id = $DB->insert_record('backup_controllers', $rec); } return $rec->id; } public static function load_controller($backupid) { global $DB; if (! $controllerrec = $DB->get_record('backup_controllers', array('backupid' => $backupid))) { throw new backup_dbops_exception('backup_controller_dbops_nonexisting'); } $controller = unserialize(base64_decode($controllerrec->controller)); if (!is_object($controller)) { // The controller field of the table did not contain a serialized object. // It is made empty after it has been used successfully, it is likely that // the user has pressed the browser back button at some point. throw new backup_dbops_exception('backup_controller_dbops_loading_invalid_controller'); } // Check checksum is ok. Sounds silly but it isn't ;-) if (!$controller->is_checksum_correct($controllerrec->checksum)) { throw new backup_dbops_exception('backup_controller_dbops_loading_checksum_mismatch'); } return $controller; } public static function create_backup_ids_temp_table($backupid) { global $CFG, $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services $xmldb_table = new xmldb_table('backup_ids_temp'); $xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); // Set default backupid (not needed but this enforce any missing backupid). That's hackery in action! $xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid); $xmldb_table->add_field('itemname', XMLDB_TYPE_CHAR, 160, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0'); $xmldb_table->add_field('parentitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null); $xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null); $xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $xmldb_table->add_key('backupid_itemname_itemid_uk', XMLDB_KEY_UNIQUE, array('backupid','itemname','itemid')); $xmldb_table->add_index('backupid_parentitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','parentitemid')); $xmldb_table->add_index('backupid_itemname_newitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','newitemid')); $dbman->create_temp_table($xmldb_table); // And create it } public static function create_backup_files_temp_table($backupid) { global $CFG, $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services $xmldb_table = new xmldb_table('backup_files_temp'); $xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); // Set default backupid (not needed but this enforce any missing backupid). That's hackery in action! $xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid); $xmldb_table->add_field('contextid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('component', XMLDB_TYPE_CHAR, 100, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('filearea', XMLDB_TYPE_CHAR, 50, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null); $xmldb_table->add_field('newcontextid', XMLDB_TYPE_INTEGER, 10, null, null, null, null); $xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null); $xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $xmldb_table->add_index('backupid_contextid_component_filearea_itemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','contextid','component','filearea','itemid')); $dbman->create_temp_table($xmldb_table); // And create it } public static function drop_backup_ids_temp_table($backupid) { global $DB; $dbman = $DB->get_manager(); // We are going to use database_manager services $targettablename = 'backup_ids_temp'; if ($dbman->table_exists($targettablename)) { $table = new xmldb_table($targettablename); $dbman->drop_table($table); // And drop it } } /** * Decode the info field from backup_ids_temp or backup_files_temp. * * @param mixed $info The info field data to decode, may be an object or a simple integer. * @return mixed The decoded information. For simple types it returns, for complex ones we decode. */ public static function decode_backup_temp_info($info) { // We encode all data except null. if ($info != null) { return unserialize(gzuncompress(base64_decode($info))); } return $info; } /** * Encode the info field for backup_ids_temp or backup_files_temp. * * @param mixed $info string The info field data to encode. * @return string An encoded string of data or null if the input is null. */ public static function encode_backup_temp_info($info) { // We encode if there is any information to keep the translations simpler. if ($info != null) { // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost. // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618. return base64_encode(gzcompress(serialize($info), 1)); } return $info; } /** * Given one type and id from controller, return the corresponding courseid */ public static function get_courseid_from_type_id($type, $id) { global $DB; if ($type == backup::TYPE_1COURSE) { return $id; // id is the course id } else if ($type == backup::TYPE_1SECTION) { if (! $courseid = $DB->get_field('course_sections', 'course', array('id' => $id))) { throw new backup_dbops_exception('course_not_found_for_section', $id); } return $courseid; } else if ($type == backup::TYPE_1ACTIVITY) { if (! $courseid = $DB->get_field('course_modules', 'course', array('id' => $id))) { throw new backup_dbops_exception('course_not_found_for_moduleid', $id); } return $courseid; } } /** * Given one activity task, return the activity information and related settings * Used by get_moodle_backup_information() */ private static function get_activity_backup_information($task) { $contentinfo = array( 'moduleid' => $task->get_moduleid(), 'sectionid' => $task->get_sectionid(), 'modulename' => $task->get_modulename(), 'title' => $task->get_name(), 'directory' => 'activities/' . $task->get_modulename() . '_' . $task->get_moduleid(), 'insubsection' => ($task->is_in_subsection()) ? 1 : '', ); // Now get activity settings // Calculate prefix to find valid settings $prefix = basename($contentinfo['directory']); $settingsinfo = array(); foreach ($task->get_settings() as $setting) { // Discard ones without valid prefix if (strpos($setting->get_name(), $prefix) !== 0) { continue; } // Validate level is correct (activity) if (!in_array($setting->get_level(), [backup_setting::ACTIVITY_LEVEL, backup_setting::SUBACTIVITY_LEVEL])) { throw new backup_controller_exception('setting_not_activity_level', $setting); } $settinginfo = array( 'level' => 'activity', 'activity' => $prefix, 'name' => $setting->get_name(), 'value' => $setting->get_value()); $settingsinfo[$setting->get_name()] = (object)$settinginfo; } return array($contentinfo, $settingsinfo); } /** * Given one section task, return the section information and related settings * Used by get_moodle_backup_information() */ private static function get_section_backup_information($task) { $contentinfo = array( 'sectionid' => $task->get_sectionid(), 'title' => $task->get_name(), 'directory' => 'sections/' . 'section_' . $task->get_sectionid(), 'parentcmid' => $task->get_delegated_cm() ?? '', 'modname' => $task->get_modname() ?? '', ); // Now get section settings // Calculate prefix to find valid settings $prefix = basename($contentinfo['directory']); $settingsinfo = array(); foreach ($task->get_settings() as $setting) { // Discard ones without valid prefix if (strpos($setting->get_name(), $prefix) !== 0) { continue; } // Validate level is correct (section) if (!in_array($setting->get_level(), [backup_setting::SECTION_LEVEL, backup_setting::SUBSECTION_LEVEL])) { throw new backup_controller_exception('setting_not_section_level', $setting); } $settinginfo = array( 'level' => 'section', 'section' => $prefix, 'name' => $setting->get_name(), 'value' => $setting->get_value()); $settingsinfo[$setting->get_name()] = (object)$settinginfo; } return array($contentinfo, $settingsinfo); } /** * Given one course task, return the course information and related settings * Used by get_moodle_backup_information() */ private static function get_course_backup_information($task) { $contentinfo = array( 'courseid' => $task->get_courseid(), 'title' => $task->get_name(), 'directory' => 'course'); // Now get course settings // Calculate prefix to find valid settings $prefix = basename($contentinfo['directory']); $settingsinfo = array(); foreach ($task->get_settings() as $setting) { // Discard ones without valid prefix if (strpos($setting->get_name(), $prefix) !== 0) { continue; } // Validate level is correct (course) if ($setting->get_level() != backup_setting::COURSE_LEVEL) { throw new backup_controller_exception('setting_not_course_level', $setting); } $settinginfo = array( 'level' => 'course', 'name' => $setting->get_name(), 'value' => $setting->get_value()); $settingsinfo[$setting->get_name()] = (object)$settinginfo; } return array($contentinfo, $settingsinfo); } /** * Given one root task, return the course information and related settings * Used by get_moodle_backup_information() */ private static function get_root_backup_information($task) { // Now get root settings $settingsinfo = array(); foreach ($task->get_settings() as $setting) { // Validate level is correct (root) if ($setting->get_level() != backup_setting::ROOT_LEVEL) { throw new backup_controller_exception('setting_not_root_level', $setting); } $settinginfo = array( 'level' => 'root', 'name' => $setting->get_name(), 'value' => $setting->get_value()); $settingsinfo[$setting->get_name()] = (object)$settinginfo; } return array(null, $settingsinfo); } /** * Get details information for main moodle_backup.xml file, extracting it from * the specified controller. * * 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 string $backupid Backup ID * @param \core\progress\base $progress Optional progress monitor */ public static function get_moodle_backup_information($backupid, ?\core\progress\base $progress = null) { // Start tracking progress if required (for load_controller). if ($progress) { $progress->start_progress('get_moodle_backup_information', 2); } $detailsinfo = array(); // Information details $contentsinfo= array(); // Information about backup contents $settingsinfo= array(); // Information about backup settings $bc = self::load_controller($backupid); // Load controller // Note that we have loaded controller. if ($progress) { $progress->progress(1); } // Details info $detailsinfo['id'] = $bc->get_id(); $detailsinfo['backup_id'] = $bc->get_backupid(); $detailsinfo['type'] = $bc->get_type(); $detailsinfo['format'] = $bc->get_format(); $detailsinfo['interactive'] = $bc->get_interactive(); $detailsinfo['mode'] = $bc->get_mode(); $detailsinfo['execution'] = $bc->get_execution(); $detailsinfo['executiontime'] = $bc->get_executiontime(); $detailsinfo['userid'] = $bc->get_userid(); $detailsinfo['courseid'] = $bc->get_courseid(); // Init content placeholders $contentsinfo['activities'] = array(); $contentsinfo['sections'] = array(); $contentsinfo['course'] = array(); // Get tasks and start nested progress. $tasks = $bc->get_plan()->get_tasks(); if ($progress) { $progress->start_progress('get_moodle_backup_information', count($tasks)); $done = 1; } // Contents info (extract information from tasks) foreach ($tasks as $task) { if ($task instanceof backup_activity_task) { // Activity task if ($task->get_setting_value('included')) { // Only return info about included activities list($contentinfo, $settings) = self::get_activity_backup_information($task); $contentsinfo['activities'][] = $contentinfo; $settingsinfo = array_merge($settingsinfo, $settings); } } else if ($task instanceof backup_section_task) { // Section task if ($task->get_setting_value('included')) { // Only return info about included sections list($contentinfo, $settings) = self::get_section_backup_information($task); $contentsinfo['sections'][] = $contentinfo; $settingsinfo = array_merge($settingsinfo, $settings); } } else if ($task instanceof backup_course_task) { // Course task list($contentinfo, $settings) = self::get_course_backup_information($task); $contentsinfo['course'][] = $contentinfo; $settingsinfo = array_merge($settingsinfo, $settings); } else if ($task instanceof backup_root_task) { // Root task list($contentinfo, $settings) = self::get_root_backup_information($task); $settingsinfo = array_merge($settingsinfo, $settings); } // Report task handled. if ($progress) { $progress->progress($done++); } } $bc->destroy(); // Always need to destroy controller to handle circular references // Finish progress reporting. if ($progress) { $progress->end_progress(); $progress->end_progress(); } return array(array((object)$detailsinfo), $contentsinfo, $settingsinfo); } /** * Update CFG->backup_version and CFG->backup_release if change in * version is detected. */ public static function apply_version_and_release() { global $CFG; if ($CFG->backup_version < backup::VERSION) { set_config('backup_version', backup::VERSION); set_config('backup_release', backup::RELEASE); } } /** * Given the backupid, detect if the backup includes "mnet" remote users or no */ public static function backup_includes_mnet_remote_users($backupid) { global $CFG, $DB; $sql = "SELECT COUNT(*) FROM {backup_ids_temp} b JOIN {user} u ON u.id = b.itemid WHERE b.backupid = ? AND b.itemname = 'userfinal' AND u.mnethostid != ?"; $count = $DB->count_records_sql($sql, array($backupid, $CFG->mnet_localhost_id)); return (int)(bool)$count; } /** * Given the backupid, determine whether this backup should include * files from the moodle file storage system. * * @param string $backupid The ID of the backup. * @return int Indicates whether files should be included in backups. */ public static function backup_includes_files($backupid) { // This function is called repeatedly in a backup with many files. // Loading the controller is a nontrivial operation (in a large test // backup it took 0.3 seconds), so we do a temporary cache of it within // this request. if (self::$includesfilescachebackupid === $backupid) { return self::$includesfilescache; } // Load controller, get value, then destroy controller and return result. self::$includesfilescachebackupid = $backupid; $bc = self::load_controller($backupid); self::$includesfilescache = $bc->get_include_files(); $bc->destroy(); return self::$includesfilescache; } /** * Given the backupid, detect if the backup contains references to external contents * * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org} * @return int */ public static function backup_includes_file_references($backupid) { global $CFG, $DB; $sql = "SELECT count(r.repositoryid) FROM {files} f LEFT JOIN {files_reference} r ON r.id = f.referencefileid JOIN {backup_ids_temp} bi ON f.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'filefinal'"; $count = $DB->count_records_sql($sql, array($backupid)); return (int)(bool)$count; } /** * Given the courseid, return some course related information we want to transport * * @param int $course the id of the course this backup belongs to */ public static function backup_get_original_course_info($courseid) { global $DB; return $DB->get_record('course', array('id' => $courseid), 'fullname, shortname, startdate, enddate, format'); } /** * Sets the default values for the settings in a backup operation * * Based on the mode of the backup it will load proper defaults * using {@link apply_admin_config_defaults}. * * @param backup_controller $controller */ public static function apply_config_defaults(backup_controller $controller) { // Based on the mode of the backup (general, automated, import, hub...) // decide the action to perform to get defaults loaded $mode = $controller->get_mode(); switch ($mode) { case backup::MODE_GENERAL: case backup::MODE_ASYNC: // Load the general defaults $settings = array( 'backup_general_users' => 'users', 'backup_general_anonymize' => 'anonymize', 'backup_general_role_assignments' => 'role_assignments', 'backup_general_activities' => 'activities', 'backup_general_blocks' => 'blocks', 'backup_general_files' => 'files', 'backup_general_filters' => 'filters', 'backup_general_comments' => 'comments', 'backup_general_badges' => 'badges', 'backup_general_calendarevents' => 'calendarevents', 'backup_general_userscompletion' => 'userscompletion', 'backup_general_logs' => 'logs', 'backup_general_histories' => 'grade_histories', 'backup_general_questionbank' => 'questionbank', 'backup_general_groups' => 'groups', 'backup_general_competencies' => 'competencies', 'backup_general_customfield' => 'customfield', 'backup_general_contentbankcontent' => 'contentbankcontent', 'backup_general_xapistate' => 'xapistate', 'backup_general_legacyfiles' => 'legacyfiles' ); self::apply_admin_config_defaults($controller, $settings, true); break; case backup::MODE_IMPORT: // Load the import defaults. $settings = array( 'backup_import_activities' => 'activities', 'backup_import_blocks' => 'blocks', 'backup_import_filters' => 'filters', 'backup_import_badges' => 'badges', 'backup_import_calendarevents' => 'calendarevents', 'backup_import_permissions' => 'permissions', 'backup_import_questionbank' => 'questionbank', 'backup_import_groups' => 'groups', 'backup_import_competencies' => 'competencies', 'backup_import_customfield' => 'customfield', 'backup_import_contentbankcontent' => 'contentbankcontent', 'backup_import_legacyfiles' => 'legacyfiles' ); self::apply_admin_config_defaults($controller, $settings, true); if ((!$controller->get_interactive()) && $controller->get_type() == backup::TYPE_1ACTIVITY) { // This is duplicate - there is no concept of defaults - these settings must be on. $settings = array( 'activities', 'blocks', 'filters', 'questionbank' ); self::force_enable_settings($controller, $settings); // Badges are not included by default when duplicating activities. self::force_settings($controller, ['badges'], false); } break; case backup::MODE_AUTOMATED: // Load the automated defaults. $settings = array( 'backup_auto_users' => 'users', 'backup_auto_role_assignments' => 'role_assignments', 'backup_auto_activities' => 'activities', 'backup_auto_blocks' => 'blocks', 'backup_auto_files' => 'files', 'backup_auto_filters' => 'filters', 'backup_auto_comments' => 'comments', 'backup_auto_badges' => 'badges', 'backup_auto_calendarevents' => 'calendarevents', 'backup_auto_userscompletion' => 'userscompletion', 'backup_auto_logs' => 'logs', 'backup_auto_histories' => 'grade_histories', 'backup_auto_questionbank' => 'questionbank', 'backup_auto_groups' => 'groups', 'backup_auto_competencies' => 'competencies', 'backup_auto_customfield' => 'customfield', 'backup_auto_contentbankcontent' => 'contentbankcontent', 'backup_auto_xapistate' => 'xapistate', 'backup_auto_legacyfiles' => 'legacyfiles' ); self::apply_admin_config_defaults($controller, $settings, false); break; default: // Nothing to do for other modes (HUB...). Some day we // can define defaults (admin UI...) for them if we want to } } /** * Turn these settings on. No defaults from admin settings. * * @param backup_controller $controller * @param array $settings a map from admin config names to setting names (Config name => Setting name) */ private static function force_enable_settings(backup_controller $controller, array $settings) { self::force_settings($controller, $settings, true); } /** * Set these settings to the given $value. No defaults from admin settings. * * @param backup_controller $controller The backup controller. * @param array $settings a map from admin config names to setting names (Config name => Setting name). * @param mixed $value the value to set the settings to. */ private static function force_settings(backup_controller $controller, array $settings, $value) { $plan = $controller->get_plan(); foreach ($settings as $config => $settingname) { if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); // We do not allow this setting to be locked for a duplicate function. if ($setting->get_status() !== base_setting::NOT_LOCKED) { $setting->set_status(base_setting::NOT_LOCKED); } $setting->set_value($value); $setting->set_status(base_setting::LOCKED_BY_CONFIG); } else { $controller->log('Unknown setting: ' . $setting, BACKUP::LOG_DEBUG); } } } /** * Sets the controller settings default values from the admin config. * * @param backup_controller $controller * @param array $settings a map from admin config names to setting names (Config name => Setting name) * @param boolean $uselocks whether "locked" admin settings should be honoured */ private static function apply_admin_config_defaults(backup_controller $controller, array $settings, $uselocks) { $plan = $controller->get_plan(); foreach ($settings as $config=>$settingname) { $value = get_config('backup', $config); if ($value === false) { // Ignore this because the config has not been set. get_config // returns false if a setting doesn't exist, '0' is returned when // the configuration is set to false. $controller->log('Could not find a value for the config ' . $config, BACKUP::LOG_DEBUG); continue; } $locked = $uselocks && (get_config('backup', $config.'_locked') == true); if ($plan->setting_exists($settingname)) { $setting = $plan->get_setting($settingname); // We can only update the setting if it isn't already locked by config or permission. if ($setting->get_status() !== base_setting::LOCKED_BY_CONFIG && $setting->get_status() !== base_setting::LOCKED_BY_PERMISSION) { $setting->set_value($value); if ($locked) { $setting->set_status(base_setting::LOCKED_BY_CONFIG); } } } else { $controller->log('Unknown setting: ' . $setting, BACKUP::LOG_DEBUG); } } } /** * Get the progress details of a backup operation. * Get backup records directly from database, if the backup has successfully completed * there will be no controller object to load. * * @param string $backupid The backup id to query. * @return array $progress The backup progress details. */ public static function get_progress($backupid) { global $DB; $progress = array(); $backuprecord = $DB->get_record( 'backup_controllers', array('backupid' => $backupid), 'status, progress, operation', MUST_EXIST); $status = $backuprecord->status; $progress = $backuprecord->progress; $operation = $backuprecord->operation; $progress = array('status' => $status, 'progress' => $progress, 'backupid' => $backupid, 'operation' => $operation); return $progress; } } util/dbops/backup_structure_dbops.class.php 0000644 00000017657 15215711721 0015243 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB support to the backup_structure stuff * * This class contains various static methods available for all the DB operations * performed by the backup_structure stuff (mainly @backup_nested_element class) * * TODO: Finish phpdocs */ abstract class backup_structure_dbops extends backup_dbops { public static function get_iterator($element, $params, $processor) { global $DB; // Check we are going to get_iterator for one backup_nested_element if (! $element instanceof backup_nested_element) { throw new base_element_struct_exception('backup_nested_element_expected'); } // If var_array, table and sql are null, and element has no final elements it is one nested element without source // Just return one 1 element iterator without information if ($element->get_source_array() === null && $element->get_source_table() === null && $element->get_source_sql() === null && count($element->get_final_elements()) == 0) { return new backup_array_iterator(array(0 => null)); } else if ($element->get_source_array() !== null) { // It's one array, return array_iterator return new backup_array_iterator($element->get_source_array()); } else if ($element->get_source_table() !== null) { // It's one table, return recordset iterator return $DB->get_recordset($element->get_source_table(), self::convert_params_to_values($params, $processor), $element->get_source_table_sortby()); } else if ($element->get_source_sql() !== null) { // It's one sql, return recordset iterator return $DB->get_recordset_sql($element->get_source_sql(), self::convert_params_to_values($params, $processor)); } else { // No sources, supress completely, using null iterator return new backup_null_iterator(); } } public static function convert_params_to_values($params, $processor) { $newparams = array(); foreach ($params as $key => $param) { $newvalue = null; // If we have a base element, get its current value, exception if not set if ($param instanceof base_atom) { if ($param->is_set()) { $newvalue = $param->get_value(); } else { throw new base_element_struct_exception('valueofparamelementnotset', $param->get_name()); } } else if (is_int($param) && $param < 0) { // Possibly one processor variable, let's process it // See @backup class for all the VAR_XXX variables available. // Note1: backup::VAR_PARENTID is handled by nested elements themselves // Note2: trying to use one non-existing var will throw exception $newvalue = $processor->get_var($param); // Else we have one raw param value, use it } else { $newvalue = $param; } $newparams[$key] = $newvalue; } return $newparams; } public static function insert_backup_ids_record($backupid, $itemname, $itemid) { global $DB; // We need to do some magic with scales (that are stored in negative way) if ($itemname == 'scale') { $itemid = -($itemid); } // Now, we skip any annotation with negatives/zero/nulls, ids table only stores true id (always > 0) if ($itemid <= 0 || is_null($itemid)) { return; } // TODO: Analyze if some static (and limited) cache by the 3 params could save us a bunch of record_exists() calls // Note: Sure it will! if (!$DB->record_exists('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname, 'itemid' => $itemid))) { $DB->insert_record('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname, 'itemid' => $itemid)); } } /** * Adds backup id database record for all files in the given file area. * * @param string $backupid Backup ID * @param int $contextid Context id * @param string $component Component * @param string $filearea File area * @param int $itemid Item id * @param \core\progress\base $progress */ public static function annotate_files($backupid, $contextid, $component, $filearea, $itemid, ?\core\progress\base $progress = null) { global $DB; $sql = 'SELECT id FROM {files} WHERE contextid = ? AND component = ?'; $params = array($contextid, $component); if (!is_null($filearea)) { // Add filearea to query and params if necessary $sql .= ' AND filearea = ?'; $params[] = $filearea; } if (!is_null($itemid)) { // Add itemid to query and params if necessary $sql .= ' AND itemid = ?'; $params[] = $itemid; } if ($progress) { $progress->start_progress(''); } $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $record) { if ($progress) { $progress->progress(); } self::insert_backup_ids_record($backupid, 'file', $record->id); } if ($progress) { $progress->end_progress(); } $rs->close(); } /** * Moves all the existing 'item' annotations to their final 'itemfinal' ones * for a given backup. * * @param string $backupid Backup ID * @param string $itemname Item name * @param \core\progress\base $progress Progress tracker */ public static function move_annotations_to_final($backupid, $itemname, \core\progress\base $progress) { global $DB; $progress->start_progress('move_annotations_to_final'); $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname)); $progress->progress(); foreach($rs as $annotation) { // If corresponding 'itemfinal' annotation does not exist, update 'item' to 'itemfinal' if (! $DB->record_exists('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname . 'final', 'itemid' => $annotation->itemid))) { $DB->set_field('backup_ids_temp', 'itemname', $itemname . 'final', array('id' => $annotation->id)); } $progress->progress(); } $rs->close(); // All the remaining $itemname annotations can be safely deleted $DB->delete_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname)); $progress->end_progress(); } /** * Returns true/false if there are annotations for a given item */ public static function annotations_exist($backupid, $itemname) { global $DB; return (bool)$DB->count_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname)); } } util/dbops/backup_dbops.class.php 0000644 00000002443 15215711721 0013106 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-dbops * @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 DB operations * * TODO: Finish phpdocs */ abstract class backup_dbops { } /* * Exception class used by all the @dbops stuff */ class backup_dbops_exception extends backup_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); } } util/structure/base_optigroup.class.php 0000644 00000014531 15215711721 0014426 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing one optigroup for conditional branching */ abstract class base_optigroup extends base_nested_element { /** @var boolean flag indicating if multiple branches can be processed (true) or no (false) */ private $multiple; /** * Constructor - instantiates one base_optigroup, specifying its basic info * * @param string $name name of the element * @param array $elements base_optigroup_elements of this group * @param bool $multiple to decide if the group allows multiple branches processing or no */ public function __construct($name, $elements = null, $multiple = false) { parent::__construct($name); $this->multiple = $multiple; if (!empty($elements)) { $this->add_children($elements); } } // Public API starts here /** * Return the level of this element, that will be, the level of the parent (doesn't consume level) * (note this os only a "cosmetic" effect (to_string) as fact as the real responsible for this * is the corresponding structure_processor for the final output. */ public function get_level() { return $this->get_parent() == null ? 1 : $this->get_parent()->get_level(); } public function to_string($showvalue = false) { $indent = str_repeat(' ', $this->get_level()); // Indent output based in level (4cc) $output = $indent . '!' . $this->get_name() . ' (level: ' . $this->get_level() . ')'; $children = $this->get_children(); if (!empty($children)) { foreach ($this->get_children() as $child) { $output .= PHP_EOL . $child->to_string($showvalue); } } return $output; } // Forbidden API starts here /** * Adding attributes is forbidden */ public function add_attributes($attributes) { throw new base_element_struct_exception('optigroup_not_attributes'); } /** * Instantiating attributes is forbidden */ protected function get_new_attribute($name) { throw new base_element_struct_exception('optigroup_not_attributes'); } /** * Adding final elements is forbidden */ public function add_final_elements($attributes) { throw new base_element_struct_exception('optigroup_not_final_elements'); } /** * Instantiating final elements is forbidden */ protected function get_new_final_element($name) { throw new base_element_struct_exception('optigroup_not_final_elements'); } // Protected API starts here protected function add_children($elements) { if ($elements instanceof base_nested_element) { // Accept 1 element, object $elements = array($elements); } if (is_array($elements)) { foreach ($elements as $element) { $this->add_child($element); } } else { throw new base_optigroup_exception('optigroup_elements_incorrect'); } } /** * Set the parent of the optigroup and, at the same time, process all the * condition params in all the childs */ protected function set_parent($element) { parent::set_parent($element); // Force condition param calculation in all children foreach ($this->get_children() as $child) { $child->set_condition($child->get_condition_param(), $child->get_condition_value()); } } /** * Recalculate all the used elements in the optigroup, observing * restrictions and passing the new used to outer level */ protected function add_used($element) { $newused = array(); // Iterate over all the element useds, filling $newused and // observing the multiple setting foreach ($element->get_used() as $used) { if (!in_array($used, $this->get_used())) { // it's a new one, add to $newused array $newused[] = $used; $this->set_used(array_merge($this->get_used(), array($used))); // add to the optigroup used array } else { // it's an existing one, exception on multiple optigroups if ($this->multiple) { throw new base_optigroup_exception('multiple_optigroup_duplicate_element', $used); } } } // Finally, inform about newused to the next grand(parent/optigroupelement) if ($newused && $this->get_parent()) { $element->set_used($newused); // Only about the newused $grandparent = $this->get_grandoptigroupelement_or_grandparent(); $grandparent->check_and_set_used($element); } } protected function is_multiple() { return $this->multiple; } } /** * base_optigroup_exception to control all the errors while building the optigroups * * This exception will be thrown each time the base_optigroup class detects some * inconsistency related with the building of the group */ class base_optigroup_exception extends base_atom_exception { /** * Constructor - instantiates one base_optigroup_exception * * @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); } } util/structure/base_atom.class.php 0000644 00000012414 15215711721 0013334 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing one atom (name/value) piece of information */ abstract class base_atom { /** @var string name of the element (maps to XML name) */ private $name; /** @var string value of the element (maps to XML content) */ private $value; /** @var bool flag to indicate when one value has been set (true) or no (false) */ private $is_set; /** * Constructor - instantiates one base_atom, specifying its basic info. * * @param string $name name of the element * @param string $value optional value of the element */ public function __construct($name) { $this->validate_name($name); // Check name $this->name = $name; $this->value = null; $this->is_set= false; } protected function validate_name($name) { // Validate various name constraints, throwing exception if needed if (empty($name)) { throw new base_atom_struct_exception('backupatomemptyname', $name); } if (preg_replace('/\s/', '', $name) != $name) { throw new base_atom_struct_exception('backupatomwhitespacename', $name); } if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) { throw new base_atom_struct_exception('backupatomnotasciiname', $name); } } /// Public API starts here public function get_name() { return $this->name; } public function get_value() { return $this->value; } public function set_value($value) { if ($this->is_set) { throw new base_atom_content_exception('backupatomalreadysetvalue', $value); } $this->value = $value; $this->is_set= true; } public function clean_value() { $this->value = null; $this->is_set= false; } public function is_set() { return $this->is_set; } public function to_string($showvalue = false) { $output = $this->name; if ($showvalue) { $value = $this->is_set ? $this->value : 'not set'; $output .= ' => ' . $value; } return $output; } } /** * base_atom abstract exception class * * This exceptions will be used by all the base_atom classes * in order to detect any problem or miss-configuration */ abstract class base_atom_exception extends moodle_exception { /** * Constructor - instantiates one base_atom_exception. * * @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); } } /** * base_atom exception to control all the errors while creating the objects * * This exception will be thrown each time the base_atom class detects some * inconsistency related with the creation of objects and their attributes * (wrong names) */ class base_atom_struct_exception extends base_atom_exception { /** * Constructor - instantiates one base_atom_struct_exception * * @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); } } /** * base_atom exception to control all the errors while setting the values * * This exception will be thrown each time the base_atom class detects some * inconsistency related with the creation of contents (values) of the objects * (bad contents, setting without cleaning...) */ class base_atom_content_exception extends base_atom_exception { /** * Constructor - instantiates one base_atom_content_exception * * @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); } } util/structure/backup_nested_element.class.php 0000644 00000032157 15215711721 0015730 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class representing one nestable element (non final) piece of information on backup */ class backup_nested_element extends base_nested_element implements processable { /** @var array To be used in case we pass one in-memory structure */ protected $var_array; /** @var string */ protected $table; // Table (without prefix) to fetch records from /** @var string */ protected $tablesortby; // The field to sort by when using the table methods /** @var string */ protected $sql; // Raw SQL to fetch records from /** @var mixed */ protected $params; // Unprocessed params as specified in the set_source() call /** @var array */ protected $procparams;// Processed (path resolved) params array /** @var array */ protected $aliases; // Define DB->final element aliases /** @var array */ protected $fileannotations; // array of file areas to be searched by file annotations /** @var int */ protected $counter; // Number of instances of this element that have been processed /** @var array */ protected $results; // Logs the results we encounter during the process. /** @var stdClass[] */ protected $logs; // Some log messages that could be retrieved later. /** * Constructor - instantiates one backup_nested_element, specifying its basic info. * * @param string $name name of the element * @param array $attributes attributes this element will handle (optional, defaults to null) * @param array $final_elements this element will handle (optional, defaults to null) */ public function __construct($name, $attributes = null, $final_elements = null) { parent::__construct($name, $attributes, $final_elements); $this->var_array = null; $this->table = null; $this->tablesortby = null; $this->sql = null; $this->params = null; $this->procparams= null; $this->aliases = array(); $this->fileannotations = array(); $this->counter = 0; $this->results = array(); $this->logs = array(); } /** * Process the nested element * * @param object $processor the processor * @return void */ public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); } $iterator = $this->get_iterator($processor); // Get the iterator over backup-able data foreach ($iterator as $key => $values) { // Process each "ocurrrence" of the nested element (recordset or array) // Fill the values of the attributes and final elements with the $values from the iterator $this->fill_values($values); // Perform pre-process tasks for the nested_element $processor->pre_process_nested_element($this); // Delegate the process of each attribute foreach ($this->get_attributes() as $attribute) { $attribute->process($processor); } // Main process tasks for the nested element, once its attributes have been processed $processor->process_nested_element($this); // Delegate the process of each final_element foreach ($this->get_final_elements() as $final_element) { $final_element->process($processor); } // Delegate the process to the optigroup if ($this->get_optigroup()) { $this->get_optigroup()->process($processor); } // Delegate the process to each child nested_element foreach ($this->get_children() as $child) { $child->process($processor); } // Perform post-process tasks for the nested element $processor->post_process_nested_element($this); // Everything processed, clean values before next iteration $this->clean_values(); // Increment counter for this element $this->counter++; // For root element, check we only have 1 element if ($this->get_parent() === null && $this->counter > 1) { throw new base_element_struct_exception('root_only_one_ocurrence', $this->get_name()); } } // Close the iterator (DB recordset / array iterator) $iterator->close(); } /** * Saves a log message to an array * * @see backup_helper::log() * @param string $message to add to the logs * @param int $level level of importance {@link backup::LOG_DEBUG} and other constants * @param mixed $a to be included in $message * @param int $depth of the message * @param display $bool supporting translation via get_string() if true * @return void */ protected function add_log($message, $level, $a = null, $depth = null, $display = false) { // Adding the result to the oldest parent. if ($this->get_parent()) { $parent = $this->get_grandparent(); $parent->add_log($message, $level, $a, $depth, $display); } else { $log = new stdClass(); $log->message = $message; $log->level = $level; $log->a = $a; $log->depth = $depth; $log->display = $display; $this->logs[] = $log; } } /** * Saves the results to an array * * @param array $result associative array * @return void */ protected function add_result($result) { if (is_array($result)) { // Adding the result to the oldest parent. if ($this->get_parent()) { $parent = $this->get_grandparent(); $parent->add_result($result); } else { $this->results = array_merge($this->results, $result); } } } /** * Returns the logs * * @return array of log objects */ public function get_logs() { return $this->logs; } /** * Returns the results * * @return associative array of results */ public function get_results() { return $this->results; } public function set_source_array($arr) { // TODO: Only elements having final elements can set source $this->var_array = $arr; } public function set_source_table($table, $params, $sortby = null) { if (!is_array($params)) { // Check we are passing array throw new base_element_struct_exception('setsourcerequiresarrayofparams'); } // TODO: Only elements having final elements can set source $this->table = $table; $this->procparams = $this->convert_table_params($params); if ($sortby) { $this->tablesortby = $sortby; } } public function set_source_sql($sql, $params) { if (!is_array($params)) { // Check we are passing array throw new base_element_struct_exception('setsourcerequiresarrayofparams'); } // TODO: Only elements having final elements can set source $this->sql = $sql; $this->procparams = $this->convert_sql_params($params); } public function set_source_alias($dbname, $finalelementname) { // Get final element $finalelement = $this->get_final_element($finalelementname); if (!$finalelement) { // Final element incorrect, throw exception throw new base_element_struct_exception('incorrectaliasfinalnamenotfound', $finalelementname); } else { $this->aliases[$dbname] = $finalelement; } } public function annotate_files($component, $filearea, $elementname, $filesctxid = null) { if (!array_key_exists($component, $this->fileannotations)) { $this->fileannotations[$component] = array(); } if ($elementname !== null) { // Check elementname is valid $elementname = $this->find_element($elementname); //TODO: no warning here? (skodak) } if (array_key_exists($filearea, $this->fileannotations[$component])) { throw new base_element_struct_exception('annotate_files_duplicate_annotation', "$component/$filearea/$elementname"); } $info = new stdclass(); $info->element = $elementname; $info->contextid = $filesctxid; $this->fileannotations[$component][$filearea] = $info; } public function annotate_ids($itemname, $elementname) { $element = $this->find_element($elementname); $element->set_annotation_item($itemname); } /** * Returns one array containing the element in the * @backup_structure and the areas to be searched */ public function get_file_annotations() { return $this->fileannotations; } public function get_source_array() { return $this->var_array; } public function get_source_table() { return $this->table; } public function get_source_table_sortby() { return $this->tablesortby; } public function get_source_sql() { return $this->sql; } public function get_counter() { return $this->counter; } /** * Simple filler that, matching by name, will fill both attributes and final elements * depending of this nested element, debugging info about non-matching elements and/or * elements present in both places. Accept both arrays and objects. */ public function fill_values($values) { $values = (array)$values; foreach ($values as $key => $value) { $found = 0; if ($attribute = $this->get_attribute($key)) { // Set value for attributes $attribute->set_value($value); $found++; } if ($final = $this->get_final_element($key)) { // Set value for final elements $final->set_value($value); $found++; } if (isset($this->aliases[$key])) { // Last chance, set value by processing final element aliases $this->aliases[$key]->set_value($value); $found++; } // Found more than once, notice // TODO: Route this through backup loggers if ($found > 1) { debugging('Key found more than once ' . $key, DEBUG_DEVELOPER); } } } // Protected API starts here protected function convert_table_params($params) { return $this->convert_sql_params($params); } protected function convert_sql_params($params) { $procparams = array(); // Reset processed params foreach ($params as $key => $param) { $procparams[$key] = $this->find_element($param); } return $procparams; } protected function find_element($param) { if ($param == backup::VAR_PARENTID) { // Look for first parent having id attribute/final_element $param = $this->find_first_parent_by_name('id'); // If the param is array, with key 'sqlparam', return the value without modifications } else if (is_array($param) && array_key_exists('sqlparam', $param)) { return $param['sqlparam']; } else if (((int)$param) >= 0) { // Search by path if param isn't a backup::XXX candidate $param = $this->find_element_by_path($param); } return $param; // Return the param unmodified } /** * Returns one instace of the @base_attribute class to work with * when attributes are added simply by name */ protected function get_new_attribute($name) { return new backup_attribute($name); } /** * Returns one instace of the @final_element class to work with * when final_elements are added simply by name */ protected function get_new_final_element($name) { return new backup_final_element($name); } /** * Returns one PHP iterator over each "ocurrence" of this nested * element (array or DB recordset). Delegated to backup_structure_dbops class */ protected function get_iterator($processor) { return backup_structure_dbops::get_iterator($this, $this->procparams, $processor); } } util/structure/backup_structure_processor.class.php 0000644 00000012777 15215711721 0017102 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class defining the process of backup structures * * This class will process the given backup structure (nested/final/attribute) * based on its definition, triggering as many actions as necessary (pre/post * triggers, ids annotations, deciding based on settings, xml output...). Somehow * one visitor pattern to allow backup structures to work with nice decoupling */ class backup_structure_processor extends base_processor { protected $writer; // xml_writer where the processor is going to output data protected $vars; // array of backup::VAR_XXX => helper value pairs to be used by source specifications /** * @var \core\progress\base Progress tracker (null if none) */ protected $progress; /** * Constructor. * * @param xml_writer $writer XML writer to save data * @param c\core\progress\base$progress Progress tracker (optional) */ public function __construct(xml_writer $writer, ?\core\progress\base $progress = null) { $this->writer = $writer; $this->progress = $progress; $this->vars = array(); } public function set_var($key, $value) { if (isset($this->vars[$key])) { throw new backup_processor_exception('processorvariablealreadyset', $key); } $this->vars[$key] = $value; } public function get_var($key) { if (!isset($this->vars[$key])) { throw new backup_processor_exception('processorvariablenotfound', $key); } return $this->vars[$key]; } public function pre_process_nested_element(base_nested_element $nested) { // Send open tag to xml_writer $attrarr = array(); foreach ($nested->get_attributes() as $attribute) { $attrarr[$attribute->get_name()] = $attribute->get_value(); } $this->writer->begin_tag($nested->get_name(), $attrarr); } public function process_nested_element(base_nested_element $nested) { // Proceed with all the file annotations for this element $fileannotations = $nested->get_file_annotations(); if ($fileannotations) { // If there are areas to search $backupid = $this->get_var(backup::VAR_BACKUPID); foreach ($fileannotations as $component => $area) { foreach ($area as $filearea => $info) { $contextid = !is_null($info->contextid) ? $info->contextid : $this->get_var(backup::VAR_CONTEXTID); $itemid = !is_null($info->element) ? $info->element->get_value() : null; backup_structure_dbops::annotate_files($backupid, $contextid, $component, $filearea, $itemid, $this->progress); } } } } public function post_process_nested_element(base_nested_element $nested) { // Send close tag to xml_writer $this->writer->end_tag($nested->get_name()); if ($this->progress) { $this->progress->progress(); } } public function process_final_element(base_final_element $final) { // Send full tag to xml_writer and annotations (only if has value) if ($final->is_set()) { $attrarr = array(); foreach ($final->get_attributes() as $attribute) { $attrarr[$attribute->get_name()] = $attribute->get_value(); } $this->writer->full_tag($final->get_name(), $final->get_value(), $attrarr); if ($this->progress) { $this->progress->progress(); } // Annotate current value if configured to do so $final->annotate($this->get_var(backup::VAR_BACKUPID)); } } public function process_attribute(base_attribute $attribute) { // Annotate current value if configured to do so $attribute->annotate($this->get_var(backup::VAR_BACKUPID)); } } /** * backup_processor exception to control all the errors while working with backup_processors * * This exception will be thrown each time the backup_processors detects some * inconsistency related with the elements to process or its configuration */ class backup_processor_exception extends base_processor_exception { /** * Constructor - instantiates one backup_processor_exception * * @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); } } util/structure/backup_attribute.class.php 0000644 00000004371 15215711721 0014735 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class representing one attribute atom (name/value) piece of information on backup */ class backup_attribute extends base_attribute implements processable, annotable { protected $annotationitem; // To store the item this element will be responsible to annotate public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); } $processor->process_attribute($this); } public function set_annotation_item($itemname) { if (!empty($this->annotationitem)) { $a = new stdclass(); $a->attribute = $this->get_name(); $a->annotating= $this->annotationitem; throw new base_element_struct_exception('attribute_already_used_for_annotation', $a); } $this->annotationitem = $itemname; } public function annotate($backupid) { if (empty($this->annotationitem)) { // We aren't annotating this item return; } if (!$this->is_set()) { throw new base_element_struct_exception('attribute_has_not_value', $this->get_name()); } backup_structure_dbops::insert_backup_ids_record($backupid, $this->annotationitem, $this->get_value()); } } util/structure/backup_optigroup_element.class.php 0000644 00000016546 15215711721 0016502 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class representing one optigroup element for conditional branching * * Objects of this class are internally nested elements, so they support having both * final elements and children (more nested elements) and are able to have one source * and all the stuff supported by nested elements. Their main differences are: * * - Support for conditional execution, using simple equality checks with outer values. * - Don't have representation in the hierarchy, so: * - Their level is the level of the parent of their enclosing optigroup. * - Act as one "path bridge" when looking for parent path values * - They don't support attributes * * Their main use is to allow conditional branching, basically for optional submodules * like question types, assignment subtypes... where different subtrees of information * must be exported. It's correct to assume that each submodule will define its own * optigroup_element for backup purposes. */ class backup_optigroup_element extends backup_nested_element { private $conditionparam; // Unprocessed param representing on path to look for value private $procconditionparam; // Processed base_element param to look for value private $conditionvalue; // Value to compare the the param value with /** * Constructor - instantiates one backup_optigroup_element * * @param string $name of the element * @param array $final_elements this element will handle (optional, defaults to null) * @param string $condition_param param (path) we are using as source for comparing (optional, defaults to null) * @param string $condition_value value we are comparing to (optional, defaults to null) */ public function __construct($name, $final_elements = null, $conditionparam = null, $conditionvalue = null) { parent::__construct($name, null, $final_elements); $this->set_condition($conditionparam, $conditionvalue); } // Public API starts here /** * Sets the condition for this optigroup */ public function set_condition($conditionparam, $conditionvalue) { // We only resolve the condition if the parent of the element (optigroup) already has parent // else, we'll resolve it once the optigroup parent is defined if ($this->get_parent() && $this->get_parent()->get_parent() && $conditionparam !== null) { $this->procconditionparam = $this->find_element($conditionparam); } $this->conditionparam = $conditionparam; $this->conditionvalue = $conditionvalue; } public function get_condition_param() { return $this->conditionparam; } public function get_condition_value() { return $this->conditionvalue; } /** * Evaluate the condition, returning if matches (true) or no (false) */ public function condition_matches() { $match = false; // By default no match $param = $this->procconditionparam; if ($param instanceof base_atom && $param->is_set()) { $match = ($param->get_value() == $this->conditionvalue); // blame $DB for not having === ! } else { $match = ($param == $this->conditionvalue); } return $match; } /** * Return the level of this element, that will be, the level of the parent (doesn't consume level) * (note this os only a "cosmetic" effect (to_string) as fact as the real responsible for this * is the corresponding structure_processor for the final output. */ public function get_level() { return $this->get_parent() == null ? 1 : $this->get_parent()->get_level(); } /** * process one optigroup_element * * Note that this ONLY processes the final elements in order to get all them * before processing any nested element. Pending nested elements are processed * by the optigroup caller. */ public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); } $iterator = $this->get_iterator($processor); // Get the iterator over backup-able data $itcounter = 0; // To check that the iterator only has 1 ocurrence foreach ($iterator as $key => $values) { // Process each "ocurrrence" of the nested element (recordset or array) // Fill the values of the attributes and final elements with the $values from the iterator $this->fill_values($values); // Delegate the process of each final_element foreach ($this->get_final_elements() as $final_element) { $final_element->process($processor); } // Everything processed, clean values before next iteration $this->clean_values(); // Increment counters for this element $this->counter++; $itcounter++; // optigroup_element, check we only have 1 element always if ($itcounter > 1) { throw new base_element_struct_exception('optigroup_element_only_one_ocurrence', $this->get_name()); } } // Close the iterator (DB recordset / array iterator) $iterator->close(); } // Forbidden API starts here /** * Adding optigroups is forbidden */ public function add_add_optigroup($optigroup) { throw new base_element_struct_exception('optigroup_element_not_optigroup'); } /** * Adding attributes is forbidden */ public function add_attributes($attributes) { throw new base_element_struct_exception('optigroup_element_not_attributes'); } /** * Instantiating attributes is forbidden */ protected function get_new_attribute($name) { throw new base_element_struct_exception('optigroup_element_not_attributes'); } // Protected API starts here /** * Returns one instace of the @final_element class to work with * when final_elements are added simply by name */ protected function get_new_final_element($name) { return new backup_final_element($name); } /** * Set the parent of the optigroup_element and, at the same time, * process the condition param */ protected function set_parent($element) { parent::set_parent($element); // Force condition param calculation $this->set_condition($this->conditionparam, $this->conditionvalue); } } util/structure/backup_optigroup.class.php 0000644 00000007531 15215711721 0014763 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class representing one optigroup element for conditional branching * * Objects of this class are internally nested elements, so they support having both * final elements and children (more nested elements) and are able to have one source * and all the stuff supported by nested elements. Their main differences are: * * - Support for conditional execution, using simple equality checks with outer values. * - Don't have representation in the hierarchy, so: * - Their level is the level of the parent of their enclosing optigroup. * - Act as one "path bridge" when looking for parent path values * - They don't support attributes * * Their main use is to allow conditional branching, basically for optional submodules * like question types, assignment subtypes... where different subtrees of information * must be exported. It's correct to assume that each submodule will define its own * optigroup_element for backup purposes. */ class backup_optigroup extends base_optigroup implements processable { public function add_child($element) { if (!($element instanceof backup_optigroup_element)) { // parameter must be backup_optigroup_element if (is_object($element)) { $found = get_class($element); } else { $found = 'non object'; } throw new base_optigroup_exception('optigroup_element_incorrect', $found); } parent::add_child($element); } public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); } // Iterate over all the children backup_optigroup_elements, delegating the process // an knowing it only handles final elements, so we'll delegate process of nested // elements below. Tricky but we need to priorize finals BEFORE nested always. foreach ($this->get_children() as $child) { if ($child->condition_matches()) { // Only if the optigroup_element condition matches $child->process($processor); if (!$this->is_multiple()) { break; // one match found and this optigroup is not multiple => break loop } } } // Now iterate again, but looking for nested elements what will go AFTER all the finals // that have been processed above foreach ($this->get_children() as $child) { if ($child->condition_matches()) { // Only if the optigroup_element condition matches foreach ($child->get_children() as $nested_element) { $nested_element->process($processor); } if (!$this->is_multiple()) { break; // one match found and this optigroup is not multiple => break loop } } } } } util/structure/restore_path_element.class.php 0000644 00000010534 15215711721 0015613 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Class representing one path to be restored from XML file */ class restore_path_element { /** @var string name of the element */ private $name; /** @var string path within the XML file this element will handle */ private $path; /** @var bool flag to define if this element will get child ones grouped or no */ private $grouped; /** @var object object instance in charge of processing this element. */ private $pobject; /** @var mixed last data read for this element or returned data by processing method */ private $data; /** * Constructor - instantiates one restore_path_element, specifying its basic info. * * @param string $name name of the thing being restored. This determines the name of the process_... method called. * @param string $path path of the element. * @param bool $grouped to gather information in grouped mode or no. */ public function __construct($name, $path, $grouped = false) { $this->validate_name($name); // Check name $this->name = $name; $this->path = $path; $this->grouped = $grouped; $this->pobject = null; $this->data = null; } protected function validate_name($name) { // Validate various name constraints, throwing exception if needed if (empty($name)) { throw new restore_path_element_exception('restore_path_element_emptyname', $name); } if (preg_replace('/\s/', '', $name) != $name) { throw new restore_path_element_exception('restore_path_element_whitespace', $name); } if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) { throw new restore_path_element_exception('restore_path_element_notasciiname', $name); } } protected function validate_pobject($pobject) { if (!is_object($pobject)) { throw new restore_path_element_exception('restore_path_element_noobject', $pobject); } if (!method_exists($pobject, $this->get_processing_method())) { throw new restore_path_element_exception('restore_path_element_missingmethod', $this->get_processing_method()); } } /// Public API starts here public function set_processing_object($pobject) { $this->validate_pobject($pobject); $this->pobject = $pobject; } public function set_data($data) { $this->data = $data; } public function get_name() { return $this->name; } public function get_path() { return $this->path; } public function is_grouped() { return $this->grouped; } public function get_processing_object() { return $this->pobject; } public function get_processing_method() { return 'process_' . $this->name; } public function get_data() { return $this->data; } } /** * restore_path_element exception class */ class restore_path_element_exception extends moodle_exception { /** * Constructor - instantiates one restore_path_element_exception * * @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); } } util/structure/backup_final_element.class.php 0000644 00000005023 15215711721 0015527 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Instantiable class representing one final element atom (name/value/parent) piece of information on backup */ class backup_final_element extends base_final_element implements processable, annotable { protected $annotationitem; // To store the item this element will be responsible to annotate public function process($processor) { if (!$processor instanceof base_processor) { // No correct processor, throw exception throw new base_element_struct_exception('incorrect_processor'); } $processor->process_final_element($this); } public function set_annotation_item($itemname) { if (!empty($this->annotationitem)) { $a = new stdclass(); $a->attribute = $this->get_name(); $a->annotating= $this->annotationitem; throw new base_element_struct_exception('element_already_used_for_annotation', $a); } $this->annotationitem = $itemname; } public function annotate($backupid) { if (empty($this->annotationitem)) { // We aren't annotating this item return; } if (!$this->is_set()) { throw new base_element_struct_exception('element_has_not_value', $this->get_name()); } backup_structure_dbops::insert_backup_ids_record($backupid, $this->annotationitem, $this->get_value()); } // Protected API starts here /** * Returns one instace of the @base_attribute class to work with * when attributes are added simply by name */ protected function get_new_attribute($name) { return new backup_attribute($name); } } util/structure/base_attribute.class.php 0000644 00000002247 15215711721 0014402 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing one attribute atom (name/value) piece of information */ abstract class base_attribute extends base_atom { public function to_string($showvalue = false) { return '@' . parent::to_string($showvalue); } } util/structure/tests/fixtures/structure_fixtures.php 0000644 00000010312 15215711721 0017275 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core_backup * @category phpunit * @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(); // Include all the needed stuff global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * helper extended base_attribute class that implements some methods for instantiating and testing */ class mock_base_attribute extends base_attribute { // Nothing to do. Just allow instances to be created } /** * helper extended final_element class that implements some methods for instantiating and testing */ class mock_base_final_element extends base_final_element { /// Implementable API protected function get_new_attribute($name) { return new mock_base_attribute($name); } } /** * helper extended nested_element class that implements some methods for instantiating and testing */ class mock_base_nested_element extends base_nested_element { /// Implementable API protected function get_new_attribute($name) { return new mock_base_attribute($name); } protected function get_new_final_element($name) { return new mock_base_final_element($name); } } /** * helper extended optigroup class that implements some methods for instantiating and testing */ class mock_base_optigroup extends base_optigroup { /// Implementable API protected function get_new_attribute($name) { return new mock_base_attribute($name); } protected function get_new_final_element($name) { return new mock_base_final_element($name); } public function is_multiple() { return parent::is_multiple(); } } /** * helper class that extends backup_final_element in order to skip its value */ class mock_skip_final_element extends backup_final_element { public function set_value($value) { $this->clean_value(); } } /** * helper class that extends backup_final_element in order to modify its value */ class mock_modify_final_element extends backup_final_element { public function set_value($value) { parent::set_value('original was ' . $value . ', now changed'); } } /** * helper class that extends backup_final_element to delegate any calculation to another class */ class mock_final_element_interceptor extends backup_final_element { public function set_value($value) { // Get grandparent name $gpname = $this->get_grandparent()->get_name(); // Get parent name $pname = $this->get_parent()->get_name(); // Get my name $myname = $this->get_name(); // Define class and function name $classname = 'mock_' . $gpname . '_' . $pname . '_interceptor'; $methodname= 'intercept_' . $pname . '_' . $myname; // Invoke the interception method $result = call_user_func(array($classname, $methodname), $value); // Finally set it parent::set_value($result); } } /** * test interceptor class (its methods are called from interceptor) */ abstract class mock_forum_forum_interceptor { static function intercept_forum_completionposts($element) { return 'intercepted!'; } } /** * Instantiable class extending base_atom in order to be able to perform tests */ class mock_base_atom extends base_atom { // Nothing new in this class, just an instantiable base_atom class // with the is_set() method public for testing purposes public function is_set() { return parent::is_set(); } } util/structure/tests/baseattribute_test.php 0000644 00000004524 15215711721 0015340 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use mock_base_attribute; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff require_once(__DIR__.'/fixtures/structure_fixtures.php'); /** * Unit test case the base_attribute class. * * Note: No really much to test here as attribute is 100% * atom extension without new functionality (name/value) * * @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 */ final class baseattribute_test extends \basic_testcase { /** * Correct base_attribute tests */ function test_base_attribute(): void { $name_with_all_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; $value_to_test = 'Some <value> to test'; // Create instance with correct names $instance = new mock_base_attribute($name_with_all_chars); $this->assertInstanceOf('base_attribute', $instance); $this->assertEquals($instance->get_name(), $name_with_all_chars); $this->assertNull($instance->get_value()); // Set value $instance->set_value($value_to_test); $this->assertEquals($instance->get_value(), $value_to_test); // Get to_string() results (with values) $instance = new mock_base_attribute($name_with_all_chars); $instance->set_value($value_to_test); $tostring = $instance->to_string(true); $this->assertTrue(strpos($tostring, '@' . $name_with_all_chars) !== false); $this->assertTrue(strpos($tostring, ' => ') !== false); $this->assertTrue(strpos($tostring, $value_to_test) !== false); } } util/structure/tests/structure_test.php 0000644 00000100137 15215711721 0014537 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_attribute; use backup_controller_dbops; use backup_final_element; use backup_nested_element; use backup_optigroup; use backup_optigroup_element; use backup_processor_exception; use backup_structure_processor; use base_element_struct_exception; use base_optigroup_exception; use base_processor; use memory_xml_output; use xml_writer; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. require_once(__DIR__.'/fixtures/structure_fixtures.php'); global $CFG; require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.php'); /** * Unit test case the all the backup structure classes. Note: Uses database * * @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 */ final class structure_test extends \advanced_testcase { /** @var int Store the inserted forum->id for use in test functions */ protected $forumid; /** @var int Store the inserted discussion1->id for use in test functions */ protected $discussionid1; /** @var int Store the inserted discussion2->id for use in test functions */ protected $discussionid2; /** @var int Store the inserted post1->id for use in test functions */ protected $postid1; /** @var int Store the inserted post2->id for use in test functions */ protected $postid2; /** @var int Store the inserted post3->id for use in test functions */ protected $postid3; /** @var int Store the inserted post4->id for use in test functions */ protected $postid4; /** @var int Official contextid for these tests */ protected $contextid; protected function setUp(): void { parent::setUp(); $this->resetAfterTest(true); $this->contextid = 666; // Let's assume this is the context for the forum $this->fill_records(); // Add common stuff needed by various test methods } private function fill_records() { global $DB; // Create one forum $forum_data = (object)array('course' => 1, 'name' => 'Test forum', 'intro' => 'Intro forum'); $this->forumid = $DB->insert_record('forum', $forum_data); // With two related file $f1_forum_data = (object)array( 'contenthash' => 'testf1', 'contextid' => $this->contextid, 'component'=>'mod_forum', 'filearea' => 'intro', 'filename' => 'tf1', 'itemid' => 0, 'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0, 'pathnamehash' => 'testf1' ); $DB->insert_record('files', $f1_forum_data); $f2_forum_data = (object)array( 'contenthash' => 'tesft2', 'contextid' => $this->contextid, 'component'=>'mod_forum', 'filearea' => 'intro', 'filename' => 'tf2', 'itemid' => 0, 'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0, 'pathnamehash' => 'testf2' ); $DB->insert_record('files', $f2_forum_data); // Create two discussions $discussion1 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd1', 'userid' => 100, 'groupid' => 200); $this->discussionid1 = $DB->insert_record('forum_discussions', $discussion1); $discussion2 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd2', 'userid' => 101, 'groupid' => 201); $this->discussionid2 = $DB->insert_record('forum_discussions', $discussion2); // Create four posts $post1 = (object)array('discussion' => $this->discussionid1, 'userid' => 100, 'subject' => 'p1', 'message' => 'm1'); $this->postid1 = $DB->insert_record('forum_posts', $post1); $post2 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid1, 'userid' => 102, 'subject' => 'p2', 'message' => 'm2'); $this->postid2 = $DB->insert_record('forum_posts', $post2); $post3 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid2, 'userid' => 103, 'subject' => 'p3', 'message' => 'm3'); $this->postid3 = $DB->insert_record('forum_posts', $post3); $post4 = (object)array('discussion' => $this->discussionid2, 'userid' => 101, 'subject' => 'p4', 'message' => 'm4'); $this->postid4 = $DB->insert_record('forum_posts', $post4); // With two related file $f1_post1 = (object)array( 'contenthash' => 'testp1', 'contextid' => $this->contextid, 'component'=>'mod_forum', 'filearea' => 'post', 'filename' => 'tp1', 'itemid' => $this->postid1, 'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0, 'pathnamehash' => 'testp1' ); $DB->insert_record('files', $f1_post1); $f1_post2 = (object)array( 'contenthash' => 'testp2', 'contextid' => $this->contextid, 'component'=>'mod_forum', 'filearea' => 'attachment', 'filename' => 'tp2', 'itemid' => $this->postid2, 'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0, 'pathnamehash' => 'testp2' ); $DB->insert_record('files', $f1_post2); // Create two ratings $rating1 = (object)array( 'contextid' => $this->contextid, 'userid' => 104, 'itemid' => $this->postid1, 'rating' => 2, 'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time()); $r1id = $DB->insert_record('rating', $rating1); $rating2 = (object)array( 'contextid' => $this->contextid, 'userid' => 105, 'itemid' => $this->postid1, 'rating' => 3, 'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time()); $r2id = $DB->insert_record('rating', $rating2); // Create 1 reads $read1 = (object)array('userid' => 102, 'forumid' => $this->forumid, 'discussionid' => $this->discussionid2, 'postid' => $this->postid4); $DB->insert_record('forum_read', $read1); } /** * Backup structures tests (construction, definition and execution) */ function test_backup_structure_construct(): void { global $DB; $backupid = 'Testing Backup ID'; // Official backupid for these tests // Create all the elements that will conform the tree $forum = new backup_nested_element('forum', array('id'), array( 'type', 'name', 'intro', 'introformat', 'assessed', 'assesstimestart', 'assesstimefinish', 'scale', 'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype', 'rsstype', 'rssarticles', 'timemodified', 'warnafter', 'blockafter', new backup_final_element('blockperiod'), new \mock_skip_final_element('completiondiscussions'), new \mock_modify_final_element('completionreplies'), new \mock_final_element_interceptor('completionposts')) ); $discussions = new backup_nested_element('discussions'); $discussion = new backup_nested_element('discussion', array('id'), array( 'forum', 'name', 'firstpost', 'userid', 'groupid', 'assessed', 'timemodified', 'usermodified', 'timestart', 'timeend') ); $posts = new backup_nested_element('posts'); $post = new backup_nested_element('post', array('id'), array( 'discussion', 'parent', 'userid', 'created', 'modified', 'mailed', 'subject', 'message', 'messageformat', 'messagetrust', 'attachment', 'totalscore', 'mailnow') ); $ratings = new backup_nested_element('ratings'); $rating = new backup_nested_element('rating', array('id'), array('userid', 'itemid', 'time', 'post_rating') ); $reads = new backup_nested_element('readposts'); $read = new backup_nested_element('read', array('id'), array( 'userid', 'discussionid', 'postid', 'firstread', 'lastread') ); $inventeds = new backup_nested_element('invented_elements', array('reason', 'version') ); $invented = new backup_nested_element('invented', null, array('one', 'two', 'three') ); $one = $invented->get_final_element('one'); $one->add_attributes(array('attr1', 'attr2')); // Build the tree $forum->add_child($discussions); $discussions->add_child($discussion); $discussion->add_child($posts); $posts->add_child($post); $post->add_child($ratings); $ratings->add_child($rating); $forum->add_child($reads); $reads->add_child($read); $forum->add_child($inventeds); $inventeds->add_child($invented); // Let's add 1 optigroup with 4 elements $alternative1 = new backup_optigroup_element('alternative1', array('name', 'value'), '../../id', $this->postid1); $alternative2 = new backup_optigroup_element('alternative2', array('name', 'value'), backup::VAR_PARENTID, $this->postid2); $alternative3 = new backup_optigroup_element('alternative3', array('name', 'value'), '/forum/discussions/discussion/posts/post/id', $this->postid3); $alternative4 = new backup_optigroup_element('alternative4', array('forumtype', 'forumname')); // Alternative without conditions // Create the optigroup, adding one element $optigroup = new backup_optigroup('alternatives', $alternative1, false); // Add second opti element $optigroup->add_child($alternative2); // Add optigroup to post element $post->add_optigroup($optigroup); // Add third opti element, on purpose after the add_optigroup() line above to check param evaluation works ok $optigroup->add_child($alternative3); // Add 4th opti element (the one without conditions, so will be present always) $optigroup->add_child($alternative4); /// Create some new nested elements, both named 'dupetest1', and add them to alternative1 and alternative2 /// (not problem as far as the optigroup in not unique) $dupetest1 = new backup_nested_element('dupetest1', null, array('field1', 'field2')); $dupetest2 = new backup_nested_element('dupetest2', null, array('field1', 'field2')); $dupetest3 = new backup_nested_element('dupetest3', null, array('field1', 'field2')); $dupetest4 = new backup_nested_element('dupetest1', null, array('field1', 'field2')); $dupetest1->add_child($dupetest3); $dupetest2->add_child($dupetest4); $alternative1->add_child($dupetest1); $alternative2->add_child($dupetest2); // Define sources $forum->set_source_table('forum', array('id' => backup::VAR_ACTIVITYID)); $discussion->set_source_sql('SELECT * FROM {forum_discussions} WHERE forum = ?', array('/forum/id') ); $post->set_source_table('forum_posts', array('discussion' => '/forum/discussions/discussion/id')); $rating->set_source_sql('SELECT * FROM {rating} WHERE itemid = ?', array(backup::VAR_PARENTID) ); $read->set_source_table('forum_read', array('forumid' => '../../id')); $inventeds->set_source_array(array((object)array('reason' => 'I love Moodle', 'version' => '1.0'), (object)array('reason' => 'I love Moodle', 'version' => '2.0'))); // 2 object array $invented->set_source_array(array((object)array('one' => 1, 'two' => 2, 'three' => 3), (object)array('one' => 11, 'two' => 22, 'three' => 33))); // 2 object array // Set optigroup_element sources $alternative1->set_source_array(array((object)array('name' => 'alternative1', 'value' => 1))); // 1 object array // Skip alternative2 source definition on purpose (will be tested) // $alternative2->set_source_array(array((object)array('name' => 'alternative2', 'value' => 2))); // 1 object array $alternative3->set_source_array(array((object)array('name' => 'alternative3', 'value' => 3))); // 1 object array // Alternative 4 source is the forum type and name, so we'll get that in ALL posts (no conditions) that // have not another alternative (post4 in our testing data in the only not matching any other alternative) $alternative4->set_source_sql('SELECT type AS forumtype, name AS forumname FROM {forum} WHERE id = ?', array('/forum/id') ); // Set children of optigroup_element source $dupetest1->set_source_array(array((object)array('field1' => '1', 'field2' => 1))); // 1 object array $dupetest2->set_source_array(array((object)array('field1' => '2', 'field2' => 2))); // 1 object array $dupetest3->set_source_array(array((object)array('field1' => '3', 'field2' => 3))); // 1 object array $dupetest4->set_source_array(array((object)array('field1' => '4', 'field2' => 4))); // 1 object array // Define some aliases $rating->set_source_alias('rating', 'post_rating'); // Map the 'rating' value from DB to 'post_rating' final element // Mark to detect files of type 'forum_intro' in forum (and not item id) $forum->annotate_files('mod_forum', 'intro', null); // Mark to detect file of type 'forum_post' and 'forum_attachment' in post (with itemid being post->id) $post->annotate_files('mod_forum', 'post', 'id'); $post->annotate_files('mod_forum', 'attachment', 'id'); // Mark various elements to be annotated $discussion->annotate_ids('user1', 'userid'); $post->annotate_ids('forum_post', 'id'); $rating->annotate_ids('user2', 'userid'); $rating->annotate_ids('forum_post', 'itemid'); // Create the backup_ids_temp table backup_controller_dbops::create_backup_ids_temp_table($backupid); // Instantiate in memory xml output $xo = new memory_xml_output(); // Instantiate xml_writer and start it $xw = new xml_writer($xo); $xw->start(); // Instantiate the backup processor $processor = new backup_structure_processor($xw); // Set some variables $processor->set_var(backup::VAR_ACTIVITYID, $this->forumid); $processor->set_var(backup::VAR_BACKUPID, $backupid); $processor->set_var(backup::VAR_CONTEXTID,$this->contextid); // Process the backup structure with the backup processor $forum->process($processor); // Stop the xml_writer $xw->stop(); // Check various counters $this->assertEquals($forum->get_counter(), $DB->count_records('forum')); $this->assertEquals($discussion->get_counter(), $DB->count_records('forum_discussions')); $this->assertEquals($rating->get_counter(), $DB->count_records('rating')); $this->assertEquals($read->get_counter(), $DB->count_records('forum_read')); $this->assertEquals($inventeds->get_counter(), 2); // Array // Perform some validations with the generated XML $dom = new \DomDocument(); $dom->loadXML($xo->get_allcontents()); $xpath = new \DOMXPath($dom); // Some more counters $query = '/forum/discussions/discussion/posts/post'; $posts = $xpath->query($query); $this->assertEquals($posts->length, $DB->count_records('forum_posts')); $query = '/forum/invented_elements/invented'; $inventeds = $xpath->query($query); $this->assertEquals($inventeds->length, 2*2); // Check ratings information against DB $ratings = $dom->getElementsByTagName('rating'); $this->assertEquals($ratings->length, $DB->count_records('rating')); foreach ($ratings as $rating) { $ratarr = array(); $ratarr['id'] = $rating->getAttribute('id'); foreach ($rating->childNodes as $node) { if ($node->nodeType != XML_TEXT_NODE) { $ratarr[$node->nodeName] = $node->nodeValue; } } $this->assertEquals($DB->get_field('rating', 'userid', array('id' => $ratarr['id'])), $ratarr['userid']); $this->assertEquals($DB->get_field('rating', 'itemid', array('id' => $ratarr['id'])), $ratarr['itemid']); $this->assertEquals($DB->get_field('rating', 'rating', array('id' => $ratarr['id'])), $ratarr['post_rating']); } // Check forum has "blockeperiod" with value 0 (was declared by object instead of name) $query = '/forum[blockperiod="0"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check forum is missing "completiondiscussions" (as we are using mock_skip_final_element) $query = '/forum/completiondiscussions'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); // Check forum has "completionreplies" with value "original was 0, now changed" (because of mock_modify_final_element) $query = '/forum[completionreplies="original was 0, now changed"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check forum has "completionposts" with value "intercepted!" (because of mock_final_element_interceptor) $query = '/forum[completionposts="intercepted!"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check there isn't any alternative2 tag, as far as it hasn't source defined $query = '//alternative2'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); // Check there are 4 "field1" elements $query = '/forum/discussions/discussion/posts/post//field1'; $result = $xpath->query($query); $this->assertEquals(4, $result->length); // Check first post has one name element with value "alternative1" $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"][name="alternative1"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check there are two "dupetest1" elements $query = '/forum/discussions/discussion/posts/post//dupetest1'; $result = $xpath->query($query); $this->assertEquals(2, $result->length); // Check second post has one name element with value "dupetest2" $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check element "dupetest2" of second post has one field1 element with value "2" $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2[field1="2"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check forth post has no name element $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"]/name'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); // Check 1st, 2nd and 3rd posts have no forumtype element $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"]/forumtype'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/forumtype'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid3.'"]/forumtype'; $result = $xpath->query($query); $this->assertEquals(0, $result->length); // Check 4th post has one forumtype element with value "general" // (because it doesn't matches alternatives 1, 2, 3, then alternative 4, // the one without conditions is being applied) $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"][forumtype="general"]'; $result = $xpath->query($query); $this->assertEquals(1, $result->length); // Check annotations information against DB // Count records in original tables $c_postsid = $DB->count_records_sql('SELECT COUNT(DISTINCT id) FROM {forum_posts}'); $c_dissuserid = $DB->count_records_sql('SELECT COUNT(DISTINCT userid) FROM {forum_discussions}'); $c_ratuserid = $DB->count_records_sql('SELECT COUNT(DISTINCT userid) FROM {rating}'); // Count records in backup_ids_table $f_forumpost = $DB->count_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'forum_post')); $f_user1 = $DB->count_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'user1')); $f_user2 = $DB->count_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'user2')); $c_notbackupid = $DB->count_records_select('backup_ids_temp', 'backupid != ?', array($backupid)); // Peform tests by comparing counts $this->assertEquals($c_notbackupid, 0); // there isn't any record with incorrect backupid $this->assertEquals($c_postsid, $f_forumpost); // All posts have been registered $this->assertEquals($c_dissuserid, $f_user1); // All users coming from discussions have been registered $this->assertEquals($c_ratuserid, $f_user2); // All users coming from ratings have been registered // Check file annotations against DB $fannotations = $DB->get_records('backup_ids_temp', array('backupid' => $backupid, 'itemname' => 'file')); $ffiles = $DB->get_records('files', array('contextid' => $this->contextid)); $this->assertEquals(count($fannotations), count($ffiles)); // Same number of recs in both (all files have been annotated) foreach ($fannotations as $annotation) { // Check ids annotated $this->assertTrue($DB->record_exists('files', array('id' => $annotation->itemid))); } // Drop the backup_ids_temp table backup_controller_dbops::drop_backup_ids_temp_table('testingid'); } /** * Backup structures wrong tests (trying to do things the wrong way) */ function test_backup_structure_wrong(): void { // Instantiate the backup processor $processor = new backup_structure_processor(new xml_writer(new memory_xml_output())); $this->assertTrue($processor instanceof base_processor); // Set one var twice $processor->set_var('onenewvariable', 999); try { $processor->set_var('onenewvariable', 999); $this->assertTrue(false, 'backup_processor_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_processor_exception); $this->assertEquals($e->errorcode, 'processorvariablealreadyset'); $this->assertEquals($e->a, 'onenewvariable'); } // Get non-existing var try { $var = $processor->get_var('nonexistingvar'); $this->assertTrue(false, 'backup_processor_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_processor_exception); $this->assertEquals($e->errorcode, 'processorvariablenotfound'); $this->assertEquals($e->a, 'nonexistingvar'); } // Create nested element and try ro get its parent id (doesn't exisit => exception) $ne = new backup_nested_element('test', 'one', 'two', 'three'); try { $ne->set_source_table('forum', array('id' => backup::VAR_PARENTID)); $ne->process($processor); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'cannotfindparentidforelement'); } // Try to process one nested/final/attribute elements without processor $ne = new backup_nested_element('test', 'one', 'two', 'three'); try { $ne->process(new \stdClass()); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'incorrect_processor'); } $fe = new backup_final_element('test'); try { $fe->process(new \stdClass()); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'incorrect_processor'); } $at = new backup_attribute('test'); try { $at->process(new \stdClass()); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'incorrect_processor'); } // Try to put an incorrect alias $ne = new backup_nested_element('test', 'one', 'two', 'three'); try { $ne->set_source_alias('last', 'nonexisting'); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'incorrectaliasfinalnamenotfound'); $this->assertEquals($e->a, 'nonexisting'); } // Try various incorrect paths specifying source $ne = new backup_nested_element('test', 'one', 'two', 'three'); try { $ne->set_source_table('forum', array('/test/subtest')); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementincorrectfinalorattribute'); $this->assertEquals($e->a, 'subtest'); } try { $ne->set_source_table('forum', array('/wrongtest')); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementincorrectgrandparent'); $this->assertEquals($e->a, 'wrongtest'); } try { $ne->set_source_table('forum', array('../nonexisting')); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementincorrectparent'); $this->assertEquals($e->a, '..'); } // Try various incorrect file annotations $ne = new backup_nested_element('test', 'one', 'two', 'three'); $ne->annotate_files('test', 'filearea', null); try { $ne->annotate_files('test', 'filearea', null); // Try to add annotations twice $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'annotate_files_duplicate_annotation'); $this->assertEquals($e->a, 'test/filearea/'); } $ne = new backup_nested_element('test', 'one', 'two', 'three'); try { $ne->annotate_files('test', 'filearea', 'four'); // Incorrect element $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementincorrectfinalorattribute'); $this->assertEquals($e->a, 'four'); } // Try to add incorrect element to backup_optigroup $bog = new backup_optigroup('test'); try { $bog->add_child(new backup_nested_element('test2')); $this->assertTrue(false, 'base_optigroup_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_optigroup_exception); $this->assertEquals($e->errorcode, 'optigroup_element_incorrect'); $this->assertEquals($e->a, 'backup_nested_element'); } $bog = new backup_optigroup('test'); try { $bog->add_child('test2'); $this->assertTrue(false, 'base_optigroup_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_optigroup_exception); $this->assertEquals($e->errorcode, 'optigroup_element_incorrect'); $this->assertEquals($e->a, 'non object'); } try { $bog = new backup_optigroup('test', new \stdClass()); $this->assertTrue(false, 'base_optigroup_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_optigroup_exception); $this->assertEquals($e->errorcode, 'optigroup_elements_incorrect'); } // Try a wrong processor with backup_optigroup $bog = new backup_optigroup('test'); try { $bog->process(new \stdClass()); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'incorrect_processor'); } // Try duplicating used elements with backup_optigroup // Adding top->down $bog = new backup_optigroup('test', null, true); $boge1 = new backup_optigroup_element('boge1'); $boge2 = new backup_optigroup_element('boge2'); $ne1 = new backup_nested_element('ne1'); $ne2 = new backup_nested_element('ne1'); $bog->add_child($boge1); $bog->add_child($boge2); $boge1->add_child($ne1); try { $boge2->add_child($ne2); $this->assertTrue(false, 'base_optigroup_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_optigroup_exception); $this->assertEquals($e->errorcode, 'multiple_optigroup_duplicate_element'); $this->assertEquals($e->a, 'ne1'); } // Adding down->top $bog = new backup_optigroup('test', null, true); $boge1 = new backup_optigroup_element('boge1'); $boge2 = new backup_optigroup_element('boge2'); $ne1 = new backup_nested_element('ne1'); $ne2 = new backup_nested_element('ne1'); $boge1->add_child($ne1); $boge2->add_child($ne2); $bog->add_child($boge1); try { $bog->add_child($boge2); $this->assertTrue(false, 'base_element_struct_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementexisting'); $this->assertEquals($e->a, 'ne1'); } } } util/structure/tests/basenestedelement_test.php 0000644 00000045330 15215711721 0016171 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use base_atom_struct_exception; use base_element_parent_exception; use base_element_struct_exception; use mock_base_attribute; use mock_base_final_element; use mock_base_nested_element; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff require_once(__DIR__.'/fixtures/structure_fixtures.php'); /** * Unit test case the base_nested_element class. * * Note: highly imbricated with base_final_element class * * @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 */ final class basenestedelement_test extends \basic_testcase { /** * Correct creation tests (attributes and final elements) */ public function test_creation(): void { // Create instance with name, attributes and values and check all them $instance = new mock_base_nested_element('NAME', array('ATTR1', 'ATTR2'), array('VAL1', 'VAL2', 'VAL3')); $this->assertInstanceOf('base_nested_element', $instance); $this->assertEquals($instance->get_name(), 'NAME'); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 2); $this->assertInstanceOf('base_attribute', $attrs['ATTR1']); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertNull($attrs['ATTR1']->get_value()); $this->assertEquals($attrs['ATTR2']->get_name(), 'ATTR2'); $this->assertNull($attrs['ATTR2']->get_value()); $finals = $instance->get_final_elements(); $this->assertTrue(is_array($finals)); $this->assertEquals(count($finals), 3); $this->assertInstanceOf('base_final_element', $finals['VAL1']); $this->assertEquals($finals['VAL1']->get_name(), 'VAL1'); $this->assertNull($finals['VAL1']->get_value()); $this->assertEquals($finals['VAL1']->get_level(), 2); $this->assertInstanceOf('base_nested_element', $finals['VAL1']->get_parent()); $this->assertEquals($finals['VAL2']->get_name(), 'VAL2'); $this->assertNull($finals['VAL2']->get_value()); $this->assertEquals($finals['VAL2']->get_level(), 2); $this->assertInstanceOf('base_nested_element', $finals['VAL1']->get_parent()); $this->assertEquals($finals['VAL3']->get_name(), 'VAL3'); $this->assertNull($finals['VAL3']->get_value()); $this->assertEquals($finals['VAL3']->get_level(), 2); $this->assertInstanceOf('base_nested_element', $finals['VAL1']->get_parent()); $this->assertNull($instance->get_parent()); $this->assertEquals($instance->get_children(), array()); $this->assertEquals($instance->get_level(), 1); // Create instance with name only $instance = new mock_base_nested_element('NAME'); $this->assertInstanceOf('base_nested_element', $instance); $this->assertEquals($instance->get_name(), 'NAME'); $this->assertEquals($instance->get_attributes(), array()); $this->assertEquals($instance->get_final_elements(), array()); $this->assertNull($instance->get_parent()); $this->assertEquals($instance->get_children(), array()); $this->assertEquals($instance->get_level(), 1); // Add some attributes $instance->add_attributes(array('ATTR1', 'ATTR2')); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 2); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertNull($attrs['ATTR1']->get_value()); $this->assertEquals($attrs['ATTR2']->get_name(), 'ATTR2'); $this->assertNull($attrs['ATTR2']->get_value()); // And some more atributes $instance->add_attributes(array('ATTR3', 'ATTR4')); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 4); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertNull($attrs['ATTR1']->get_value()); $this->assertEquals($attrs['ATTR2']->get_name(), 'ATTR2'); $this->assertNull($attrs['ATTR2']->get_value()); $this->assertEquals($attrs['ATTR3']->get_name(), 'ATTR3'); $this->assertNull($attrs['ATTR3']->get_value()); $this->assertEquals($attrs['ATTR4']->get_name(), 'ATTR4'); $this->assertNull($attrs['ATTR4']->get_value()); // Add some final elements $instance->add_final_elements(array('VAL1', 'VAL2', 'VAL3')); $finals = $instance->get_final_elements(); $this->assertTrue(is_array($finals)); $this->assertEquals(count($finals), 3); $this->assertEquals($finals['VAL1']->get_name(), 'VAL1'); $this->assertNull($finals['VAL1']->get_value()); $this->assertEquals($finals['VAL2']->get_name(), 'VAL2'); $this->assertNull($finals['VAL2']->get_value()); $this->assertEquals($finals['VAL3']->get_name(), 'VAL3'); $this->assertNull($finals['VAL3']->get_value()); // Add some more final elements $instance->add_final_elements('VAL4'); $finals = $instance->get_final_elements(); $this->assertTrue(is_array($finals)); $this->assertEquals(count($finals), 4); $this->assertEquals($finals['VAL1']->get_name(), 'VAL1'); $this->assertNull($finals['VAL1']->get_value()); $this->assertEquals($finals['VAL2']->get_name(), 'VAL2'); $this->assertNull($finals['VAL2']->get_value()); $this->assertEquals($finals['VAL3']->get_name(), 'VAL3'); $this->assertNull($finals['VAL3']->get_value()); $this->assertEquals($finals['VAL4']->get_name(), 'VAL4'); $this->assertNull($finals['VAL4']->get_value()); // Get to_string() results (with values) $instance = new mock_base_nested_element('PARENT', array('ATTR1', 'ATTR2'), array('FINAL1', 'FINAL2', 'FINAL3')); $child1 = new mock_base_nested_element('CHILD1', null, new mock_base_final_element('FINAL4')); $child2 = new mock_base_nested_element('CHILD2', null, new mock_base_final_element('FINAL5')); $instance->add_child($child1); $instance->add_child($child2); $children = $instance->get_children(); $final_elements = $children['CHILD1']->get_final_elements(); $final_elements['FINAL4']->set_value('final4value'); $final_elements['FINAL4']->add_attributes('ATTR4'); $grandchild = new mock_base_nested_element('GRANDCHILD', new mock_base_attribute('ATTR5')); $child2->add_child($grandchild); $attrs = $grandchild->get_attributes(); $attrs['ATTR5']->set_value('attr5value'); $tostring = $instance->to_string(true); $this->assertTrue(strpos($tostring, 'PARENT (level: 1)') !== false); $this->assertTrue(strpos($tostring, ' => ') !== false); $this->assertTrue(strpos($tostring, '#FINAL4 (level: 3) => final4value') !== false); $this->assertTrue(strpos($tostring, '@ATTR5 => attr5value') !== false); $this->assertTrue(strpos($tostring, '#FINAL5 (level: 3) => not set') !== false); // Clean values $instance = new mock_base_nested_element('PARENT', array('ATTR1', 'ATTR2'), array('FINAL1', 'FINAL2', 'FINAL3')); $child1 = new mock_base_nested_element('CHILD1', null, new mock_base_final_element('FINAL4')); $child2 = new mock_base_nested_element('CHILD2', null, new mock_base_final_element('FINAL4')); $instance->add_child($child1); $instance->add_child($child2); $children = $instance->get_children(); $final_elements = $children['CHILD1']->get_final_elements(); $final_elements['FINAL4']->set_value('final4value'); $final_elements['FINAL4']->add_attributes('ATTR4'); $grandchild = new mock_base_nested_element('GRANDCHILD', new mock_base_attribute('ATTR4')); $child2->add_child($grandchild); $attrs = $grandchild->get_attributes(); $attrs['ATTR4']->set_value('attr4value'); $this->assertEquals($final_elements['FINAL4']->get_value(), 'final4value'); $this->assertEquals($attrs['ATTR4']->get_value(), 'attr4value'); $instance->clean_values(); $this->assertNull($final_elements['FINAL4']->get_value()); $this->assertNull($attrs['ATTR4']->get_value()); } /** * Incorrect creation tests (attributes and final elements) */ function test_wrong_creation(): void { // Create instance with invalid name try { $instance = new mock_base_nested_element(''); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // Create instance with incorrect (object) final element try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_nested_element('TEST', null, $obj); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Create instance with array containing incorrect (object) final element try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_nested_element('TEST', null, array($obj)); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Create instance with array containing duplicate final elements try { $instance = new mock_base_nested_element('TEST', null, array('VAL1', 'VAL2', 'VAL1')); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to get value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->get_value(); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to set value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->set_value('some_value'); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to clean one value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->clean_value('some_value'); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } } /** * Correct tree tests (children stuff) */ function test_tree(): void { // Create parent and child instances, tree-ing them $parent = new mock_base_nested_element('PARENT'); $child = new mock_base_nested_element('CHILD'); $parent->add_child($child); $this->assertEquals($parent->get_children(), array('CHILD' => $child)); $this->assertEquals($child->get_parent(), $parent); $check_children = $parent->get_children(); $check_child = $check_children['CHILD']; $check_parent = $check_child->get_parent(); $this->assertEquals($check_child->get_name(), 'CHILD'); $this->assertEquals($check_parent->get_name(), 'PARENT'); $this->assertEquals($check_child->get_level(), 2); $this->assertEquals($check_parent->get_level(), 1); $this->assertEquals($check_parent->get_children(), array('CHILD' => $child)); $this->assertEquals($check_child->get_parent(), $parent); // Add parent to grandparent $grandparent = new mock_base_nested_element('GRANDPARENT'); $grandparent->add_child($parent); $this->assertEquals($grandparent->get_children(), array('PARENT' => $parent)); $this->assertEquals($parent->get_parent(), $grandparent); $this->assertEquals($parent->get_children(), array('CHILD' => $child)); $this->assertEquals($child->get_parent(), $parent); $this->assertEquals($child->get_level(), 3); $this->assertEquals($parent->get_level(), 2); $this->assertEquals($grandparent->get_level(), 1); // Add grandchild to child $grandchild = new mock_base_nested_element('GRANDCHILD'); $child->add_child($grandchild); $this->assertEquals($child->get_children(), array('GRANDCHILD' => $grandchild)); $this->assertEquals($grandchild->get_parent(), $child); $this->assertEquals($grandchild->get_level(), 4); $this->assertEquals($child->get_level(), 3); $this->assertEquals($parent->get_level(), 2); $this->assertEquals($grandparent->get_level(), 1); // Add another child to parent $child2 = new mock_base_nested_element('CHILD2'); $parent->add_child($child2); $this->assertEquals($parent->get_children(), array('CHILD' => $child, 'CHILD2' => $child2)); $this->assertEquals($child2->get_parent(), $parent); $this->assertEquals($grandchild->get_level(), 4); $this->assertEquals($child->get_level(), 3); $this->assertEquals($child2->get_level(), 3); $this->assertEquals($parent->get_level(), 2); $this->assertEquals($grandparent->get_level(), 1); } /** * Incorrect tree tests (children stuff) */ function test_wrong_tree(): void { // Add null object child $parent = new mock_base_nested_element('PARENT'); $child = null; try { $parent->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add non base_element object child $parent = new mock_base_nested_element('PARENT'); $child = new \stdClass(); try { $parent->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add existing element (being parent) $parent = new mock_base_nested_element('PARENT'); $child = new mock_base_nested_element('PARENT'); try { $parent->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add existing element (being grandparent) $grandparent = new mock_base_nested_element('GRANDPARENT'); $parent = new mock_base_nested_element('PARENT'); $child = new mock_base_nested_element('GRANDPARENT'); $grandparent->add_child($parent); try { $parent->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add existing element (being grandchild) $grandparent = new mock_base_nested_element('GRANDPARENT'); $parent = new mock_base_nested_element('PARENT'); $child = new mock_base_nested_element('GRANDPARENT'); $parent->add_child($child); try { $grandparent->add_child($parent); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add existing element (being cousin) $grandparent = new mock_base_nested_element('GRANDPARENT'); $parent1 = new mock_base_nested_element('PARENT1'); $parent2 = new mock_base_nested_element('PARENT2'); $child1 = new mock_base_nested_element('CHILD1'); $child2 = new mock_base_nested_element('CHILD1'); $grandparent->add_child($parent1); $parent1->add_child($child1); $parent2->add_child($child2); try { $grandparent->add_child($parent2); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Add element to two parents $parent1 = new mock_base_nested_element('PARENT1'); $parent2 = new mock_base_nested_element('PARENT2'); $child = new mock_base_nested_element('CHILD'); $parent1->add_child($child); try { $parent2->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_parent_exception); } // Add child element already used by own final elements $nested = new mock_base_nested_element('PARENT1', null, array('FINAL1', 'FINAL2')); $child = new mock_base_nested_element('FINAL2', null, array('FINAL3', 'FINAL4')); try { $nested->add_child($child); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); $this->assertEquals($e->errorcode, 'baseelementchildnameconflict'); $this->assertEquals($e->a, 'FINAL2'); } } } util/structure/tests/baseatom_test.php 0000644 00000011016 15215711721 0014267 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use base_atom_content_exception; use base_atom_struct_exception; use mock_base_atom; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff require_once(__DIR__.'/fixtures/structure_fixtures.php'); /** * Unit test case the base_atom class. * * Note: as it's abstract we are testing mock_base_atom instantiable class instead * * @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 */ final class baseatom_test extends \basic_testcase { /** * Correct base_atom_tests */ function test_base_atom(): void { $name_with_all_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; $value_to_test = 'Some <value> to test'; // Create instance with correct names $instance = new mock_base_atom($name_with_all_chars); $this->assertInstanceOf('base_atom', $instance); $this->assertEquals($instance->get_name(), $name_with_all_chars); $this->assertFalse($instance->is_set()); $this->assertNull($instance->get_value()); // Set value $instance->set_value($value_to_test); $this->assertEquals($instance->get_value(), $value_to_test); $this->assertTrue($instance->is_set()); // Clean value $instance->clean_value(); $this->assertFalse($instance->is_set()); $this->assertNull($instance->get_value()); // Get to_string() results (with values) $instance = new mock_base_atom($name_with_all_chars); $instance->set_value($value_to_test); $tostring = $instance->to_string(true); $this->assertTrue(strpos($tostring, $name_with_all_chars) !== false); $this->assertTrue(strpos($tostring, ' => ') !== false); $this->assertTrue(strpos($tostring, $value_to_test) !== false); // Get to_string() results (without values) $tostring = $instance->to_string(false); $this->assertTrue(strpos($tostring, $name_with_all_chars) !== false); $this->assertFalse(strpos($tostring, ' => ')); $this->assertFalse(strpos($tostring, $value_to_test)); } /** * Throwing exception base_atom tests */ function test_base_atom_exceptions(): void { // empty names try { $instance = new mock_base_atom(''); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // whitespace names try { $instance = new mock_base_atom('TESTING ATOM'); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // ascii names try { $instance = new mock_base_atom('TESTING-ATOM'); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } try { $instance = new mock_base_atom('TESTING_ATOM_Á'); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // setting already set value $instance = new mock_base_atom('TEST'); $instance->set_value('test'); try { $instance->set_value('test'); $this->fail("Expecting base_atom_content_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_content_exception); } } } util/structure/tests/baseoptigroup_test.php 0000644 00000014006 15215711721 0015361 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use base_atom_struct_exception; use base_element_struct_exception; use mock_base_attribute; use mock_base_final_element; use mock_base_nested_element; use mock_base_optigroup; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff require_once(__DIR__.'/fixtures/structure_fixtures.php'); /** * Unit test case the base_optigroup class. * * Note: highly imbricated with nested/final base elements * * @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 */ final class baseoptigroup_test extends \basic_testcase { /** * Correct creation tests (s) */ function test_creation(): void { $instance = new mock_base_optigroup('optigroup', null, true); $this->assertInstanceOf('base_optigroup', $instance); $this->assertEquals($instance->get_name(), 'optigroup'); $this->assertNull($instance->get_parent()); $this->assertEquals($instance->get_children(), array()); $this->assertEquals($instance->get_level(), 1); $this->assertTrue($instance->is_multiple()); // Get to_string() results (with values) $child1 = new mock_base_nested_element('child1', null, new mock_base_final_element('four')); $child2 = new mock_base_nested_element('child2', null, new mock_base_final_element('five')); $instance->add_child($child1); $instance->add_child($child2); $children = $instance->get_children(); $final_elements = $children['child1']->get_final_elements(); $final_elements['four']->set_value('final4value'); $final_elements['four']->add_attributes('attr4'); $grandchild = new mock_base_nested_element('grandchild', new mock_base_attribute('attr5')); $child2->add_child($grandchild); $attrs = $grandchild->get_attributes(); $attrs['attr5']->set_value('attr5value'); $tostring = $instance->to_string(true); $this->assertTrue(strpos($tostring, '!optigroup (level: 1)') !== false); $this->assertTrue(strpos($tostring, '?child2 (level: 2) =>') !== false); $this->assertTrue(strpos($tostring, ' => ') !== false); $this->assertTrue(strpos($tostring, '#four (level: 3) => final4value') !== false); $this->assertTrue(strpos($tostring, '@attr5 => attr5value') !== false); $this->assertTrue(strpos($tostring, '#five (level: 3) => not set') !== false); } /** * Incorrect creation tests (attributes and final elements) */ function test_wrong_creation(): void { // Create instance with invalid name try { $instance = new mock_base_nested_element(''); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // Create instance with incorrect (object) final element try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_nested_element('TEST', null, $obj); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Create instance with array containing incorrect (object) final element try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_nested_element('TEST', null, array($obj)); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Create instance with array containing duplicate final elements try { $instance = new mock_base_nested_element('TEST', null, array('VAL1', 'VAL2', 'VAL1')); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to get value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->get_value(); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to set value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->set_value('some_value'); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } // Try to clean one value of base_nested_element $instance = new mock_base_nested_element('TEST'); try { $instance->clean_value('some_value'); $this->fail("Expecting base_element_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_struct_exception); } } } util/structure/tests/basefinalelement_test.php 0000644 00000016455 15215711721 0016006 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use base_atom_struct_exception; use base_attribute; use base_element_attribute_exception; use mock_base_attribute; use mock_base_final_element; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff require_once(__DIR__.'/fixtures/structure_fixtures.php'); /** * Unit test case the base_final_element class. * * Note: highly imbricated with base_nested_element class * * @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 */ final class basefinalelement_test extends \basic_testcase { /** * Correct base_final_element tests */ function test_base_final_element(): void { // Create instance with name $instance = new mock_base_final_element('TEST'); $this->assertInstanceOf('base_final_element', $instance); $this->assertEquals($instance->get_name(), 'TEST'); $this->assertNull($instance->get_value()); $this->assertEquals($instance->get_attributes(), array()); $this->assertNull($instance->get_parent()); $this->assertEquals($instance->get_level(), 1); // Set value $instance->set_value('value'); $this->assertEquals($instance->get_value(), 'value'); // Create instance with name and one object attribute $instance = new mock_base_final_element('TEST', new mock_base_attribute('ATTR1')); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 1); $this->assertTrue($attrs['ATTR1'] instanceof base_attribute); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertNull($attrs['ATTR1']->get_value()); // Create instance with name and various object attributes $attr1 = new mock_base_attribute('ATTR1'); $attr1->set_value('attr1_value'); $attr2 = new mock_base_attribute('ATTR2'); $instance = new mock_base_final_element('TEST', array($attr1, $attr2)); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 2); $this->assertTrue($attrs['ATTR1'] instanceof base_attribute); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertEquals($attrs['ATTR1']->get_value(), 'attr1_value'); $this->assertTrue($attrs['ATTR2'] instanceof base_attribute); $this->assertEquals($attrs['ATTR2']->get_name(), 'ATTR2'); $this->assertNull($attrs['ATTR2']->get_value()); // Create instance with name and one string attribute $instance = new mock_base_final_element('TEST', 'ATTR1'); $attrs = $instance->get_attributes(); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 1); $this->assertTrue($attrs['ATTR1'] instanceof base_attribute); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertNull($attrs['ATTR1']->get_value()); // Create instance with name and various object attributes $instance = new mock_base_final_element('TEST', array('ATTR1', 'ATTR2')); $attrs = $instance->get_attributes(); $attrs['ATTR1']->set_value('attr1_value'); $this->assertTrue(is_array($attrs)); $this->assertEquals(count($attrs), 2); $this->assertTrue($attrs['ATTR1'] instanceof base_attribute); $this->assertEquals($attrs['ATTR1']->get_name(), 'ATTR1'); $this->assertEquals($attrs['ATTR1']->get_value(), 'attr1_value'); $this->assertTrue($attrs['ATTR2'] instanceof base_attribute); $this->assertEquals($attrs['ATTR2']->get_name(), 'ATTR2'); $this->assertNull($attrs['ATTR2']->get_value()); // Clean values $instance = new mock_base_final_element('TEST', array('ATTR1', 'ATTR2')); $instance->set_value('instance_value'); $attrs = $instance->get_attributes(); $attrs['ATTR1']->set_value('attr1_value'); $this->assertEquals($instance->get_value(), 'instance_value'); $this->assertEquals($attrs['ATTR1']->get_value(), 'attr1_value'); $instance->clean_values(); $this->assertNull($instance->get_value()); $this->assertNull($attrs['ATTR1']->get_value()); // Get to_string() results (with values) $instance = new mock_base_final_element('TEST', array('ATTR1', 'ATTR2')); $instance->set_value('final element value'); $attrs = $instance->get_attributes(); $attrs['ATTR1']->set_value('attr1 value'); $tostring = $instance->to_string(true); $this->assertTrue(strpos($tostring, '#TEST (level: 1)') !== false); $this->assertTrue(strpos($tostring, ' => ') !== false); $this->assertTrue(strpos($tostring, 'final element value') !== false); $this->assertTrue(strpos($tostring, 'attr1 value') !== false); } /** * Exception base_final_element tests */ function test_base_final_element_exceptions(): void { // Create instance with invalid name try { $instance = new mock_base_final_element(''); $this->fail("Expecting base_atom_struct_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_atom_struct_exception); } // Create instance with incorrect (object) attribute try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_final_element('TEST', $obj); $this->fail("Expecting base_element_attribute_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_attribute_exception); } // Create instance with array containing incorrect (object) attribute try { $obj = new \stdClass; $obj->name = 'test_attr'; $instance = new mock_base_final_element('TEST', array($obj)); $this->fail("Expecting base_element_attribute_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_attribute_exception); } // Create instance with array containing duplicate attributes try { $instance = new mock_base_final_element('TEST', array('ATTR1', 'ATTR2', 'ATTR1')); $this->fail("Expecting base_element_attribute_exception exception, none occurred"); } catch (\Exception $e) { $this->assertTrue($e instanceof base_element_attribute_exception); } } } util/structure/base_nested_element.class.php 0000644 00000023165 15215711721 0015374 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing one nestable element (non final) piece of information */ abstract class base_nested_element extends base_final_element { /** @var array final elements of the element (maps to XML final elements of the tag) */ private $final_elements; /** @var array children base_elements of this element (describes structure of the XML file) */ private $children; /** @var base_optigroup optional group of this element (branches to be processed conditionally) */ private $optigroup; /** @var array elements already used by the base_element, to avoid circular references */ private $used; /** * Constructor - instantiates one base_nested_element, specifying its basic info. * * @param string $name name of the element * @param array $attributes attributes this element will handle (optional, defaults to null) * @param array $final_elements this element will handle (optional, defaults to null) */ public function __construct($name, $attributes = null, $final_elements = null) { parent::__construct($name, $attributes); $this->final_elements = array(); if (!empty($final_elements)) { $this->add_final_elements($final_elements); } $this->children = array(); $this->optigroup = null; $this->used[] = $name; } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // Before reseting anything, call destroy recursively foreach ($this->children as $child) { $child->destroy(); } foreach ($this->final_elements as $element) { $element->destroy(); } if ($this->optigroup) { $this->optigroup->destroy(); } // Everything has been destroyed recursively, now we can reset safely $this->children = array(); $this->final_elements = array(); $this->optigroup = null; // Delegate to parent to destroy other bits parent::destroy(); } protected function get_used() { return $this->used; } protected function set_used($used) { $this->used = $used; } protected function add_used($element) { $this->used = array_merge($this->used, $element->get_used()); } protected function check_and_set_used($element) { // First of all, check the element being added doesn't conflict with own final elements if (array_key_exists($element->get_name(), $this->final_elements)) { throw new base_element_struct_exception('baseelementchildnameconflict', $element->get_name()); } $grandparent = $this->get_grandoptigroupelement_or_grandparent(); if ($existing = array_intersect($grandparent->get_used(), $element->get_used())) { // Check the element isn't being used already throw new base_element_struct_exception('baseelementexisting', implode($existing)); } $grandparent->add_used($element); // If the parent is one optigroup, add the element useds to it too if ($grandparent->get_parent() instanceof base_optigroup) { $grandparent->get_parent()->add_used($element); } } /// Public API starts here public function get_final_elements() { return $this->final_elements; } public function get_final_element($name) { if (array_key_exists($name, $this->final_elements)) { return $this->final_elements[$name]; } else { return null; } } public function get_children() { return $this->children; } public function get_child($name) { if (array_key_exists($name, $this->children)) { return $this->children[$name]; } else { return null; } } public function get_optigroup() { return $this->optigroup; } public function add_final_elements($final_elements) { if ($final_elements instanceof base_final_element || is_string($final_elements)) { // Accept 1 final_element, object or string $final_elements = array($final_elements); } if (is_array($final_elements)) { foreach ($final_elements as $final_element) { if (is_string($final_element)) { // Accept string final_elements $final_element = $this->get_new_final_element($final_element); } if (!($final_element instanceof base_final_element)) { throw new base_element_struct_exception('baseelementnofinalelement', get_class($final_element)); } if (array_key_exists($final_element->get_name(), $this->final_elements)) { throw new base_element_struct_exception('baseelementexists', $final_element->get_name()); } $this->final_elements[$final_element->get_name()] = $final_element; $final_element->set_parent($this); } } else { throw new base_element_struct_exception('baseelementincorrect'); } } public function add_child($element) { if (!is_object($element) || !($element instanceof base_nested_element)) { // parameter must be a base_nested_element if (!is_object($element) || !($found = get_class($element))) { $found = 'non object'; } throw new base_element_struct_exception('nestedelementincorrect', $found); } $this->check_and_set_used($element); $this->children[$element->get_name()] = $element; $element->set_parent($this); } public function add_optigroup($optigroup) { if (!($optigroup instanceof base_optigroup)) { // parameter must be a base_optigroup if (!$found = get_class($optigroup)) { $found = 'non object'; } throw new base_element_struct_exception('optigroupincorrect', $found); } if ($this->optigroup !== null) { throw new base_element_struct_exception('optigroupalreadyset', $found); } $this->check_and_set_used($optigroup); $this->optigroup = $optigroup; $optigroup->set_parent($this); } public function get_value() { throw new base_element_struct_exception('nestedelementnotvalue'); } public function set_value($value) { throw new base_element_struct_exception('nestedelementnotvalue'); } public function clean_value() { throw new base_element_struct_exception('nestedelementnotvalue'); } public function clean_values() { parent::clean_values(); if (!empty($this->final_elements)) { foreach ($this->final_elements as $final_element) { $final_element->clean_values(); } } if (!empty($this->children)) { foreach ($this->children as $child) { $child->clean_values(); } } if (!empty($this->optigroup)) { $this->optigroup->clean_values(); } } public function to_string($showvalue = false) { $output = parent::to_string($showvalue); if (!empty($this->final_elements)) { foreach ($this->final_elements as $final_element) { $output .= PHP_EOL . $final_element->to_string($showvalue); } } if (!empty($this->children)) { foreach ($this->children as $child) { $output .= PHP_EOL . $child->to_string($showvalue); } } if (!empty($this->optigroup)) { $output .= PHP_EOL . $this->optigroup->to_string($showvalue); } return $output; } // Implementable API /** * Returns one instace of the @final_element class to work with * when final_elements are added simply by name */ abstract protected function get_new_final_element($name); } /** * base_element exception to control all the errors while building the nested tree * * This exception will be thrown each time the base_element class detects some * inconsistency related with the building of the nested tree representing one base part * (invalid objects, circular references, double parents...) */ class base_element_struct_exception extends base_atom_exception { /** * Constructor - instantiates one base_element_struct_exception * * @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); } } util/structure/base_processor.class.php 0000644 00000004161 15215711721 0014413 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing the required implementation for classes able to process structure classes */ abstract class base_processor { abstract function pre_process_nested_element(base_nested_element $nested); abstract function process_nested_element(base_nested_element $nested); abstract function post_process_nested_element(base_nested_element $nested); abstract function process_final_element(base_final_element $final); abstract function process_attribute(base_attribute $attribute); } /** * base_processor abstract exception class * * This exceptions will be used by all the processor classes * in order to detect any problem or miss-configuration */ abstract class base_processor_exception extends moodle_exception { /** * Constructor - instantiates one base_processor_exception. * * @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); } } util/structure/base_final_element.class.php 0000644 00000024657 15215711721 0015212 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-structure * @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 */ /** * Abstract class representing one final element atom (name/value/parent) piece of information */ abstract class base_final_element extends base_atom { /** @var array base_attributes of the element (maps to XML attributes of the tag) */ private $attributes; /** @var base_nested_element parent of this element (describes structure of the XML file) */ private $parent; /** * Constructor - instantiates one base_final_element, specifying its basic info. * * @param string $name name of the element * @param array $attributes attributes this element will handle (optional, defaults to null) */ public function __construct($name, $attributes = null) { parent::__construct($name); $this->attributes = array(); if (!empty($attributes)) { $this->add_attributes($attributes); } $this->parent = null; } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset $this->attributes = array(); $this->parent = null; } protected function set_parent($element) { if ($this->parent) { $info = new stdClass(); $info->currparent= $this->parent->get_name(); $info->newparent = $element->get_name(); $info->element = $this->get_name(); throw new base_element_parent_exception('baseelementhasparent', $info); } $this->parent = $element; } protected function get_grandparent() { $parent = $this->parent; if ($parent instanceof base_nested_element) { return $parent->get_grandparent(); } else { return $this; } } protected function get_grandoptigroupelement_or_grandparent() { $parent = $this->parent; if ($parent instanceof base_optigroup) { return $this; // Have found one parent optigroup, so I (first child of optigroup) am } else if ($parent instanceof base_nested_element) { return $parent->get_grandoptigroupelement_or_grandparent(); // Continue searching } else { return $this; } } protected function find_element_by_path($path) { $patharr = explode('/', trim($path, '/')); // Split the path trimming slashes if (substr($path, 0, 1) == '/') { // Absolute path, go to grandparent and process if (!$this->get_grandparent() instanceof base_nested_element) { throw new base_element_struct_exception('baseelementincorrectgrandparent', $patharr[0]); } else if ($this->get_grandparent()->get_name() !== $patharr[0]) { throw new base_element_struct_exception('baseelementincorrectgrandparent', $patharr[0]); } else { $newpath = implode('/', array_slice($patharr, 1)); // Take out 1st element return $this->get_grandparent()->find_element_by_path($newpath); // Process as relative in grandparent } } else { if ($patharr[0] == '..') { // Go to parent if (!$this->get_parent() instanceof base_nested_element) { throw new base_element_struct_exception('baseelementincorrectparent', $patharr[0]); } else { $newpath = implode('/', array_slice($patharr, 1)); // Take out 1st element return $this->get_parent()->find_element_by_path($newpath); // Process as relative in parent } } else if (count($patharr) > 1) { // Go to next child if (!$this->get_child($patharr[0]) instanceof base_nested_element) { throw new base_element_struct_exception('baseelementincorrectchild', $patharr[0]); } else { $newpath = implode('/', array_slice($patharr, 1)); // Take out 1st element return $this->get_child($patharr[0])->find_element_by_path($newpath); // Process as relative in parent } } else { // Return final element or attribute if ($this->get_final_element($patharr[0]) instanceof base_final_element) { return $this->get_final_element($patharr[0]); } else if ($this->get_attribute($patharr[0]) instanceof base_attribute) { return $this->get_attribute($patharr[0]); } else { throw new base_element_struct_exception('baseelementincorrectfinalorattribute', $patharr[0]); } } } } protected function find_first_parent_by_name($name) { if ($parent = $this->get_parent()) { // If element has parent $element = $parent->get_final_element($name); // Look for name into parent finals $attribute = $parent->get_attribute($name); // Look for name into parent attrs if ($element instanceof base_final_element) { return $element; } else if ($attribute instanceof base_attribute) { return $attribute; } else { // Not found, go up 1 level and continue searching return $parent->find_first_parent_by_name($name); } } else { // No more parents available, return the original backup::VAR_PARENTID, exception throw new base_element_struct_exception('cannotfindparentidforelement', $name); } } /// Public API starts here public function get_attributes() { return $this->attributes; } public function get_attribute($name) { if (array_key_exists($name, $this->attributes)) { return $this->attributes[$name]; } else { return null; } } public function get_parent() { return $this->parent; } public function get_level() { return $this->parent == null ? 1 : $this->parent->get_level() + 1; } public function add_attributes($attributes) { if ($attributes instanceof base_attribute || is_string($attributes)) { // Accept 1 attribute, object or string $attributes = array($attributes); } if (is_array($attributes)) { foreach ($attributes as $attribute) { if (is_string($attribute)) { // Accept string attributes $attribute = $this->get_new_attribute($attribute); } if (!($attribute instanceof base_attribute)) { throw new base_element_attribute_exception('baseelementnoattribute', get_class($attribute)); } if (array_key_exists($attribute->get_name(), $this->attributes)) { throw new base_element_attribute_exception('baseelementattributeexists', $attribute->get_name()); } $this->attributes[$attribute->get_name()] = $attribute; } } else { throw new base_element_attribute_exception('baseelementattributeincorrect'); } } public function clean_values() { parent::clean_value(); if (!empty($this->attributes)) { foreach ($this->attributes as $attribute) { $attribute->clean_value(); } } } public function to_string($showvalue = false) { // Decide the correct prefix $prefix = '#'; // default if ($this->parent instanceof base_optigroup) { $prefix = '?'; } else if ($this instanceof base_nested_element) { $prefix = ''; } $indent = str_repeat(' ', $this->get_level()); // Indent output based in level (4cc) $output = $indent . $prefix . $this->get_name() . ' (level: ' . $this->get_level() . ')'; if ($showvalue) { $value = $this->is_set() ? $this->get_value() : 'not set'; $output .= ' => ' . $value; } if (!empty($this->attributes)) { foreach ($this->attributes as $attribute) { $output .= PHP_EOL . $indent . ' ' . $attribute->to_string($showvalue); } } return $output; } // Implementable API /** * Returns one instace of the @base_attribute class to work with * when attributes are added simply by name */ abstract protected function get_new_attribute($name); } /** * base_element exception to control all the errors related with parents handling */ class base_element_parent_exception extends base_atom_exception { /** * Constructor - instantiates one base_element_parent_exception * * @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); } } /** * base_element exception to control all the errors related with attributes handling */ class base_element_attribute_exception extends base_atom_exception { /** * Constructor - instantiates one base_element_attribute_exception * * @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); } } util/plan/base_task.class.php 0000644 00000022255 15215711721 0012234 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the basis for one execution (backup/restore) task * * TODO: Finish phpdocs */ abstract class base_task implements checksumable, executable, loggable { /** @var string */ protected $name; // One simple name for identification purposes /** @var backup_plan|restore_plan */ protected $plan; // Plan this is part of /** @var base_setting[] */ protected $settings; // One array of base_setting elements to define this task /** @var base_step[] */ protected $steps; // One array of base_step elements /** @var bool */ protected $built; // Flag to know if one task has been built /** @var bool */ protected $executed; // Flag to know if one task has been executed /** * Constructor - instantiates one object of this class */ public function __construct($name, $plan = null) { if (!is_null($plan) && !($plan instanceof base_plan)) { throw new base_task_exception('wrong_base_plan_specified'); } $this->name = $name; $this->plan = $plan; $this->settings = array(); $this->steps = array(); $this->built = false; $this->executed = false; if (!is_null($plan)) { // Add the task to the plan if specified $plan->add_task($this); } } public function get_name() { return $this->name; } public function get_steps() { return $this->steps; } public function get_settings() { return $this->settings; } /** * Returns the weight of this task, an approximation of the amount of time * it will take. By default this value is 1. It can be increased for longer * tasks. * * @return int Weight */ public function get_weight() { return 1; } public function get_setting($name) { // First look in task settings $result = null; foreach ($this->settings as $key => $setting) { if ($setting->get_name() == $name) { if ($result != null) { throw new base_task_exception('multiple_settings_by_name_found', $name); } else { $result = $setting; } } } if ($result) { return $result; } else { // Fallback to plan settings return $this->plan->get_setting($name); } } /** * Check if a setting with the given name exists. * * This method find first in the current settings and then in the plan settings. * * @param string $name Setting name * @return bool */ public function setting_exists($name) { foreach ($this->settings as $setting) { if ($setting->get_name() == $name) { return true; } } return $this->plan->setting_exists($name); } public function get_setting_value($name) { return $this->get_setting($name)->get_value(); } public function get_courseid() { return $this->plan->get_courseid(); } public function get_basepath() { return $this->plan->get_basepath(); } public function get_taskbasepath() { return $this->get_basepath(); } public function get_logger() { return $this->plan->get_logger(); } /** * Gets the progress reporter, which can be used to report progress within * the backup or restore process. * * @return \core\progress\base Progress reporting object */ public function get_progress() { return $this->plan->get_progress(); } public function log($message, $level, $a = null, $depth = null, $display = false) { backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } public function add_step($step) { if (! $step instanceof base_step) { throw new base_task_exception('wrong_base_step_specified'); } // link the step with the task $step->set_task($this); $this->steps[] = $step; } public function set_plan($plan) { if (! $plan instanceof base_plan) { throw new base_task_exception('wrong_base_plan_specified'); } $this->plan = $plan; $this->define_settings(); // Settings are defined when plan & task are linked } /** * Function responsible for building the steps of any task * (must set the $built property to true) */ abstract public function build(); /** * Function responsible for executing the steps of any task * (setting the $executed property to true) */ public function execute() { if (!$this->built) { throw new base_task_exception('base_task_not_built', $this->name); } if ($this->executed) { throw new base_task_exception('base_task_already_executed', $this->name); } // Starts progress based on the weight of this task and number of steps. $progress = $this->get_progress(); $progress->start_progress($this->get_name(), count($this->steps), $this->get_weight()); $done = 0; // Execute all steps. foreach ($this->steps as $step) { $result = $step->execute(); // If step returns array, it will be forwarded to plan // (TODO: shouldn't be array but proper result object) if (is_array($result) and !empty($result)) { $this->add_result($result); } $done++; $progress->progress($done); } // Mark as executed if any step has been executed if (!empty($this->steps)) { $this->executed = true; } // Finish progress for this task. $progress->end_progress(); } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // Before reseting anything, call destroy recursively foreach ($this->steps as $step) { $step->destroy(); } foreach ($this->settings as $setting) { $setting->destroy(); } // Everything has been destroyed recursively, now we can reset safely $this->steps = array(); $this->settings = array(); $this->plan = null; } public function is_checksum_correct($checksum) { return $this->calculate_checksum() === $checksum; } public function calculate_checksum() { // Let's do it using name and settings and steps return md5($this->name . '-' . backup_general_helper::array_checksum_recursive($this->settings) . backup_general_helper::array_checksum_recursive($this->steps)); } /** * Add the given info to the current plan's results. * * @see base_plan::add_result() * @param array $result associative array describing a result of a task/step */ public function add_result($result) { if (!is_null($this->plan)) { $this->plan->add_result($result); } else { debugging('Attempting to add a result of a task not binded with a plan', DEBUG_DEVELOPER); } } /** * Return the current plan's results * * @return array|null */ public function get_results() { if (!is_null($this->plan)) { return $this->plan->get_results(); } else { debugging('Attempting to get results of a task not binded with a plan', DEBUG_DEVELOPER); return null; } } // Protected API starts here /** * This function is invoked on activity creation in order to add all the settings * that are associated with one task. The function will, directly, inject the settings * in the task. */ abstract protected function define_settings(); protected function add_setting($setting) { if (! $setting instanceof base_setting) { throw new base_setting_exception('wrong_base_setting_specified'); } $this->settings[] = $setting; } } /* * Exception class used by all the @base_task stuff */ class base_task_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } util/plan/restore_task.class.php 0000644 00000011754 15215711721 0013007 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuf for one restore task (a collection of steps) * * TODO: Finish phpdocs */ abstract class restore_task extends base_task { /** * Constructor - instantiates one object of this class */ public function __construct($name, $plan = null) { if (!is_null($plan) && !($plan instanceof restore_plan)) { throw new restore_task_exception('wrong_restore_plan_specified'); } parent::__construct($name, $plan); } public function get_restoreid() { return $this->plan->get_restoreid(); } public function get_info() { return $this->plan->get_info(); } public function get_target() { return $this->plan->get_target(); } public function get_userid() { return $this->plan->get_userid(); } public function get_decoder() { return $this->plan->get_decoder(); } public function is_samesite() { return $this->plan->is_samesite(); } public function is_missing_modules() { return $this->plan->is_missing_modules(); } public function is_excluding_activities() { return $this->plan->is_excluding_activities(); } public function set_preloaded_information() { $this->plan->set_preloaded_information(); } public function get_preloaded_information() { return $this->plan->get_preloaded_information(); } public function get_tempdir() { return $this->plan->get_tempdir(); } public function get_old_courseid() { return $this->plan->get_info()->original_course_id; } public function get_old_contextid() { return $this->plan->get_info()->original_course_contextid; } public function get_old_system_contextid() { return $this->plan->get_info()->original_system_contextid; } /** * Given a commment area, return the itemname that contains the itemid mappings * * By default, both are the same (commentarea = itemname), so return it. If some * plugins use a different approach, this method can be overriden in its task. * * @param string $commentarea area defined for this comment * @return string itemname that contains the related itemid mapping */ public function get_comment_mapping_itemname($commentarea) { return $commentarea; } /** * If the task has been executed, launch its after_restore() * method if available */ public function execute_after_restore() { if ($this->executed) { foreach ($this->steps as $step) { if (method_exists($step, 'launch_after_restore_methods')) { $step->launch_after_restore_methods(); } } } if ($this->executed && method_exists($this, 'after_restore')) { $this->after_restore(); } } /** * Compares the provided moodle version with the one the backup was taken from. * * @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX) * @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()} * @return bool True if the comparison passes. */ public function backup_version_compare(int $version, string $operator) { return $this->plan->backup_version_compare($version, $operator); } /** * Compares the provided moodle release with the one the backup was taken from. * * @param string $release Moodle release (X.Y or X.Y.Z) * @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()} * @return bool True if the comparison passes. */ public function backup_release_compare(string $release, string $operator) { return $this->plan->backup_release_compare($release, $operator); } } /* * Exception class used by all the @restore_task stuff */ class restore_task_exception extends base_task_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/backup_step.class.php 0000644 00000003467 15215711721 0012604 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuf for one backup step * * TODO: Finish phpdocs */ abstract class backup_step extends base_step { /** * Constructor - instantiates one object of this class */ public function __construct($name, $task = null) { if (!is_null($task) && !($task instanceof backup_task)) { throw new backup_step_exception('wrong_backup_task_specified'); } parent::__construct($name, $task); } protected function get_backupid() { if (is_null($this->task)) { throw new backup_step_exception('not_specified_backup_task'); } return $this->task->get_backupid(); } } /* * Exception class used by all the @backup_step stuff */ class backup_step_exception extends base_step_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/restore_structure_step.class.php 0000644 00000060546 15215711721 0015143 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuff to restore one xml file * * TODO: Finish phpdocs */ abstract class restore_structure_step extends restore_step { protected $filename; // Name of the file to be parsed protected $contentprocessor; // xml parser processor being used // (need it here, apart from parser // thanks to serialized data to process - // say thanks to blocks!) protected $pathelements; // Array of pathelements to process protected $elementsoldid; // Array to store last oldid used on each element protected $elementsnewid; // Array to store last newid used on each element protected $pathlock; // Path currently locking processing of children const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore // all children below path processor returning it /** * Constructor - instantiates one object of this class */ public function __construct($name, $filename, $task = null) { if (!is_null($task) && !($task instanceof restore_task)) { throw new restore_step_exception('wrong_restore_task_specified'); } $this->filename = $filename; $this->contentprocessor = null; $this->pathelements = array(); $this->elementsoldid = array(); $this->elementsnewid = array(); $this->pathlock = null; parent::__construct($name, $task); } final public function execute() { if (!$this->execute_condition()) { // Check any condition to execute this return; } $fullpath = $this->task->get_taskbasepath(); // We MUST have one fullpath here, else, error if (empty($fullpath)) { throw new restore_step_exception('restore_structure_step_undefined_fullpath'); } // Append the filename to the fullpath $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; // And it MUST exist if (!file_exists($fullpath)) { // Shouldn't happen ever, but... throw new restore_step_exception('missing_moodle_backup_xml_file', $fullpath); } // Get restore_path elements array adapting and preparing it for processing $structure = $this->define_structure(); if (!is_array($structure)) { throw new restore_step_exception('restore_step_structure_not_array', $this->get_name()); } $this->prepare_pathelements($structure); // Create parser and processor $xmlparser = new progressive_parser(); $xmlparser->set_file($fullpath); $xmlprocessor = new restore_structure_parser_processor($this->task->get_courseid(), $this); $this->contentprocessor = $xmlprocessor; // Save the reference to the contentprocessor // as far as we are going to need it out // from parser (blame serialized data!) $xmlparser->set_processor($xmlprocessor); // Add pathelements to processor foreach ($this->pathelements as $element) { $xmlprocessor->add_path($element->get_path(), $element->is_grouped()); } // Set up progress tracking. $progress = $this->get_task()->get_progress(); $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE); $xmlparser->set_progress($progress); // And process it, dispatch to target methods in step will start automatically $xmlparser->process(); // Have finished, launch the after_execute method of all the processing objects $this->launch_after_execute_methods(); $progress->end_progress(); } /** * Receive one chunk of information form the xml parser processor and * dispatch it, following the naming rules */ final public function process($data) { if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen throw new restore_step_exception('restore_structure_step_missing_path', $data['path']); } $element = $this->pathelements[$data['path']]; $object = $element->get_processing_object(); $method = $element->get_processing_method(); $rdata = null; if (empty($object)) { // No processing object defined throw new restore_step_exception('restore_structure_step_missing_pobject', $object); } // Release the lock if we aren't anymore within children of it if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) { $this->pathlock = null; } if (is_null($this->pathlock)) { // Only dispatch if there isn't any lock $rdata = $object->$method($data['tags']); // Dispatch to proper object/method } // If the dispatched method returns SKIP_ALL_CHILDREN, we grab current path in order to // lock dispatching to any children if ($rdata === self::SKIP_ALL_CHILDREN) { // Check we haven't any previous lock if (!is_null($this->pathlock)) { throw new restore_step_exception('restore_structure_step_already_skipping', $data['path']); } // Set the lock $this->pathlock = $data['path'] . '/'; // Lock everything below current path // Continue with normal processing of return values } else if ($rdata !== null) { // If the method has returned any info, set element data to it $element->set_data($rdata); } else { // Else, put the original parsed data $element->set_data($data); } } /** * To send ids pairs to backup_ids_table and to store them into paths * * This method will send the given itemname and old/new ids to the * backup_ids_temp table, and, at the same time, will save the new id * into the corresponding restore_path_element for easier access * by children. Also will inject the known old context id for the task * in case it's going to be used for restoring files later */ public function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) { if ($restorefiles && $parentid) { throw new restore_step_exception('set_mapping_cannot_specify_both_restorefiles_and_parentitemid'); } // If we haven't specified one context for the files, use the task one if (is_null($filesctxid)) { $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null; } else { // Use the specified one $parentitemid = $restorefiles ? $filesctxid : null; } // We have passed one explicit parentid, apply it $parentitemid = !is_null($parentid) ? $parentid : $parentitemid; // Let's call the low level one restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid); // Now, if the itemname matches any pathelement->name, store the latest $newid if (array_key_exists($itemname, $this->elementsoldid)) { // If present in $this->elementsoldid, is valid, put both ids $this->elementsoldid[$itemname] = $oldid; $this->elementsnewid[$itemname] = $newid; } } /** * Returns the latest (parent) old id mapped by one pathelement */ public function get_old_parentid($itemname) { return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null; } /** * Returns the latest (parent) new id mapped by one pathelement */ public function get_new_parentid($itemname) { return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null; } /** * Return the new id of a mapping for the given itemname * * @param string $itemname the type of item * @param int $oldid the item ID from the backup * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false */ public function get_mappingid($itemname, $oldid, $ifnotfound = false) { $mapping = $this->get_mapping($itemname, $oldid); return $mapping ? $mapping->newitemid : $ifnotfound; } /** * Return the complete mapping from the given itemname, itemid */ public function get_mapping($itemname, $oldid) { return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid); } /** * Add all the existing file, given their component and filearea and one backup_ids itemname to match with */ public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { // If the current progress object is set up and ready to receive // indeterminate progress, then use it, otherwise don't. (This check is // just in case this function is ever called from somewhere not within // the execute() method here, which does set up progress like this.) $progress = $this->get_task()->get_progress(); if (!$progress->is_in_progress_section() || $progress->get_current_max() !== \core\progress\base::INDETERMINATE) { $progress = null; } $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid; $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid, null, false, $progress); $resultstoadd = array(); foreach ($results as $result) { $this->log($result->message, $result->level); $resultstoadd[$result->code] = true; } $this->task->add_result($resultstoadd); } /** * As far as restore structure steps are implementing restore_plugin stuff, they need to * have the parent task available for wrapping purposes (get course/context....) * @return restore_task|null */ public function get_task() { return $this->task; } // Protected API starts here /** * Add plugin structure to any element in the structure restore tree * * @param string $plugintype type of plugin as defined by core_component::get_plugin_types() * @param restore_path_element $element element in the structure restore tree that * we are going to add plugin information to */ protected function add_plugin_structure($plugintype, $element) { global $CFG; // Check the requested plugintype is a valid one if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) { throw new restore_step_exception('incorrect_plugin_type', $plugintype); } // Get all the restore path elements, looking across all the plugin dirs $pluginsdirs = core_component::get_plugin_list($plugintype); foreach ($pluginsdirs as $name => $pluginsdir) { // We need to add also backup plugin classes on restore, they may contain // some stuff used both in backup & restore $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin'; $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php'; if (file_exists($backupfile)) { require_once($backupfile); } // Now add restore plugin classes and prepare stuff $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin'; $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php'; if (file_exists($restorefile)) { require_once($restorefile); $restoreplugin = new $restoreclassname($plugintype, $name, $this); // Add plugin paths to the step $this->prepare_pathelements($restoreplugin->define_plugin_structure($element)); } } } /** * Add subplugin structure for a given plugin to any element in the structure restore tree * * This method allows the injection of subplugins (of a specific plugin) parsing and proccessing * to any element in the restore structure. * * NOTE: Initially subplugins were only available for activities (mod), so only the * {@link restore_activity_structure_step} class had support for them, always * looking for /mod/modulenanme subplugins. This new method is a generalization of the * existing one for activities, supporting all subplugins injecting information everywhere. * * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json. * @param restore_path_element $element element in the structure restore tree that * we are going to add subplugin information to. * @param string $plugintype type of the plugin. * @param string $pluginname name of the plugin. * @return void */ protected function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) { global $CFG; // This global declaration is required, because where we do require_once($backupfile); // That file may in turn try to do require_once($CFG->dirroot ...). // That worked in the past, we should keep it working. // Verify if this is a BC call for an activity restore. See NOTE above for this special case. if ($plugintype === null and $pluginname === null) { $plugintype = 'mod'; $pluginname = $this->task->get_modulename(); // TODO: Once all the calls have been changed to add both not null plugintype and pluginname, add a debugging here. } // Check the requested plugintype is a valid one. if (!array_key_exists($plugintype, core_component::get_plugin_types())) { throw new restore_step_exception('incorrect_plugin_type', $plugintype); } // Check the requested pluginname, for the specified plugintype, is a valid one. if (!array_key_exists($pluginname, core_component::get_plugin_list($plugintype))) { throw new restore_step_exception('incorrect_plugin_name', array($plugintype, $pluginname)); } // Check the requested subplugintype is a valid one. $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}"); if (null === $subplugins) { throw new restore_step_exception('plugin_missing_subplugins_configuration', array($plugintype, $pluginname)); } if (!array_key_exists($subplugintype, $subplugins)) { throw new restore_step_exception('incorrect_subplugin_type', $subplugintype); } // Every subplugin optionally can have a common/parent subplugin // class for shared stuff. $parentclass = 'restore_' . $plugintype . '_' . $pluginname . '_' . $subplugintype . '_subplugin'; $parentfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/backup/moodle2/' . $parentclass . '.class.php'; if (file_exists($parentfile)) { require_once($parentfile); } // Get all the restore path elements, looking across all the subplugin dirs. $subpluginsdirs = core_component::get_plugin_list($subplugintype); foreach ($subpluginsdirs as $name => $subpluginsdir) { $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin'; $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php'; if (file_exists($restorefile)) { require_once($restorefile); $restoresubplugin = new $classname($subplugintype, $name, $this); // Add subplugin paths to the step. $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element)); } } } /** * Launch all the after_execute methods present in all the processing objects * * This method will launch all the after_execute methods that can be defined * both in restore_plugin and restore_structure_step classes * * For restore_plugin classes the name of the method to be executed will be * "after_execute_" + connection point (as far as can be multiple connection * points in the same class) * * For restore_structure_step classes is will be, simply, "after_execute". Note * that this is executed *after* the plugin ones */ protected function launch_after_execute_methods() { $alreadylaunched = array(); // To avoid multiple executions foreach ($this->pathelements as $key => $pathelement) { // Get the processing object $pobject = $pathelement->get_processing_object(); // Skip null processors (child of grouped ones for sure) if (is_null($pobject)) { continue; } // Skip restore structure step processors (this) if ($pobject instanceof restore_structure_step) { continue; } // Skip already launched processing objects if (in_array($pobject, $alreadylaunched, true)) { continue; } // Add processing object to array of launched ones $alreadylaunched[] = $pobject; // If the processing object has support for // launching after_execute methods, use it if (method_exists($pobject, 'launch_after_execute_methods')) { $pobject->launch_after_execute_methods(); } } // Finally execute own (restore_structure_step) after_execute method $this->after_execute(); } /** * Launch all the after_restore methods present in all the processing objects * * This method will launch all the after_restore methods that can be defined * both in restore_plugin class * * For restore_plugin classes the name of the method to be executed will be * "after_restore_" + connection point (as far as can be multiple connection * points in the same class) */ public function launch_after_restore_methods() { $alreadylaunched = array(); // To avoid multiple executions foreach ($this->pathelements as $pathelement) { // Get the processing object $pobject = $pathelement->get_processing_object(); // Skip null processors (child of grouped ones for sure) if (is_null($pobject)) { continue; } // Skip restore structure step processors (this) if ($pobject instanceof restore_structure_step) { continue; } // Skip already launched processing objects if (in_array($pobject, $alreadylaunched, true)) { continue; } // Add processing object to array of launched ones $alreadylaunched[] = $pobject; // If the processing object has support for // launching after_restore methods, use it if (method_exists($pobject, 'launch_after_restore_methods')) { $pobject->launch_after_restore_methods(); } } // Finally execute own (restore_structure_step) after_restore method $this->after_restore(); } /** * This method will be executed after the whole structure step have been processed * * After execution method for code needed to be executed after the whole structure * has been processed. Useful for cleaning tasks, files process and others. Simply * overwrite in in your steps if needed */ protected function after_execute() { // do nothing by default } /** * This method will be executed after the rest of the restore has been processed. * * Use if you need to update IDs based on things which are restored after this * step has completed. */ protected function after_restore() { // do nothing by default } /** * Prepare the pathelements for processing, looking for duplicates, applying * processing objects and other adjustments */ protected function prepare_pathelements($elementsarr) { // First iteration, push them to new array, indexed by name // detecting duplicates in names or paths $names = array(); $paths = array(); foreach($elementsarr as $element) { if (!$element instanceof restore_path_element) { throw new restore_step_exception('restore_path_element_wrong_class', get_class($element)); } if (array_key_exists($element->get_name(), $names)) { throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name()); } if (array_key_exists($element->get_path(), $paths)) { throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path()); } $names[$element->get_name()] = true; $paths[$element->get_path()] = $element; } // Now, for each element not having one processing object, if // not child of grouped element, assign $this (the step itself) as processing element // Note method must exist or we'll get one @restore_path_element_exception foreach ($paths as $pelement) { if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) { $pelement->set_processing_object($this); } // Populate $elementsoldid and $elementsoldid based on available pathelements $this->elementsoldid[$pelement->get_name()] = null; $this->elementsnewid[$pelement->get_name()] = null; } // Done, add them to pathelements (dupes by key - path - are discarded) $this->pathelements = array_merge($this->pathelements, $paths); } /** * Given one pathelement, return true if grouped parent was found * * @param restore_path_element $pelement the element we are interested in. * @param restore_path_element[] $elements the elements that exist. * @return bool true if this element is inside a grouped parent. */ public function grouped_parent_exists($pelement, $elements) { foreach ($elements as $element) { if ($pelement->get_path() == $element->get_path()) { continue; // Don't compare against itself. } // If element is grouped and parent of pelement, return true. if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) { return true; } } return false; // No grouped parent found. } /** * To conditionally decide if one step will be executed or no * * For steps needing to be executed conditionally, based in dynamic * conditions (at execution time vs at declaration time) you must * override this function. It will return true if the step must be * executed and false if not */ protected function execute_condition() { return true; } /** * Function that will return the structure to be processed by this restore_step. * Must return one array of @restore_path_element elements */ abstract protected function define_structure(); } util/plan/restore_plan.class.php 0000644 00000020716 15215711721 0012775 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementable class defining the needed stuf for one restore plan * * TODO: Finish phpdocs */ class restore_plan extends base_plan implements loggable { /** * * @var restore_controller */ protected $controller; // The restore controller building/executing this plan protected $basepath; // Fullpath to dir where backup is available protected $preloaded; // When executing the plan, do we have preloaded (from checks) info protected $decoder; // restore_decode_processor in charge of decoding all the interlinks protected $missingmodules; // to flag if restore has detected some missing module protected $excludingdactivities; // to flag if restore settings are excluding any activity /** * Constructor - instantiates one object of this class */ public function __construct($controller) { global $CFG; if (! $controller instanceof restore_controller) { throw new restore_plan_exception('wrong_restore_controller_specified'); } $backuptempdir = make_backup_temp_directory(''); $this->controller = $controller; $this->basepath = $backuptempdir . '/' . $controller->get_tempdir(); $this->preloaded = false; $this->decoder = new restore_decode_processor($this->get_restoreid(), $this->get_info()->original_wwwroot, $CFG->wwwroot); $this->missingmodules = false; $this->excludingdactivities = false; parent::__construct('restore_plan'); } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset $this->controller = null; // Delegate to base plan the rest parent::destroy(); } public function build() { restore_plan_builder::build_plan($this->controller); // We are moodle2 always, go straight to builder $this->built = true; } public function get_restoreid() { return $this->controller->get_restoreid(); } public function get_courseid() { return $this->controller->get_courseid(); } public function get_mode() { return $this->controller->get_mode(); } public function get_basepath() { return $this->basepath; } public function get_logger() { return $this->controller->get_logger(); } /** * Gets the progress reporter, which can be used to report progress within * the backup or restore process. * * @return \core\progress\base Progress reporting object */ public function get_progress() { return $this->controller->get_progress(); } public function get_info() { return $this->controller->get_info(); } public function get_target() { return $this->controller->get_target(); } public function get_userid() { return $this->controller->get_userid(); } public function get_decoder() { return $this->decoder; } public function is_samesite() { return $this->controller->is_samesite(); } public function is_missing_modules() { return $this->missingmodules; } public function is_excluding_activities() { return $this->excludingdactivities; } public function set_preloaded_information() { $this->preloaded = true; } public function get_preloaded_information() { return $this->preloaded; } public function get_tempdir() { return $this->controller->get_tempdir(); } public function set_missing_modules() { $this->missingmodules = true; } public function set_excluding_activities() { $this->excludingdactivities = true; } public function log($message, $level, $a = null, $depth = null, $display = false) { backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } /** * Function responsible for executing the tasks of any plan */ public function execute() { if ($this->controller->get_status() != backup::STATUS_AWAITING) { throw new restore_controller_exception('restore_not_executable_awaiting_required', $this->controller->get_status()); } $this->controller->set_status(backup::STATUS_EXECUTING); parent::execute(); $this->controller->set_status(backup::STATUS_FINISHED_OK); // Check if we are restoring a course. if ($this->controller->get_type() === backup::TYPE_1COURSE) { // Check to see if we are on the same site to pass original course info. $issamesite = $this->controller->is_samesite(); $otherarray = array('type' => $this->controller->get_type(), 'target' => $this->controller->get_target(), 'mode' => $this->controller->get_mode(), 'operation' => $this->controller->get_operation(), 'samesite' => $issamesite ); if ($this->controller->is_samesite()) { $otherarray['originalcourseid'] = $this->controller->get_info()->original_course_id; } // Trigger a course restored event. $event = \core\event\course_restored::create(array( 'objectid' => $this->get_courseid(), 'userid' => $this->get_userid(), 'context' => context_course::instance($this->get_courseid()), 'other' => $otherarray )); $event->trigger(); } } /** * Execute the after_restore methods of all the executed tasks in the plan */ public function execute_after_restore() { // Simply iterate over each task in the plan and delegate to them the execution $progress = $this->get_progress(); $progress->start_progress($this->get_name() . ': executing execute_after_restore for all tasks', count($this->tasks)); /** @var base_task $task */ foreach ($this->tasks as $task) { $task->execute_after_restore(); $progress->increment_progress(); } $progress->end_progress(); } /** * Compares the provided moodle version with the one the backup was taken from. * * @param int $version Moodle version number (YYYYMMDD or YYYYMMDDXX) * @param string $operator Operator to compare the provided version to the backup version. {@see version_compare()} * @return bool True if the comparison passes. */ public function backup_version_compare(int $version, string $operator): bool { preg_match('/(\d{' . strlen($version) . '})/', $this->get_info()->moodle_version, $matches); $backupbuild = (int)$matches[1]; return version_compare($backupbuild, $version, $operator); } /** * Compares the provided moodle release with the one the backup was taken from. * * @param string $release Moodle release (X.Y or X.Y.Z) * @param string $operator Operator to compare the provided release to the backup release. {@see version_compare()} * @return bool True if the comparison passes. */ public function backup_release_compare(string $release, string $operator): bool { return version_compare($this->get_info()->backup_release, $release, $operator); } } /* * Exception class used by all the @restore_plan stuff */ class restore_plan_exception extends base_plan_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/backup_plan.class.php 0000644 00000013013 15215711721 0012547 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Implementable class defining the needed stuf for one backup plan * * TODO: Finish phpdocs */ class backup_plan extends base_plan implements loggable { protected $controller; // The backup controller building/executing this plan protected $basepath; // Fullpath to dir where backup is created protected $excludingdactivities; /** * The role ids to keep in a copy operation. * @var array */ protected $keptroles = array(); /** * Constructor - instantiates one object of this class */ public function __construct($controller) { if (! $controller instanceof backup_controller) { throw new backup_plan_exception('wrong_backup_controller_specified'); } $backuptempdir = make_backup_temp_directory(''); $this->controller = $controller; $this->basepath = $backuptempdir . '/' . $controller->get_backupid(); $this->excludingdactivities = false; parent::__construct('backup_plan'); } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset $this->controller = null; // Delegate to base plan the rest parent::destroy(); } public function build() { backup_factory::build_plan($this->controller); // Dispatch to correct format $this->built = true; } public function get_backupid() { return $this->controller->get_backupid(); } public function get_type() { return $this->controller->get_type(); } public function get_mode() { return $this->controller->get_mode(); } public function get_courseid() { return $this->controller->get_courseid(); } public function get_basepath() { return $this->basepath; } public function get_logger() { return $this->controller->get_logger(); } /** * Gets the progress reporter, which can be used to report progress within * the backup or restore process. * * @return \core\progress\base Progress reporting object */ public function get_progress() { return $this->controller->get_progress(); } public function is_excluding_activities() { return $this->excludingdactivities; } public function set_excluding_activities() { $this->excludingdactivities = true; } /** * Sets the user roles that should be kept in the destination course * for a course copy operation. * * @param array $roleids */ public function set_kept_roles(array $roleids): void { $this->keptroles = $roleids; } /** * Get the user roles that should be kept in the destination course * for a course copy operation. * * @return array */ public function get_kept_roles(): array { return $this->keptroles; } public function log($message, $level, $a = null, $depth = null, $display = false) { backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } /** * Function responsible for executing the tasks of any plan */ public function execute() { if ($this->controller->get_status() != backup::STATUS_AWAITING) { throw new backup_controller_exception('backup_not_executable_awaiting_required', $this->controller->get_status()); } $this->controller->set_status(backup::STATUS_EXECUTING); parent::execute(); $this->controller->set_status(backup::STATUS_FINISHED_OK); if ($this->controller->get_type() === backup::TYPE_1COURSE) { // Trigger a course_backup_created event. $otherarray = array('format' => $this->controller->get_format(), 'mode' => $this->controller->get_mode(), 'interactive' => $this->controller->get_interactive(), 'type' => $this->controller->get_type(), 'backupid' => $this->controller->get_backupid() ); $event = \core\event\course_backup_created::create(array( 'objectid' => $this->get_courseid(), 'context' => context_course::instance($this->get_courseid()), 'other' => $otherarray )); $event->trigger(); } } } /* * Exception class used by all the @backup_plan stuff */ class backup_plan_exception extends base_plan_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/backup_structure_step.class.php 0000644 00000026450 15215711721 0014721 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuff to backup one @backup_structure * * TODO: Finish phpdocs */ abstract class backup_structure_step extends backup_step { /** * Name of the file to be generated * @var string */ protected $filename; /** * xml content transformer being used (need it here, apart from xml_writer, * thanks to serialized data to process - say thanks to blocks!) * @var backup_xml_transformer|null */ protected $contenttransformer; /** * Constructor - instantiates one object of this class */ public function __construct($name, $filename, $task = null) { if (!is_null($task) && !($task instanceof backup_task)) { throw new backup_step_exception('wrong_backup_task_specified'); } $this->filename = $filename; $this->contenttransformer = null; parent::__construct($name, $task); } public function execute() { if (!$this->execute_condition()) { // Check any condition to execute this return; } $fullpath = $this->task->get_taskbasepath(); // We MUST have one fullpath here, else, error if (empty($fullpath)) { throw new backup_step_exception('backup_structure_step_undefined_fullpath'); } // Append the filename to the fullpath $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; // Create output, transformer, writer, processor $xo = new file_xml_output($fullpath); $xt = null; if (class_exists('backup_xml_transformer')) { $xt = new backup_xml_transformer($this->get_courseid()); $this->contenttransformer = $xt; // Save the reference to the transformer // as far as we are going to need it out // from xml_writer (blame serialized data!) } $xw = new xml_writer($xo, $xt); $progress = $this->task->get_progress(); $progress->start_progress($this->get_name()); $pr = new backup_structure_processor($xw, $progress); // Set processor variables from settings foreach ($this->get_settings() as $setting) { $pr->set_var($setting->get_name(), $setting->get_value()); } // Add backupid as one more var for processor $pr->set_var(backup::VAR_BACKUPID, $this->get_backupid()); // Get structure definition $structure = $this->define_structure(); if (! $structure instanceof backup_nested_element) { throw new backup_step_exception('backup_structure_step_wrong_structure'); } // Start writer $xw->start(); // Process structure definition $structure->process($pr); // Get the results from the nested elements $results = $structure->get_results(); // Get the log messages to append to the log $logs = $structure->get_logs(); foreach ($logs as $log) { $this->log($log->message, $log->level, $log->a, $log->depth, $log->display); } // Close everything $xw->stop(); $progress->end_progress(); // Destroy the structure. It helps PHP 5.2 memory a lot! $structure->destroy(); return $results; } /** * As far as backup structure steps are implementing backup_plugin stuff, they need to * have the parent task available for wrapping purposes (get course/context....) */ public function get_task() { return $this->task; } // Protected API starts here /** * Add plugin structure to any element in the structure backup tree * * @param string $plugintype type of plugin as defined by core_component::get_plugin_types() * @param backup_nested_element $element element in the structure backup tree that * we are going to add plugin information to * @param bool $multiple to define if multiple plugins can produce information * for each instance of $element (true) or no (false) */ protected function add_plugin_structure($plugintype, $element, $multiple) { global $CFG; // Check the requested plugintype is a valid one if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) { throw new backup_step_exception('incorrect_plugin_type', $plugintype); } // Arrived here, plugin is correct, let's create the optigroup $optigroupname = $plugintype . '_' . $element->get_name() . '_plugin'; $optigroup = new backup_optigroup($optigroupname, null, $multiple); $element->add_child($optigroup); // Add optigroup to stay connected since beginning // Get all the optigroup_elements, looking across all the plugin dirs $pluginsdirs = core_component::get_plugin_list($plugintype); foreach ($pluginsdirs as $name => $plugindir) { $classname = 'backup_' . $plugintype . '_' . $name . '_plugin'; $backupfile = $plugindir . '/backup/moodle2/' . $classname . '.class.php'; if (file_exists($backupfile)) { require_once($backupfile); $backupplugin = new $classname($plugintype, $name, $optigroup, $this); // Add plugin returned structure to optigroup $backupplugin->define_plugin_structure($element->get_name()); } } } /** * Add subplugin structure for a given plugin to any element in the structure backup tree. * * This method allows the injection of subplugins (of a specified plugin) data to any * element in any backup structure. * * NOTE: Initially subplugins were only available for activities (mod), so only the * {@link backup_activity_structure_step} class had support for them, always * looking for /mod/modulenanme subplugins. This new method is a generalization of the * existing one for activities, supporting all subplugins injecting information everywhere. * * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json. * @param backup_nested_element $element element in the backup tree (anywhere) that * we are going to add subplugin information to. * @param bool $multiple to define if multiple subplugins can produce information * for each instance of $element (true) or no (false). * @param string $plugintype type of the plugin. * @param string $pluginname name of the plugin. * @return void */ protected function add_subplugin_structure($subplugintype, $element, $multiple, $plugintype = null, $pluginname = null) { global $CFG; // This global declaration is required, because where we do require_once($backupfile); // That file may in turn try to do require_once($CFG->dirroot ...). // That worked in the past, we should keep it working. // Verify if this is a BC call for an activity backup. See NOTE above for this special case. if ($plugintype === null and $pluginname === null) { $plugintype = 'mod'; $pluginname = $this->task->get_modulename(); // TODO: Once all the calls have been changed to add both not null plugintype and pluginname, add a debugging here. } // Check the requested plugintype is a valid one. if (!array_key_exists($plugintype, core_component::get_plugin_types())) { throw new backup_step_exception('incorrect_plugin_type', $plugintype); } // Check the requested pluginname, for the specified plugintype, is a valid one. if (!array_key_exists($pluginname, core_component::get_plugin_list($plugintype))) { throw new backup_step_exception('incorrect_plugin_name', array($plugintype, $pluginname)); } // Check the requested subplugintype is a valid one. $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}"); if (null === $subplugins) { throw new backup_step_exception('plugin_missing_subplugins_configuration', [$plugintype, $pluginname]); } if (!array_key_exists($subplugintype, $subplugins)) { throw new backup_step_exception('incorrect_subplugin_type', $subplugintype); } // Arrived here, subplugin is correct, let's create the optigroup. $optigroupname = $subplugintype . '_' . $element->get_name() . '_subplugin'; $optigroup = new backup_optigroup($optigroupname, null, $multiple); $element->add_child($optigroup); // Add optigroup to stay connected since beginning. // Every subplugin optionally can have a common/parent subplugin // class for shared stuff. $parentclass = 'backup_' . $plugintype . '_' . $pluginname . '_' . $subplugintype . '_subplugin'; $parentfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/backup/moodle2/' . $parentclass . '.class.php'; if (file_exists($parentfile)) { require_once($parentfile); } // Get all the optigroup_elements, looking over all the subplugin dirs. $subpluginsdirs = core_component::get_plugin_list($subplugintype); foreach ($subpluginsdirs as $name => $subpluginsdir) { $classname = 'backup_' . $subplugintype . '_' . $name . '_subplugin'; $backupfile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php'; if (file_exists($backupfile)) { require_once($backupfile); $backupsubplugin = new $classname($subplugintype, $name, $optigroup, $this); // Add subplugin returned structure to optigroup. $backupsubplugin->define_subplugin_structure($element->get_name()); } } } /** * To conditionally decide if one step will be executed or no * * For steps needing to be executed conditionally, based in dynamic * conditions (at execution time vs at declaration time) you must * override this function. It will return true if the step must be * executed and false if not */ protected function execute_condition() { return true; } /** * Define the structure to be processed by this backup step. * * @return backup_nested_element */ abstract protected function define_structure(); } util/plan/restore_step.class.php 0000644 00000013717 15215711721 0013021 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuf for one restore step * * TODO: Finish phpdocs */ abstract class restore_step extends base_step { /** * Constructor - instantiates one object of this class */ public function __construct($name, $task = null) { if (!is_null($task) && !($task instanceof restore_task)) { throw new restore_step_exception('wrong_restore_task_specified'); } parent::__construct($name, $task); } protected function get_restoreid() { if (is_null($this->task)) { throw new restore_step_exception('not_specified_restore_task'); } return $this->task->get_restoreid(); } /** * Apply course startdate offset based in original course startdate and course_offset_startdate setting * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple * executions in the same request * * Note: The policy is to roll date only for configurations and not for user data. see MDL-9367. * * @param int $value Time value (seconds since epoch), or empty for nothing * @return int Time value after applying the date offset, or empty for nothing */ public function apply_date_offset($value) { // Empties don't offset - zeros (int and string), false and nulls return original value. if (empty($value)) { return $value; } static $cache = array(); // Lookup cache. if (isset($cache[$this->get_restoreid()])) { return $value + $cache[$this->get_restoreid()]; } // No cache, let's calculate the offset. $original = $this->task->get_info()->original_course_startdate; $setting = 0; if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019). $settingobject = $this->task->get_setting('course_startdate'); if (method_exists($settingobject, 'get_normalized_value')) { $setting = $settingobject->get_normalized_value(); } else { $setting = $settingobject->get_value(); } } if (empty($original) || empty($setting)) { // Original course has not startdate or setting doesn't exist, offset = 0. $cache[$this->get_restoreid()] = 0; } else { // Arrived here, let's calculate the real offset. $cache[$this->get_restoreid()] = $setting - $original; } // Return the passed value with cached offset applied. return $value + $cache[$this->get_restoreid()]; } /** * Returns symmetric-key AES-256 decryption of base64 encoded contents. * * This method is used in restore operations to decrypt contents encrypted with * {@link encrypted_final_element} automatically decoding (base64) and decrypting * contents using the key stored in backup_encryptkey config. * * Requires openssl, cipher availability, and key existence (backup * automatically sets it if missing). Integrity is provided via HMAC. * * @param string $value {@link encrypted_final_element} value to decode and decrypt. * @return string|null decoded and decrypted value or null if the operation can not be performed. */ public function decrypt($value) { // No openssl available, skip this field completely. if (!function_exists('openssl_encrypt')) { return null; } // No hash available, skip this field completely. if (!function_exists('hash_hmac')) { return null; } // Cypher not available, skip this field completely. if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) { return null; } // Get the decrypt key. Skip if missing. $key = get_config('backup', 'backup_encryptkey'); if ($key === false) { return null; } // And decode it. $key = base64_decode($key); // Arrived here, let's proceed with authentication (provides integrity). $hmaclen = 32; // SHA256 is 32 bytes. $ivlen = openssl_cipher_iv_length(backup::CIPHER); list($hmac, $iv, $text) = array_values(unpack("a{$hmaclen}hmac/a{$ivlen}iv/a*text", base64_decode($value))); // Verify HMAC matches expectations, skip if not (integrity failed). if (!hash_equals($hmac, hash_hmac('sha256', $iv . $text, $key, true))) { return null; } // Arrived here, integrity is ok, let's decrypt. $result = openssl_decrypt($text, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv); // For some reason decrypt failed (strange, HMAC check should have deteted it), skip this field completely. if ($result === false) { return null; } return $result; } } /* * Exception class used by all the @restore_step stuff */ class restore_step_exception extends base_step_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/tests/step_test.php 0000644 00000053151 15215711721 0012347 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use backup_nested_element; use backup_optigroup; use backup_plan; use backup_plugin_element; use backup_step; use backup_step_exception; use backup_subplugin_element; use base_step; use base_step_exception; use restore_path_element; use restore_plugin; use restore_step_exception; use restore_subplugin; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/plan_fixtures.php'); /** * @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 */ final class step_test extends \advanced_testcase { protected $moduleid; // course_modules id used for testing protected $sectionid; // course_sections id used for testing protected $courseid; // course id used for testing protected $userid; // user record used for testing protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3)); $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid)); $this->moduleid = $coursemodule->id; $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id)); $this->courseid = $coursemodule->course; $this->userid = 2; // admin // Disable all loggers $CFG->backup_error_log_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 base_step class */ function test_base_step(): void { $bp = new \mock_base_plan('planname'); // We need one plan $bt = new \mock_base_task('taskname', $bp); // We need one task // Instantiate $bs = new \mock_base_step('stepname', $bt); $this->assertTrue($bs instanceof base_step); $this->assertEquals($bs->get_name(), 'stepname'); } /** * test backup_step class */ function test_backup_step(): void { // We need one (non interactive) controller for instatiating plan $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); // We need one plan $bp = new backup_plan($bc); // We need one task $bt = new \mock_backup_task('taskname', $bp); // Instantiate step $bs = new \mock_backup_step('stepname', $bt); $this->assertTrue($bs instanceof backup_step); $this->assertEquals($bs->get_name(), 'stepname'); $bc->destroy(); } /** * test restore_step class, decrypt method */ public function test_restore_step_decrypt(): void { $this->resetAfterTest(true); if (!function_exists('openssl_encrypt')) { $this->markTestSkipped('OpenSSL extension is not loaded.'); } else if (!function_exists('hash_hmac')) { $this->markTestSkipped('Hash extension is not loaded.'); } else if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) { $this->markTestSkipped('Expected cipher not available: ' . backup::CIPHER); } $bt = new \mock_restore_task_basepath('taskname'); $bs = new \mock_restore_structure_step('steptest', null, $bt); $this->assertTrue(method_exists($bs, 'decrypt')); // Let's prepare a string for being decrypted. $secret = 'This is a secret message that nobody else will be able to read but me 💩 '; $key = hash('md5', 'Moodle rocks and this is not secure key, who cares, it is a test'); $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(backup::CIPHER)); $message = $iv . openssl_encrypt($secret, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv); $hmac = hash_hmac('sha256', $message, $key, true); $crypt = base64_encode($hmac . $message); // Running it without a key configured, returns null. $this->assertNull($bs->decrypt($crypt)); // Store the key into config. set_config('backup_encryptkey', base64_encode($key), 'backup'); // Verify decrypt works and returns original. $this->assertSame($secret, $bs->decrypt($crypt)); // Finally, test the integrity failure detection is working. // (this can be caused by changed hmac, key or message, in // this case we are just forcing it via changed hmac). $hmac = md5($message); $crypt = base64_encode($hmac . $message); $this->assertNull($bs->decrypt($crypt)); } /** * test backup_structure_step class */ function test_backup_structure_step(): void { global $CFG; $file = $CFG->tempdir . '/test/test_backup_structure_step.txt'; // Remove the test dir and any content @remove_dir(dirname($file)); // Recreate test dir if (!check_dir_exists(dirname($file), true, true)) { throw new \moodle_exception('error_creating_temp_dir', 'error', dirname($file)); } // We need one (non interactive) controller for instatiating plan $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); // We need one plan $bp = new backup_plan($bc); // We need one task with mocked basepath $bt = new \mock_backup_task_basepath('taskname'); $bp->add_task($bt); // Instantiate backup_structure_step (and add it to task) $bs = new \mock_backup_structure_step('steptest', basename($file), $bt); // Execute backup_structure_step $bs->execute(); // Test file has been created $this->assertTrue(file_exists($file)); // Some simple tests with contents $contents = file_get_contents($file); $this->assertTrue(strpos($contents, '<?xml version="1.0"') !== false); $this->assertTrue(strpos($contents, '<test id="1">') !== false); $this->assertTrue(strpos($contents, '<field1>value1</field1>') !== false); $this->assertTrue(strpos($contents, '<field2>value2</field2>') !== false); $this->assertTrue(strpos($contents, '</test>') !== false); $bc->destroy(); unlink($file); // delete file // Remove the test dir and any content @remove_dir(dirname($file)); } /** * Verify the add_plugin_structure() backup method behavior and created structures. */ public function test_backup_structure_step_add_plugin_structure(): void { // Create mocked task, step and element. $bt = new \mock_backup_task_basepath('taskname'); $bs = new \mock_backup_structure_step('steptest', null, $bt); $el = new backup_nested_element('question', array('id'), array('one', 'two', 'qtype')); // Wrong plugintype. try { $bs->add_plugin_structure('fakeplugin', $el, true); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('incorrect_plugin_type', $e->errorcode); } // Correct plugintype qtype call (@ 'question' level). $bs->add_plugin_structure('qtype', $el, false); $ch = $el->get_children(); $this->assertEquals(1, count($ch)); $og = reset($ch); $this->assertTrue($og instanceof backup_optigroup); $ch = $og->get_children(); $this->assertTrue(array_key_exists('optigroup_qtype_calculatedsimple_question', $ch)); $this->assertTrue($ch['optigroup_qtype_calculatedsimple_question'] instanceof backup_plugin_element); } /** * Verify the add_subplugin_structure() backup method behavior and created structures. */ public function test_backup_structure_step_add_subplugin_structure(): void { // Create mocked task, step and element. $bt = new \mock_backup_task_basepath('taskname'); $bs = new \mock_backup_structure_step('steptest', null, $bt); $el = new backup_nested_element('workshop', array('id'), array('one', 'two', 'qtype')); // Wrong plugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, true, 'fakeplugintype', 'fakepluginname'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('incorrect_plugin_type', $e->errorcode); } // Wrong plugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'fakepluginname'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('incorrect_plugin_name', $e->errorcode); } // Wrong plugin not having subplugins. try { $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'page'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode); } // Wrong BC (defaulting to mod and modulename) use not having subplugins. try { $bt->set_modulename('page'); $bs->add_subplugin_structure('fakesubplugin', $el, true); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode); } // Wrong subplugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, true, 'mod', 'workshop'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('incorrect_subplugin_type', $e->errorcode); } // Wrong BC subplugin type. try { $bt->set_modulename('workshop'); $bs->add_subplugin_structure('fakesubplugin', $el, true); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals('incorrect_subplugin_type', $e->errorcode); } // Correct call to workshopform subplugin (@ 'workshop' level). $bs->add_subplugin_structure('workshopform', $el, true, 'mod', 'workshop'); $ch = $el->get_children(); $this->assertEquals(1, count($ch)); $og = reset($ch); $this->assertTrue($og instanceof backup_optigroup); $ch = $og->get_children(); $this->assertTrue(array_key_exists('optigroup_workshopform_accumulative_workshop', $ch)); $this->assertTrue($ch['optigroup_workshopform_accumulative_workshop'] instanceof backup_subplugin_element); // Correct BC call to workshopform subplugin (@ 'assessment' level). $el = new backup_nested_element('assessment', array('id'), array('one', 'two', 'qtype')); $bt->set_modulename('workshop'); $bs->add_subplugin_structure('workshopform', $el, true); $ch = $el->get_children(); $this->assertEquals(1, count($ch)); $og = reset($ch); $this->assertTrue($og instanceof backup_optigroup); $ch = $og->get_children(); $this->assertTrue(array_key_exists('optigroup_workshopform_accumulative_assessment', $ch)); $this->assertTrue($ch['optigroup_workshopform_accumulative_assessment'] instanceof backup_subplugin_element); // TODO: Add some test covering a non-mod subplugin once we have some implemented in core. } /** * Verify the add_plugin_structure() restore method behavior and created structures. */ public function test_restore_structure_step_add_plugin_structure(): void { // Create mocked task, step and element. $bt = new \mock_restore_task_basepath('taskname'); $bs = new \mock_restore_structure_step('steptest', null, $bt); $el = new restore_path_element('question', '/some/path/to/question'); // Wrong plugintype. try { $bs->add_plugin_structure('fakeplugin', $el); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('incorrect_plugin_type', $e->errorcode); } // Correct plugintype qtype call (@ 'question' level). $bs->add_plugin_structure('qtype', $el); $patheles = $bs->get_pathelements(); // Verify some well-known qtype plugin restore_path_elements have been added. $keys = array( '/some/path/to/question/plugin_qtype_calculated_question/answers/answer', '/some/path/to/question/plugin_qtype_calculated_question/dataset_definitions/dataset_definition', '/some/path/to/question/plugin_qtype_calculated_question/calculated_options/calculated_option', '/some/path/to/question/plugin_qtype_essay_question/essay', '/some/path/to/question/plugin_qtype_random_question', '/some/path/to/question/plugin_qtype_truefalse_question/answers/answer'); foreach ($keys as $key) { // Verify the element exists. $this->assertArrayHasKey($key, $patheles); // Verify the element is a restore_path_element. $this->assertTrue($patheles[$key] instanceof restore_path_element); // Check it has a processing object. $po = $patheles[$key]->get_processing_object(); $this->assertTrue($po instanceof restore_plugin); } } /** * Verify the add_subplugin_structure() restore method behavior and created structures. */ public function test_restore_structure_step_add_subplugin_structure(): void { // Create mocked task, step and element. $bt = new \mock_restore_task_basepath('taskname'); $bs = new \mock_restore_structure_step('steptest', null, $bt); $el = new restore_path_element('workshop', '/path/to/workshop'); // Wrong plugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, 'fakeplugintype', 'fakepluginname'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('incorrect_plugin_type', $e->errorcode); } // Wrong plugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'fakepluginname'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('incorrect_plugin_name', $e->errorcode); } // Wrong plugin not having subplugins. try { $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'page'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode); } // Wrong BC (defaulting to mod and modulename) use not having subplugins. try { $bt->set_modulename('page'); $bs->add_subplugin_structure('fakesubplugin', $el); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('plugin_missing_subplugins_configuration', $e->errorcode); } // Wrong subplugin type. try { $bs->add_subplugin_structure('fakesubplugin', $el, 'mod', 'workshop'); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('incorrect_subplugin_type', $e->errorcode); } // Wrong BC subplugin type. try { $bt->set_modulename('workshop'); $bs->add_subplugin_structure('fakesubplugin', $el); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof restore_step_exception); $this->assertEquals('incorrect_subplugin_type', $e->errorcode); } // Correct call to workshopform subplugin (@ 'workshop' level). $bt = new \mock_restore_task_basepath('taskname'); $bs = new \mock_restore_structure_step('steptest', null, $bt); $el = new restore_path_element('workshop', '/path/to/workshop'); $bs->add_subplugin_structure('workshopform', $el, 'mod', 'workshop'); $patheles = $bs->get_pathelements(); // Verify some well-known workshopform subplugin restore_path_elements have been added. $keys = array( '/path/to/workshop/subplugin_workshopform_accumulative_workshop/workshopform_accumulative_dimension', '/path/to/workshop/subplugin_workshopform_comments_workshop/workshopform_comments_dimension', '/path/to/workshop/subplugin_workshopform_numerrors_workshop/workshopform_numerrors_map', '/path/to/workshop/subplugin_workshopform_rubric_workshop/workshopform_rubric_config'); foreach ($keys as $key) { // Verify the element exists. $this->assertArrayHasKey($key, $patheles); // Verify the element is a restore_path_element. $this->assertTrue($patheles[$key] instanceof restore_path_element); // Check it has a processing object. $po = $patheles[$key]->get_processing_object(); $this->assertTrue($po instanceof restore_subplugin); } // Correct BC call to workshopform subplugin (@ 'assessment' level). $bt = new \mock_restore_task_basepath('taskname'); $bs = new \mock_restore_structure_step('steptest', null, $bt); $el = new restore_path_element('assessment', '/a/assessment'); $bt->set_modulename('workshop'); $bs->add_subplugin_structure('workshopform', $el); $patheles = $bs->get_pathelements(); // Verify some well-known workshopform subplugin restore_path_elements have been added. $keys = array( '/a/assessment/subplugin_workshopform_accumulative_assessment/workshopform_accumulative_grade', '/a/assessment/subplugin_workshopform_comments_assessment/workshopform_comments_grade', '/a/assessment/subplugin_workshopform_numerrors_assessment/workshopform_numerrors_grade', '/a/assessment/subplugin_workshopform_rubric_assessment/workshopform_rubric_grade'); foreach ($keys as $key) { // Verify the element exists. $this->assertArrayHasKey($key, $patheles); // Verify the element is a restore_path_element. $this->assertTrue($patheles[$key] instanceof restore_path_element); // Check it has a processing object. $po = $patheles[$key]->get_processing_object(); $this->assertTrue($po instanceof restore_subplugin); } // TODO: Add some test covering a non-mod subplugin once we have some implemented in core. } /** * wrong base_step class tests */ function test_base_step_wrong(): void { // Try to pass one wrong task try { $bt = new \mock_base_step('teststep', new \stdClass()); $this->assertTrue(false, 'base_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_step_exception); $this->assertEquals($e->errorcode, 'wrong_base_task_specified'); } } /** * wrong backup_step class tests */ function test_backup_test_wrong(): void { // Try to pass one wrong task try { $bt = new \mock_backup_step('teststep', new \stdClass()); $this->assertTrue(false, 'backup_step_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_step_exception); $this->assertEquals($e->errorcode, 'wrong_backup_task_specified'); } } } util/plan/tests/fixtures/plan_fixtures.php 0000644 00000013117 15215711721 0015067 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core_backup * @category phpunit * @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(); // Include all the needed stuff 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->dirroot . '/backup/moodle2/backup_custom_fields.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_subplugin.class.php'); /** * Instantiable class extending base_plan in order to be able to perform tests */ class mock_base_plan extends base_plan { public function build() { } public function get_progress() { return null; } } /** * Instantiable class extending base_step in order to be able to perform tests */ class mock_base_step extends base_step { public function execute() { } } /** * Instantiable class extending backup_step in order to be able to perform tests */ class mock_backup_step extends backup_step { public function execute() { } } /** * Instantiable class extending backup_task in order to mockup get_taskbasepath() */ class mock_backup_task_basepath extends backup_task { /** @var string name of the mod plugin (activity) being used in the tests */ private $modulename; public function build() { // Nothing to do } public function define_settings() { // Nothing to do } public function get_taskbasepath() { global $CFG; return $CFG->tempdir . '/test'; } public function set_modulename($modulename) { $this->modulename = $modulename; } public function get_modulename() { return $this->modulename; } } /** * Instantiable class extending restore_task in order to mockup get_taskbasepath() */ class mock_restore_task_basepath extends restore_task { /** @var string name of the mod plugin (activity) being used in the tests */ private $modulename; public function build() { // Nothing to do. } public function define_settings() { // Nothing to do. } public function set_modulename($modulename) { $this->modulename = $modulename; } public function get_modulename() { return $this->modulename; } } /** * Instantiable class extending backup_structure_step in order to be able to perform tests */ class mock_backup_structure_step extends backup_structure_step { public function define_structure() { // Create really simple structure (1 nested with 1 attr and 2 fields) $test = new backup_nested_element('test', array('id'), array('field1', 'field2') ); $test->set_source_array(array(array('id' => 1, 'field1' => 'value1', 'field2' => 'value2'))); return $test; } public function add_plugin_structure($plugintype, $element, $multiple) { parent::add_plugin_structure($plugintype, $element, $multiple); } public function add_subplugin_structure($subplugintype, $element, $multiple, $plugintype = null, $pluginname = null) { parent::add_subplugin_structure($subplugintype, $element, $multiple, $plugintype, $pluginname); } } class mock_restore_structure_step extends restore_structure_step { public function define_structure() { // Create a really simple structure (1 element). $test = new restore_path_element('test', '/tests/test'); return array($test); } public function add_plugin_structure($plugintype, $element) { parent::add_plugin_structure($plugintype, $element); } public function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) { parent::add_subplugin_structure($subplugintype, $element, $plugintype, $pluginname); } public function get_pathelements() { return $this->pathelements; } } /** * Instantiable class extending activity_backup_setting to be added to task and perform tests */ class mock_fullpath_activity_setting extends activity_backup_setting { public function process_change($setting, $ctype, $oldv) { // Nothing to do } } /** * Instantiable class extending activity_backup_setting to be added to task and perform tests */ class mock_backupid_activity_setting extends activity_backup_setting { public function process_change($setting, $ctype, $oldv) { // Nothing to do } } /** * Instantiable class extending base_task in order to be able to perform tests */ class mock_base_task extends base_task { public function build() { } public function define_settings() { } } /** * Instantiable class extending backup_task in order to be able to perform tests */ class mock_backup_task extends backup_task { public function build() { } public function define_settings() { } } util/plan/tests/plan_test.php 0000644 00000013410 15215711721 0012320 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use backup_controller_exception; use backup_plan; use backup_plan_exception; use base_plan; use base_plan_exception; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/plan_fixtures.php'); /** * @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 */ final class plan_test extends \advanced_testcase { protected $moduleid; // course_modules id used for testing protected $sectionid; // course_sections id used for testing protected $courseid; // course id used for testing protected $userid; // user record used for testing protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3)); $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid)); $this->moduleid = $coursemodule->id; $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id)); $this->courseid = $coursemodule->course; $this->userid = 2; // admin // Disable all loggers $CFG->backup_error_log_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 base_plan class */ function test_base_plan(): void { // Instantiate $bp = new \mock_base_plan('name'); $this->assertTrue($bp instanceof base_plan); $this->assertEquals($bp->get_name(), 'name'); $this->assertTrue(is_array($bp->get_settings())); $this->assertEquals(count($bp->get_settings()), 0); $this->assertTrue(is_array($bp->get_tasks())); $this->assertEquals(count($bp->get_tasks()), 0); } /** * test backup_plan class */ function test_backup_plan(): void { // We need one (non interactive) controller for instantiating plan $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); // Instantiate one backup plan $bp = new backup_plan($bc); $this->assertTrue($bp instanceof backup_plan); $this->assertEquals($bp->get_name(), 'backup_plan'); // Calculate checksum and check it $checksum = $bp->calculate_checksum(); $this->assertTrue($bp->is_checksum_correct($checksum)); $bc->destroy(); } /** * wrong base_plan class tests */ function test_base_plan_wrong(): void { // We need one (non interactive) controller for instantiating plan $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); // Instantiate one backup plan $bp = new backup_plan($bc); // Add wrong task try { $bp->add_task(new \stdClass()); $this->assertTrue(false, 'base_plan_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_plan_exception); $this->assertEquals($e->errorcode, 'wrong_base_task_specified'); } } /** * wrong backup_plan class tests */ function test_backup_plan_wrong(): void { // Try to pass one wrong controller try { $bp = new backup_plan(new \stdClass()); $this->assertTrue(false, 'backup_plan_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_plan_exception); $this->assertEquals($e->errorcode, 'wrong_backup_controller_specified'); } try { $bp = new backup_plan(null); $this->assertTrue(false, 'backup_plan_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_plan_exception); $this->assertEquals($e->errorcode, 'wrong_backup_controller_specified'); } // Try to build one non-existent format plan (when creating the controller) // We need one (non interactive) controller for instatiating plan try { $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, 'non_existing_format', backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); $this->assertTrue(false, 'backup_controller_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_controller_exception); $this->assertEquals($e->errorcode, 'backup_check_unsupported_format'); $this->assertEquals($e->a, 'non_existing_format'); } } } util/plan/tests/task_test.php 0000644 00000012140 15215711721 0012327 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use base_task; use base_task_exception; use backup_controller; use backup_plan; use backup_task; use backup_task_exception; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__.'/fixtures/plan_fixtures.php'); /** * @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 */ final class task_test extends \advanced_testcase { protected $moduleid; // course_modules id used for testing protected $sectionid; // course_sections id used for testing protected $courseid; // course id used for testing protected $userid; // user record used for testing protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(); $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3)); $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid)); $this->moduleid = $coursemodule->id; $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id)); $this->courseid = $coursemodule->course; $this->userid = 2; // admin // Disable all loggers $CFG->backup_error_log_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 base_task class */ function test_base_task(): void { $bp = new \mock_base_plan('planname'); // We need one plan // Instantiate $bt = new \mock_base_task('taskname', $bp); $this->assertTrue($bt instanceof base_task); $this->assertEquals($bt->get_name(), 'taskname'); $this->assertTrue(is_array($bt->get_settings())); $this->assertEquals(count($bt->get_settings()), 0); $this->assertTrue(is_array($bt->get_steps())); $this->assertEquals(count($bt->get_steps()), 0); } /** * test backup_task class */ function test_backup_task(): void { // We need one (non interactive) controller for instatiating plan $bc = new backup_controller(backup::TYPE_1ACTIVITY, $this->moduleid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid); // We need one plan $bp = new backup_plan($bc); // Instantiate task $bt = new \mock_backup_task('taskname', $bp); $this->assertTrue($bt instanceof backup_task); $this->assertEquals($bt->get_name(), 'taskname'); // Calculate checksum and check it $checksum = $bt->calculate_checksum(); $this->assertTrue($bt->is_checksum_correct($checksum)); $bc->destroy(); } /** * wrong base_task class tests */ function test_base_task_wrong(): void { // Try to pass one wrong plan try { $bt = new \mock_base_task('tasktest', new \stdClass()); $this->assertTrue(false, 'base_task_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_task_exception); $this->assertEquals($e->errorcode, 'wrong_base_plan_specified'); } // Add wrong step to task $bp = new \mock_base_plan('planname'); // We need one plan // Instantiate $bt = new \mock_base_task('taskname', $bp); try { $bt->add_step(new \stdClass()); $this->assertTrue(false, 'base_task_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof base_task_exception); $this->assertEquals($e->errorcode, 'wrong_base_step_specified'); } } /** * wrong backup_task class tests */ function test_backup_task_wrong(): void { // Try to pass one wrong plan try { $bt = new \mock_backup_task('tasktest', new \stdClass()); $this->assertTrue(false, 'backup_task_exception expected'); } catch (\Exception $e) { $this->assertTrue($e instanceof backup_task_exception); $this->assertEquals($e->errorcode, 'wrong_backup_plan_specified'); } } } util/plan/backup_task.class.php 0000644 00000004075 15215711721 0012567 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuf for one backup task (a collection of steps) * * TODO: Finish phpdocs */ abstract class backup_task extends base_task { /** * Constructor - instantiates one object of this class */ public function __construct($name, $plan = null) { if (!is_null($plan) && !($plan instanceof backup_plan)) { throw new backup_task_exception('wrong_backup_plan_specified'); } parent::__construct($name, $plan); } public function get_backupid() { return $this->plan->get_backupid(); } public function is_excluding_activities() { return $this->plan->is_excluding_activities(); } /** * Get the user roles that should be kept in the destination course * for a course copy operation. * * @return array */ public function get_kept_roles(): array { return $this->plan->get_kept_roles(); } } /* * Exception class used by all the @backup_task stuff */ class backup_task_exception extends base_task_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, $a, $debuginfo); } } util/plan/restore_execution_step.class.php 0000644 00000002471 15215711721 0015077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuff to execute code on restore * * TODO: Finish phpdocs */ abstract class restore_execution_step extends restore_step { public function execute() { // Simple, for now return $this->define_execution(); } // Protected API starts here /** * Function that will contain all the code to be executed */ abstract protected function define_execution(); } util/plan/base_step.class.php 0000644 00000007670 15215711721 0012251 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the basis for one execution (backup/restore) step * * TODO: Finish phpdocs */ abstract class base_step implements executable, loggable { /** @var string One simple name for identification purposes */ protected $name; /** @var base_task|null Task this is part of */ protected $task; /** * Constructor - instantiates one object of this class */ public function __construct($name, $task = null) { if (!is_null($task) && !($task instanceof base_task)) { throw new base_step_exception('wrong_base_task_specified'); } $this->name = $name; $this->task = $task; if (!is_null($task)) { // Add the step to the task if specified $task->add_step($this); } } public function get_name() { return $this->name; } public function set_task($task) { if (! $task instanceof base_task) { throw new base_step_exception('wrong_base_task_specified'); } $this->task = $task; } /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // No need to destroy anything recursively here, direct reset $this->task = null; } public function log($message, $level, $a = null, $depth = null, $display = false) { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger()); } /// Protected API starts here protected function get_settings() { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } return $this->task->get_settings(); } protected function get_setting($name) { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } return $this->task->get_setting($name); } protected function setting_exists($name) { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } return $this->task->setting_exists($name); } protected function get_setting_value($name) { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } return $this->task->get_setting_value($name); } protected function get_courseid() { if (is_null($this->task)) { throw new base_step_exception('not_specified_base_task'); } return $this->task->get_courseid(); } protected function get_basepath() { return $this->task->get_basepath(); } protected function get_logger() { return $this->task->get_logger(); } } /* * Exception class used by all the @base_step stuff */ class base_step_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } util/plan/base_plan.class.php 0000644 00000016424 15215711721 0012225 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the basis for one execution (backup/restore) plan * * TODO: Finish phpdocs */ abstract class base_plan implements checksumable, executable { protected $name; // One simple name for identification purposes protected $settings; // One array of (accumulated from tasks) base_setting elements protected $tasks; // One array of base_task elements protected $results; // One array of results received from tasks protected $built; // Flag to know if one plan has been built /** * Constructor - instantiates one object of this class */ public function __construct($name) { $this->name = $name; $this->settings = array(); $this->tasks = array(); $this->results = array(); $this->built = false; } public function get_name() { return $this->name; } public function add_task($task) { if (! $task instanceof base_task) { throw new base_plan_exception('wrong_base_task_specified'); } $this->tasks[] = $task; // link the task with the plan $task->set_plan($this); // Append task settings to plan array, if not present, for comodity foreach ($task->get_settings() as $key => $setting) { // Check if there is already a setting for this name. $name = $setting->get_name(); if (!isset($this->settings[$name])) { // There is no setting, so add it. $this->settings[$name] = $setting; } else if ($this->settings[$name] != $setting) { // If the setting already exists AND it is not the same setting, // then throw an error. (I.e. you're allowed to add the same // setting twice, but cannot add two different ones with same // name.) throw new base_plan_exception('multiple_settings_by_name_found', $name); } } } public function get_tasks() { return $this->tasks; } /** * Add the passed info to the plan results * * At the moment we expect an associative array structure to be merged into * the current results. In the future, some sort of base_result class may * be introduced. * * @param array $result associative array describing a result of a task/step */ public function add_result($result) { if (!is_array($result)) { throw new coding_exception('Associative array is expected as a parameter of add_result()'); } $this->results = array_merge($this->results, $result); } /** * Return the results collected via {@link self::add_result()} method * * @return array */ public function get_results() { return $this->results; } public function get_settings() { return $this->settings; } /** * return one setting by name, useful to request root/course settings * that are, by definition, unique by name. * * @param string $name name of the setting * @return base_setting * @throws base_plan_exception if setting name is not found. */ public function get_setting($name) { $result = null; if (isset($this->settings[$name])) { $result = $this->settings[$name]; } else { throw new base_plan_exception('setting_by_name_not_found', $name); } return $result; } /** * For debug only. Get a simple test display of all the settings. * * @return string */ public function debug_display_all_settings_values(): string { $result = ''; foreach ($this->settings as $name => $setting) { $result .= $name . ': ' . $setting->get_value() . "\n"; } return $result; } /** * Wrapper over @get_setting() that returns if the requested setting exists or no */ public function setting_exists($name) { try { $this->get_setting($name); return true; } catch (base_plan_exception $e) { // Nothing to do } return false; } /** * Function responsible for building the tasks of any plan * with their corresponding settings * (must set the $built property to true) */ abstract public function build(); public function is_checksum_correct($checksum) { return $this->calculate_checksum() === $checksum; } public function calculate_checksum() { // Let's do it using name and tasks (settings are part of tasks) return md5($this->name . '-' . backup_general_helper::array_checksum_recursive($this->tasks)); } /** * Function responsible for executing the tasks of any plan */ public function execute() { if (!$this->built) { throw new base_plan_exception('base_plan_not_built'); } // Calculate the total weight of all tasks and start progress tracking. $progress = $this->get_progress(); $totalweight = 0; foreach ($this->tasks as $task) { $totalweight += $task->get_weight(); } $progress->start_progress($this->get_name(), $totalweight); // Build and execute all tasks. foreach ($this->tasks as $task) { $task->build(); $task->execute(); } // Finish progress tracking. $progress->end_progress(); } /** * Gets the progress reporter, which can be used to report progress within * the backup or restore process. * * @return \core\progress\base Progress reporting object */ abstract public function get_progress(); /** * Destroy all circular references. It helps PHP 5.2 a lot! */ public function destroy() { // Before reseting anything, call destroy recursively foreach ($this->tasks as $task) { $task->destroy(); } foreach ($this->settings as $setting) { $setting->destroy(); } // Everything has been destroyed recursively, now we can reset safely $this->tasks = array(); $this->settings = array(); } } /* * Exception class used by all the @base_plan stuff */ class base_plan_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } util/plan/backup_execution_step.class.php 0000644 00000002466 15215711721 0014665 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-plan * @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 the needed stuff to execute code on backup * * TODO: Finish phpdocs */ abstract class backup_execution_step extends backup_step { public function execute() { // Simple, for now return $this->define_execution(); } // Protected API starts here /** * Function that will contain all the code to be executed */ abstract protected function define_execution(); } externallib.php 0000644 00000035473 15215711721 0007604 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * External backup API. * * @package core_backup * @category external * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; defined('MOODLE_INTERNAL') || die; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Backup external functions. * * @package core_backup * @category external * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.7 */ class core_backup_external extends external_api { /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.7 */ public static function get_async_backup_progress_parameters() { return new external_function_parameters( array( 'backupids' => new external_multiple_structure( new external_value(PARAM_ALPHANUM, 'Backup id to get progress for', VALUE_REQUIRED, null, NULL_ALLOWED), 'Backup id to get progress for', VALUE_REQUIRED ), 'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), ) ); } /** * Get asynchronous backup progress. * * @param string $backupids The ids of the backup to get progress for. * @param int $contextid The context the backup relates to. * @return array $results The array of results. * @since Moodle 3.7 */ public static function get_async_backup_progress($backupids, $contextid) { // Release session lock. \core\session\manager::write_close(); // Parameter validation. self::validate_parameters( self::get_async_backup_progress_parameters(), array( 'backupids' => $backupids, 'contextid' => $contextid ) ); // Context validation. list($context, $course, $cm) = get_context_info_array($contextid); self::validate_context($context); if ($cm) { require_capability('moodle/backup:backupactivity', $context); } else { require_capability('moodle/backup:backupcourse', $context); } $results = array(); foreach ($backupids as $backupid) { $results[] = backup_controller_dbops::get_progress($backupid); } return $results; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 3.7 */ public static function get_async_backup_progress_returns() { return new external_multiple_structure( new external_single_structure( array( 'status' => new external_value(PARAM_INT, 'Backup Status'), 'progress' => new external_value(PARAM_FLOAT, 'Backup progress'), 'backupid' => new external_value(PARAM_ALPHANUM, 'Backup id'), 'operation' => new external_value(PARAM_ALPHANUM, 'operation type'), ), 'Backup completion status' ), 'Backup data' ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.7 */ public static function get_async_backup_links_backup_parameters() { return new external_function_parameters( array( 'filename' => new external_value(PARAM_FILE, 'Backup filename', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'backupid' => new external_value(PARAM_ALPHANUMEXT, 'Backup id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), ) ); } /** * Get the data to be used when generating the table row for an asynchronous backup, * the table row updates via ajax when backup is complete. * * @param string $filename The file name of the backup file. * @param int $contextid The context the backup relates to. * @param string $backupid The backup ID to get the backup settings. * @since Moodle 3.7 */ public static function get_async_backup_links_backup($filename, $contextid, $backupid) { // Release session lock. \core\session\manager::write_close(); // Parameter validation. self::validate_parameters( self::get_async_backup_links_backup_parameters(), array( 'filename' => $filename, 'contextid' => $contextid, 'backupid' => $backupid, ) ); // Context validation. list($context, $course, $cm) = get_context_info_array($contextid); self::validate_context($context); require_capability('moodle/backup:backupcourse', $context); // Backups without user info or with the anonymise functionality enabled are sent // to user's "user_backup" file area. $filearea = 'backup'; // Get useful info to render async status in correct area. $bc = \backup_controller::load_controller($backupid); list($hasusers, $isannon) = \async_helper::get_userdata_backup_settings($bc); if ($hasusers && !$isannon) { if ($cm) { $filearea = 'activity'; } else { $filearea = 'course'; } } $results = \async_helper::get_backup_file_info($filename, $filearea, $contextid); return $results; } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.7 */ public static function get_async_backup_links_backup_returns() { return new external_single_structure( array( 'filesize' => new external_value(PARAM_TEXT, 'Backup file size'), 'fileurl' => new external_value(PARAM_URL, 'Backup file URL'), 'restoreurl' => new external_value(PARAM_URL, 'Backup restore URL'), ), 'Table row data.'); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.7 */ public static function get_async_backup_links_restore_parameters() { return new external_function_parameters( array( 'backupid' => new external_value(PARAM_ALPHANUMEXT, 'Backup id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), 'contextid' => new external_value(PARAM_INT, 'Context id', VALUE_REQUIRED, null, NULL_NOT_ALLOWED), ) ); } /** * Get the data to be used when generating the table row for an asynchronous restore, * the table row updates via ajax when restore is complete. * * @param string $backupid The id of the backup record. * @param int $contextid The context the restore relates to. * @return array $results The array of results. * @since Moodle 3.7 */ public static function get_async_backup_links_restore($backupid, $contextid) { // Release session lock. \core\session\manager::write_close(); // Parameter validation. self::validate_parameters( self::get_async_backup_links_restore_parameters(), array( 'backupid' => $backupid, 'contextid' => $contextid ) ); // Context validation. if ($contextid == 0) { $copyrec = \async_helper::get_backup_record($backupid); $context = context_course::instance($copyrec->itemid); } else { $context = context::instance_by_id($contextid); } self::validate_context($context); require_capability('moodle/restore:restorecourse', $context); $results = \async_helper::get_restore_url($backupid); return $results; } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.7 */ public static function get_async_backup_links_restore_returns() { return new external_single_structure( array( 'restoreurl' => new external_value(PARAM_URL, 'Restore url'), ), 'Table row data.'); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.9 */ public static function get_copy_progress_parameters() { return new external_function_parameters( array( 'copies' => new external_multiple_structure( new external_single_structure( array( 'backupid' => new external_value(PARAM_ALPHANUM, 'Backup id'), 'restoreid' => new external_value(PARAM_ALPHANUM, 'Restore id'), 'operation' => new external_value(PARAM_ALPHANUM, 'Operation type'), ), 'Copy data' ), 'Copy data' ), ) ); } /** * Get the data to be used when generating the table row for a course copy, * the table row updates via ajax when copy is complete. * * @param array $copies Array of ids. * @return array $results The array of results. * @since Moodle 3.9 */ public static function get_copy_progress($copies) { // Release session lock. \core\session\manager::write_close(); // Parameter validation. self::validate_parameters( self::get_copy_progress_parameters(), array('copies' => $copies) ); $results = array(); foreach ($copies as $copy) { if ($copy['operation'] == \backup::OPERATION_BACKUP) { $copyid = $copy['backupid']; } else { $copyid = $copy['restoreid']; } $copyrec = \async_helper::get_backup_record($copyid); $context = context_course::instance($copyrec->itemid); self::validate_context($context); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $context); if ($copy['operation'] == \backup::OPERATION_BACKUP) { $result = \backup_controller_dbops::get_progress($copyid); if ($result['status'] == \backup::STATUS_FINISHED_OK) { $copyid = $copy['restoreid']; } } $results[] = \backup_controller_dbops::get_progress($copyid); } return $results; } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.9 */ public static function get_copy_progress_returns() { return new external_multiple_structure( new external_single_structure( array( 'status' => new external_value(PARAM_INT, 'Copy Status'), 'progress' => new external_value(PARAM_FLOAT, 'Copy progress'), 'backupid' => new external_value(PARAM_ALPHANUM, 'Copy id'), 'operation' => new external_value(PARAM_ALPHANUM, 'Operation type'), ), 'Copy completion status' ), 'Copy data' ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 3.9 */ public static function submit_copy_form_parameters() { return new external_function_parameters( array( 'jsonformdata' => new external_value(PARAM_RAW, 'The data from the create copy form, encoded as a json array') ) ); } /** * Submit the course group form. * * @param string $jsonformdata The data from the form, encoded as a json array. * @return int new group id. */ public static function submit_copy_form($jsonformdata) { // Release session lock. \core\session\manager::write_close(); // We always must pass webservice params through validate_parameters. $params = self::validate_parameters( self::submit_copy_form_parameters(), array('jsonformdata' => $jsonformdata) ); $formdata = json_decode($params['jsonformdata']); $data = array(); parse_str($formdata, $data); $context = context_course::instance($data['courseid']); self::validate_context($context); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $context); // Submit the form data. $course = get_course($data['courseid']); $mform = new \core_backup\output\copy_form( null, array('course' => $course, 'returnto' => '', 'returnurl' => ''), 'post', '', ['class' => 'ignoredirty'], true, $data); $mdata = $mform->get_data(); if ($mdata) { // Create the copy task. $copydata = \copy_helper::process_formdata($mdata); $copyids = \copy_helper::create_copy($copydata); } else { throw new moodle_exception('copyformfail', 'backup'); } return json_encode($copyids); } /** * Returns description of method result value. * * @return \core_external\external_description * @since Moodle 3.9 */ public static function submit_copy_form_returns() { return new external_value(PARAM_RAW, 'JSON response.'); } } copy.php 0000644 00000007106 15215711721 0006235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This script is used to configure and execute the course copy proccess. * * @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 */ require_once('../config.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); defined('MOODLE_INTERNAL') || die(); $courseid = required_param('id', PARAM_INT); $returnto = optional_param('returnto', 'course', PARAM_ALPHANUM); // Generic navigation return page switch. $returnurl = optional_param('returnurl', '', PARAM_LOCALURL); // A return URL. returnto must also be set to 'url'. $url = new moodle_url('/backup/copy.php', array('id' => $courseid)); $course = get_course($courseid); $coursecontext = context_course::instance($course->id); // Security and access checks. require_login($course, false); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $coursecontext); if ($returnurl != '') { $returnurl = new moodle_url($returnurl); } else if ($returnto == 'catmanage') { // Redirect to category management page. $returnurl = new moodle_url('/course/management.php', array('categoryid' => $course->category)); } else { // Redirect back to course page if we came from there. $returnurl = new moodle_url('/course/view.php', array('id' => $courseid)); } // Setup the page. $title = get_string('copycoursetitle', 'backup', $course->shortname); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_title($title); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('coursereuse'); // Get data ready for mform. $mform = new \core_backup\output\copy_form( $url, array('course' => $course, 'returnto' => $returnto, 'returnurl' => $returnurl)); if ($mform->is_cancelled()) { // The form has been cancelled, take them back to what ever the return to is. redirect($returnurl); } else if ($mdata = $mform->get_data()) { // Process the form and create the copy task. $copydata = \copy_helper::process_formdata($mdata); \copy_helper::create_copy($copydata); if (!empty($mdata->submitdisplay)) { // Redirect to the copy progress overview. $progressurl = new moodle_url('/backup/copyprogress.php', array('id' => $courseid)); redirect($progressurl); } else { // Redirect to the course view page. $coursesurl = new moodle_url('/course/view.php', array('id' => $courseid)); redirect($coursesurl); } } else { // This branch is executed if the form is submitted but the data doesn't validate, // or on the first display of the form. // Build the page output. echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('copycourse'); $mform->display(); echo $OUTPUT->footer(); } view.php 0000644 00000004621 15215711721 0006234 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Page to view the course reuse actions. * * @package core_backup * @copyright 2023 Sara Arjona <sara@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../config.php'); // Course id. $courseid = required_param('id', PARAM_INT); $PAGE->set_url(new moodle_url('/backup/view.php', ['id' => $courseid])); // Basic access checks. if (!$course = $DB->get_record('course', ['id' => $courseid])) { throw new \moodle_exception('invalidcourseid'); } require_login($course); $title = get_string('coursereuse'); // Only append the course name if the course ID is not the site ID. if ($courseid != SITEID) { $title .= moodle_page::TITLE_SEPARATOR . $course->fullname; } // Otherwise, output the page with a notification stating that there are no available course reuse actions. $PAGE->set_title($title); $PAGE->set_pagelayout('incourse'); $PAGE->set_heading($course->fullname); $PAGE->set_pagetype('course-view-' . $course->format); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('coursereuse')); // Check if there is at least one displayable course reuse action. $hasactions = false; if ($coursereusenode = $PAGE->settingsnav->find('coursereuse', \navigation_node::TYPE_CONTAINER)) { foreach ($coursereusenode->children as $child) { if ($child->display) { $hasactions = true; break; } } } if ($hasactions) { echo $OUTPUT->render_from_template('core/report_link_page', ['node' => $coursereusenode]); } else { throw new \moodle_exception( 'accessdenied', 'admin', new moodle_url('/course/view.php', ['id' => $courseid]) ); } echo $OUTPUT->footer(); restorefile_form.php 0000644 00000002705 15215711721 0010631 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Import backup file form * @package moodlecore * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir.'/formslib.php'); class course_restore_form extends moodleform { function definition() { $mform =& $this->_form; $contextid = $this->_customdata['contextid']; $mform->addElement('hidden', 'contextid', $contextid); $mform->setType('contextid', PARAM_INT); $mform->addElement('filepicker', 'backupfile', get_string('backupfile', 'backup')); $mform->addRule('backupfile', get_string('required'), 'required'); $submit_string = get_string('restore'); $this->add_action_buttons(false, $submit_string); } } copyprogress.php 0000644 00000004606 15215711721 0010024 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This script is used to configure and execute the course copy proccess. * * @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 */ require_once('../config.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); defined('MOODLE_INTERNAL') || die(); $courseid = required_param('id', PARAM_INT); $url = new moodle_url('/backup/copyprogress.php', array('id' => $courseid)); $course = get_course($courseid); $coursecontext = context_course::instance($course->id); // Security and access checks. require_login($course, false); $copycaps = \core_course\management\helper::get_course_copy_capabilities(); require_all_capabilities($copycaps, $coursecontext); // Setup the page. $title = get_string('copyprogresstitle', 'backup'); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_title($title); $PAGE->set_heading($course->fullname); $PAGE->set_secondary_active_tab('coursereuse'); $PAGE->requires->js_call_amd('core_backup/async_backup', 'asyncCopyAllStatus'); $PAGE->secondarynav->set_overflow_selected_node('copy'); // Build the page output. echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('copycourse'); echo $OUTPUT->heading_with_help(get_string('copyprogressheading', 'backup'), 'copyprogressheading', 'backup'); echo $OUTPUT->container_start(); $renderer = $PAGE->get_renderer('core', 'backup'); echo $renderer->copy_progress_viewer($USER->id, $courseid); echo $OUTPUT->container_end(); echo $OUTPUT->footer(); backupfilesedit_form.php 0000644 00000004214 15215711721 0011441 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manage backup files * @package moodlecore * @copyright 2010 Dongsheng Cai <dongsheng@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->libdir.'/formslib.php'); class backup_files_edit_form extends moodleform { /** * Form definition. */ public function definition() { $mform =& $this->_form; $types = (FILE_INTERNAL | FILE_REFERENCE | FILE_CONTROLLED_LINK); $options = array('subdirs' => 0, 'maxfiles' => -1, 'accepted_types' => '*', 'return_types' => $types); $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options); $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']); $mform->setType('contextid', PARAM_INT); $mform->addElement('hidden', 'currentcontext', $this->_customdata['currentcontext']); $mform->setType('currentcontext', PARAM_INT); $mform->addElement('hidden', 'filearea', $this->_customdata['filearea']); $mform->setType('filearea', PARAM_AREA); $mform->addElement('hidden', 'component', $this->_customdata['component']); $mform->setType('component', PARAM_COMPONENT); $mform->addElement('hidden', 'returnurl', $this->_customdata['returnurl']); $mform->setType('returnurl', PARAM_LOCALURL); $this->add_action_buttons(true, get_string('savechanges')); $this->set_data($this->_customdata['data']); } } backup.php 0000644 00000023505 15215711721 0006531 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This script is used to configure and execute the backup proccess. * * @package core * @subpackage backup * @copyright Moodle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require_once('../config.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); // Backup of large courses requires extra memory. Use the amount configured // in admin settings. raise_memory_limit(MEMORY_EXTRA); $courseid = required_param('id', PARAM_INT); $sectionid = optional_param('section', null, PARAM_INT); $cmid = optional_param('cm', null, PARAM_INT); $cancel = optional_param('cancel', '', PARAM_ALPHA); $previous = optional_param('previous', false, PARAM_BOOL); /** * Part of the forms in stages after initial, is POST never GET */ $backupid = optional_param('backup', false, PARAM_ALPHANUM); // Determine if we are performing realtime for asynchronous backups. $backupmode = backup::MODE_GENERAL; if (async_helper::is_async_enabled()) { $backupmode = backup::MODE_ASYNC; } $courseurl = new moodle_url('/course/view.php', array('id' => $courseid)); $url = new moodle_url('/backup/backup.php', array('id'=>$courseid)); if ($sectionid !== null) { $url->param('section', $sectionid); } if ($cmid !== null) { $url->param('cm', $cmid); } $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $id = $courseid; $cm = null; $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); $contextid = $coursecontext->id; $type = backup::TYPE_1COURSE; if (!is_null($sectionid)) { $section = $DB->get_record('course_sections', array('course'=>$course->id, 'id'=>$sectionid), '*', MUST_EXIST); $type = backup::TYPE_1SECTION; $id = $sectionid; } if (!is_null($cmid)) { $cm = get_coursemodule_from_id(null, $cmid, $course->id, false, MUST_EXIST); $type = backup::TYPE_1ACTIVITY; $id = $cmid; } require_login($course, false, $cm); switch ($type) { case backup::TYPE_1COURSE : require_capability('moodle/backup:backupcourse', $coursecontext); $heading = $course->fullname; $PAGE->set_secondary_active_tab('coursereuse'); break; case backup::TYPE_1SECTION : require_capability('moodle/backup:backupsection', $coursecontext); if ((string)$section->name !== '') { $sectionname = format_string($section->name, true, array('context' => $coursecontext)); $heading = get_string('backupsection', 'backup', $sectionname); $PAGE->navbar->add($sectionname); } else { $heading = get_string('backupsection', 'backup', $section->section); $PAGE->navbar->add(get_string('section').' '.$section->section); } break; case backup::TYPE_1ACTIVITY : $activitycontext = context_module::instance($cm->id); require_capability('moodle/backup:backupactivity', $activitycontext); $contextid = $activitycontext->id; $heading = get_string('backupactivity', 'backup', $cm->name); break; default : throw new \moodle_exception('unknownbackuptype'); } $PAGE->set_title($heading); $PAGE->set_heading($heading); $PAGE->activityheader->disable(); if (empty($cancel)) { // Do not print the header if user cancelled the process, as we are going to redirect the user. echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('backup'); echo html_writer::tag('div', get_string('backupinfo'), ['class' => 'pb-3']); } // Only let user perform a backup if we aren't in async mode, or if we are // and there are no pending backups for this item for this user. if (!async_helper::is_async_pending($id, 'course', 'backup')) { // The mix of business logic and display elements below makes me sad. // This needs to refactored into the renderer and seperated out. if (!($bc = backup_ui::load_controller($backupid))) { $bc = new backup_controller($type, $id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, $backupmode, $USER->id, backup::RELEASESESSION_YES); // The backup id did not relate to a valid controller so we made a new controller. // Now we need to reset the backup id to match the new controller. $backupid = $bc->get_backupid(); } // Prepare a progress bar which can display optionally during long-running // operations while setting up the UI. $slowprogress = new \core\progress\display_if_slow(get_string('preparingui', 'backup')); $renderer = $PAGE->get_renderer('core', 'backup'); $backup = new backup_ui($bc); if ($backup->get_stage() == backup_ui::STAGE_SCHEMA && !$previous) { // After schema stage, we are probably going to get to the confirmation stage, // The confirmation stage has 2 sets of progress, so this is needed to prevent // it showing 2 progress bars. $twobars = true; $slowprogress->start_progress('', 2); } else { $twobars = false; } $backup->get_controller()->set_progress($slowprogress); $backup->process(); if ($backup->enforce_changed_dependencies()) { debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER); } $loghtml = ''; if ($backup->get_stage() == backup_ui::STAGE_FINAL) { // Before we perform the backup check settings to see if user // or setting defaults are set to exclude files from the backup. if ($backup->get_setting_value('files') == 0) { $renderer->set_samesite_notification(); } if ($backupmode != backup::MODE_ASYNC) { // Synchronous backup handling. // Display an extra backup step bar so that we can show the 'processing' step first. echo html_writer::start_div('', array('id' => 'executionprogress')); echo $renderer->progress_bar($backup->get_progress_bar()); $backup->get_controller()->set_progress(new \core\progress\display()); // Prepare logger and add to end of chain. $logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO); $backup->get_controller()->add_logger($logger); // Carry out actual backup. $backup->execute(); // Backup controller gets saved/loaded so the logger object changes and we // have to retrieve it. $logger = $backup->get_controller()->get_logger(); while (!is_a($logger, 'core_backup_html_logger')) { $logger = $logger->get_next(); } // Get HTML from logger. if ($CFG->debugdisplay) { $loghtml = $logger->get_html(); } // Hide the progress display and first backup step bar (the 'finished' step will show next). echo html_writer::end_div(); echo html_writer::script('document.getElementById("executionprogress").style.display = "none";'); } else { // Async backup handling. $backup->get_controller()->finish_ui(); echo html_writer::start_div('', array('id' => 'executionprogress')); echo $renderer->progress_bar($backup->get_progress_bar()); echo html_writer::end_div(); // Create adhoc task for backup. $asynctask = new \core\task\asynchronous_backup_task(); $asynctask->set_custom_data(array('backupid' => $backupid)); $asynctask->set_userid($USER->id); \core\task\manager::queue_adhoc_task($asynctask); // Add ajax progress bar and initiate ajax via a template. $restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid)); $progresssetup = array( 'backupid' => $backupid, 'contextid' => $contextid, 'courseurl' => $courseurl->out(), 'restoreurl' => $restoreurl->out(), 'headingident' => 'backup' ); echo $renderer->render_from_template('core/async_backup_status', $progresssetup); } } else { $backup->save_controller(); } if ($backup->get_stage() != backup_ui::STAGE_FINAL) { // Displaying UI can require progress reporting, so do it here before outputting // the backup stage bar (as part of the existing progress bar, if required). $ui = $backup->display($renderer); if ($twobars) { $slowprogress->end_progress(); } echo $renderer->progress_bar($backup->get_progress_bar()); echo $ui; // Display log data if there was any. if ($loghtml != '' && $backupmode != backup::MODE_ASYNC) { echo $renderer->log_display($loghtml); } } $backup->destroy(); unset($backup); } else { // User has a pending async operation. echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error'); echo $OUTPUT->container(get_string('pendingasyncdetail', 'backup')); echo $OUTPUT->continue_button($courseurl); } echo $OUTPUT->footer(); moodle2/restore_qtype_plugin.class.php 0000644 00000063447 15215711721 0014225 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_qtype_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class extending standard restore_plugin in order to implement some * helper methods related with the questions (qtype plugin) * * TODO: Finish phpdocs */ abstract class restore_qtype_plugin extends restore_plugin { /* * A simple answer to id cache for a single questions answers. * @var array */ private $questionanswercache = array(); /* * The id of the current question in the questionanswercache. * @var int */ private $questionanswercacheid = null; /** * @var array List of fields to exclude form hashing during restore. */ protected array $excludedhashfields = []; /** * Add to $paths the restore_path_elements needed * to handle question_answers for a given question * Used by various qtypes (calculated, essay, multianswer, * multichoice, numerical, shortanswer, truefalse) */ protected function add_question_question_answers(&$paths) { // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $elename = 'question_answer'; $elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works $paths[] = new restore_path_element($elename, $elepath); $this->exclude_identity_hash_fields([ '/options/answers/id', '/options/answers/question', ]); } /** * Add to $paths the restore_path_elements needed * to handle question_numerical_units for a given question * Used by various qtypes (calculated, numerical) */ protected function add_question_numerical_units(&$paths) { // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $elename = 'question_numerical_unit'; $elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works $paths[] = new restore_path_element($elename, $elepath); $this->exclude_identity_hash_fields([ '/options/units/id', '/options/units/question', ]); } /** * Add to $paths the restore_path_elements needed * to handle question_numerical_options for a given question * Used by various qtypes (calculated, numerical) */ protected function add_question_numerical_options(&$paths) { // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $elename = 'question_numerical_option'; $elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works $paths[] = new restore_path_element($elename, $elepath); $this->exclude_identity_hash_fields(['/options/question']); } /** * Add to $paths the restore_path_elements needed * to handle question_datasets (defs and items) for a given question * Used by various qtypes (calculated, numerical) */ protected function add_question_datasets(&$paths) { // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $elename = 'question_dataset_definition'; $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition'); // we used get_recommended_name() so this works $paths[] = new restore_path_element($elename, $elepath); $elename = 'question_dataset_item'; $elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item'); $paths[] = new restore_path_element($elename, $elepath); $this->exclude_identity_hash_fields([ '/options/datasets/id', '/options/datasets/question', '/options/datasets/category', '/options/datasets/type', '/options/datasets/items/id', // The following fields aren't included in the backup or DB structure, but are parsed from the options field. '/options/datasets/status', '/options/datasets/distribution', '/options/datasets/minimum', '/options/datasets/maximum', '/options/datasets/decimals', // This field is set dynamically from the count of items in the dataset, it is not backed up. '/options/datasets/number_of_items', ]); } /** * Processes the answer element (question answers). Common for various qtypes. * It handles both creation (if the question is being created) and mapping * (if the question already existed and is being reused) */ public function process_question_answer($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // In the past, there were some sloppily rounded fractions around. Fix them up. $changes = array( '-0.66666' => '-0.6666667', '-0.33333' => '-0.3333333', '-0.16666' => '-0.1666667', '-0.142857' => '-0.1428571', '0.11111' => '0.1111111', '0.142857' => '0.1428571', '0.16666' => '0.1666667', '0.33333' => '0.3333333', '0.333333' => '0.3333333', '0.66666' => '0.6666667', ); if (array_key_exists($data->fraction, $changes)) { $data->fraction = $changes[$data->fraction]; } // If the question has been created by restore, we need to create its question_answers too if ($questioncreated) { // Adjust some columns $data->question = $newquestionid; $data->answer = $data->answertext; // Insert record $newitemid = $DB->insert_record('question_answers', $data); // The question existed, we need to map the existing question_answers } else { // Have we cached the current question? if ($this->questionanswercacheid !== $newquestionid) { // The question changed, purge and start again! $this->questionanswercache = array(); $params = array('question' => $newquestionid); $answers = $DB->get_records('question_answers', $params, '', 'id, answer'); $this->questionanswercacheid = $newquestionid; // Cache all cleaned answers for a simple text match. foreach ($answers as $answer) { $clean = core_text::trim_ctrl_chars($answer->answer); // Clean CTRL chars. $clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending. $this->questionanswercache[$clean] = $answer->id; } } $rules = restore_course_task::define_decode_rules(); $rulesactivity = restore_quiz_activity_task::define_decode_rules(); $rules = array_merge($rules, $rulesactivity); $decoder = $this->task->get_decoder(); foreach ($rules as $rule) { $decoder->add_rule($rule); } $contentdecoded = $decoder->decode_content($data->answertext); if ($contentdecoded) { $data->answertext = $contentdecoded; } if (!isset($this->questionanswercache[$data->answertext])) { // If we haven't found the matching answer, something has gone really wrong, the question in the DB // is missing answers, throw an exception. $info = new stdClass(); $info->filequestionid = $oldquestionid; $info->dbquestionid = $newquestionid; $info->answer = s($data->answertext); throw new restore_step_exception('error_question_answers_missing_in_db', $info); } $newitemid = $this->questionanswercache[$data->answertext]; } // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files) $this->set_mapping('question_answer', $oldid, $newitemid); } /** * Processes the numerical_unit element (question numerical units). Common for various qtypes. * It handles both creation (if the question is being created) and mapping * (if the question already existed and is being reused) */ public function process_question_numerical_unit($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question has been created by restore, we need to create its question_numerical_units too if ($questioncreated) { // Adjust some columns $data->question = $newquestionid; // Insert record $newitemid = $DB->insert_record('question_numerical_units', $data); } } /** * Processes the numerical_option element (question numerical options). Common for various qtypes. * It handles both creation (if the question is being created) and mapping * (if the question already existed and is being reused) */ public function process_question_numerical_option($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question has been created by restore, we need to create its question_numerical_options too if ($questioncreated) { // Adjust some columns $data->question = $newquestionid; // Insert record $newitemid = $DB->insert_record('question_numerical_options', $data); // Create mapping (not needed, no files nor childs nor states here) //$this->set_mapping('question_numerical_option', $oldid, $newitemid); } } /** * Processes the dataset_definition element (question dataset definitions). Common for various qtypes. * It handles both creation (if the question is being created) and mapping * (if the question already existed and is being reused) */ public function process_question_dataset_definition($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question is mapped, nothing to do if (!$questioncreated) { return; } // Arrived here, let's see if the question_dataset_definition already exists in category or no // (by category, name, type and enough items). Only for "shared" definitions (category != 0). // If exists, reuse it, else, create it as "not shared" (category = 0) $data->category = $this->get_mappingid('question_category', $data->category); // If category is shared, look for definitions $founddefid = null; if ($data->category) { $candidatedefs = $DB->get_records_sql("SELECT id, itemcount FROM {question_dataset_definitions} WHERE category = ? AND name = ? AND type = ?", array($data->category, $data->name, $data->type)); foreach ($candidatedefs as $candidatedef) { if ($candidatedef->itemcount >= $data->itemcount) { // Check it has enough items $founddefid = $candidatedef->id; break; // end loop, shared definition match found } } // If there were candidates but none fulfilled the itemcount condition, create definition as not shared if ($candidatedefs && !$founddefid) { $data->category = 0; } } // If haven't found any shared definition match, let's create it if (!$founddefid) { $newitemid = $DB->insert_record('question_dataset_definitions', $data); // Set mapping, so dataset items will know if they must be created $this->set_mapping('question_dataset_definition', $oldid, $newitemid); // If we have found one shared definition match, use it } else { $newitemid = $founddefid; // Set mapping to 0, so dataset items will know they don't need to be created $this->set_mapping('question_dataset_definition', $oldid, 0); } // Arrived here, we have one $newitemid (create or reused). Create the question_datasets record $questiondataset = new stdClass(); $questiondataset->question = $newquestionid; $questiondataset->datasetdefinition = $newitemid; $DB->insert_record('question_datasets', $questiondataset); } /** * Processes the dataset_item element (question dataset items). Common for various qtypes. * It handles both creation (if the question is being created) and mapping * (if the question already existed and is being reused) */ public function process_question_dataset_item($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question is mapped, nothing to do if (!$questioncreated) { return; } // Detect if the question_dataset_definition is being created $newdefinitionid = $this->get_new_parentid('question_dataset_definition'); // If the definition is reused, nothing to do if (!$newdefinitionid) { return; } // let's create the question_dataset_items $data->definition = $newdefinitionid; $data->itemnumber = $data->number; $DB->insert_record('question_dataset_items', $data); } /** * Do any re-coding necessary in the student response. * @param int $questionid the new id of the question * @param int $sequencenumber of the step within the qusetion attempt. * @param array the response data from the backup. * @return array the recoded response. */ public function recode_response($questionid, $sequencenumber, array $response) { return $response; } /** * Decode legacy question_states.answer for this qtype. Used when restoring * 2.0 attempt data. */ public function recode_legacy_state_answer($state) { // By default, return answer unmodified, qtypes needing recode will override this return $state->answer; } /** * Return the contents of the questions stuff that must be processed by the links decoder * * Only common stuff to all plugins, in this case: * - question: text and feedback * - question_answers: text and feedback * - question_hints: hint * * Note each qtype will have, if needed, its own define_decode_contents method */ public static function define_plugin_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('question', ['questiontext', 'generalfeedback'], 'question_created'); $contents[] = new restore_decode_content('question_answers', ['answer', 'feedback'], 'question_answer'); $contents[] = new restore_decode_content('question_hints', ['hint'], 'question_hint'); return $contents; } /** * Add fields to the list of fields excluded from hashing. * * This allows common methods to add fields to the exclusion list. * * @param array $fields * @return void */ private function exclude_identity_hash_fields(array $fields): void { $this->excludedhashfields = array_merge($this->excludedhashfields, $fields); } /** * Return fields to be excluded from hashing during restores. * * @return array */ final public function get_excluded_identity_hash_fields(): array { return array_unique(array_merge( $this->excludedhashfields, $this->define_excluded_identity_hash_fields(), )); } /** * Return a list of paths to fields to be removed from questiondata before creating an identity hash. * * Fields that should be excluded from common elements such as answers or numerical units that are used by the plugin will * be excluded automatically. This method just needs to define any specific to this plugin, such as foreign keys used in the * plugin's tables. * * The returned array should be a list of slash-delimited paths to locate the fields to be removed from the questiondata object. * For example, if you want to remove the field `$questiondata->options->questionid`, the path would be '/options/questionid'. * If a field in the path is an array, the rest of the path will be applied to each object in the array. So if you have * `$questiondata->options->answers[]`, the path '/options/answers/id' will remove the 'id' field from each element of the * 'answers' array. * * @return array */ protected function define_excluded_identity_hash_fields(): array { return []; } /** * Convert the backup structure of this question type into a structure matching its question data * * This should take the hierarchical array of tags from the question's backup structure, and return a structure that matches * that returned when calling {@see get_question_options()} for this question type. * See https://docs.moodle.org/dev/Question_data_structures#Representation_1:_%24questiondata for an explanation of this * structure. * * This data will then be used to produce an identity hash for comparison with questions in the database. * * This base implementation deals with all common backup elements created by the add_question_*_options() methods in this class, * plus elements added by ::define_question_plugin_structure() named for the qtype. The question type will need to extend * this function if ::define_question_plugin_structure() adds any other elements to the backup. * * @param array $backupdata The hierarchical array of tags from the backup. * @return \stdClass The questiondata object. */ public static function convert_backup_to_questiondata(array $backupdata): \stdClass { // Create an object from the top-level fields. $questiondata = (object) array_filter($backupdata, fn($tag) => !is_array($tag)); $qtype = $questiondata->qtype; $questiondata->options = new stdClass(); if (isset($backupdata["plugin_qtype_{$qtype}_question"][$qtype])) { $questiondata->options = (object) $backupdata["plugin_qtype_{$qtype}_question"][$qtype][0]; } if (isset($backupdata["plugin_qtype_{$qtype}_question"]['answers'])) { $questiondata->options->answers = array_map( fn($answer) => (object) $answer, $backupdata["plugin_qtype_{$qtype}_question"]['answers']['answer'], ); } if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_options'])) { $questiondata->options = (object) array_merge( (array) $questiondata->options, $backupdata["plugin_qtype_{$qtype}_question"]['numerical_options']['numerical_option'][0], ); } if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_units'])) { $questiondata->options->units = array_map( fn($unit) => (object) $unit, $backupdata["plugin_qtype_{$qtype}_question"]['numerical_units']['numerical_unit'], ); } if (isset($backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions'])) { $questiondata->options->datasets = array_map( fn($dataset) => (object) $dataset, $backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions']['dataset_definition'], ); } if (isset($questiondata->options->datasets)) { foreach ($questiondata->options->datasets as $dataset) { if (isset($dataset->dataset_items)) { $dataset->items = array_map( fn($item) => (object) $item, $dataset->dataset_items['dataset_item'], ); unset($dataset->dataset_items); } } } if (isset($backupdata['question_hints'])) { $questiondata->hints = array_map( fn($hint) => (object) $hint, $backupdata['question_hints']['question_hint'], ); } return $questiondata; } /** * Remove excluded fields from the questiondata structure. * * This removes fields that will not match or not be present in the question data structure produced by * {@see self::convert_backup_to_questiondata()} and {@see get_question_options()} (such as IDs), so that the remaining data can * be used to produce an identity hash for comparing the two. * * For plugins, it should be sufficient to override {@see self::define_excluded_identity_hash_fields()} with a list of paths * specific to the plugin type. Overriding this method is only necessary if the plugin's * {@see question_type::get_question_options()} method adds additional data to the question that is not included in the backup. * * @param stdClass $questiondata * @param array $excludefields Paths to the fields to exclude. * @return stdClass The $questiondata with excluded fields removed. */ public static function remove_excluded_question_data(stdClass $questiondata, array $excludefields = []): stdClass { // All questions will need to exclude 'id' (used by question and other tables), 'questionid' (used by hints and options), // 'createdby' and 'modifiedby' (since they won't map between sites). $defaultexcludes = [ '/id', '/createdby', '/modifiedby', '/hints/id', '/hints/questionid', '/options/id', '/options/questionid', ]; $excludefields = array_unique(array_merge($excludefields, $defaultexcludes)); foreach ($excludefields as $excludefield) { $pathparts = explode('/', ltrim($excludefield, '/')); $data = $questiondata; self::unset_excluded_fields($data, $pathparts); } return $questiondata; } /** * Iterate through the elements of path to an excluded field, and unset the final element. * * If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields * in each child of the array. * * @param stdClass|array $data The questiondata object, or a subsection of it. * @param array $pathparts The remaining elements in the path to the excluded field. * @return void */ private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void { $element = array_shift($pathparts); if (!isset($data->{$element})) { // This element is not present in the data structure, nothing to unset. return; } if (is_object($data->{$element})) { self::unset_excluded_fields($data->{$element}, $pathparts); } else if (is_array($data->{$element})) { foreach ($data->{$element} as $item) { self::unset_excluded_fields($item, $pathparts); } } else if (empty($pathparts)) { // This is the last element of the path and it's a scalar value, unset it. unset($data->{$element}); } } } moodle2/backup_gradingform_plugin.class.php 0000644 00000002544 15215711721 0015133 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class backup_gradingform_plugin responsible for advanced grading form plugin backup * * @package core_backup * @subpackage moodle2 * @copyright 2011 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Base class for backup all advanced grading form plugins * * As an example of implementation see {@link backup_gradingform_rubric_plugin} * * @copyright 2011 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category backup */ abstract class backup_gradingform_plugin extends backup_plugin { } moodle2/backup_subplugin.class.php 0000644 00000007113 15215711721 0013263 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_subplugin class * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class implementing the subplugins support for moodle2 backups * * TODO: Finish phpdocs * TODO: Make this subclass of backup_plugin */ abstract class backup_subplugin { protected $subplugintype; protected $subpluginname; protected $connectionpoint; protected $optigroup; // Optigroup, parent of all optigroup elements protected $step; protected $task; public function __construct($subplugintype, $subpluginname, $optigroup, $step) { $this->subplugintype = $subplugintype; $this->subpluginname = $subpluginname; $this->optigroup = $optigroup; $this->connectionpoint = ''; $this->step = $step; $this->task = $step->get_task(); } public function define_subplugin_structure($connectionpoint) { $this->connectionpoint = $connectionpoint; $methodname = 'define_' . $connectionpoint . '_subplugin_structure'; if (method_exists($this, $methodname)) { $this->$methodname(); } } // Protected API starts here // backup_step/structure_step/task wrappers /** * Returns the value of one (task/plan) setting */ protected function get_setting_value($name) { if (is_null($this->task)) { throw new backup_step_exception('not_specified_backup_task'); } return $this->task->get_setting_value($name); } // end of backup_step/structure_step/task wrappers /** * Factory method that will return one backup_subplugin_element (backup_optigroup_element) * with its name automatically calculated, based one the subplugin being handled (type, name) */ protected function get_subplugin_element($final_elements = null, $conditionparam = null, $conditionvalue = null) { // Something exclusive for this backup_subplugin_element (backup_optigroup_element) // because it hasn't XML representation $name = 'optigroup_' . $this->subplugintype . '_' . $this->subpluginname . '_' . $this->connectionpoint; $optigroup_element = new backup_subplugin_element($name, $final_elements, $conditionparam, $conditionvalue); $this->optigroup->add_child($optigroup_element); // Add optigroup_element to stay connected since beginning return $optigroup_element; } /** * Simple helper function that suggests one name for the main nested element in subplugins * It's not mandatory to use it but recommended ;-) */ protected function get_recommended_name() { return 'subplugin_' . $this->subplugintype . '_' . $this->subpluginname . '_' . $this->connectionpoint; } } moodle2/restore_report_plugin.class.php 0000644 00000002031 15215711721 0014354 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); /** * Restore for plugin report. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_report_plugin extends restore_plugin { // Use default parent behaviour } moodle2/backup_qtype_plugin.class.php 0000644 00000020030 15215711721 0013764 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_qtype_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class extending standard backup_plugin in order to implement some * helper methods related with the questions (qtype plugin) * * TODO: Finish phpdocs */ abstract class backup_qtype_plugin extends backup_plugin { /** * Attach to $element (usually questions) the needed backup structures * for question_answers for a given question * Used by various qtypes (calculated, essay, multianswer, * multichoice, numerical, shortanswer, truefalse) */ protected function add_question_question_answers($element) { // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_answers_bad_parent_element', $element); } // Define the elements $answers = new backup_nested_element('answers'); $answer = new backup_nested_element('answer', array('id'), array( 'answertext', 'answerformat', 'fraction', 'feedback', 'feedbackformat')); // Build the tree $element->add_child($answers); $answers->add_child($answer); // Set the sources $answer->set_source_table('question_answers', array('question' => backup::VAR_PARENTID), 'id ASC'); // Aliases $answer->set_source_alias('answer', 'answertext'); // don't need to annotate ids nor files } /** * Attach to $element (usually questions) the needed backup structures * for question_numerical_units for a given question * Used both by calculated and numerical qtypes */ protected function add_question_numerical_units($element) { // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_numerical_units_bad_parent_element', $element); } // Define the elements $units = new backup_nested_element('numerical_units'); $unit = new backup_nested_element('numerical_unit', array('id'), array( 'multiplier', 'unit')); // Build the tree $element->add_child($units); $units->add_child($unit); // Set the sources $unit->set_source_table('question_numerical_units', array('question' => backup::VAR_PARENTID), 'id ASC'); // don't need to annotate ids nor files } /** * Attach to $element (usually questions) the needed backup structures * for question_numerical_options for a given question * Used both by calculated and numerical qtypes */ protected function add_question_numerical_options($element) { // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_numerical_options_bad_parent_element', $element); } // Define the elements $options = new backup_nested_element('numerical_options'); $option = new backup_nested_element('numerical_option', array('id'), array( 'showunits', 'unitsleft', 'unitgradingtype', 'unitpenalty')); // Build the tree $element->add_child($options); $options->add_child($option); // Set the sources $option->set_source_table('question_numerical_options', array('question' => backup::VAR_PARENTID)); // don't need to annotate ids nor files } /** * Attach to $element (usually questions) the needed backup structures * for question_datasets for a given question * Used by calculated qtypes */ protected function add_question_datasets($element) { // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_datasets_bad_parent_element', $element); } // Define the elements $definitions = new backup_nested_element('dataset_definitions'); $definition = new backup_nested_element('dataset_definition', array('id'), array( 'category', 'name', 'type', 'options', 'itemcount')); $items = new backup_nested_element('dataset_items'); $item = new backup_nested_element('dataset_item', array('id'), array( 'number', 'value')); // Build the tree $element->add_child($definitions); $definitions->add_child($definition); $definition->add_child($items); $items->add_child($item); // Set the sources $definition->set_source_sql('SELECT qdd.* FROM {question_dataset_definitions} qdd JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id WHERE qd.question = ?', array(backup::VAR_PARENTID)); $item->set_source_table('question_dataset_items', array('definition' => backup::VAR_PARENTID)); // Aliases $item->set_source_alias('itemnumber', 'number'); // don't need to annotate ids nor files } /** * Returns all the components and fileareas used by all the installed qtypes * * The method introspects each qtype, asking it about fileareas used. Then, * one 2-level array is returned. 1st level is the component name (qtype_xxxx) * and 2nd level is one array of filearea => mappings to look * * Note that this function is used both in backup and restore, so it is important * to use the same mapping names (usually, name of the table in singular) always * * TODO: Surely this can be promoted to backup_plugin easily and make it to * work for ANY plugin, not only qtypes (but we don't need it for now) */ public static function get_components_and_fileareas($filter = null) { $components = array(); // Get all the plugins of this type $qtypes = core_component::get_plugin_list('qtype'); foreach ($qtypes as $name => $path) { // Apply filter if specified if (!is_null($filter) && $filter != $name) { continue; } // Calculate the componentname $componentname = 'qtype_' . $name; // Get the plugin fileareas (all them MUST belong to the same component) $classname = 'backup_qtype_' . $name . '_plugin'; if (class_exists($classname)) { $elements = call_user_func(array($classname, 'get_qtype_fileareas')); if ($elements) { // If there are elements, add them to $components $components[$componentname] = $elements; } } } return $components; } /** * Returns one array with filearea => mappingname elements for the qtype * * Used by {@link get_components_and_fileareas} to know about all the qtype * files to be processed both in backup and restore. */ public static function get_qtype_fileareas() { // By default, return empty array, only qtypes having own fileareas will override this return array(); } } moodle2/restore_theme_plugin.class.php 0000644 00000002050 15215711721 0014144 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); /** * Restore for course plugin: theme. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_theme_plugin extends restore_plugin { // Use default parent behaviour } moodle2/restore_final_task.class.php 0000644 00000024630 15215711721 0013607 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_final_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Final task that provides all the final steps necessary in order to finish one * restore like gradebook, interlinks... apart from some final cleaning * * TODO: Finish phpdocs */ class restore_final_task extends restore_task { /** * Create all the steps that will be part of this task */ public function build() { // Move all the CONTEXT_MODULE question qcats to their // final (newly created) module context $this->add_step(new restore_move_module_questions_categories('move_module_question_categories')); // Create all the question files now that every question is in place // and every category has its final contextid associated $this->add_step(new restore_create_question_files('create_question_files')); // Review all the block_position records in backup_ids in order // match them now that all the contexts are created populating DB // as needed. Only if we are restoring blocks. if ($this->get_setting_value('blocks')) { $this->add_step(new restore_review_pending_block_positions('review_block_positions')); } // Gradebook. Don't restore the gradebook unless activities are being restored. if ($this->get_setting_value('activities')) { $this->add_step(new restore_gradebook_structure_step('gradebook_step','gradebook.xml')); $this->add_step(new restore_grade_history_structure_step('grade_history', 'grade_history.xml')); } // Course completion. $this->add_step(new restore_course_completion_structure_step('course_completion', 'completion.xml')); // Conditionally restore course badges. if ($this->get_setting_value('badges')) { $this->add_step(new restore_badges_structure_step('course_badges', 'badges.xml')); } // Review all the legacy module_availability records in backup_ids in // order to match them with existing modules / grade items and convert // into the new system. $this->add_step(new restore_process_course_modules_availability('process_modules_availability')); // Update restored availability data to account for changes in IDs // during backup/restore. $this->add_step(new restore_update_availability('update_availability')); // Refresh action events conditionally. if ($this->get_setting_value('activities')) { $this->add_step(new restore_calendar_action_events('restoring_action_events')); } // Decode all the interlinks $this->add_step(new restore_decode_interlinks('decode_interlinks')); // Restore course logs (conditionally). They are restored here because we need all // the activities to be already restored. if ($this->get_setting_value('logs')) { // Legacy logs. $this->add_step(new restore_course_logs_structure_step('course_logs', 'course/logs.xml')); // New log stores. $this->add_step(new restore_course_logstores_structure_step('course_logstores', 'course/logstores.xml')); // Last access to course logs. $this->add_step(new restore_course_loglastaccess_structure_step('course_loglastaccess', 'course/loglastaccess.xml')); } // Review all the executed tasks having one after_restore method // executing it to perform some final adjustments of information // not available when the task was executed. // This step is always the last one performing modifications on restored information // Don't add any new step after it. Only aliases queue, cache rebuild and clean are allowed. $this->add_step(new restore_execute_after_restore('executing_after_restore')); // All files were sent to the filepool by now. We need to process // the aliases yet as they were not actually created but stashed for us instead. // We execute this step after executing_after_restore so that there can't be no // more files sent to the filepool after this. $this->add_step(new restore_process_file_aliases_queue('process_file_aliases_queue')); // Rebuild course cache to see results, whoah! $this->add_step(new restore_rebuild_course_cache('rebuild_course_cache')); // Clean the temp dir (conditionally) and drop temp table $this->add_step(new restore_drop_and_clean_temp_stuff('drop_and_clean_temp_stuff')); // If restoring to a new course or overwriting config, reindex the whole course. if (\core_search\manager::is_indexing_enabled()) { $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE; $wholecourse = $wholecourse || $this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf'); if ($wholecourse) { $this->add_step(new restore_course_search_index('course_search_index')); } } $this->built = true; } /** * Special method, only available in the restore_final_task, able to invoke the * restore_plan execute_after_restore() method, so restore_execute_after_restore step * will be able to launch all the after_restore() methods of the executed tasks */ public function launch_execute_after_restore() { $this->plan->execute_after_restore(); } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * course logs. It must return one array * of {@link restore_log_rule} objects * * Note these are course logs, but are defined and restored * in final task because we need all the activities to be * restored in order to handle some log records properly */ public static function define_restore_log_rules() { $rules = array(); // module 'course' rules $rules[] = new restore_log_rule('course', 'view', 'view.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'guest', 'view.php?id={course}', null); $rules[] = new restore_log_rule('course', 'user report', 'user.php?id={course}&user={user}&mode=[mode]', null); $rules[] = new restore_log_rule('course', 'add mod', '../mod/[modname]/view.php?id={course_module}', '[modname] {[modname]}'); $rules[] = new restore_log_rule('course', 'update mod', '../mod/[modname]/view.php?id={course_module}', '[modname] {[modname]}'); $rules[] = new restore_log_rule('course', 'delete mod', 'view.php?id={course}', null); $rules[] = new restore_log_rule('course', 'update', 'view.php?id={course}', ''); $rules[] = new restore_log_rule('course', 'enrol', 'view.php?id={course}', '{user}'); $rules[] = new restore_log_rule('course', 'unenrol', 'view.php?id={course}', '{user}'); $rules[] = new restore_log_rule('course', 'editsection', 'editsection.php?id={course_section}', null); $rules[] = new restore_log_rule('course', 'new', 'view.php?id={course}', ''); $rules[] = new restore_log_rule('course', 'recent', 'recent.php?id={course}', ''); $rules[] = new restore_log_rule('course', 'report log', 'report/log/index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'report live', 'report/live/index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'report outline', 'report/outline/index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'report participation', 'report/participation/index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'report stats', 'report/stats/index.php?id={course}', '{course}'); $rules[] = new restore_log_rule('course', 'view section', 'view.php?id={course}§ionid={course_section}', '{course_section}'); // module 'grade' rules $rules[] = new restore_log_rule('grade', 'update', 'report/grader/index.php?id={course}', null); // module 'user' rules $rules[] = new restore_log_rule('user', 'view', 'view.php?id={user}&course={course}', '{user}'); $rules[] = new restore_log_rule('user', 'change password', 'view.php?id={user}&course={course}', '{user}'); $rules[] = new restore_log_rule('user', 'login', 'view.php?id={user}&course={course}', '{user}'); $rules[] = new restore_log_rule('user', 'logout', 'view.php?id={user}&course={course}', '{user}'); $rules[] = new restore_log_rule('user', 'view all', 'index.php?id={course}', ''); $rules[] = new restore_log_rule('user', 'update', 'view.php?id={user}&course={course}', ''); // rules from other tasks (activities) not belonging to one module instance (cmid = 0), so are restored here $rules = array_merge($rules, restore_logs_processor::register_log_rules_for_course()); // Calendar rules. $rules[] = new restore_log_rule('calendar', 'add', 'event.php?action=edit&id={event}', '[name]'); $rules[] = new restore_log_rule('calendar', 'edit', 'event.php?action=edit&id={event}', '[name]'); $rules[] = new restore_log_rule('calendar', 'edit all', 'event.php?action=edit&id={event}', '[name]'); // TODO: Other logs like 'upload'... will go here return $rules; } // Protected API starts here /** * Define the common setting that any restore type will have */ protected function define_settings() { // This task has not settings (could have them, like destination or so in the future, let's see) } } moodle2/backup_local_plugin.class.php 0000644 00000002242 15215711721 0013721 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_local_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard backup_plugin in order to implement some * helper methods related with the local plugins */ abstract class backup_local_plugin extends backup_plugin {} moodle2/restore_activity_task.class.php 0000644 00000036532 15215711721 0014356 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_activity_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * abstract activity task that provides all the properties and common tasks to be performed * when one activity is being restored * * TODO: Finish phpdocs */ abstract class restore_activity_task extends restore_task { protected $info; // info related to activity gathered from backup file protected $modulename; // name of the module protected $moduleid; // new (target) id of the course module protected $oldmoduleid; // old (original) id of the course module protected $oldmoduleversion; // old (original) version of the module protected $contextid; // new (target) context of the activity protected $oldcontextid;// old (original) context of the activity protected $activityid; // new (target) id of the activity protected $oldactivityid;// old (original) id of the activity /** * Constructor - instantiates one object of this class */ public function __construct($name, $info, $plan = null) { $this->info = $info; $this->modulename = $this->info->modulename; $this->moduleid = 0; $this->oldmoduleid = $this->info->moduleid; $this->oldmoduleversion = 0; $this->contextid = 0; $this->oldcontextid = 0; $this->activityid = 0; $this->oldactivityid = 0; parent::__construct($name, $plan); } /** * Activity tasks have their own directory to read files */ public function get_taskbasepath() { return $this->get_basepath() . '/' . $this->info->directory; } public function set_moduleid($moduleid) { $this->moduleid = $moduleid; } public function set_old_moduleversion($oldmoduleversion) { $this->oldmoduleversion = $oldmoduleversion; } public function set_activityid($activityid) { $this->activityid = $activityid; } public function set_old_activityid($activityid) { $this->oldactivityid = $activityid; } public function set_contextid($contextid) { $this->contextid = $contextid; } public function set_old_contextid($contextid) { $this->oldcontextid = $contextid; } public function get_modulename() { return $this->modulename; } public function get_moduleid() { return $this->moduleid; } /** * Return if the activity is inside a subsection. * * @return bool */ public function is_in_subsection(): bool { return !empty($this->info->insubsection); } /** * Returns the old course module id (cmid of activity which will be restored) * * @return int */ public function get_old_moduleid() { return $this->oldmoduleid; } public function get_old_moduleversion() { return $this->oldmoduleversion; } public function get_activityid() { return $this->activityid; } public function get_old_activityid() { return $this->oldactivityid; } public function get_contextid() { return $this->contextid; } public function get_old_contextid() { return $this->oldcontextid; } /** * Create all the steps that will be part of this task */ public function build() { // If we have decided not to restore activities, prevent anything to be built if (!$this->get_setting_value('activities')) { $this->built = true; return; } // Load he course_module estructure, generating it (with instance = 0) // but allowing the creation of the target context needed in following steps $this->add_step(new restore_module_structure_step('module_info', 'module.xml')); // Here we add all the common steps for any activity and, in the point of interest // we call to define_my_steps() is order to get the particular ones inserted in place. $this->define_my_steps(); // Roles (optionally role assignments and always role overrides) $this->add_step(new restore_ras_and_caps_structure_step('course_ras_and_caps', 'roles.xml')); // Filters (conditionally) if ($this->get_setting_value('filters')) { $this->add_step(new restore_filters_structure_step('activity_filters', 'filters.xml')); } // Comments (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new restore_comments_structure_step('activity_comments', 'comments.xml')); } // Calendar events (conditionally) if ($this->get_setting_value('calendarevents')) { $this->add_step(new restore_calendarevents_structure_step('activity_calendar', 'calendar.xml')); } // Grades (module-related, rest of gradebook is restored later if possible: cats, calculations...) $this->add_step(new restore_activity_grades_structure_step('activity_grades', 'grades.xml')); // Advanced grading methods attached to the module $this->add_step(new restore_activity_grading_structure_step('activity_grading', 'grading.xml')); // Grade history. The setting 'grade_history' is handled in the step. $this->add_step(new restore_activity_grade_history_structure_step('activity_grade_history', 'grade_history.xml')); // Userscompletion (conditionally) if ($this->get_setting_value('userscompletion')) { $this->add_step(new restore_userscompletion_structure_step('activity_userscompletion', 'completion.xml')); } // Logs (conditionally) if ($this->get_setting_value('logs')) { // Legacy logs. $this->add_step(new restore_activity_logs_structure_step('activity_logs', 'logs.xml')); // New log stores. $this->add_step(new restore_activity_logstores_structure_step('activity_logstores', 'logstores.xml')); } // Activity competencies. $this->add_step(new restore_activity_competencies_structure_step('activity_competencies', 'competencies.xml')); // Search reindexing, if enabled and if not restoring entire course. if (\core_search\manager::is_indexing_enabled()) { $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE; $wholecourse = $wholecourse || ($this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf')); if (!$wholecourse) { $this->add_step(new restore_activity_search_index('activity_search_index')); } } // The xAPI state (conditionally). if ($this->get_setting_value('xapistate')) { $this->add_step(new restore_xapistate_structure_step('activity_xapistate', 'xapistate.xml')); } // At the end, mark it as built $this->built = true; } /** * Exceptionally override the execute method, so, based in the activity_included setting, we are able * to skip the execution of one task completely */ public function execute() { // Find activity_included_setting if (!$this->get_setting_value('included')) { $this->log('activity skipped by _included setting', backup::LOG_DEBUG, $this->name); $this->plan->set_excluding_activities(); // Inform plan we are excluding actvities } else { // Setting tells us it's ok to execute parent::execute(); } } /** * Specialisation that, first of all, looks for the setting within * the task with the the prefix added and later, delegates to parent * without adding anything */ public function get_setting($name) { $namewithprefix = $this->info->modulename . '_' . $this->info->moduleid . '_' . $name; $result = null; foreach ($this->settings as $key => $setting) { if ($setting->get_name() == $namewithprefix) { if ($result != null) { throw new base_task_exception('multiple_settings_by_name_found', $namewithprefix); } else { $result = $setting; } } } if ($result) { return $result; } else { // Fallback to parent return parent::get_setting($name); } } /** * Define (add) particular steps that each activity can have */ abstract protected function define_my_steps(); /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { throw new coding_exception('define_decode_contents() method needs to be overridden in each subclass of restore_activity_task'); } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { throw new coding_exception('define_decode_rules() method needs to be overridden in each subclass of restore_activity_task'); } /** * Define the restore log rules that will be applied * by the {@link restore_logs_processor} when restoring * activity logs. It must return one array * of {@link restore_log_rule} objects */ public static function define_restore_log_rules() { throw new coding_exception('define_restore_log_rules() method needs to be overridden in each subclass of restore_activity_task'); } // Protected API starts here /** * Define the common setting that any restore activity will have */ protected function define_settings() { // All the settings related to this activity will include this prefix $settingprefix = $this->info->modulename . '_' . $this->info->moduleid . '_'; // All these are common settings to be shared by all activities $activityincluded = $this->add_activity_included_setting($settingprefix); $this->add_activity_userinfo_setting($settingprefix, $activityincluded); // End of common activity settings, let's add the particular ones. $this->define_my_settings(); } /** * Add the activity included setting to the task. * * @param string $settingprefix the identifier of the setting * @return activity_backup_setting the setting added */ protected function add_activity_included_setting(string $settingprefix): activity_backup_setting { // Define activity_included (to decide if the whole task must be really executed) // Dependent of: // - activities root setting. // - sectionincluded setting (if exists). $settingname = $settingprefix . 'included'; if ($this->is_in_subsection()) { $activityincluded = new restore_subactivity_generic_setting($settingname, base_setting::IS_BOOLEAN, true); } else { $activityincluded = new restore_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true); } $activityincluded->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename), $this->modulename, ['class' => 'ms-1'])); $this->add_setting($activityincluded); // Look for "activities" root setting. $activities = $this->plan->get_setting('activities'); $activities->add_dependency($activityincluded); // Look for "sectionincluded" section setting (if exists). $settingname = 'section_' . $this->info->sectionid . '_included'; if ($this->plan->setting_exists($settingname)) { $sectionincluded = $this->plan->get_setting($settingname); $sectionincluded->add_dependency($activityincluded); } return $activityincluded; } /** * Add the activity userinfo setting to the task. * * @param string $settingprefix the identifier of the setting * @param activity_backup_setting $includefield the activity included setting * @return activity_backup_setting the setting added */ protected function add_activity_userinfo_setting( string $settingprefix, activity_backup_setting $includefield ): activity_backup_setting { // Define activityuserinfo. Dependent of: // - users root setting. // - sectionuserinfo setting (if exists). // - activity included setting. $settingname = $settingprefix . 'userinfo'; $defaultvalue = false; if (isset($this->info->settings[$settingname]) && $this->info->settings[$settingname]) { // Only enabled when available $defaultvalue = true; } if ($this->is_in_subsection()) { $activityuserinfo = new restore_subactivity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue); } else { $activityuserinfo = new restore_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue); } if (!$defaultvalue) { // This is a bit hacky, but if there is no user data to restore, then // we replace the standard check-box with a select menu with the // single choice 'No', and the select menu is clever enough that if // there is only one choice, it just displays a static string. // // It would probably be better design to have a special UI class // setting_ui_checkbox_or_no, rather than this hack, but I am not // going to do that today. $activityuserinfo->set_ui( new backup_setting_ui_select( $activityuserinfo, '-', [0 => get_string('no')] ) ); } else { $activityuserinfo->get_ui()->set_label('-'); } $this->add_setting($activityuserinfo); // Look for "users" root setting. $users = $this->plan->get_setting('users'); $users->add_dependency($activityuserinfo); // Look for "sectionuserinfo" section setting (if exists). $settingname = 'section_' . $this->info->sectionid . '_userinfo'; if ($this->plan->setting_exists($settingname)) { $sectionuserinfo = $this->plan->get_setting($settingname); $sectionuserinfo->add_dependency($activityuserinfo); } // Look for "activity included" setting. $includefield->add_dependency($activityuserinfo); return $activityuserinfo; } /** * Define (add) particular settings that each activity can have */ abstract protected function define_my_settings(); } moodle2/restore_default_block_task.class.php 0000644 00000003312 15215711721 0015306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_default_block_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Default block task to restore blocks not having own DB structures to be added * * TODO: Finish phpdocs */ class restore_default_block_task extends restore_block_task { // Nothing to do, it's just the restore_block_task in action // with required methods doing nothing special protected function define_my_settings() { } protected function define_my_steps() { } public function get_fileareas() { return array(); } public function get_configdata_encoded_attributes() { return array(); } public static function define_decode_contents() { return array(); } public static function define_decode_rules() { return array(); } } moodle2/restore_plugin.class.php 0000644 00000025405 15215711721 0012773 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class implementing the plugins support for moodle2 restore * * TODO: Finish phpdocs */ abstract class restore_plugin { /** @var string */ protected $plugintype; /** @var string */ protected $pluginname; /** @var restore_path_element */ protected $connectionpoint; /** @var restore_structure_step */ protected $step; /** @var restore_course_task|restore_activity_task */ protected $task; /** * restore_plugin constructor. * * @param string $plugintype * @param string $pluginname * @param restore_structure_step $step */ public function __construct($plugintype, $pluginname, $step) { $this->plugintype = $plugintype; $this->pluginname = $pluginname; $this->step = $step; $this->task = $step->get_task(); $this->connectionpoint = ''; } public function define_plugin_structure($connectionpoint) { if (!$connectionpoint instanceof restore_path_element) { throw new restore_step_exception('restore_path_element_required', $connectionpoint); } $paths = array(); $this->connectionpoint = $connectionpoint; $methodname = 'define_' . basename($this->connectionpoint->get_path()) . '_plugin_structure'; if (method_exists($this, $methodname)) { if ($pluginpaths = $this->$methodname()) { foreach ($pluginpaths as $path) { if ($path->get_processing_object() === null && !$this->step->grouped_parent_exists($path, $paths)) { $path->set_processing_object($this); } $paths[] = $path; } } } return $paths; } /** * after_execute dispatcher for any restore_plugin class * * This method will dispatch execution to the corresponding * after_execute_xxx() method when available, with xxx * being the connection point of the instance, so plugin * classes with multiple connection points will support * multiple after_execute methods, one for each connection point */ public function launch_after_execute_methods() { // Check if the after_execute method exists and launch it $afterexecute = 'after_execute_' . basename($this->connectionpoint->get_path()); if (method_exists($this, $afterexecute)) { $this->$afterexecute(); } } /** * after_restore dispatcher for any restore_plugin class * * This method will dispatch execution to the corresponding * after_restore_xxx() method when available, with xxx * being the connection point of the instance, so plugin * classes with multiple connection points will support * multiple after_restore methods, one for each connection point */ public function launch_after_restore_methods() { // Check if the after_restore method exists and launch it $afterrestore = 'after_restore_' . basename($this->connectionpoint->get_path()); if (method_exists($this, $afterrestore)) { $this->$afterrestore(); } } /** * Returns one array with all the decode contents * to be processed by the links decoder * * This method, given one plugin type, returns one * array of {@link restore_decode_content} objects * that will be added to the restore decoder in order * to perform modifications under the plugin contents. * * The objects are retrieved by calling to the {@link define_decode_contents} * method (when available), first in the main restore_xxxx_plugin class * and later on each of the available subclasses */ public static function get_restore_decode_contents($plugintype) { $decodecontents = array(); // Check the requested plugintype is a valid one if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) { throw new backup_step_exception('incorrect_plugin_type', $plugintype); } // Check the base plugin class exists $classname = 'restore_' . $plugintype . '_plugin'; if (!class_exists($classname)) { throw new backup_step_exception('plugin_class_not_found', $classname); } // First, call to the define_plugin_decode_contents in the base plugin class // (must exist by design in all the plugin base classes) if (method_exists($classname, 'define_plugin_decode_contents')) { $decodecontents = array_merge($decodecontents, call_user_func(array($classname, 'define_plugin_decode_contents'))); } // Now, iterate over all the possible plugins available // (only the needed ones have been loaded, so they will // be the ones being asked here). Fetch their restore contents // by calling (if exists) to their define_decode_contents() method $plugins = core_component::get_plugin_list($plugintype); foreach ($plugins as $plugin => $plugindir) { $classname = 'restore_' . $plugintype . '_' . $plugin . '_plugin'; if (class_exists($classname)) { if (method_exists($classname, 'define_decode_contents')) { $decodecontents = array_merge($decodecontents, call_user_func(array($classname, 'define_decode_contents'))); } } } return $decodecontents; } /** * Define the contents in the plugin that must be * processed by the link decoder */ public static function define_plugin_decode_contents() { throw new coding_exception('define_plugin_decode_contents() method needs to be overridden in each subclass of restore_plugin'); } // Protected API starts here // restore_step/structure_step/task wrappers protected function get_restoreid() { if (is_null($this->task)) { throw new restore_step_exception('not_specified_restore_task'); } return $this->task->get_restoreid(); } /** * To send ids pairs to backup_ids_table and to store them into paths * * This method will send the given itemname and old/new ids to the * backup_ids_temp table, and, at the same time, will save the new id * into the corresponding restore_path_element for easier access * by children. Also will inject the known old context id for the task * in case it's going to be used for restoring files later */ protected function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) { $this->step->set_mapping($itemname, $oldid, $newid, $restorefiles, $filesctxid, $parentid); } /** * Returns the latest (parent) old id mapped by one pathelement */ protected function get_old_parentid($itemname) { return $this->step->get_old_parentid($itemname); } /** * Returns the latest (parent) new id mapped by one pathelement */ protected function get_new_parentid($itemname) { return $this->step->get_new_parentid($itemname); } /** * Return the new id of a mapping for the given itemname * * @param string $itemname the type of item * @param int $oldid the item ID from the backup * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false */ protected function get_mappingid($itemname, $oldid, $ifnotfound = false) { return $this->step->get_mappingid($itemname, $oldid, $ifnotfound); } /** * Return the complete mapping from the given itemname, itemid */ protected function get_mapping($itemname, $oldid) { return $this->step->get_mapping($itemname, $oldid); } /** * Add all the existing file, given their component and filearea and one backup_ids itemname to match with */ protected function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { $this->step->add_related_files($component, $filearea, $mappingitemname, $filesctxid, $olditemid); } /** * Apply course startdate offset based in original course startdate and course_offset_startdate setting * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple * executions in the same request */ protected function apply_date_offset($value) { return $this->step->apply_date_offset($value); } /** * Returns the value of one (task/plan) setting */ protected function get_setting_value($name) { if (is_null($this->task)) { throw new restore_step_exception('not_specified_restore_task'); } return $this->task->get_setting_value($name); } // end of restore_step/structure_step/task wrappers /** * Simple helper function that returns the name for the restore_path_element * It's not mandatory to use it but recommended ;-) */ protected function get_namefor($name = '') { $name = $name !== '' ? '_' . $name : ''; return $this->plugintype . '_' . $this->pluginname . $name; } /** * Simple helper function that returns the base (prefix) of the path for the restore_path_element * Useful if we used get_recommended_name() in backup. It's not mandatory to use it but recommended ;-) */ protected function get_pathfor($path = '') { $path = trim($path, '/') !== '' ? '/' . trim($path, '/') : ''; return $this->connectionpoint->get_path() . '/' . 'plugin_' . $this->plugintype . '_' . $this->pluginname . '_' . basename($this->connectionpoint->get_path()) . $path; } /** * Get the task we are part of. * * @return restore_activity_task|restore_course_task the task. */ protected function get_task() { return $this->task; } } moodle2/backup_coursereport_plugin.class.php 0000644 00000002502 15215711721 0015362 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); /** * Base class for course report backup plugins. * * NOTE: When you back up a course, it potentially may run backup for all * course reports. In order to control whether a particular report gets * backed up, a course report should make use of the second and third * parameters in get_plugin_element(). * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 onwards The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_coursereport_plugin extends backup_plugin { // Use default parent behaviour } moodle2/restore_plan_builder.class.php 0000644 00000025243 15215711721 0014135 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_plan_builder class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); require_once($CFG->dirroot . '/backup/moodle2/restore_root_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_course_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_section_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_activity_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_final_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_default_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_format_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_local_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_theme_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_report_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_coursereport_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_plagiarism_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_enrol_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_coursereport_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plagiarism_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_enrol_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_subplugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_settingslib.php'); require_once($CFG->dirroot . '/backup/moodle2/restore_stepslib.php'); // Load all the activity tasks for moodle2 format $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { $taskpath = $moddir . '/backup/moodle2/restore_' . $mod . '_activity_task.class.php'; if (plugin_supports('mod', $mod, FEATURE_BACKUP_MOODLE2)) { if (file_exists($taskpath)) { require_once($taskpath); } } } // Load all the block tasks for moodle2 format $blocks = core_component::get_plugin_list('block'); foreach ($blocks as $block => $blockdir) { $taskpath = $blockdir . '/backup/moodle2/restore_' . $block . '_block_task.class.php'; if (file_exists($taskpath)) { require_once($taskpath); } } /** * Abstract class defining the static method in charge of building the whole * restore plan, based in @restore_controller preferences. * * TODO: Finish phpdocs */ abstract class restore_plan_builder { /** * Dispatches, based on type to specialised builders */ public static function build_plan($controller) { $plan = $controller->get_plan(); // Add the root task, responsible for // preparing everything, creating the // needed structures (users, roles), // preloading information to temp table // and other init tasks $plan->add_task(new restore_root_task('root_task')); $controller->get_progress()->progress(); switch ($controller->get_type()) { case backup::TYPE_1ACTIVITY: self::build_activity_plan($controller, key($controller->get_info()->activities)); break; case backup::TYPE_1SECTION: self::build_section_plan($controller, key($controller->get_info()->sections)); break; case backup::TYPE_1COURSE: self::build_course_plan($controller, $controller->get_courseid()); break; } // Add the final task, responsible for closing // all the pending bits (remapings, inter-links // conversion...) // and perform other various final actions. $plan->add_task(new restore_final_task('final_task')); $controller->get_progress()->progress(); } // Protected API starts here /** * Restore one 1-activity backup */ protected static function build_activity_plan($controller, $activityid) { $plan = $controller->get_plan(); $info = $controller->get_info(); $infoactivity = $info->activities[$activityid]; // Add the activity task, responsible for restoring // all the module related information. So it conditionally // as far as the module can be missing on restore if ($task = restore_factory::get_restore_activity_task($infoactivity)) { // can be missing $plan->add_task($task); $controller->get_progress()->progress(); // Some activities may have delegated section integrations. self::build_delegated_section_plan($controller, $infoactivity->moduleid); // For the given activity path, add as many block tasks as necessary // TODO: Add blocks, we need to introspect xml here $blocks = backup_general_helper::get_blocks_from_path($task->get_taskbasepath()); foreach ($blocks as $basepath => $name) { if ($task = restore_factory::get_restore_block_task($name, $basepath)) { $plan->add_task($task); $controller->get_progress()->progress(); } else { // TODO: Debug information about block not supported } } } else { // Activity is missing in target site, inform plan about that $plan->set_missing_modules(); } } /** * Build a course module delegated section backup plan. * @param restore_controller $controller * @param int $cmid the parent course module id. */ protected static function build_delegated_section_plan($controller, $cmid) { $info = $controller->get_info(); // Find if some section depends on that course module. $delegatedsectionid = null; foreach ($info->sections as $sectionid => $section) { // Delegated sections are not course responsability. if (isset($section->parentcmid) && $section->parentcmid == $cmid) { $delegatedsectionid = $sectionid; break; } } if (!$delegatedsectionid) { return; } self::build_section_plan($controller, $delegatedsectionid); } /** * Restore one 1-section backup */ protected static function build_section_plan($controller, $sectionid) { $plan = $controller->get_plan(); $info = $controller->get_info(); $infosection = $info->sections[$sectionid]; // Add the section task, responsible for restoring // all the section related information $plan->add_task(restore_factory::get_restore_section_task($infosection)); $controller->get_progress()->progress(); // For the given section, add as many activity tasks as necessary foreach ($info->activities as $activityid => $activity) { if ($activity->sectionid != $infosection->sectionid) { continue; } if (plugin_supports('mod', $activity->modulename, FEATURE_BACKUP_MOODLE2)) { // Check we support the format self::build_activity_plan($controller, $activityid); } else { // TODO: Debug information about module not supported } } } /** * Restore one 1-course backup */ protected static function build_course_plan($controller, $courseid) { $plan = $controller->get_plan(); $info = $controller->get_info(); // Add the course task, responsible for restoring // all the course related information $task = restore_factory::get_restore_course_task($info->course, $courseid); $plan->add_task($task); $controller->get_progress()->progress(); // For the given course path, add as many block tasks as necessary // TODO: Add blocks, we need to introspect xml here $blocks = backup_general_helper::get_blocks_from_path($task->get_taskbasepath()); foreach ($blocks as $basepath => $name) { if ($task = restore_factory::get_restore_block_task($name, $basepath)) { $plan->add_task($task); $controller->get_progress()->progress(); } else { // TODO: Debug information about block not supported } } // For the given course, add as many section tasks as necessary foreach ($info->sections as $sectionid => $section) { // Delegated sections are not course responsability. if (isset($section->parentcmid) && !empty($section->parentcmid)) { continue; } self::build_section_plan($controller, $sectionid); } } } moodle2/backup_plan_builder.class.php 0000644 00000023742 15215711721 0013721 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_plan_builder class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); require_once($CFG->dirroot . '/backup/moodle2/backup_root_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_activity_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_section_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_course_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_final_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qbank_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_report_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_coursereport_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plagiarism_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_enrol_plugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_subplugin.class.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_settingslib.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_stepslib.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_custom_fields.php'); // Load all the activity tasks for moodle2 format $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { $taskpath = $moddir . '/backup/moodle2/backup_' . $mod . '_activity_task.class.php'; if (plugin_supports('mod', $mod, FEATURE_BACKUP_MOODLE2)) { if (file_exists($taskpath)) { require_once($taskpath); } } } // Load all the block tasks for moodle2 format $blocks = core_component::get_plugin_list('block'); foreach ($blocks as $block => $blockdir) { $taskpath = $blockdir . '/backup/moodle2/backup_' . $block . '_block_task.class.php'; if (file_exists($taskpath)) { require_once($taskpath); } } /** * Abstract class defining the static method in charge of building the whole * backup plan, based in @backup_controller preferences. * * TODO: Finish phpdocs */ abstract class backup_plan_builder { /** * Dispatches, based on type to specialised builders */ public static function build_plan($controller) { $plan = $controller->get_plan(); // Add the root task, responsible for storing global settings // and some init tasks $plan->add_task(new backup_root_task('root_task')); switch ($controller->get_type()) { case backup::TYPE_1ACTIVITY: self::build_activity_plan($controller, $controller->get_id()); break; case backup::TYPE_1SECTION: self::build_section_plan($controller, $controller->get_id()); break; case backup::TYPE_1COURSE: self::build_course_plan($controller, $controller->get_id()); break; } // Add the final task, responsible for outputting // all the global xml files (groups, users, // gradebook, questions, roles, files...) and // the main moodle_backup.xml file // and perform other various final actions. $plan->add_task(new backup_final_task('final_task')); } /** * Return one array of supported backup types */ public static function supported_backup_types() { return array(backup::TYPE_1COURSE, backup::TYPE_1SECTION, backup::TYPE_1ACTIVITY); } // Protected API starts here /** * Build one 1-activity backup */ protected static function build_activity_plan($controller, $id) { $plan = $controller->get_plan(); // Add the activity task, responsible for outputting // all the module related information try { $plan->add_task(backup_factory::get_backup_activity_task($controller->get_format(), $id)); // Some activities may have delegated section integrations. self::build_delegated_section_plan($controller, $id); // For the given activity, add as many block tasks as necessary $blockids = backup_plan_dbops::get_blockids_from_moduleid($id); foreach ($blockids as $blockid) { try { $plan->add_task(backup_factory::get_backup_block_task($controller->get_format(), $blockid, $id)); } catch (backup_task_exception $e) { $a = stdClass(); $a->mid = $id; $a->bid = $blockid; $controller->log(get_string('error_block_for_module_not_found', 'backup', $a), backup::LOG_WARNING); } } } catch (backup_task_exception $e) { $controller->log(get_string('error_course_module_not_found', 'backup', $id), backup::LOG_WARNING); } } /** * Build a course module delegated section backup plan. * @param backup_controller $controller * @param int $cmid the parent course module id. */ protected static function build_delegated_section_plan($controller, $cmid) { global $CFG, $DB; // Check moduleid exists. if (!$coursemodule = get_coursemodule_from_id(false, $cmid)) { $controller->log(get_string('error_course_module_not_found', 'backup', $cmid), backup::LOG_WARNING); } $classname = 'mod_' . $coursemodule->modname . '\courseformat\sectiondelegate'; if (!class_exists($classname)) { return; } $sectionid = null; try { $sectionid = $classname::delegated_section_id($coursemodule); } catch (dml_exception $error) { $controller->log(get_string('error_delegate_section_not_found', 'backup', $cmid), backup::LOG_WARNING); return; } $plan = $controller->get_plan(); $sectiontask = backup_factory::get_backup_section_task($controller->get_format(), $sectionid); $sectiontask->set_delegated_cm($cmid); $plan->add_task($sectiontask); // For the given section, add as many activity tasks as necessary. $coursemodules = backup_plan_dbops::get_modules_from_sectionid($sectionid); foreach ($coursemodules as $coursemodule) { if (plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) { self::build_activity_plan($controller, $coursemodule->id); } } } /** * Build one 1-section backup */ protected static function build_section_plan($controller, $id) { $plan = $controller->get_plan(); // Add the section task, responsible for outputting // all the section related information $plan->add_task(backup_factory::get_backup_section_task($controller->get_format(), $id)); // For the given section, add as many activity tasks as necessary $coursemodules = backup_plan_dbops::get_modules_from_sectionid($id); foreach ($coursemodules as $coursemodule) { if (plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) { // Check we support the format self::build_activity_plan($controller, $coursemodule->id); } else { // TODO: Debug information about module not supported } } } /** * Build one 1-course backup */ protected static function build_course_plan($controller, $id) { $plan = $controller->get_plan(); // Add the course task, responsible for outputting // all the course related information $plan->add_task(backup_factory::get_backup_course_task($controller->get_format(), $id)); // For the given course, add as many section tasks as necessary $sections = backup_plan_dbops::get_sections_from_courseid($id); foreach ($sections as $sectionid) { // Delegated sections are not course responsability. $sectiondata = backup_plan_dbops::get_section_from_id($sectionid); if (!empty($sectiondata->component)) { continue; } self::build_section_plan($controller, $sectionid); } // For the given course, add as many block tasks as necessary $blockids = backup_plan_dbops::get_blockids_from_courseid($id); foreach ($blockids as $blockid) { $plan->add_task(backup_factory::get_backup_block_task($controller->get_format(), $blockid)); } } } moodle2/backup_format_plugin.class.php 0000644 00000003500 15215711721 0014115 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_format_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard backup_plugin in order to implement some * helper methods related with the course formats (format plugin) * * TODO: Finish phpdocs */ abstract class backup_format_plugin extends backup_plugin { protected $courseformat; // To store the format (course->format) of the instance public function __construct($plugintype, $pluginname, $optigroup, $step) { parent::__construct($plugintype, $pluginname, $optigroup, $step); $this->courseformat = backup_plan_dbops::get_courseformat_from_courseid($this->task->get_courseid()); } /** * Return the condition encapsulated into sqlparam format * to get evaluated by value, not by path nor processor setting */ protected function get_format_condition() { return array('sqlparam' => $this->courseformat); } } moodle2/backup_enrol_plugin.class.php 0000644 00000002407 15215711721 0013751 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_enrol_plugin class. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2014 University of Wisconsin * @author Matt petro * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Base class for enrol backup plugins. * * @package core_backup * @copyright 2014 University of Wisconsin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_enrol_plugin extends backup_plugin { // Use default parent behaviour. } moodle2/backup_settingslib.php 0000644 00000024313 15215711721 0012477 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines classes used to handle backup settings * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // TODO: Reduce these to the minimum because ui/dependencies are 100% separated // Root backup settings /** * root generic setting to store different things without dependencies */ class backup_generic_setting extends root_backup_setting {} /** * root setting to handle backup file names (no dependencies nor anything else) */ class backup_filename_setting extends backup_generic_setting { /** * Instantiates a setting object * * @param string $name Name of the setting * @param string $vtype Type of the setting, eg {@link base_setting::IS_TEXT} * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, eg {@link base_setting::VISIBLE} * @param int $status Status of the setting with regards to the locking, eg {@link base_setting::NOT_LOCKED} */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); } public function set_ui_filename($label, $value, ?array $options = null) { $this->make_ui(self::UI_HTML_TEXTFIELD, $label, null, $options); $this->set_value($value); } } /** * root setting to control if backup will include user information * A lot of other settings are dependent of this (module's user info, * grades user info, messages, blogs... */ class backup_users_setting extends backup_generic_setting {} /** * root setting to control if backup will include permission information by roles */ class backup_permissions_setting extends backup_generic_setting { } /** * root setting to control if backup will include group information depends on @backup_users_setting * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright 2014 Matt Sammarco */ class backup_groups_setting extends backup_generic_setting { } /** * root setting to control if backup will include custom field information * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright 2018 Daniel Neis Araujo */ class backup_customfield_setting extends backup_generic_setting { } /** * root setting to control if backup will include activities or no. * A lot of other settings (_included at activity levels) * are dependent of this setting */ class backup_activities_setting extends backup_generic_setting {} /** * root setting to control if backup will generate anonymized * user info or no, depends of @backup_users_setting so only is * available if the former is enabled (apart from security * that can change it */ class backup_anonymize_setting extends root_backup_setting {} /** * root setting to control if backup will include * role assignments or no (any level), depends of @backup_users_setting * exactly in the same way than @backup_anonymize_setting so we extend from it */ class backup_role_assignments_setting extends backup_anonymize_setting {} /** * root setting to control if backup will include * logs or no (any level), depends of @backup_users_setting * exactly in the same way than @backup_anonymize_setting so we extend from it */ class backup_logs_setting extends backup_anonymize_setting {} /** * root setting to control if backup will include * comments or no (any level), depends of @backup_users_setting * exactly in the same way than @backup_anonymize_setting so we extend from it */ class backup_comments_setting extends backup_anonymize_setting {} /** * root setting to control if backup will include badges or not, * depends on @backup_activities_setting */ class backup_badges_setting extends backup_generic_setting {} /** * root setting to control if backup will include * calender events or no (any level), depends of @backup_users_setting * exactly in the same way than @backup_anonymize_setting so we extend from it */ class backup_calendarevents_setting extends backup_anonymize_setting {} /** * root setting to control if backup will include * users completion data or no (any level), depends of @backup_users_setting * exactly in the same way than @backup_anonymize_setting so we extend from it */ class backup_userscompletion_setting extends backup_anonymize_setting {} /** * root setting to control if backup will include competencies or not. */ class backup_competencies_setting extends backup_generic_setting { /** * backup_competencies_setting constructor. */ public function __construct() { $defaultvalue = false; $visibility = base_setting::HIDDEN; $status = base_setting::LOCKED_BY_CONFIG; if (\core_competency\api::is_enabled()) { $defaultvalue = true; $visibility = base_setting::VISIBLE; $status = base_setting::NOT_LOCKED; } parent::__construct('competencies', base_setting::IS_BOOLEAN, $defaultvalue, $visibility, $status); } } // Section backup settings /** * generic section setting to pass various settings between tasks and steps */ class backup_section_generic_setting extends section_backup_setting {} /** * Setting to define if one section is included or no. Activities _included * settings depend of them if available */ class backup_section_included_setting extends section_backup_setting {} /** * section backup setting to control if section will include * user information or no, depends of @backup_users_setting */ class backup_section_userinfo_setting extends section_backup_setting {} /** * Subsection base class (section delegated to a course module). */ class subsection_backup_setting extends section_backup_setting { /** * Class constructor. * * @param string $name Name of the setting * @param string $vtype Type of the setting, for example base_setting::IS_TEXT * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE * @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); $this->level = self::SUBSECTION_LEVEL; } } /** * generic section setting to pass various settings between tasks and steps */ class backup_subsection_generic_setting extends subsection_backup_setting { } /** * Setting to define if one section is included or no. Activities _included * settings depend of them if available */ class backup_subsection_included_setting extends subsection_backup_setting { } /** * section backup setting to control if section will include * user information or no, depends of @backup_users_setting */ class backup_subsection_userinfo_setting extends subsection_backup_setting { } // Activity backup settings /** * generic activity setting to pass various settings between tasks and steps */ class backup_activity_generic_setting extends activity_backup_setting {} /** * activity backup setting to control if activity will * be included or no, depends of @backup_activities_setting and * optionally parent section included setting */ class backup_activity_included_setting extends activity_backup_setting {} /** * activity backup setting to control if activity will include * user information or no, depends of @backup_users_setting */ class backup_activity_userinfo_setting extends activity_backup_setting {} /** * Subactivity base class (activity inside a delegated section). */ class subactivity_backup_setting extends activity_backup_setting { /** * Class constructor. * * @param string $name Name of the setting * @param string $vtype Type of the setting, for example base_setting::IS_TEXT * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE * @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); $this->level = self::SUBACTIVITY_LEVEL; } } /** * Generic subactivity (activity inside a delegated section) setting to pass various settings between tasks and steps */ class backup_subactivity_generic_setting extends subactivity_backup_setting { } /** * Subactivity (activity inside a delegated section) backup setting to control if activity will * be included or no, depends of @backup_activities_setting and * optionally parent section included setting */ class backup_subactivity_included_setting extends subactivity_backup_setting { } /** * Subactivity (activity inside a delegated section) backup setting to control if activity will include * user information or no, depends of @backup_users_setting */ class backup_subactivity_userinfo_setting extends subactivity_backup_setting { } /** * Root setting to control if backup will include content bank content or no */ class backup_contentbankcontent_setting extends backup_generic_setting { } /** * Root setting to control if backup will include xAPI state or not. */ class backup_xapistate_setting extends backup_generic_setting { } moodle2/restore_section_task.class.php 0000644 00000024675 15215711721 0014173 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_section_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * section task that provides all the properties and common steps to be performed * when one section is being restored * * TODO: Finish phpdocs */ class restore_section_task extends restore_task { protected $info; // info related to section gathered from backup file protected $contextid; // course context id protected $sectionid; // new (target) id of the course section /** * Constructor - instantiates one object of this class */ public function __construct($name, $info, $plan = null) { $this->info = $info; $this->sectionid = 0; parent::__construct($name, $plan); } /** * Section tasks have their own directory to read files */ public function get_taskbasepath() { return $this->get_basepath() . '/sections/section_' . $this->info->sectionid; } /** * Get the course module that is delegating this section. * * @return int|null the course module id that is delegating this section */ public function get_delegated_cm(): ?int { if (!isset($this->info->parentcmid) || empty($this->info->parentcmid)) { return null; } return intval($this->info->parentcmid); } /** * Get the delegated activity modname if any. * * @return string|null the modname of the delegated activity */ public function get_modname(): ?string { if (!isset($this->info->modname) || empty($this->info->modname)) { return null; } return $this->info->modname; } public function set_sectionid($sectionid) { $this->sectionid = $sectionid; } public function get_contextid() { return $this->contextid; } public function get_sectionid() { return $this->sectionid; } /** * Create all the steps that will be part of this task */ public function build() { // Define the task contextid (the course one) $this->contextid = context_course::instance($this->get_courseid())->id; // We always try to restore as much info from sections as possible, no matter of the type // of restore (new, existing, deleting, import...). MDL-27764 $this->add_step(new restore_section_structure_step('course_info', 'section.xml')); // At the end, mark it as built $this->built = true; } /** * Exceptionally override the execute method, so, based in the section_included setting, we are able * to skip the execution of one task completely */ public function execute() { // Find activity_included_setting if (!$this->get_setting_value('included')) { $this->log('activity skipped by _included setting', backup::LOG_DEBUG, $this->name); } else { // Setting tells us it's ok to execute parent::execute(); } } /** * Specialisation that, first of all, looks for the setting within * the task with the the prefix added and later, delegates to parent * without adding anything */ public function get_setting($name) { $namewithprefix = 'section_' . $this->info->sectionid . '_' . $name; $result = null; foreach ($this->settings as $key => $setting) { if ($setting->get_name() == $namewithprefix) { if ($result != null) { throw new base_task_exception('multiple_settings_by_name_found', $namewithprefix); } else { $result = $setting; } } } if ($result) { return $result; } else { // Fallback to parent return parent::get_setting($name); } } /** * Define the contents in the course that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('course_sections', 'summary', 'course_section'); return $contents; } /** * Define the decoding rules for links belonging * to the sections to be executed by the link decoder */ public static function define_decode_rules() { return array(); } // Protected API starts here /** * Define the common setting that any restore section will have */ protected function define_settings() { // All the settings related to this activity will include this prefix $settingprefix = 'section_' . $this->info->sectionid . '_'; // All these are common settings to be shared by all sections. $sectionincluded = $this->add_section_included_setting($settingprefix); $this->add_section_userinfo_setting($settingprefix, $sectionincluded); } /** * Add the section included setting to the task. * * @param string $settingprefix the identifier of the setting * @return section_backup_setting the setting added */ protected function add_section_included_setting(string $settingprefix): section_backup_setting { global $DB; // Define sectionincluded (to decide if the whole task must be really executed). $settingname = $settingprefix . 'included'; $delegatedcmid = $this->get_delegated_cm(); if ($delegatedcmid) { $sectionincluded = new restore_subsection_included_setting($settingname, base_setting::IS_BOOLEAN, true); // Subsections depends on the parent activity included setting. $settingname = $this->get_modname() . '_' . $delegatedcmid . '_included'; if ($this->plan->setting_exists($settingname)) { $cmincluded = $this->plan->get_setting($settingname); $cmincluded->add_dependency( $sectionincluded, ); } $label = get_string('subsectioncontent', 'backup'); } else { $sectionincluded = new restore_section_included_setting($settingname, base_setting::IS_BOOLEAN, true); if (is_number($this->info->title)) { $label = get_string('includesection', 'backup', $this->info->title); } else if (empty($this->info->title)) { // Don't throw error if title is empty, gracefully continue restore. $this->log( 'Section title missing in backup for section id ' . $this->info->sectionid, backup::LOG_WARNING, $this->name ); $label = get_string('unnamedsection', 'backup'); } else { $label = $this->info->title; } } $sectionincluded->get_ui()->set_label($label); $this->add_setting($sectionincluded); return $sectionincluded; } /** * Add the section userinfo setting to the task. * * @param string $settingprefix the identifier of the setting * @param section_backup_setting $includefield the section included setting * @return section_backup_setting the setting added */ protected function add_section_userinfo_setting( string $settingprefix, section_backup_setting $includefield ): section_backup_setting { // Define sectionuserinfo. Dependent of: // - users root setting. // - sectionincluded setting. $settingname = $settingprefix . 'userinfo'; $defaultvalue = false; if (isset($this->info->settings[$settingname]) && $this->info->settings[$settingname]) { // Only enabled when available $defaultvalue = true; } $delegatedcmid = $this->get_delegated_cm(); if ($delegatedcmid) { $sectionuserinfo = new restore_subsection_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue); // Subsections depends on the parent activity included setting. $settingname = $this->get_modname() . '_' . $delegatedcmid . '_userinfo'; if ($this->plan->setting_exists($settingname)) { $cmincluded = $this->plan->get_setting($settingname); $cmincluded->add_dependency( $sectionuserinfo, ); } } else { $sectionuserinfo = new restore_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue); } if (!$defaultvalue) { // This is a bit hacky, but if there is no user data to restore, then // we replace the standard check-box with a select menu with the // single choice 'No', and the select menu is clever enough that if // there is only one choice, it just displays a static string. // // It would probably be better design to have a special UI class // setting_ui_checkbox_or_no, rather than this hack, but I am not // going to do that today. $sectionuserinfo->set_ui( new backup_setting_ui_select($sectionuserinfo, get_string('includeuserinfo', 'backup'), [0 => get_string('no')]) ); } else { $sectionuserinfo->get_ui()->set_label(get_string('includeuserinfo', 'backup')); } $this->add_setting($sectionuserinfo); // Look for "users" root setting. $users = $this->plan->get_setting('users'); $users->add_dependency($sectionuserinfo); // Look for "section included" section setting. $includefield->add_dependency($sectionuserinfo); return $sectionuserinfo; } } moodle2/backup_stepslib.php 0000644 00000361557 15215711721 0012013 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines various backup steps that will be used by common tasks in backup * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Create the temp dir where backup/restore will happen and create temp ids table. */ class create_and_clean_temp_stuff extends backup_execution_step { protected function define_execution() { $progress = $this->task->get_progress(); $progress->start_progress('Deleting backup directories'); backup_helper::check_and_create_backup_dir($this->get_backupid());// Create backup temp dir backup_helper::clear_backup_dir($this->get_backupid(), $progress); // Empty temp dir, just in case backup_controller_dbops::drop_backup_ids_temp_table($this->get_backupid()); // Drop ids temp table backup_controller_dbops::create_backup_ids_temp_table($this->get_backupid()); // Create ids temp table $progress->end_progress(); } } /** * Delete the temp dir used by backup/restore (conditionally) and drop temp ids table. * Note we delete the directory but not the corresponding log file that will be * there until cron cleans it up. */ class drop_and_clean_temp_stuff extends backup_execution_step { protected $skipcleaningtempdir = false; protected function define_execution() { global $CFG; backup_controller_dbops::drop_backup_ids_temp_table($this->get_backupid()); // Drop ids temp table // Delete temp dir conditionally: // 1) If $CFG->keeptempdirectoriesonbackup is not enabled // 2) If backup temp dir deletion has been marked to be avoided if (empty($CFG->keeptempdirectoriesonbackup) && !$this->skipcleaningtempdir) { $progress = $this->task->get_progress(); $progress->start_progress('Deleting backup dir'); backup_helper::delete_backup_dir($this->get_backupid(), $progress); // Empty backup dir $progress->end_progress(); } } public function skip_cleaning_temp_dir($skip) { $this->skipcleaningtempdir = $skip; } } /** * Create the directory where all the task (activity/block...) information will be stored */ class create_taskbasepath_directory extends backup_execution_step { protected function define_execution() { global $CFG; $basepath = $this->task->get_taskbasepath(); if (!check_dir_exists($basepath, true, true)) { throw new backup_step_exception('cannot_create_taskbasepath_directory', $basepath); } } } /** * Abstract structure step, parent of all the activity structure steps. Used to wrap the * activity structure definition within the main <activity ...> tag. */ abstract class backup_activity_structure_step extends backup_structure_step { /** * Wraps any activity backup structure within the common 'activity' element * that will include common to all activities information like id, context... * * @param backup_nested_element $activitystructure the element to wrap * @return backup_nested_element the $activitystructure wrapped by the common 'activity' element */ protected function prepare_activity_structure($activitystructure) { // Create the wrap element $activity = new backup_nested_element('activity', array('id', 'moduleid', 'modulename', 'contextid'), null); // Build the tree $activity->add_child($activitystructure); // Set the source $activityarr = array((object)array( 'id' => $this->task->get_activityid(), 'moduleid' => $this->task->get_moduleid(), 'modulename' => $this->task->get_modulename(), 'contextid' => $this->task->get_contextid())); $activity->set_source_array($activityarr); // Return the root element (activity) return $activity; } /** * Set a delegate section itemid mapping. * * @param string $pluginname the name of the plugin that is delegating the section. * @param int $itemid the itemid of the section being delegated. */ protected function set_delegated_section_mapping(string $pluginname, int $itemid) { backup_structure_dbops::insert_backup_ids_record( $this->get_backupid(), "course_section::$pluginname::$itemid", $this->task->get_moduleid() ); } } /** * Helper code for use by any plugin that stores question attempt data that it needs to back up. */ trait backup_questions_attempt_data_trait { /** * Attach to $element (usually attempts) the needed backup structures * for question_usages and all the associated data. * * @param backup_nested_element $element the element that will contain all the question_usages data. * @param string $usageidname the name of the element that holds the usageid. * This must be child of $element, and must be a final element. * @param string $nameprefix this prefix is added to all the element names we create. * Element names in the XML must be unique, so if you are using usages in * two different ways, you must give a prefix to at least one of them. If * you only use one sort of usage, then you can just use the default empty prefix. * This should include a trailing underscore. For example "myprefix_" */ protected function add_question_usages($element, $usageidname, $nameprefix = '') { global $CFG; require_once($CFG->dirroot . '/question/engine/lib.php'); // Check $element is one nested_backup_element if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_states_bad_parent_element', $element); } if (! $element->get_final_element($usageidname)) { throw new backup_step_exception('question_states_bad_question_attempt_element', $usageidname); } $quba = new backup_nested_element($nameprefix . 'question_usage', array('id'), array('component', 'preferredbehaviour')); $qas = new backup_nested_element($nameprefix . 'question_attempts'); $qa = new backup_nested_element($nameprefix . 'question_attempt', array('id'), array( 'slot', 'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'maxfraction', 'flagged', 'questionsummary', 'rightanswer', 'responsesummary', 'timemodified')); $steps = new backup_nested_element($nameprefix . 'steps'); $step = new backup_nested_element($nameprefix . 'step', array('id'), array( 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid')); $response = new backup_nested_element($nameprefix . 'response'); $variable = new backup_nested_element($nameprefix . 'variable', null, array('name', 'value')); // Build the tree $element->add_child($quba); $quba->add_child($qas); $qas->add_child($qa); $qa->add_child($steps); $steps->add_child($step); $step->add_child($response); $response->add_child($variable); // Set the sources $quba->set_source_table('question_usages', array('id' => '../' . $usageidname)); $qa->set_source_table('question_attempts', array('questionusageid' => backup::VAR_PARENTID), 'slot ASC'); $step->set_source_table('question_attempt_steps', array('questionattemptid' => backup::VAR_PARENTID), 'sequencenumber ASC'); $variable->set_source_table('question_attempt_step_data', array('attemptstepid' => backup::VAR_PARENTID)); // Annotate ids $qa->annotate_ids('question', 'questionid'); $step->annotate_ids('user', 'userid'); // Annotate files $fileareas = question_engine::get_all_response_file_areas(); foreach ($fileareas as $filearea) { $step->annotate_files('question', $filearea, 'id'); } } } /** * Helper to backup question reference data for an instance. */ trait backup_question_reference_data_trait { /** * Backup the related data from reference table for the instance. * * @param backup_nested_element $element * @param string $component * @param string $questionarea */ protected function add_question_references($element, $component, $questionarea) { // Check $element is one nested_backup_element. if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_states_bad_parent_element', $element); } $reference = new backup_nested_element('question_reference', ['id'], ['usingcontextid', 'component', 'questionarea', 'questionbankentryid', 'version']); $element->add_child($reference); $reference->set_source_table('question_references', [ 'usingcontextid' => backup::VAR_CONTEXTID, 'component' => backup_helper::is_sqlparam($component), 'questionarea' => backup_helper::is_sqlparam($questionarea), 'itemid' => backup::VAR_PARENTID ]); } } /** * Helper to backup question set reference data for an instance. */ trait backup_question_set_reference_trait { /** * Backup the related data from set_reference table for the instance. * * @param backup_nested_element $element * @param string $component * @param string $questionarea */ protected function add_question_set_references($element, $component, $questionarea) { // Check $element is one nested_backup_element. if (! $element instanceof backup_nested_element) { throw new backup_step_exception('question_states_bad_parent_element', $element); } $setreference = new backup_nested_element('question_set_reference', ['id'], ['usingcontextid', 'component', 'questionarea', 'questionscontextid', 'filtercondition']); $element->add_child($setreference); $setreference->set_source_table('question_set_references', [ 'usingcontextid' => backup::VAR_CONTEXTID, 'component' => backup_helper::is_sqlparam($component), 'questionarea' => backup_helper::is_sqlparam($questionarea), 'itemid' => backup::VAR_PARENTID ]); } } /** * Abstract structure step to help activities that store question attempt data, reference data and set reference data. * * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_questions_activity_structure_step extends backup_activity_structure_step { use backup_questions_attempt_data_trait; use backup_question_reference_data_trait; use backup_question_set_reference_trait; } /** * backup structure step in charge of calculating the categories to be * included in backup, based in the context being backuped (module/course) * and the already annotated questions present in backup_ids_temp */ class backup_calculate_question_categories extends backup_execution_step { protected function define_execution() { backup_question_dbops::calculate_question_categories($this->get_backupid(), $this->task->get_contextid()); } } /** * backup structure step in charge of deleting all the questions annotated * in the backup_ids_temp table */ class backup_delete_temp_questions extends backup_execution_step { protected function define_execution() { backup_question_dbops::delete_temp_questions($this->get_backupid()); } } /** * Abstract structure step, parent of all the block structure steps. Used to wrap the * block structure definition within the main <block ...> tag */ abstract class backup_block_structure_step extends backup_structure_step { protected function prepare_block_structure($blockstructure) { // Create the wrap element $block = new backup_nested_element('block', array('id', 'blockname', 'contextid'), null); // Build the tree $block->add_child($blockstructure); // Set the source $blockarr = array((object)array( 'id' => $this->task->get_blockid(), 'blockname' => $this->task->get_blockname(), 'contextid' => $this->task->get_contextid())); $block->set_source_array($blockarr); // Return the root element (block) return $block; } } /** * structure step that will generate the module.xml file for the activity, * accumulating various information about the activity, annotating groupings * and completion/avail conf */ class backup_module_structure_step extends backup_structure_step { protected function define_structure() { global $DB; // Define each element separated $module = new backup_nested_element('module', array('id', 'version'), array( 'modulename', 'sectionid', 'sectionnumber', 'idnumber', 'added', 'score', 'indent', 'visible', 'visibleoncoursepage', 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionpassgrade', 'completionview', 'completionexpected', 'availability', 'showdescription', 'downloadcontent', 'lang')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname')); // attach format plugin structure to $module element, only one allowed $this->add_plugin_structure('format', $module, false); // Attach report plugin structure to $module element, multiple allowed. $this->add_plugin_structure('report', $module, true); // attach plagiarism plugin structure to $module element, there can be potentially // many plagiarism plugins storing information about this course $this->add_plugin_structure('plagiarism', $module, true); // attach local plugin structure to $module, multiple allowed $this->add_plugin_structure('local', $module, true); // Attach admin tools plugin structure to $module. $this->add_plugin_structure('tool', $module, true); $module->add_child($tags); $tags->add_child($tag); // Set the sources $concat = $DB->sql_concat("'mod_'", 'm.name'); $module->set_source_sql(" SELECT cm.*, cp.value AS version, m.name AS modulename, s.id AS sectionid, s.section AS sectionnumber FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module JOIN {config_plugins} cp ON cp.plugin = $concat AND cp.name = 'version' JOIN {course_sections} s ON s.id = cm.section WHERE cm.id = ?", array(backup::VAR_MODID)); $tag->set_source_sql("SELECT t.id, t.name, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = 'course_modules' AND ti.component = 'core' AND ti.itemid = ?", array(backup::VAR_MODID)); // Define annotations $module->annotate_ids('grouping', 'groupingid'); // Return the root element ($module) return $module; } } /** * structure step that will generate the section.xml file for the section * annotating files */ class backup_section_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated $section = new backup_nested_element( 'section', ['id'], [ 'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible', 'availabilityjson', 'component', 'itemid', 'timemodified', ] ); // attach format plugin structure to $section element, only one allowed $this->add_plugin_structure('format', $section, false); // attach local plugin structure to $section element, multiple allowed $this->add_plugin_structure('local', $section, true); // Add nested elements for course_format_options table $formatoptions = new backup_nested_element('course_format_options', array('id'), array( 'format', 'name', 'value')); $section->add_child($formatoptions); // Define sources. $section->set_source_table('course_sections', array('id' => backup::VAR_SECTIONID)); $formatoptions->set_source_sql('SELECT cfo.id, cfo.format, cfo.name, cfo.value FROM {course} c JOIN {course_format_options} cfo ON cfo.courseid = c.id AND cfo.format = c.format WHERE c.id = ? AND cfo.sectionid = ?', array(backup::VAR_COURSEID, backup::VAR_SECTIONID)); // Aliases $section->set_source_alias('section', 'number'); // The 'availability' field needs to be renamed because it clashes with // the old nested element structure for availability data. $section->set_source_alias('availability', 'availabilityjson'); // Set annotations $section->annotate_files('course', 'section', 'id'); return $section; } } /** * structure step that will generate the course.xml file for the course, including * course category reference, tags, modules restriction information * and some annotations (files & groupings) */ class backup_course_structure_step extends backup_structure_step { protected function define_structure() { global $DB; // Define each element separated $course = new backup_nested_element('course', array('id', 'contextid'), array( 'shortname', 'fullname', 'idnumber', 'summary', 'summaryformat', 'format', 'showgrades', 'newsitems', 'startdate', 'enddate', 'marker', 'maxbytes', 'legacyfiles', 'showreports', 'visible', 'groupmode', 'groupmodeforce', 'defaultgroupingid', 'lang', 'theme', 'timecreated', 'timemodified', 'requested', 'showactivitydates', 'showcompletionconditions', 'pdfexportfont', 'enablecompletion', 'completionstartonenrol', 'completionnotify')); $category = new backup_nested_element('category', array('id'), array( 'name', 'description')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', array('id'), array( 'name', 'rawname')); $customfields = new backup_nested_element('customfields'); $customfield = new backup_nested_element('customfield', array('id'), array( 'shortname', 'type', 'value', 'valueformat', 'valuetrust', )); $courseformatoptions = new backup_nested_element('courseformatoptions'); $courseformatoption = new backup_nested_element('courseformatoption', [], [ 'courseid', 'format', 'sectionid', 'name', 'value' ]); // attach format plugin structure to $course element, only one allowed $this->add_plugin_structure('format', $course, false); // attach theme plugin structure to $course element; multiple themes can // save course data (in case of user theme, legacy theme, etc) $this->add_plugin_structure('theme', $course, true); // attach general report plugin structure to $course element; multiple // reports can save course data if required $this->add_plugin_structure('report', $course, true); // attach course report plugin structure to $course element; multiple // course reports can save course data if required $this->add_plugin_structure('coursereport', $course, true); // attach plagiarism plugin structure to $course element, there can be potentially // many plagiarism plugins storing information about this course $this->add_plugin_structure('plagiarism', $course, true); // attach local plugin structure to $course element; multiple local plugins // can save course data if required $this->add_plugin_structure('local', $course, true); // Attach admin tools plugin structure to $course element; multiple plugins // can save course data if required. $this->add_plugin_structure('tool', $course, true); // Build the tree $course->add_child($category); $course->add_child($tags); $tags->add_child($tag); $course->add_child($customfields); $customfields->add_child($customfield); $course->add_child($courseformatoptions); $courseformatoptions->add_child($courseformatoption); // Set the sources $courserec = $DB->get_record('course', array('id' => $this->task->get_courseid())); $courserec->contextid = $this->task->get_contextid(); // Add 'numsections' in order to be able to restore in previous versions of Moodle. // Even though Moodle does not officially support restore into older verions of Moodle from the // version where backup was made, without 'numsections' restoring will go very wrong. if (!property_exists($courserec, 'numsections') && course_get_format($courserec)->uses_sections()) { $courserec->numsections = course_get_format($courserec)->get_last_section_number(); } $course->set_source_array(array($courserec)); $categoryrec = $DB->get_record('course_categories', array('id' => $courserec->category)); $category->set_source_array(array($categoryrec)); $tag->set_source_sql('SELECT t.id, t.name, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.itemid = ?', array( backup_helper::is_sqlparam('course'), backup::VAR_PARENTID)); // Section level settings are dealt with in backup_section_structure_step. // We only need to deal with course level (sectionid = 0) here. $courseformatoption->set_source_sql('SELECT id, format, sectionid, name, value FROM {course_format_options} WHERE courseid = ? AND sectionid = 0', [ backup::VAR_PARENTID ]); // Custom fields. if ($this->get_setting_value('customfield')) { $handler = core_course\customfield\course_handler::create(); $fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid()); $handler->backup_define_structure($this->task->get_courseid(), $customfield); $customfield->set_source_array($fieldsforbackup); } // Some annotations $course->annotate_ids('grouping', 'defaultgroupingid'); $course->annotate_files('course', 'summary', null); $course->annotate_files('course', 'overviewfiles', null); if ($this->get_setting_value('legacyfiles')) { $course->annotate_files('course', 'legacy', null); } // Return root element ($course) return $course; } } /** * structure step that will generate the enrolments.xml file for the given course */ class backup_enrolments_structure_step extends backup_structure_step { /** * Skip enrolments on the front page. * @return bool */ protected function execute_condition() { return ($this->get_courseid() != SITEID); } protected function define_structure() { global $DB; // To know if we are including users $users = $this->get_setting_value('users'); $keptroles = $this->task->get_kept_roles(); // Define each element separated $enrolments = new backup_nested_element('enrolments'); $enrols = new backup_nested_element('enrols'); $enrol = new backup_nested_element('enrol', array('id'), array( 'enrol', 'status', 'name', 'enrolperiod', 'enrolstartdate', 'enrolenddate', 'expirynotify', 'expirythreshold', 'notifyall', 'password', 'cost', 'currency', 'roleid', 'customint1', 'customint2', 'customint3', 'customint4', 'customint5', 'customint6', 'customint7', 'customint8', 'customchar1', 'customchar2', 'customchar3', 'customdec1', 'customdec2', 'customtext1', 'customtext2', 'customtext3', 'customtext4', 'timecreated', 'timemodified')); $userenrolments = new backup_nested_element('user_enrolments'); $enrolment = new backup_nested_element('enrolment', array('id'), array( 'status', 'userid', 'timestart', 'timeend', 'modifierid', 'timemodified')); // Build the tree $enrolments->add_child($enrols); $enrols->add_child($enrol); $enrol->add_child($userenrolments); $userenrolments->add_child($enrolment); // Define sources - the instances are restored using the same sortorder, we do not need to store it in xml and deal with it afterwards. $enrol->set_source_table('enrol', array('courseid' => backup::VAR_COURSEID), 'sortorder ASC'); // User enrolments only added only if users included. if (empty($keptroles) && $users) { $enrolment->set_source_table('user_enrolments', array('enrolid' => backup::VAR_PARENTID)); $enrolment->annotate_ids('user', 'userid'); } else if (!empty($keptroles)) { list($insql, $inparams) = $DB->get_in_or_equal($keptroles); $params = array( backup::VAR_CONTEXTID, backup::VAR_PARENTID ); foreach ($inparams as $inparam) { $params[] = backup_helper::is_sqlparam($inparam); } $enrolment->set_source_sql( "SELECT ue.* FROM {user_enrolments} ue INNER JOIN {role_assignments} ra ON ue.userid = ra.userid WHERE ra.contextid = ? AND ue.enrolid = ? AND ra.roleid $insql", $params); $enrolment->annotate_ids('user', 'userid'); } $enrol->annotate_ids('role', 'roleid'); // Add enrol plugin structure. $this->add_plugin_structure('enrol', $enrol, true); return $enrolments; } } /** * structure step that will generate the roles.xml file for the given context, observing * the role_assignments setting to know if that part needs to be included */ class backup_roles_structure_step extends backup_structure_step { protected function define_structure() { // To know if we are including role assignments $roleassignments = $this->get_setting_value('role_assignments'); // Define each element separated $roles = new backup_nested_element('roles'); $overrides = new backup_nested_element('role_overrides'); $override = new backup_nested_element('override', array('id'), array( 'roleid', 'capability', 'permission', 'timemodified', 'modifierid')); $assignments = new backup_nested_element('role_assignments'); $assignment = new backup_nested_element('assignment', array('id'), array( 'roleid', 'userid', 'timemodified', 'modifierid', 'component', 'itemid', 'sortorder')); // Build the tree $roles->add_child($overrides); $roles->add_child($assignments); $overrides->add_child($override); $assignments->add_child($assignment); // Define sources $override->set_source_table('role_capabilities', array('contextid' => backup::VAR_CONTEXTID)); // Assignments only added if specified if ($roleassignments) { $assignment->set_source_table('role_assignments', array('contextid' => backup::VAR_CONTEXTID)); } // Define id annotations $override->annotate_ids('role', 'roleid'); $assignment->annotate_ids('role', 'roleid'); $assignment->annotate_ids('user', 'userid'); //TODO: how do we annotate the itemid? the meaning depends on the content of component table (skodak) return $roles; } } /** * structure step that will generate the roles.xml containing the * list of roles used along the whole backup process. Just raw * list of used roles from role table */ class backup_final_roles_structure_step extends backup_structure_step { protected function define_structure() { // Define elements $rolesdef = new backup_nested_element('roles_definition'); $role = new backup_nested_element('role', array('id'), array( 'name', 'shortname', 'nameincourse', 'description', 'sortorder', 'archetype')); // Build the tree $rolesdef->add_child($role); // Define sources $role->set_source_sql("SELECT r.*, rn.name AS nameincourse FROM {role} r JOIN {backup_ids_temp} bi ON r.id = bi.itemid LEFT JOIN {role_names} rn ON r.id = rn.roleid AND rn.contextid = ? WHERE bi.backupid = ? AND bi.itemname = 'rolefinal'", array(backup::VAR_CONTEXTID, backup::VAR_BACKUPID)); // Return main element (rolesdef) return $rolesdef; } } /** * structure step that will generate the scales.xml containing the * list of scales used along the whole backup process. */ class backup_final_scales_structure_step extends backup_structure_step { protected function define_structure() { // Define elements $scalesdef = new backup_nested_element('scales_definition'); $scale = new backup_nested_element('scale', array('id'), array( 'courseid', 'userid', 'name', 'scale', 'description', 'descriptionformat', 'timemodified')); // Build the tree $scalesdef->add_child($scale); // Define sources $scale->set_source_sql("SELECT s.* FROM {scale} s JOIN {backup_ids_temp} bi ON s.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'scalefinal'", array(backup::VAR_BACKUPID)); // Annotate scale files (they store files in system context, so pass it instead of default one) $scale->annotate_files('grade', 'scale', 'id', context_system::instance()->id); // Return main element (scalesdef) return $scalesdef; } } /** * structure step that will generate the outcomes.xml containing the * list of outcomes used along the whole backup process. */ class backup_final_outcomes_structure_step extends backup_structure_step { protected function define_structure() { // Define elements $outcomesdef = new backup_nested_element('outcomes_definition'); $outcome = new backup_nested_element('outcome', array('id'), array( 'courseid', 'userid', 'shortname', 'fullname', 'scaleid', 'description', 'descriptionformat', 'timecreated', 'timemodified','usermodified')); // Build the tree $outcomesdef->add_child($outcome); // Define sources $outcome->set_source_sql("SELECT o.* FROM {grade_outcomes} o JOIN {backup_ids_temp} bi ON o.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'outcomefinal'", array(backup::VAR_BACKUPID)); // Annotate outcome files (they store files in system context, so pass it instead of default one) $outcome->annotate_files('grade', 'outcome', 'id', context_system::instance()->id); // Return main element (outcomesdef) return $outcomesdef; } } /** * structure step in charge of constructing the filters.xml file for all the filters found * in activity */ class backup_filters_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated $filters = new backup_nested_element('filters'); $actives = new backup_nested_element('filter_actives'); $active = new backup_nested_element('filter_active', null, array('filter', 'active')); $configs = new backup_nested_element('filter_configs'); $config = new backup_nested_element('filter_config', null, array('filter', 'name', 'value')); // Build the tree $filters->add_child($actives); $filters->add_child($configs); $actives->add_child($active); $configs->add_child($config); // Define sources list($activearr, $configarr) = filter_get_all_local_settings($this->task->get_contextid()); $active->set_source_array($activearr); $config->set_source_array($configarr); // Return the root element (filters) return $filters; } } /** * Structure step in charge of constructing the comments.xml file for all the comments found in a given context. */ class backup_comments_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated. $comments = new backup_nested_element('comments'); $comment = new backup_nested_element('comment', array('id'), array( 'component', 'commentarea', 'itemid', 'content', 'format', 'userid', 'timecreated')); // Build the tree. $comments->add_child($comment); // Define sources. $comment->set_source_table('comments', array('contextid' => backup::VAR_CONTEXTID)); // Define id annotations. $comment->annotate_ids('user', 'userid'); // Return the root element (comments). return $comments; } } /** * structure step in charge of constructing the badges.xml file for all the badges found * in a given context */ class backup_badges_structure_step extends backup_structure_step { protected function define_structure() { global $CFG; require_once($CFG->libdir . '/badgeslib.php'); // Define each element separated. $badges = new backup_nested_element('badges'); $badge = new backup_nested_element('badge', array('id'), array('name', 'description', 'timecreated', 'timemodified', 'usercreated', 'usermodified', 'issuername', 'issuerurl', 'issuercontact', 'expiredate', 'expireperiod', 'type', 'courseid', 'message', 'messagesubject', 'attachment', 'notification', 'status', 'nextcron', 'version', 'language', 'imageauthorname', 'imageauthoremail', 'imageauthorurl', 'imagecaption')); $criteria = new backup_nested_element('criteria'); $criterion = new backup_nested_element('criterion', array('id'), array('badgeid', 'criteriatype', 'method', 'description', 'descriptionformat')); $endorsement = new backup_nested_element('endorsement', array('id'), array('badgeid', 'issuername', 'issuerurl', 'issueremail', 'claimid', 'claimcomment', 'dateissued')); $alignments = new backup_nested_element('alignments'); $alignment = new backup_nested_element('alignment', array('id'), array('badgeid', 'targetname', 'targeturl', 'targetdescription', 'targetframework', 'targetcode')); $relatedbadges = new backup_nested_element('relatedbadges'); $relatedbadge = new backup_nested_element('relatedbadge', array('id'), array('badgeid', 'relatedbadgeid')); $parameters = new backup_nested_element('parameters'); $parameter = new backup_nested_element('parameter', array('id'), array('critid', 'name', 'value', 'criteriatype')); $manual_awards = new backup_nested_element('manual_awards'); $manual_award = new backup_nested_element('manual_award', array('id'), array('badgeid', 'recipientid', 'issuerid', 'issuerrole', 'datemet')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', ['id'], ['name', 'rawname']); // Build the tree. $badges->add_child($badge); // Have the activities been included? Only if that's the case, the criteria will be included too. $activitiesincluded = !$this->task->is_excluding_activities(); if ($activitiesincluded) { $badge->add_child($criteria); $criteria->add_child($criterion); $criterion->add_child($parameters); $parameters->add_child($parameter); } $badge->add_child($endorsement); $badge->add_child($alignments); $alignments->add_child($alignment); $badge->add_child($relatedbadges); $relatedbadges->add_child($relatedbadge); $badge->add_child($manual_awards); $manual_awards->add_child($manual_award); $badge->add_child($tags); $tags->add_child($tag); // Define sources. $parametersql = ' SELECT * FROM {badge} WHERE courseid = :courseid AND status != ' . BADGE_STATUS_ARCHIVED; $parameterparams = [ 'courseid' => backup::VAR_COURSEID ]; $badge->set_source_sql($parametersql, $parameterparams); if ($activitiesincluded) { $criterion->set_source_table('badge_criteria', ['badgeid' => backup::VAR_PARENTID]); $parametersql = 'SELECT cp.*, c.criteriatype FROM {badge_criteria_param} cp JOIN {badge_criteria} c ON cp.critid = c.id WHERE critid = :critid'; $parameterparams = ['critid' => backup::VAR_PARENTID]; $parameter->set_source_sql($parametersql, $parameterparams); } $endorsement->set_source_table('badge_endorsement', array('badgeid' => backup::VAR_PARENTID)); $alignment->set_source_table('badge_alignment', array('badgeid' => backup::VAR_PARENTID)); $relatedbadge->set_source_table('badge_related', array('badgeid' => backup::VAR_PARENTID)); $manual_award->set_source_table('badge_manual_award', array('badgeid' => backup::VAR_PARENTID)); $tag->set_source_sql('SELECT t.id, t.name, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.itemid = ?', [backup_helper::is_sqlparam('badge'), backup::VAR_PARENTID]); // Define id annotations. $badge->annotate_ids('user', 'usercreated'); $badge->annotate_ids('user', 'usermodified'); if ($activitiesincluded) { $criterion->annotate_ids('badge', 'badgeid'); $parameter->annotate_ids('criterion', 'critid'); } $endorsement->annotate_ids('badge', 'badgeid'); $alignment->annotate_ids('badge', 'badgeid'); $relatedbadge->annotate_ids('badge', 'badgeid'); $relatedbadge->annotate_ids('badge', 'relatedbadgeid'); $badge->annotate_files('badges', 'badgeimage', 'id'); $manual_award->annotate_ids('badge', 'badgeid'); $manual_award->annotate_ids('user', 'recipientid'); $manual_award->annotate_ids('user', 'issuerid'); $manual_award->annotate_ids('role', 'issuerrole'); // Return the root element ($badges). return $badges; } } /** * structure step in charge of constructing the calender.xml file for all the events found * in a given context */ class backup_calendarevents_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated $events = new backup_nested_element('events'); $event = new backup_nested_element('event', array('id'), array( 'name', 'description', 'format', 'courseid', 'groupid', 'userid', 'repeatid', 'modulename', 'instance', 'type', 'eventtype', 'timestart', 'timeduration', 'timesort', 'visible', 'uuid', 'sequence', 'timemodified', 'priority', 'location')); // Build the tree $events->add_child($event); // Define sources if ($this->name == 'course_calendar') { $calendar_items_sql ="SELECT * FROM {event} WHERE courseid = :courseid AND (eventtype = 'course' OR eventtype = 'group')"; $calendar_items_params = array('courseid'=>backup::VAR_COURSEID); $event->set_source_sql($calendar_items_sql, $calendar_items_params); } else if ($this->name == 'activity_calendar') { // We don't backup action events. $params = array('instance' => backup::VAR_ACTIVITYID, 'modulename' => backup::VAR_MODNAME, 'type' => array('sqlparam' => CALENDAR_EVENT_TYPE_ACTION)); // If we don't want to include the userinfo in the backup then setting the courseid // will filter out all of the user override events (which have a course id of zero). $coursewhere = ""; if (!$this->get_setting_value('userinfo')) { $params['courseid'] = backup::VAR_COURSEID; $coursewhere = " AND courseid = :courseid"; } $calendarsql = "SELECT * FROM {event} WHERE instance = :instance AND type <> :type AND modulename = :modulename"; $calendarsql = $calendarsql . $coursewhere; $event->set_source_sql($calendarsql, $params); } else { $event->set_source_table('event', array('courseid' => backup::VAR_COURSEID, 'instance' => backup::VAR_ACTIVITYID, 'modulename' => backup::VAR_MODNAME)); } // Define id annotations $event->annotate_ids('user', 'userid'); $event->annotate_ids('group', 'groupid'); $event->annotate_files('calendar', 'event_description', 'id'); // Return the root element (events) return $events; } } /** * structure step in charge of constructing the gradebook.xml file for all the gradebook config in the course * NOTE: the backup of the grade items themselves is handled by backup_activity_grades_structure_step */ class backup_gradebook_structure_step extends backup_structure_step { /** * We need to decide conditionally, based on dynamic information * about the execution of this step. Only will be executed if all * the module gradeitems have been already included in backup */ protected function execute_condition() { $courseid = $this->get_courseid(); if ($courseid == SITEID) { return false; } return backup_plan_dbops::require_gradebook_backup($courseid, $this->get_backupid()); } protected function define_structure() { global $CFG, $DB; // are we including user info? $userinfo = $this->get_setting_value('users'); $gradebook = new backup_nested_element('gradebook'); //grade_letters are done in backup_activity_grades_structure_step() //calculated grade items $grade_items = new backup_nested_element('grade_items'); $grade_item = new backup_nested_element('grade_item', array('id'), array( 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin', 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated', 'timemodified')); $this->add_plugin_structure('local', $grade_item, true); $grade_grades = new backup_nested_element('grade_grades'); $grade_grade = new backup_nested_element('grade_grade', array('id'), array( 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat', 'timecreated', 'timemodified', 'aggregationstatus', 'aggregationweight')); //grade_categories $grade_categories = new backup_nested_element('grade_categories'); $grade_category = new backup_nested_element('grade_category', array('id'), array( //'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'timecreated', 'timemodified', 'hidden')); $letters = new backup_nested_element('grade_letters'); $letter = new backup_nested_element('grade_letter', 'id', array( 'lowerboundary', 'letter')); $grade_settings = new backup_nested_element('grade_settings'); $grade_setting = new backup_nested_element('grade_setting', 'id', array( 'name', 'value')); $gradebook_attributes = new backup_nested_element('attributes', null, array('calculations_freeze')); // Build the tree $gradebook->add_child($gradebook_attributes); $gradebook->add_child($grade_categories); $grade_categories->add_child($grade_category); $gradebook->add_child($grade_items); $grade_items->add_child($grade_item); $grade_item->add_child($grade_grades); $grade_grades->add_child($grade_grade); $gradebook->add_child($letters); $letters->add_child($letter); $gradebook->add_child($grade_settings); $grade_settings->add_child($grade_setting); // Define sources // Add attribute with gradebook calculation freeze date if needed. $attributes = new stdClass(); $gradebookcalculationfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid()); if ($gradebookcalculationfreeze) { $attributes->calculations_freeze = $gradebookcalculationfreeze; } $gradebook_attributes->set_source_array([$attributes]); //Include manual, category and the course grade item $grade_items_sql ="SELECT * FROM {grade_items} WHERE courseid = :courseid AND (itemtype='manual' OR itemtype='course' OR itemtype='category')"; $grade_items_params = array('courseid'=>backup::VAR_COURSEID); $grade_item->set_source_sql($grade_items_sql, $grade_items_params); if ($userinfo) { $grade_grade->set_source_table('grade_grades', array('itemid' => backup::VAR_PARENTID)); } $grade_category_sql = "SELECT gc.*, gi.sortorder FROM {grade_categories} gc JOIN {grade_items} gi ON (gi.iteminstance = gc.id) WHERE gc.courseid = :courseid AND (gi.itemtype='course' OR gi.itemtype='category') ORDER BY gc.parent ASC";//need parent categories before their children $grade_category_params = array('courseid'=>backup::VAR_COURSEID); $grade_category->set_source_sql($grade_category_sql, $grade_category_params); $letter->set_source_table('grade_letters', array('contextid' => backup::VAR_CONTEXTID)); // Set the grade settings source, forcing the inclusion of minmaxtouse if not present. $settings = array(); $rs = $DB->get_recordset('grade_settings', array('courseid' => $this->get_courseid())); foreach ($rs as $record) { $settings[$record->name] = $record; } $rs->close(); if (!isset($settings['minmaxtouse'])) { $settings['minmaxtouse'] = (object) array('name' => 'minmaxtouse', 'value' => $CFG->grade_minmaxtouse); } $grade_setting->set_source_array($settings); // Annotations (both as final as far as they are going to be exported in next steps) $grade_item->annotate_ids('scalefinal', 'scaleid'); // Straight as scalefinal because it's > 0 $grade_item->annotate_ids('outcomefinal', 'outcomeid'); //just in case there are any users not already annotated by the activities $grade_grade->annotate_ids('userfinal', 'userid'); // Return the root element return $gradebook; } } /** * Step in charge of constructing the grade_history.xml file containing the grade histories. */ class backup_grade_history_structure_step extends backup_structure_step { /** * Limit the execution. * * This applies the same logic than the one applied to {@link backup_gradebook_structure_step}, * because we do not want to save the history of items which are not backed up. At least for now. */ protected function execute_condition() { $courseid = $this->get_courseid(); if ($courseid == SITEID) { return false; } return backup_plan_dbops::require_gradebook_backup($courseid, $this->get_backupid()); } protected function define_structure() { // Settings to use. $userinfo = $this->get_setting_value('users'); $history = $this->get_setting_value('grade_histories'); // Create the nested elements. $bookhistory = new backup_nested_element('grade_history'); $grades = new backup_nested_element('grade_grades'); $grade = new backup_nested_element('grade_grade', array('id'), array( 'action', 'oldid', 'source', 'loggeduser', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat', 'timemodified')); // Build the tree. $bookhistory->add_child($grades); $grades->add_child($grade); // This only happens if we are including user info and history. if ($userinfo && $history) { // Only keep the history of grades related to items which have been backed up, The query is // similar (but not identical) to the one used in backup_gradebook_structure_step::define_structure(). $gradesql = "SELECT ggh.* FROM {grade_grades_history} ggh JOIN {grade_items} gi ON ggh.itemid = gi.id WHERE gi.courseid = :courseid AND (gi.itemtype = 'manual' OR gi.itemtype = 'course' OR gi.itemtype = 'category')"; $grade->set_source_sql($gradesql, array('courseid' => backup::VAR_COURSEID)); } // Annotations. (Final annotations as this step is part of the final task). $grade->annotate_ids('scalefinal', 'rawscaleid'); $grade->annotate_ids('userfinal', 'loggeduser'); $grade->annotate_ids('userfinal', 'userid'); $grade->annotate_ids('userfinal', 'usermodified'); // Return the root element. return $bookhistory; } } /** * structure step in charge if constructing the completion.xml file for all the users completion * information in a given activity */ class backup_userscompletion_structure_step extends backup_structure_step { /** * Skip completion on the front page. * @return bool */ protected function execute_condition() { return ($this->get_courseid() != SITEID); } protected function define_structure() { // Define each element separated $completions = new backup_nested_element('completions'); $completion = new backup_nested_element('completion', array('id'), array( 'userid', 'completionstate', 'viewed', 'timemodified')); // Build the tree $completions->add_child($completion); // Define sources $completion->set_source_table('course_modules_completion', array('coursemoduleid' => backup::VAR_MODID)); // Define id annotations $completion->annotate_ids('user', 'userid'); $completionviews = new backup_nested_element('completionviews'); $completionview = new backup_nested_element('completionview', ['id'], ['userid', 'timecreated']); // Build the tree. $completionviews->add_child($completionview); // Define sources. $completionview->set_source_table('course_modules_viewed', ['coursemoduleid' => backup::VAR_MODID]); // Define id annotations. $completionview->annotate_ids('user', 'userid'); $completions->add_child($completionviews); // Return the root element (completions). return $completions; } } /** * structure step in charge of constructing the main groups.xml file for all the groups and * groupings information already annotated */ class backup_groups_structure_step extends backup_structure_step { protected function define_structure() { // To know if we are including users. $userinfo = $this->get_setting_value('users'); // To know if we are including groups and groupings. $groupinfo = $this->get_setting_value('groups'); // Define each element separated $groups = new backup_nested_element('groups'); $group = new backup_nested_element('group', array('id'), array( 'name', 'idnumber', 'description', 'descriptionformat', 'enrolmentkey', 'picture', 'visibility', 'participation', 'timecreated', 'timemodified')); $groupcustomfields = new backup_nested_element('groupcustomfields'); $groupcustomfield = new backup_nested_element('groupcustomfield', ['id'], [ 'shortname', 'type', 'value', 'valueformat', 'valuetrust', 'groupid']); $members = new backup_nested_element('group_members'); $member = new backup_nested_element('group_member', array('id'), array( 'userid', 'timeadded', 'component', 'itemid')); $groupings = new backup_nested_element('groupings'); $grouping = new backup_nested_element('grouping', 'id', array( 'name', 'idnumber', 'description', 'descriptionformat', 'configdata', 'timecreated', 'timemodified')); $groupingcustomfields = new backup_nested_element('groupingcustomfields'); $groupingcustomfield = new backup_nested_element('groupingcustomfield', ['id'], [ 'shortname', 'type', 'value', 'valueformat', 'valuetrust', 'groupingid']); $groupinggroups = new backup_nested_element('grouping_groups'); $groupinggroup = new backup_nested_element('grouping_group', array('id'), array( 'groupid', 'timeadded')); // Build the tree $groups->add_child($group); $groups->add_child($groupcustomfields); $groupcustomfields->add_child($groupcustomfield); $groups->add_child($groupings); $group->add_child($members); $members->add_child($member); $groupings->add_child($grouping); $groupings->add_child($groupingcustomfields); $groupingcustomfields->add_child($groupingcustomfield); $grouping->add_child($groupinggroups); $groupinggroups->add_child($groupinggroup); // Define sources // This only happens if we are including groups/groupings. if ($groupinfo) { $group->set_source_sql(" SELECT g.* FROM {groups} g JOIN {backup_ids_temp} bi ON g.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'groupfinal'", [backup_helper::is_sqlparam($this->get_backupid())] ); $grouping->set_source_sql(" SELECT g.* FROM {groupings} g JOIN {backup_ids_temp} bi ON g.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'groupingfinal'", [backup_helper::is_sqlparam($this->get_backupid())] ); $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID)); // This only happens if we are including users. if ($userinfo) { $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID)); } // Custom fields. if ($this->get_setting_value('customfield')) { $groupcustomfieldarray = $this->get_group_custom_fields_for_backup( $group->get_source_sql(), [$this->get_backupid()] ); $groupcustomfield->set_source_array($groupcustomfieldarray); $groupingcustomfieldarray = $this->get_grouping_custom_fields_for_backup( $grouping->get_source_sql(), [$this->get_backupid()] ); $groupingcustomfield->set_source_array($groupingcustomfieldarray); } } // Define id annotations (as final) $member->annotate_ids('userfinal', 'userid'); // Define file annotations $group->annotate_files('group', 'description', 'id'); $group->annotate_files('group', 'icon', 'id'); $grouping->annotate_files('grouping', 'description', 'id'); // Return the root element (groups) return $groups; } /** * Get custom fields array for group * * @param string $groupsourcesql * @param array $groupsourceparams * @return array */ protected function get_group_custom_fields_for_backup(string $groupsourcesql, array $groupsourceparams): array { global $DB; $handler = \core_group\customfield\group_handler::create(); $fieldsforbackup = []; if ($groups = $DB->get_records_sql($groupsourcesql, $groupsourceparams)) { foreach ($groups as $group) { $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($group->id)); } } return $fieldsforbackup; } /** * Get custom fields array for grouping * * @param string $groupingsourcesql * @param array $groupingsourceparams * @return array */ protected function get_grouping_custom_fields_for_backup(string $groupingsourcesql, array $groupingsourceparams): array { global $DB; $handler = \core_group\customfield\grouping_handler::create(); $fieldsforbackup = []; if ($groupings = $DB->get_records_sql($groupingsourcesql, $groupingsourceparams)) { foreach ($groupings as $grouping) { $fieldsforbackup = array_merge($fieldsforbackup, $handler->get_instance_data_for_backup($grouping->id)); } } return $fieldsforbackup; } } /** * structure step in charge of constructing the main users.xml file for all the users already * annotated (final). Includes custom profile fields, preferences, tags, role assignments and * overrides. */ class backup_users_structure_step extends backup_structure_step { protected function define_structure() { global $CFG; // To know if we are anonymizing users $anonymize = $this->get_setting_value('anonymize'); // To know if we are including role assignments $roleassignments = $this->get_setting_value('role_assignments'); // Define each element separate. $users = new backup_nested_element('users'); // Create the array of user fields by hand, as far as we have various bits to control // anonymize option, password backup, mnethostid... // First, the fields not needing anonymization nor special handling $normalfields = array( 'confirmed', 'policyagreed', 'deleted', 'lang', 'theme', 'timezone', 'firstaccess', 'lastaccess', 'lastlogin', 'currentlogin', 'mailformat', 'maildigest', 'maildisplay', 'autosubscribe', 'trackforums', 'timecreated', 'timemodified', 'trustbitmask'); // Then, the fields potentially needing anonymization $anonfields = array( 'username', 'idnumber', 'email', 'phone1', 'phone2', 'institution', 'department', 'address', 'city', 'country', 'lastip', 'picture', 'description', 'descriptionformat', 'imagealt', 'auth'); $anonfields = array_merge($anonfields, \core_user\fields::get_name_fields()); // Add anonymized fields to $userfields with custom final element foreach ($anonfields as $field) { if ($anonymize) { $userfields[] = new anonymizer_final_element($field); } else { $userfields[] = $field; // No anonymization, normally added } } // mnethosturl requires special handling (custom final element) $userfields[] = new mnethosturl_final_element('mnethosturl'); // password added conditionally if (!empty($CFG->includeuserpasswordsinbackup)) { $userfields[] = 'password'; } // Merge all the fields $userfields = array_merge($userfields, $normalfields); $user = new backup_nested_element('user', array('id', 'contextid'), $userfields); $customfields = new backup_nested_element('custom_fields'); $customfield = new backup_nested_element('custom_field', array('id'), array( 'field_name', 'field_type', 'field_data')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', array('id'), array( 'name', 'rawname')); $preferences = new backup_nested_element('preferences'); $preference = new backup_nested_element('preference', array('id'), array( 'name', 'value')); $roles = new backup_nested_element('roles'); $overrides = new backup_nested_element('role_overrides'); $override = new backup_nested_element('override', array('id'), array( 'roleid', 'capability', 'permission', 'timemodified', 'modifierid')); $assignments = new backup_nested_element('role_assignments'); $assignment = new backup_nested_element('assignment', array('id'), array( 'roleid', 'userid', 'timemodified', 'modifierid', 'component', //TODO: MDL-22793 add itemid here 'sortorder')); // Build the tree $users->add_child($user); $user->add_child($customfields); $customfields->add_child($customfield); $user->add_child($tags); $tags->add_child($tag); $user->add_child($preferences); $preferences->add_child($preference); $user->add_child($roles); $roles->add_child($overrides); $roles->add_child($assignments); $overrides->add_child($override); $assignments->add_child($assignment); // Define sources $user->set_source_sql('SELECT u.*, c.id AS contextid, m.wwwroot AS mnethosturl FROM {user} u JOIN {backup_ids_temp} bi ON bi.itemid = u.id LEFT JOIN {context} c ON c.instanceid = u.id AND c.contextlevel = ' . CONTEXT_USER . ' LEFT JOIN {mnet_host} m ON m.id = u.mnethostid WHERE bi.backupid = ? AND bi.itemname = ?', array( backup_helper::is_sqlparam($this->get_backupid()), backup_helper::is_sqlparam('userfinal'))); // All the rest on information is only added if we arent // in an anonymized backup if (!$anonymize) { $customfield->set_source_sql('SELECT f.id, f.shortname, f.datatype, d.data FROM {user_info_field} f JOIN {user_info_data} d ON d.fieldid = f.id WHERE d.userid = ?', array(backup::VAR_PARENTID)); $customfield->set_source_alias('shortname', 'field_name'); $customfield->set_source_alias('datatype', 'field_type'); $customfield->set_source_alias('data', 'field_data'); $tag->set_source_sql('SELECT t.id, t.name, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemtype = ? AND ti.itemid = ?', array( backup_helper::is_sqlparam('user'), backup::VAR_PARENTID)); $preference->set_source_table('user_preferences', array('userid' => backup::VAR_PARENTID)); $override->set_source_table('role_capabilities', array('contextid' => '/users/user/contextid')); // Assignments only added if specified if ($roleassignments) { $assignment->set_source_table('role_assignments', array('contextid' => '/users/user/contextid')); } // Define id annotations (as final) $override->annotate_ids('rolefinal', 'roleid'); } // Return root element (users) return $users; } } /** * structure step in charge of constructing the block.xml file for one * given block (instance and positions). If the block has custom DB structure * that will go to a separate file (different step defined in block class) */ class backup_block_instance_structure_step extends backup_structure_step { protected function define_structure() { global $DB; // Define each element separated $block = new backup_nested_element('block', array('id', 'contextid', 'version'), array( 'blockname', 'parentcontextid', 'showinsubcontexts', 'pagetypepattern', 'subpagepattern', 'defaultregion', 'defaultweight', 'configdata', 'timecreated', 'timemodified')); $positions = new backup_nested_element('block_positions'); $position = new backup_nested_element('block_position', array('id'), array( 'contextid', 'pagetype', 'subpage', 'visible', 'region', 'weight')); // Build the tree $block->add_child($positions); $positions->add_child($position); // Transform configdata information if needed (process links and friends) $blockrec = $DB->get_record('block_instances', array('id' => $this->task->get_blockid())); if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) { $configdata = array_filter( (array) unserialize_object(base64_decode($blockrec->configdata)), static function($value): bool { return !($value instanceof __PHP_Incomplete_Class); } ); foreach ($configdata as $attribute => $value) { if (in_array($attribute, $attrstotransform)) { $configdata[$attribute] = $this->contenttransformer->process($value); } } $blockrec->configdata = base64_encode(serialize((object)$configdata)); } $blockrec->contextid = $this->task->get_contextid(); // Get the version of the block $blockrec->version = get_config('block_'.$this->task->get_blockname(), 'version'); // Define sources $block->set_source_array(array($blockrec)); $position->set_source_table('block_positions', array('blockinstanceid' => backup::VAR_PARENTID)); // File anotations (for fileareas specified on each block) foreach ($this->task->get_fileareas() as $filearea) { $block->annotate_files('block_' . $this->task->get_blockname(), $filearea, null); } // Return the root element (block) return $block; } } /** * structure step in charge of constructing the logs.xml file for all the log records found * in course. Note that we are sending to backup ALL the log records having cmid = 0. That * includes some records that won't be restoreable (like 'upload', 'calendar'...) but we do * that just in case they become restored some day in the future */ class backup_course_logs_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated $logs = new backup_nested_element('logs'); $log = new backup_nested_element('log', array('id'), array( 'time', 'userid', 'ip', 'module', 'action', 'url', 'info')); // Build the tree $logs->add_child($log); // Define sources (all the records belonging to the course, having cmid = 0) $log->set_source_table('log', array('course' => backup::VAR_COURSEID, 'cmid' => backup_helper::is_sqlparam(0))); // Annotations // NOTE: We don't annotate users from logs as far as they MUST be // always annotated by the course (enrol, ras... whatever) // Return the root element (logs) return $logs; } } /** * structure step in charge of constructing the logs.xml file for all the log records found * in activity */ class backup_activity_logs_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separated $logs = new backup_nested_element('logs'); $log = new backup_nested_element('log', array('id'), array( 'time', 'userid', 'ip', 'module', 'action', 'url', 'info')); // Build the tree $logs->add_child($log); // Define sources $log->set_source_table('log', array('cmid' => backup::VAR_MODID)); // Annotations // NOTE: We don't annotate users from logs as far as they MUST be // always annotated by the activity (true participants). // Return the root element (logs) return $logs; } } /** * Structure step in charge of constructing the logstores.xml file for the course logs. * * This backup step will backup the logs for all the enabled logstore subplugins supporting * it, for logs belonging to the course level. */ class backup_course_logstores_structure_step extends backup_structure_step { protected function define_structure() { // Define the structure of logstores container. $logstores = new backup_nested_element('logstores'); $logstore = new backup_nested_element('logstore'); $logstores->add_child($logstore); // Add the tool_log logstore subplugins information to the logstore element. $this->add_subplugin_structure('logstore', $logstore, true, 'tool', 'log'); return $logstores; } } /** * Structure step in charge of constructing the loglastaccess.xml file for the course logs. * * This backup step will backup the logs of the user_lastaccess table. */ class backup_course_loglastaccess_structure_step extends backup_structure_step { /** * This function creates the structures for the loglastaccess.xml file. * Expected structure would look like this. * <loglastaccesses> * <loglastaccess id=2> * <userid>5</userid> * <timeaccess>1616887341</timeaccess> * </loglastaccess> * </loglastaccesses> * * @return backup_nested_element */ protected function define_structure() { // To know if we are including userinfo. $userinfo = $this->get_setting_value('users'); // Define the structure of logstores container. $lastaccesses = new backup_nested_element('lastaccesses'); $lastaccess = new backup_nested_element('lastaccess', array('id'), array('userid', 'timeaccess')); // Define build tree. $lastaccesses->add_child($lastaccess); // This element should only happen if we are including user info. if ($userinfo) { // Define sources. $lastaccess->set_source_sql(' SELECT id, userid, timeaccess FROM {user_lastaccess} WHERE courseid = ?', array(backup::VAR_COURSEID)); // Define userid annotation to user. $lastaccess->annotate_ids('user', 'userid'); } // Return the root element (lastaccessess). return $lastaccesses; } } /** * Structure step in charge of constructing the logstores.xml file for the activity logs. * * Note: Activity structure is completely equivalent to the course one, so just extend it. */ class backup_activity_logstores_structure_step extends backup_course_logstores_structure_step { } /** * Course competencies backup structure step. */ class backup_course_competencies_structure_step extends backup_structure_step { protected function define_structure() { $userinfo = $this->get_setting_value('users'); $wrapper = new backup_nested_element('course_competencies'); $settings = new backup_nested_element('settings', array('id'), array('pushratingstouserplans')); $wrapper->add_child($settings); $sql = 'SELECT s.pushratingstouserplans FROM {' . \core_competency\course_competency_settings::TABLE . '} s WHERE s.courseid = :courseid'; $settings->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID)); $competencies = new backup_nested_element('competencies'); $wrapper->add_child($competencies); $competency = new backup_nested_element('competency', null, array('id', 'idnumber', 'ruleoutcome', 'sortorder', 'frameworkid', 'frameworkidnumber')); $competencies->add_child($competency); $sql = 'SELECT c.id, c.idnumber, cc.ruleoutcome, cc.sortorder, f.id AS frameworkid, f.idnumber AS frameworkidnumber FROM {' . \core_competency\course_competency::TABLE . '} cc JOIN {' . \core_competency\competency::TABLE . '} c ON c.id = cc.competencyid JOIN {' . \core_competency\competency_framework::TABLE . '} f ON f.id = c.competencyframeworkid WHERE cc.courseid = :courseid ORDER BY cc.sortorder'; $competency->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID)); $usercomps = new backup_nested_element('user_competencies'); $wrapper->add_child($usercomps); if ($userinfo) { $usercomp = new backup_nested_element('user_competency', null, array('userid', 'competencyid', 'proficiency', 'grade')); $usercomps->add_child($usercomp); $sql = 'SELECT ucc.userid, ucc.competencyid, ucc.proficiency, ucc.grade FROM {' . \core_competency\user_competency_course::TABLE . '} ucc WHERE ucc.courseid = :courseid AND ucc.grade IS NOT NULL'; $usercomp->set_source_sql($sql, array('courseid' => backup::VAR_COURSEID)); $usercomp->annotate_ids('user', 'userid'); } return $wrapper; } /** * Execute conditions. * * @return bool */ protected function execute_condition() { // Do not execute if competencies are not included. if (!$this->get_setting_value('competencies')) { return false; } return true; } } /** * Activity competencies backup structure step. */ class backup_activity_competencies_structure_step extends backup_structure_step { protected function define_structure() { $wrapper = new backup_nested_element('course_module_competencies'); $competencies = new backup_nested_element('competencies'); $wrapper->add_child($competencies); $competency = new backup_nested_element('competency', null, array('idnumber', 'ruleoutcome', 'sortorder', 'frameworkidnumber', 'overridegrade')); $competencies->add_child($competency); $sql = 'SELECT c.idnumber, cmc.ruleoutcome, cmc.overridegrade, cmc.sortorder, f.idnumber AS frameworkidnumber FROM {' . \core_competency\course_module_competency::TABLE . '} cmc JOIN {' . \core_competency\competency::TABLE . '} c ON c.id = cmc.competencyid JOIN {' . \core_competency\competency_framework::TABLE . '} f ON f.id = c.competencyframeworkid WHERE cmc.cmid = :coursemoduleid ORDER BY cmc.sortorder'; $competency->set_source_sql($sql, array('coursemoduleid' => backup::VAR_MODID)); return $wrapper; } /** * Execute conditions. * * @return bool */ protected function execute_condition() { // Do not execute if competencies are not included. if (!$this->get_setting_value('competencies')) { return false; } return true; } } /** * structure in charge of constructing the inforef.xml file for all the items we want * to have referenced there (users, roles, files...) */ class backup_inforef_structure_step extends backup_structure_step { protected function define_structure() { // Items we want to include in the inforef file. $items = backup_helper::get_inforef_itemnames(); // Build the tree $inforef = new backup_nested_element('inforef'); // For each item, conditionally, if there are already records, build element foreach ($items as $itemname) { if (backup_structure_dbops::annotations_exist($this->get_backupid(), $itemname)) { $elementroot = new backup_nested_element($itemname . 'ref'); $element = new backup_nested_element($itemname, array(), array('id')); $inforef->add_child($elementroot); $elementroot->add_child($element); $element->set_source_sql(" SELECT itemid AS id FROM {backup_ids_temp} WHERE backupid = ? AND itemname = ?", array(backup::VAR_BACKUPID, backup_helper::is_sqlparam($itemname))); } } // We don't annotate anything there, but rely in the next step // (move_inforef_annotations_to_final) that will change all the // already saved 'inforref' entries to their 'final' annotations. return $inforef; } } /** * This step will get all the annotations already processed to inforef.xml file and * transform them into 'final' annotations. */ class move_inforef_annotations_to_final extends backup_execution_step { protected function define_execution() { // Items we want to include in the inforef file $items = backup_helper::get_inforef_itemnames(); $progress = $this->task->get_progress(); $progress->start_progress($this->get_name(), count($items)); $done = 1; foreach ($items as $itemname) { // Delegate to dbops backup_structure_dbops::move_annotations_to_final($this->get_backupid(), $itemname, $progress); $progress->progress($done++); } $progress->end_progress(); } } /** * structure in charge of constructing the files.xml file with all the * annotated (final) files along the process. At, the same time, and * using one specialised nested_element, will copy them form moodle storage * to backup storage */ class backup_final_files_structure_step extends backup_structure_step { protected function define_structure() { // Define elements $files = new backup_nested_element('files'); $file = new file_nested_element('file', array('id'), array( 'contenthash', 'contextid', 'component', 'filearea', 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'timecreated', 'timemodified', 'source', 'author', 'license', 'sortorder', 'repositorytype', 'repositoryid', 'reference')); // Build the tree $files->add_child($file); // Define sources $file->set_source_sql("SELECT f.*, r.type AS repositorytype, fr.repositoryid, fr.reference FROM {files} f LEFT JOIN {files_reference} fr ON fr.id = f.referencefileid LEFT JOIN {repository_instances} ri ON ri.id = fr.repositoryid LEFT JOIN {repository} r ON r.id = ri.typeid JOIN {backup_ids_temp} bi ON f.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'filefinal'", array(backup::VAR_BACKUPID)); return $files; } } /** * Structure step in charge of creating the main moodle_backup.xml file * where all the information related to the backup, settings, license and * other information needed on restore is added*/ class backup_main_structure_step extends backup_structure_step { protected function define_structure() { global $CFG; $info = array(); $info['name'] = $this->get_setting_value('filename'); $info['moodle_version'] = $CFG->version; $info['moodle_release'] = $CFG->release; $info['backup_version'] = $CFG->backup_version; $info['backup_release'] = $CFG->backup_release; $info['backup_date'] = time(); $info['backup_uniqueid']= $this->get_backupid(); $info['mnet_remoteusers']=backup_controller_dbops::backup_includes_mnet_remote_users($this->get_backupid()); $info['include_files'] = backup_controller_dbops::backup_includes_files($this->get_backupid()); $info['include_file_references_to_external_content'] = backup_controller_dbops::backup_includes_file_references($this->get_backupid()); $info['original_wwwroot']=$CFG->wwwroot; $info['original_site_identifier_hash'] = md5(get_site_identifier()); $info['original_course_id'] = $this->get_courseid(); $originalcourseinfo = backup_controller_dbops::backup_get_original_course_info($this->get_courseid()); $info['original_course_format'] = $originalcourseinfo->format; $info['original_course_fullname'] = $originalcourseinfo->fullname; $info['original_course_shortname'] = $originalcourseinfo->shortname; $info['original_course_startdate'] = $originalcourseinfo->startdate; $info['original_course_enddate'] = $originalcourseinfo->enddate; $info['original_course_contextid'] = context_course::instance($this->get_courseid())->id; $info['original_system_contextid'] = context_system::instance()->id; // Get more information from controller list($dinfo, $cinfo, $sinfo) = backup_controller_dbops::get_moodle_backup_information( $this->get_backupid(), $this->get_task()->get_progress()); // Define elements $moodle_backup = new backup_nested_element('moodle_backup'); $information = new backup_nested_element('information', null, array( 'name', 'moodle_version', 'moodle_release', 'backup_version', 'backup_release', 'backup_date', 'mnet_remoteusers', 'include_files', 'include_file_references_to_external_content', 'original_wwwroot', 'original_site_identifier_hash', 'original_course_id', 'original_course_format', 'original_course_fullname', 'original_course_shortname', 'original_course_startdate', 'original_course_enddate', 'original_course_contextid', 'original_system_contextid')); $details = new backup_nested_element('details'); $detail = new backup_nested_element('detail', array('backup_id'), array( 'type', 'format', 'interactive', 'mode', 'execution', 'executiontime')); $contents = new backup_nested_element('contents'); $activities = new backup_nested_element('activities'); $activity = new backup_nested_element( 'activity', null, ['moduleid', 'sectionid', 'modulename', 'title', 'directory', 'insubsection'] ); $sections = new backup_nested_element('sections'); $section = new backup_nested_element( 'section', null, ['sectionid', 'title', 'directory', 'parentcmid', 'modname'] ); $course = new backup_nested_element('course', null, array( 'courseid', 'title', 'directory')); $settings = new backup_nested_element('settings'); $setting = new backup_nested_element('setting', null, array( 'level', 'section', 'activity', 'name', 'value')); // Build the tree $moodle_backup->add_child($information); $information->add_child($details); $details->add_child($detail); $information->add_child($contents); if (!empty($cinfo['activities'])) { $contents->add_child($activities); $activities->add_child($activity); } if (!empty($cinfo['sections'])) { $contents->add_child($sections); $sections->add_child($section); } if (!empty($cinfo['course'])) { $contents->add_child($course); } $information->add_child($settings); $settings->add_child($setting); // Set the sources $information->set_source_array(array((object)$info)); $detail->set_source_array($dinfo); $activity->set_source_array($cinfo['activities']); $section->set_source_array($cinfo['sections']); $course->set_source_array($cinfo['course']); $setting->set_source_array($sinfo); // Prepare some information to be sent to main moodle_backup.xml file return $moodle_backup; } } /** * Execution step that will generate the final zip (.mbz) file with all the contents */ class backup_zip_contents extends backup_execution_step implements file_progress { /** * @var bool True if we have started tracking progress */ protected $startedprogress; protected function define_execution() { // Get basepath $basepath = $this->get_basepath(); // Get the list of files in directory $filestemp = get_directory_list($basepath, '', false, true, true); $files = array(); foreach ($filestemp as $file) { // Add zip paths and fs paths to all them $files[$file] = $basepath . '/' . $file; } // Add the log file if exists $logfilepath = $basepath . '.log'; if (file_exists($logfilepath)) { $files['moodle_backup.log'] = $logfilepath; } // Calculate the zip fullpath (in OS temp area it's always backup.mbz) $zipfile = $basepath . '/backup.mbz'; // Get the zip packer $zippacker = get_file_packer('application/vnd.moodle.backup'); // Track overall progress for the 2 long-running steps (archive to // pathname, get backup information). $reporter = $this->task->get_progress(); $reporter->start_progress('backup_zip_contents', 2); // Zip files $result = $zippacker->archive_to_pathname($files, $zipfile, true, $this); // If any sub-progress happened, end it. if ($this->startedprogress) { $this->task->get_progress()->end_progress(); $this->startedprogress = false; } else { // No progress was reported, manually move it on to the next overall task. $reporter->progress(1); } // Something went wrong. if ($result === false) { @unlink($zipfile); throw new backup_step_exception('error_zip_packing', '', 'An error was encountered while trying to generate backup zip'); } // Read to make sure it is a valid backup. Refer MDL-37877 . Delete it, if found not to be valid. try { backup_general_helper::get_backup_information_from_mbz($zipfile, $this); } catch (backup_helper_exception $e) { @unlink($zipfile); throw new backup_step_exception('error_zip_packing', '', $e->debuginfo); } // If any sub-progress happened, end it. if ($this->startedprogress) { $this->task->get_progress()->end_progress(); $this->startedprogress = false; } else { $reporter->progress(2); } $reporter->end_progress(); } /** * Implementation for file_progress interface to display unzip progress. * * @param int $progress Current progress * @param int $max Max value */ public function progress($progress = file_progress::INDETERMINATE, $max = file_progress::INDETERMINATE) { $reporter = $this->task->get_progress(); // Start tracking progress if necessary. if (!$this->startedprogress) { $reporter->start_progress('extract_file_to_dir', ($max == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $max); $this->startedprogress = true; } // Pass progress through to whatever handles it. $reporter->progress(($progress == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $progress); } } /** * This step will send the generated backup file to its final destination */ class backup_store_backup_file extends backup_execution_step { protected function define_execution() { // Get basepath $basepath = $this->get_basepath(); // Calculate the zip fullpath (in OS temp area it's always backup.mbz) $zipfile = $basepath . '/backup.mbz'; $has_file_references = backup_controller_dbops::backup_includes_file_references($this->get_backupid()); // Perform storage and return it (TODO: shouldn't be array but proper result object) return array( 'backup_destination' => backup_helper::store_backup_file($this->get_backupid(), $zipfile, $this->task->get_progress()), 'include_file_references_to_external_content' => $has_file_references ); } } /** * This step will search for all the activity (not calculations, categories nor aggregations) grade items * and put them to the backup_ids tables, to be used later as base to backup them */ class backup_activity_grade_items_to_ids extends backup_execution_step { protected function define_execution() { // Fetch all activity grade items if ($items = grade_item::fetch_all(array( 'itemtype' => 'mod', 'itemmodule' => $this->task->get_modulename(), 'iteminstance' => $this->task->get_activityid(), 'courseid' => $this->task->get_courseid()))) { // Annotate them in backup_ids foreach ($items as $item) { backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), 'grade_item', $item->id); } } } } /** * This step allows enrol plugins to annotate custom fields. * * @package core_backup * @copyright 2014 University of Wisconsin * @author Matt Petro * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_enrolments_execution_step extends backup_execution_step { /** * Function that will contain all the code to be executed. */ protected function define_execution() { global $DB; $plugins = enrol_get_plugins(true); $enrols = $DB->get_records('enrol', array( 'courseid' => $this->task->get_courseid())); // Allow each enrol plugin to add annotations. foreach ($enrols as $enrol) { if (isset($plugins[$enrol->enrol])) { $plugins[$enrol->enrol]->backup_annotate_custom_fields($this, $enrol); } } } /** * Annotate a single name/id pair. * This can be called from {@link enrol_plugin::backup_annotate_custom_fields()}. * * @param string $itemname * @param int $itemid */ public function annotate_id($itemname, $itemid) { backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), $itemname, $itemid); } } /** * This step will annotate all the groups and groupings belonging to the course */ class backup_annotate_course_groups_and_groupings extends backup_execution_step { protected function define_execution() { global $DB; // Get all the course groups if ($groups = $DB->get_records('groups', array( 'courseid' => $this->task->get_courseid()))) { foreach ($groups as $group) { backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), 'group', $group->id); } } // Get all the course groupings if ($groupings = $DB->get_records('groupings', array( 'courseid' => $this->task->get_courseid()))) { foreach ($groupings as $grouping) { backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), 'grouping', $grouping->id); } } } } /** * This step will annotate all the groups belonging to already annotated groupings */ class backup_annotate_groups_from_groupings extends backup_execution_step { protected function define_execution() { global $DB; // Fetch all the annotated groupings if ($groupings = $DB->get_records('backup_ids_temp', array( 'backupid' => $this->get_backupid(), 'itemname' => 'grouping'))) { foreach ($groupings as $grouping) { if ($groups = $DB->get_records('groupings_groups', array( 'groupingid' => $grouping->itemid))) { foreach ($groups as $group) { backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), 'group', $group->groupid); } } } } } } /** * This step will annotate all the scales belonging to already annotated outcomes */ class backup_annotate_scales_from_outcomes extends backup_execution_step { protected function define_execution() { global $DB; // Fetch all the annotated outcomes if ($outcomes = $DB->get_records('backup_ids_temp', array( 'backupid' => $this->get_backupid(), 'itemname' => 'outcome'))) { foreach ($outcomes as $outcome) { if ($scale = $DB->get_record('grade_outcomes', array( 'id' => $outcome->itemid))) { // Annotate as scalefinal because it's > 0 backup_structure_dbops::insert_backup_ids_record($this->get_backupid(), 'scalefinal', $scale->scaleid); } } } } } /** * This step will generate all the file annotations for the already * annotated (final) question_categories. It calculates the different * contexts that are being backup and, annotates all the files * on every context belonging to the "question" component. As far as * we are always including *complete* question banks it is safe and * optimal to do that in this (one pass) way */ class backup_annotate_all_question_files extends backup_execution_step { protected function define_execution() { global $DB; // Get all the different contexts for the final question_categories // annotated along the whole backup $rs = $DB->get_recordset_sql("SELECT DISTINCT qc.contextid FROM {question_categories} qc JOIN {backup_ids_temp} bi ON bi.itemid = qc.id WHERE bi.backupid = ? AND bi.itemname = 'question_categoryfinal'", array($this->get_backupid())); // To know about qtype specific components/fileareas $components = backup_qtype_plugin::get_components_and_fileareas(); $progress = $this->task->get_progress(); $progress->start_progress($this->get_name()); // Let's loop foreach($rs as $record) { // Backup all the file areas the are managed by the core question component. // That is, by the question_type base class. In particular, we don't want // to include files belonging to responses here. backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'partiallycorrectfeedback', null, $progress); backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null, $progress); // For files belonging to question types, we make the leap of faith that // all the files belonging to the question type are part of the question definition, // so we can just backup all the files in bulk, without specifying each // file area name separately. foreach ($components as $component => $fileareas) { backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null, $progress); } } $progress->end_progress(); $rs->close(); } } /** * structure step in charge of constructing the questions.xml file for all the * question categories and questions required by the backup * and letters related to one activity. */ class backup_questions_structure_step extends backup_structure_step { protected function define_structure() { // Define each element separately. $qcategories = new backup_nested_element('question_categories'); $qcategory = new backup_nested_element('question_category', ['id'], [ 'name', 'contextid', 'contextlevel', 'contextinstanceid', 'info', 'infoformat', 'stamp', 'parent', 'sortorder', 'idnumber', ]); $questionbankentries = new backup_nested_element('question_bank_entries'); $questionbankentry = new backup_nested_element('question_bank_entry', ['id'], [ 'questioncategoryid', 'idnumber', 'ownerid', ]); $questionversions = new backup_nested_element('question_version'); $questionverion = new backup_nested_element('question_versions', ['id'], ['version', 'status']); $questions = new backup_nested_element('questions'); $question = new backup_nested_element('question', ['id'], [ 'parent', 'name', 'questiontext', 'questiontextformat', 'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype', 'length', 'stamp', 'timecreated', 'timemodified', 'createdby', 'modifiedby', ]); // Attach qtype plugin structure to $question element, only one allowed. $this->add_plugin_structure('qtype', $question, false); // Attach qbank plugin stucture to $question element, multiple allowed. $this->add_plugin_structure('qbank', $question, true); // attach local plugin stucture to $question element, multiple allowed $this->add_plugin_structure('local', $question, true); $qhints = new backup_nested_element('question_hints'); $qhint = new backup_nested_element('question_hint', ['id'], [ 'hint', 'hintformat', 'shownumcorrect', 'clearwrong', 'options', ]); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', ['id', 'contextid'], ['name', 'rawname']); // Build the initial tree. $qcategories->add_child($qcategory); $qcategory->add_child($questionbankentries); $questionbankentries->add_child($questionbankentry); $questionbankentry->add_child($questionversions); $questionversions->add_child($questionverion); $questionverion->add_child($questions); $questions->add_child($question); $question->add_child($qhints); $qhints->add_child($qhint); // Add question tags. $question->add_child($tags); $tags->add_child($tag); $qcategory->set_source_sql(" SELECT gc.*, contextlevel, instanceid AS contextinstanceid FROM {question_categories} gc JOIN {backup_ids_temp} bi ON bi.itemid = gc.id JOIN {context} co ON co.id = gc.contextid WHERE bi.backupid = ? AND bi.itemname = 'question_categoryfinal'", [backup::VAR_BACKUPID]); $questionbankentry->set_source_table('question_bank_entries', ['questioncategoryid' => backup::VAR_PARENTID]); $questionverion->set_source_table('question_versions', ['questionbankentryid' => backup::VAR_PARENTID]); $question->set_source_sql(' SELECT q.* FROM {question} q JOIN {question_versions} qv ON qv.questionid = q.id JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid WHERE qv.id = ?', [backup::VAR_PARENTID]); $qhint->set_source_sql(' SELECT * FROM {question_hints} WHERE questionid = :questionid ORDER BY id', ['questionid' => backup::VAR_PARENTID]); $tag->set_source_sql("SELECT t.id, ti.contextid, t.name, t.rawname FROM {tag} t JOIN {tag_instance} ti ON ti.tagid = t.id WHERE ti.itemid = ? AND ti.itemtype = 'question' AND ti.component = 'core_question'", [backup::VAR_PARENTID]); // Don't need to annotate ids nor files. // ...(already done by {@see backup_annotate_all_question_files()}. return $qcategories; } } /** * This step will generate all the file annotations for the already * annotated (final) users. Need to do this here because each user * has its own context and structure tasks only are able to handle * one context. Also, this step will guarantee that every user has * its context created (req for other steps) */ class backup_annotate_all_user_files extends backup_execution_step { protected function define_execution() { global $DB; // List of fileareas we are going to annotate $fileareas = array('profile', 'icon'); // Fetch all annotated (final) users $rs = $DB->get_recordset('backup_ids_temp', array( 'backupid' => $this->get_backupid(), 'itemname' => 'userfinal')); $progress = $this->task->get_progress(); $progress->start_progress($this->get_name()); foreach ($rs as $record) { $userid = $record->itemid; $userctx = context_user::instance($userid, IGNORE_MISSING); if (!$userctx) { continue; // User has not context, sure it's a deleted user, so cannot have files } // Proceed with every user filearea foreach ($fileareas as $filearea) { // We don't need to specify itemid ($userid - 5th param) as far as by // context we can get all the associated files. See MDL-22092 backup_structure_dbops::annotate_files($this->get_backupid(), $userctx->id, 'user', $filearea, null); $progress->progress(); } } $progress->end_progress(); $rs->close(); } } /** * Defines the backup step for advanced grading methods attached to the activity module */ class backup_activity_grading_structure_step extends backup_structure_step { /** * Include the grading.xml only if the module supports advanced grading */ protected function execute_condition() { // No grades on the front page. if ($this->get_courseid() == SITEID) { return false; } return plugin_supports('mod', $this->get_task()->get_modulename(), FEATURE_ADVANCED_GRADING, false); } /** * Declares the gradable areas structures and data sources */ protected function define_structure() { // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define the elements $areas = new backup_nested_element('areas'); $area = new backup_nested_element('area', array('id'), array( 'areaname', 'activemethod')); $definitions = new backup_nested_element('definitions'); $definition = new backup_nested_element('definition', array('id'), array( 'method', 'name', 'description', 'descriptionformat', 'status', 'timecreated', 'timemodified', 'options')); $instances = new backup_nested_element('instances'); $instance = new backup_nested_element('instance', array('id'), array( 'raterid', 'itemid', 'rawgrade', 'status', 'feedback', 'feedbackformat', 'timemodified')); // Build the tree including the method specific structures // (beware - the order of how gradingform plugins structures are attached is important) $areas->add_child($area); // attach local plugin stucture to $area element, multiple allowed $this->add_plugin_structure('local', $area, true); $area->add_child($definitions); $definitions->add_child($definition); $this->add_plugin_structure('gradingform', $definition, true); // attach local plugin stucture to $definition element, multiple allowed $this->add_plugin_structure('local', $definition, true); $definition->add_child($instances); $instances->add_child($instance); $this->add_plugin_structure('gradingform', $instance, false); // attach local plugin stucture to $instance element, multiple allowed $this->add_plugin_structure('local', $instance, true); // Define data sources $area->set_source_table('grading_areas', array('contextid' => backup::VAR_CONTEXTID, 'component' => array('sqlparam' => 'mod_'.$this->get_task()->get_modulename()))); $definition->set_source_table('grading_definitions', array('areaid' => backup::VAR_PARENTID)); if ($userinfo) { $instance->set_source_table('grading_instances', array('definitionid' => backup::VAR_PARENTID)); } // Annotate references $definition->annotate_files('grading', 'description', 'id'); $instance->annotate_ids('user', 'raterid'); // Return the root element return $areas; } } /** * structure step in charge of constructing the grades.xml file for all the grade items * and letters related to one activity */ class backup_activity_grades_structure_step extends backup_structure_step { /** * No grades on the front page. * @return bool */ protected function execute_condition() { return ($this->get_courseid() != SITEID); } protected function define_structure() { global $CFG; require_once($CFG->libdir . '/grade/constants.php'); // To know if we are including userinfo $userinfo = $this->get_setting_value('userinfo'); // Define each element separated $book = new backup_nested_element('activity_gradebook'); $items = new backup_nested_element('grade_items'); $item = new backup_nested_element('grade_item', array('id'), array( 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin', 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef', 'aggregationcoef2', 'weightoverride', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated', 'timemodified')); $grades = new backup_nested_element('grade_grades'); $grade = new backup_nested_element('grade_grade', array('id'), array( 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat', 'timecreated', 'timemodified', 'aggregationstatus', 'aggregationweight')); $letters = new backup_nested_element('grade_letters'); $letter = new backup_nested_element('grade_letter', 'id', array( 'lowerboundary', 'letter')); // Build the tree $book->add_child($items); $items->add_child($item); $item->add_child($grades); $grades->add_child($grade); $book->add_child($letters); $letters->add_child($letter); // Define sources $item->set_source_sql("SELECT gi.* FROM {grade_items} gi JOIN {backup_ids_temp} bi ON gi.id = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'grade_item'", array(backup::VAR_BACKUPID)); // This only happens if we are including user info if ($userinfo) { $grade->set_source_table('grade_grades', array('itemid' => backup::VAR_PARENTID)); $grade->annotate_files(GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, 'id'); } $letter->set_source_table('grade_letters', array('contextid' => backup::VAR_CONTEXTID)); // Annotations $item->annotate_ids('scalefinal', 'scaleid'); // Straight as scalefinal because it's > 0 $item->annotate_ids('outcome', 'outcomeid'); $grade->annotate_ids('user', 'userid'); $grade->annotate_ids('user', 'usermodified'); // Return the root element (book) return $book; } } /** * Structure step in charge of constructing the grade history of an activity. * * This step is added to the task regardless of the setting 'grade_histories'. * The reason is to allow for a more flexible step in case the logic needs to be * split accross different settings to control the history of items and/or grades. */ class backup_activity_grade_history_structure_step extends backup_structure_step { /** * No grades on the front page. * @return bool */ protected function execute_condition() { return ($this->get_courseid() != SITEID); } protected function define_structure() { global $CFG; require_once($CFG->libdir . '/grade/constants.php'); // Settings to use. $userinfo = $this->get_setting_value('userinfo'); $history = $this->get_setting_value('grade_histories'); // Create the nested elements. $bookhistory = new backup_nested_element('grade_history'); $grades = new backup_nested_element('grade_grades'); $grade = new backup_nested_element('grade_grade', array('id'), array( 'action', 'oldid', 'source', 'loggeduser', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin', 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked', 'locktime', 'exported', 'overridden', 'excluded', 'feedback', 'feedbackformat', 'information', 'informationformat', 'timemodified')); // Build the tree. $bookhistory->add_child($grades); $grades->add_child($grade); // This only happens if we are including user info and history. if ($userinfo && $history) { // Define sources. Only select the history related to existing activity items. $grade->set_source_sql("SELECT ggh.* FROM {grade_grades_history} ggh JOIN {backup_ids_temp} bi ON ggh.itemid = bi.itemid WHERE bi.backupid = ? AND bi.itemname = 'grade_item'", array(backup::VAR_BACKUPID)); $grade->annotate_files(GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA, 'id'); } // Annotations. $grade->annotate_ids('scalefinal', 'rawscaleid'); // Straight as scalefinal because it's > 0. $grade->annotate_ids('user', 'loggeduser'); $grade->annotate_ids('user', 'userid'); $grade->annotate_ids('user', 'usermodified'); // Return the root element. return $bookhistory; } } /** * Backups up the course completion information for the course. */ class backup_course_completion_structure_step extends backup_structure_step { protected function execute_condition() { // No completion on front page. if ($this->get_courseid() == SITEID) { return false; } // Check that all activities have been included if ($this->task->is_excluding_activities()) { return false; } return true; } /** * The structure of the course completion backup * * @return backup_nested_element */ protected function define_structure() { // To know if we are including user completion info $userinfo = $this->get_setting_value('userscompletion'); $cc = new backup_nested_element('course_completion'); $criteria = new backup_nested_element('course_completion_criteria', array('id'), array( 'course', 'criteriatype', 'module', 'moduleinstance', 'courseinstanceshortname', 'enrolperiod', 'timeend', 'gradepass', 'role', 'roleshortname' )); $criteriacompletions = new backup_nested_element('course_completion_crit_completions'); $criteriacomplete = new backup_nested_element('course_completion_crit_compl', array('id'), array( 'criteriaid', 'userid', 'gradefinal', 'unenrolled', 'timecompleted' )); $coursecompletions = new backup_nested_element('course_completions', array('id'), array( 'userid', 'course', 'timeenrolled', 'timestarted', 'timecompleted', 'reaggregate' )); $aggregatemethod = new backup_nested_element('course_completion_aggr_methd', array('id'), array( 'course','criteriatype','method','value' )); $cc->add_child($criteria); $criteria->add_child($criteriacompletions); $criteriacompletions->add_child($criteriacomplete); $cc->add_child($coursecompletions); $cc->add_child($aggregatemethod); // We need some extra data for the restore. // - courseinstances shortname rather than an ID. // - roleshortname in case restoring on a different site. $sourcesql = "SELECT ccc.*, c.shortname AS courseinstanceshortname, r.shortname AS roleshortname FROM {course_completion_criteria} ccc LEFT JOIN {course} c ON c.id = ccc.courseinstance LEFT JOIN {role} r ON r.id = ccc.role WHERE ccc.course = ?"; $criteria->set_source_sql($sourcesql, array(backup::VAR_COURSEID)); $aggregatemethod->set_source_table('course_completion_aggr_methd', array('course' => backup::VAR_COURSEID)); if ($userinfo) { $criteriacomplete->set_source_table('course_completion_crit_compl', array('criteriaid' => backup::VAR_PARENTID)); $coursecompletions->set_source_table('course_completions', array('course' => backup::VAR_COURSEID)); } $criteria->annotate_ids('role', 'role'); $criteriacomplete->annotate_ids('user', 'userid'); $coursecompletions->annotate_ids('user', 'userid'); return $cc; } } /** * Backup completion defaults for each module type. * * @package core_backup * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_completion_defaults_structure_step extends backup_structure_step { /** * To conditionally decide if one step will be executed or no */ protected function execute_condition() { // No completion on front page. if ($this->get_courseid() == SITEID) { return false; } return true; } /** * The structure of the course completion backup * * @return backup_nested_element */ protected function define_structure() { $cc = new backup_nested_element('course_completion_defaults'); $defaults = new backup_nested_element('course_completion_default', array('id'), array( 'modulename', 'completion', 'completionview', 'completionusegrade', 'completionpassgrade', 'completionexpected', 'customrules' )); // Use module name instead of module id so we can insert into another site later. $sourcesql = "SELECT d.id, m.name as modulename, d.completion, d.completionview, d.completionusegrade, d.completionpassgrade, d.completionexpected, d.customrules FROM {course_completion_defaults} d join {modules} m on d.module = m.id WHERE d.course = ?"; $defaults->set_source_sql($sourcesql, array(backup::VAR_COURSEID)); $cc->add_child($defaults); return $cc; } } /** * Structure step in charge of constructing the contentbank.xml file for all the contents found in a given context */ class backup_contentbankcontent_structure_step extends backup_structure_step { /** * Define structure for content bank step */ protected function define_structure() { // Define each element separated. $contents = new backup_nested_element('contents'); $content = new backup_nested_element('content', ['id'], [ 'name', 'contenttype', 'instanceid', 'configdata', 'usercreated', 'usermodified', 'timecreated', 'timemodified']); // Build the tree. $contents->add_child($content); // Define sources. $content->set_source_table('contentbank_content', ['contextid' => backup::VAR_CONTEXTID]); // Define annotations. $content->annotate_ids('user', 'usercreated'); $content->annotate_ids('user', 'usermodified'); $content->annotate_files('contentbank', 'public', 'id'); // Return the root element (contents). return $contents; } } /** * Structure step in charge of constructing the xapistate.xml file for all the xAPI states found in a given context. */ class backup_xapistate_structure_step extends backup_structure_step { /** * Define structure for content bank step */ protected function define_structure() { // Define each element separated. $states = new backup_nested_element('states'); $state = new backup_nested_element( 'state', ['id'], ['component', 'userid', 'itemid', 'stateid', 'statedata', 'registration', 'timecreated', 'timemodified'] ); // Build the tree. $states->add_child($state); // Define sources. $state->set_source_table('xapi_states', ['itemid' => backup::VAR_CONTEXTID]); // Define annotations. $state->annotate_ids('user', 'userid'); // Return the root element (contents). return $states; } } moodle2/backup_plugin.class.php 0000644 00000007563 15215711721 0012562 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class implementing the plugins support for moodle2 backups * * TODO: Finish phpdocs */ abstract class backup_plugin { /** @var string */ protected $plugintype; /** @var string */ protected $pluginname; /** @var string */ protected $connectionpoint; /** @var backup_optigroup_element */ protected $optigroup; // Optigroup, parent of all optigroup elements /** @var backup_structure_step */ protected $step; /** @var backup_course_task|backup_activity_task */ protected $task; /** * backup_plugin constructor. * * @param string $plugintype * @param string $pluginname * @param backup_optigroup_element $optigroup * @param backup_structure_step $step */ public function __construct($plugintype, $pluginname, $optigroup, $step) { $this->plugintype = $plugintype; $this->pluginname = $pluginname; $this->optigroup = $optigroup; $this->connectionpoint = ''; $this->step = $step; $this->task = $step->get_task(); } public function define_plugin_structure($connectionpoint) { $this->connectionpoint = $connectionpoint; $methodname = 'define_' . $connectionpoint . '_plugin_structure'; if (method_exists($this, $methodname)) { $this->$methodname(); } } // Protected API starts here // backup_step/structure_step/task wrappers /** * Returns the value of one (task/plan) setting */ protected function get_setting_value($name) { if (is_null($this->task)) { throw new backup_step_exception('not_specified_backup_task'); } return $this->task->get_setting_value($name); } // end of backup_step/structure_step/task wrappers /** * Factory method that will return one backup_plugin_element (backup_optigroup_element) * with its name automatically calculated, based one the plugin being handled (type, name) */ protected function get_plugin_element($final_elements = null, $conditionparam = null, $conditionvalue = null) { // Something exclusive for this backup_plugin_element (backup_optigroup_element) // because it hasn't XML representation $name = 'optigroup_' . $this->plugintype . '_' . $this->pluginname . '_' . $this->connectionpoint; $optigroup_element = new backup_plugin_element($name, $final_elements, $conditionparam, $conditionvalue); $this->optigroup->add_child($optigroup_element); // Add optigroup_element to stay connected since beginning return $optigroup_element; } /** * Simple helper function that suggests one name for the main nested element in plugins * It's not mandatory to use it but recommended ;-) */ protected function get_recommended_name() { return 'plugin_' . $this->plugintype . '_' . $this->pluginname . '_' . $this->connectionpoint; } } moodle2/restore_tool_plugin.class.php 0000644 00000002372 15215711721 0014026 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin tool restore plugin base. * * @package core_backup * @subpackage moodle2 * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Admin tool restore plugin base class. * * @package core_backup * @subpackage moodle2 * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_tool_plugin extends restore_plugin { // Use default parent behaviour. } moodle2/backup_section_task.class.php 0000644 00000025210 15215711721 0013737 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_section_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * section task that provides all the properties and common steps to be performed * when one section is being backup * * TODO: Finish phpdocs */ class backup_section_task extends backup_task { protected $sectionid; /** * @var stdClass $section The database section object. */ protected stdClass $section; /** * @var int|null $delegatedcmid the course module that is delegating this section (if any) */ protected ?int $delegatedcmid = null; /** * Constructor - instantiates one object of this class */ public function __construct($name, $sectionid, $plan = null) { global $DB; // Check section exists if (!$section = $DB->get_record('course_sections', array('id' => $sectionid))) { throw new backup_task_exception('section_task_section_not_found', $sectionid); } $this->section = $section; $this->sectionid = $sectionid; parent::__construct($name, $plan); } /** * Set the course module that is delegating this section. * * Delegated section can belong to any kind of plugin. However, when a delegated * section belongs to a course module, the UI will present all settings according. * * @param int $cmid the course module id that is delegating this section */ public function set_delegated_cm(int $cmid) { $this->delegatedcmid = $cmid; } /** * Get the course module that is delegating this section. * * @return int|null the course module id that is delegating this section */ public function get_delegated_cm(): ?int { return $this->delegatedcmid; } /** * Get the delegate activity modname (if any). * * @return string|null the modname of the delegated activity */ public function get_modname(): ?string { if (empty($this->section->component)) { return null; } return core_component::normalize_component($this->section->component)[1]; } public function get_sectionid() { return $this->sectionid; } /** * Section tasks have their own directory to write files */ public function get_taskbasepath() { return $this->get_basepath() . '/sections/section_' . $this->sectionid; } /** * Create all the steps that will be part of this task */ public function build() { // Set the backup::VAR_CONTEXTID setting to course context as far as next steps require that $coursectxid = context_course::instance($this->get_courseid())->id; $this->add_section_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $coursectxid); // Add some extra settings that related processors are going to need $this->add_section_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid); $this->add_section_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid()); // Create the section directory $this->add_step(new create_taskbasepath_directory('create_section_directory')); // Create the section.xml common file (course_sections) $this->add_step(new backup_section_structure_step('section_commons', 'section.xml')); // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) $this->add_step(new backup_inforef_structure_step('section_inforef', 'inforef.xml')); // Migrate the already exported inforef entries to final ones $this->add_step(new move_inforef_annotations_to_final('migrate_inforef')); // At the end, mark it as built $this->built = true; } /** * Exceptionally override the execute method, so, based in the section_included setting, we are able * to skip the execution of one task completely */ public function execute() { // Find section_included_setting if (!$this->get_setting_value('included')) { $this->log('section skipped by _included setting', backup::LOG_DEBUG, $this->name); } else { // Setting tells us it's ok to execute parent::execute(); } } /** * Specialisation that, first of all, looks for the setting within * the task with the the prefix added and later, delegates to parent * without adding anything */ public function get_setting($name) { $namewithprefix = 'section_' . $this->sectionid . '_' . $name; $result = null; foreach ($this->settings as $key => $setting) { if ($setting->get_name() == $namewithprefix) { if ($result != null) { throw new base_task_exception('multiple_settings_by_name_found', $namewithprefix); } else { $result = $setting; } } } if ($result) { return $result; } else { // Fallback to parent return parent::get_setting($name); } } // Protected API starts here /** * Define the common setting that any backup section will have. */ protected function define_settings() { global $DB; // All the settings related to this activity will include this prefix. $settingprefix = 'section_' . $this->sectionid . '_'; $incudefield = $this->add_section_included_setting($settingprefix); $this->add_section_userinfo_setting($settingprefix, $incudefield); } /** * Add a setting to the task. This method is used to add a setting to the task * * @param int|string $identifier the identifier of the setting * @param string $type the type of the setting * @param string|int $value the value of the setting * @return section_backup_setting the setting added */ protected function add_section_setting(int|string $identifier, string $type, string|int $value): section_backup_setting { if ($this->get_delegated_cm()) { $setting = new backup_subsection_generic_setting($identifier, $type, $value); } else { $setting = new backup_section_generic_setting($identifier, $type, $value); } $this->add_setting($setting); return $setting; } /** * Add the section included setting to the task. * * @param string $settingprefix the identifier of the setting * @return section_backup_setting the setting added */ protected function add_section_included_setting(string $settingprefix): section_backup_setting { global $DB; $course = $DB->get_record('course', ['id' => $this->section->course], '*', MUST_EXIST); // Define sectionincluded (to decide if the whole task must be really executed). $settingname = $settingprefix . 'included'; $delegatedcmid = $this->get_delegated_cm(); if ($delegatedcmid) { $sectionincluded = new backup_subsection_included_setting($settingname, base_setting::IS_BOOLEAN, true); // Subsections depends on the parent activity included setting. $settingname = $this->get_modname() . '_' . $delegatedcmid . '_included'; if ($this->plan->setting_exists($settingname)) { $cmincluded = $this->plan->get_setting($settingname); $cmincluded->add_dependency( $sectionincluded, ); } $sectionincluded->get_ui()->set_label(get_string('subsectioncontent', 'backup')); } else { $sectionincluded = new backup_section_included_setting($settingname, base_setting::IS_BOOLEAN, true); $sectionincluded->get_ui()->set_label(get_section_name($course, $this->section)); } $this->add_setting($sectionincluded); return $sectionincluded; } /** * Add the section userinfo setting to the task. * * @param string $settingprefix the identifier of the setting * @param section_backup_setting $includefield the setting to depend on * @return section_backup_setting the setting added */ protected function add_section_userinfo_setting( string $settingprefix, section_backup_setting $includefield ): section_backup_setting { // Define sectionuserinfo. Dependent of: // - users root setting. // - section_included setting. $settingname = $settingprefix . 'userinfo'; $delegatedcmid = $this->get_delegated_cm(); if ($delegatedcmid) { $sectionuserinfo = new backup_subsection_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true); // Subsections depends on the parent activity included setting. $settingname = $this->get_modname() . '_' . $delegatedcmid . '_userinfo'; if ($this->plan->setting_exists($settingname)) { $cmincluded = $this->plan->get_setting($settingname); $cmincluded->add_dependency( $sectionuserinfo, ); } } else { $sectionuserinfo = new backup_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true); } $sectionuserinfo->get_ui()->set_label(get_string('includeuserinfo', 'backup')); $sectionuserinfo->get_ui()->set_visually_hidden_label( get_string('section_prefix', 'core_backup', $this->section->name ?: $this->section->section) ); $this->add_setting($sectionuserinfo); // Look for "users" root setting. $users = $this->plan->get_setting('users'); $users->add_dependency($sectionuserinfo); // Look for "section_included" section setting. $includefield->add_dependency($sectionuserinfo); return $sectionuserinfo; } } moodle2/restore_plagiarism_plugin.class.php 0000644 00000003506 15215711721 0015201 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_plagiarism_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard restore_plugin in order to implement some * helper methods related with the plagiarism plugins * * TODO: Finish phpdocs */ abstract class restore_plagiarism_plugin extends restore_plugin { public function define_plugin_structure($connectionpoint) { global $CFG; if (!$connectionpoint instanceof restore_path_element) { throw new restore_step_exception('restore_path_element_required', $connectionpoint); } //check if enabled at site level and plugin is enabled. require_once($CFG->libdir . '/plagiarismlib.php'); $enabledplugins = plagiarism_load_available_plugins(); if (!array_key_exists($this->pluginname, $enabledplugins)) { return array(); } return parent::define_plugin_structure($connectionpoint); } } moodle2/restore_gradingform_plugin.class.php 0000644 00000003313 15215711721 0015344 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Contains class restore_gradingform_plugin responsible for advanced grading form plugin backup * * @package core_backup * @subpackage moodle2 * @copyright 2011 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Base class for restoring all advanced grading form plugins * * As an example of implementation see {@link restore_gradingform_rubric_plugin} * * @copyright 2011 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @category backup */ abstract class restore_gradingform_plugin extends restore_plugin { /** * Helper method returning the mapping identifierto use for * grading form instance's itemid field * * @param array $areaname the name of the area the form is defined for * @return string the mapping identifier */ public static function itemid_mapping($areaname) { return 'grading_item_'.$areaname; } } moodle2/restore_format_plugin.class.php 0000644 00000002444 15215711721 0014341 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_format_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard restore_plugin in order to implement some * helper methods related with the course formats (format plugin) * * TODO: Finish phpdocs */ abstract class restore_format_plugin extends restore_plugin { // Love these classes. :-) Nothing special to customize here for now } moodle2/restore_block_task.class.php 0000644 00000015446 15215711721 0013615 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_block_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * abstract block task that provides all the properties and common steps to be performed * when one block is being restored * * TODO: Finish phpdocs */ abstract class restore_block_task extends restore_task { protected $taskbasepath; // To store the basepath of this block protected $blockname; // Name of the block protected $contextid; // new (target) context of the block protected $oldcontextid;// old (original) context of the block protected $blockid; // new (target) id of the block protected $oldblockid; // old (original) id of the block /** * Constructor - instantiates one object of this class */ public function __construct($name, $taskbasepath, $plan = null) { $this->taskbasepath = $taskbasepath; $this->blockname = ''; $this->contextid = 0; $this->oldcontextid = 0; $this->blockid = 0; $this->oldblockid = 0; parent::__construct($name, $plan); } /** * Block tasks have their own directory to write files */ public function get_taskbasepath() { return $this->taskbasepath; } /** * Create all the steps that will be part of this task */ public function build() { // If we have decided not to backup blocks, prevent anything to be built if (!$this->get_setting_value('blocks')) { $this->built = true; return; } // If "child" of activity task and it has been excluded, nothing to do $parent = basename(dirname(dirname($this->taskbasepath))); if ($parent != 'course') { $includedsetting = $parent . '_included'; if (!$this->get_setting_value($includedsetting)) { $this->built = true; return; } } // Process the block.xml common file (instance + positions) $this->add_step(new restore_block_instance_structure_step('block_commons', 'block.xml')); // Here we add all the common steps for any block and, in the point of interest // we call to define_my_steps() in order to get the particular ones inserted in place. $this->define_my_steps(); // Restore block role assignments and overrides (internally will observe the role_assignments setting) $this->add_step(new restore_ras_and_caps_structure_step('block_ras_and_caps', 'roles.xml')); // Restore block comments (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new restore_comments_structure_step('block_comments', 'comments.xml')); } // Search reindexing (if enabled). if (\core_search\manager::is_indexing_enabled()) { $wholecourse = $this->get_target() == backup::TARGET_NEW_COURSE; $wholecourse = $wholecourse || $this->setting_exists('overwrite_conf') && $this->get_setting_value('overwrite_conf'); if (!$wholecourse) { $this->add_step(new restore_block_search_index('block_search_index')); } } // At the end, mark it as built $this->built = true; } public function set_blockname($blockname) { $this->blockname = $blockname; } public function get_blockname() { return $this->blockname; } public function set_blockid($blockid) { $this->blockid = $blockid; } public function get_blockid() { return $this->blockid; } public function set_old_blockid($blockid) { $this->oldblockid = $blockid; } public function get_old_blockid() { return $this->oldblockid; } public function set_contextid($contextid) { $this->contextid = $contextid; } public function get_contextid() { return $this->contextid; } public function set_old_contextid($contextid) { $this->oldcontextid = $contextid; } public function get_old_contextid() { return $this->oldcontextid; } /** * Define one array() of fileareas that each block controls */ abstract public function get_fileareas(); /** * Define one array() of configdata attributes * that need to be decoded */ abstract public function get_configdata_encoded_attributes(); /** * Helper method to safely unserialize block configuration during restore * * @param string $configdata The original base64 encoded block config, as retrieved from the block_instances table * @return stdClass */ protected function decode_configdata(string $configdata): stdClass { return unserialize_object(base64_decode($configdata)); } /** * Define the contents in the activity that must be * processed by the link decoder */ public static function define_decode_contents() { throw new coding_exception('define_decode_contents() method needs to be overridden in each subclass of restore_block_task'); } /** * Define the decoding rules for links belonging * to the activity to be executed by the link decoder */ public static function define_decode_rules() { throw new coding_exception('define_decode_rules() method needs to be overridden in each subclass of restore_block_task'); } // Protected API starts here /** * Define the common setting that any backup block will have */ protected function define_settings() { // Nothing to add, blocks doesn't have common settings (for now) // End of common activity settings, let's add the particular ones $this->define_my_settings(); } /** * Define (add) particular settings that each block can have */ abstract protected function define_my_settings(); /** * Define (add) particular steps that each block can have */ abstract protected function define_my_steps(); } moodle2/backup_xml_transformer.class.php 0000644 00000021503 15215711721 0014474 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_xml_transformer class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); // Cache for storing link encoders, so that we don't need to call // register_link_encoders each time backup_xml_transformer is constructed // TODO MDL-25290 replace global with MUC code. global $LINKS_ENCODERS_CACHE; $LINKS_ENCODERS_CACHE = array(); /** * Class implementing the @xml_contenttransformed logic to be applied in moodle2 backups * * TODO: Finish phpdocs */ class backup_xml_transformer extends xml_contenttransformer { private $absolute_links_encoders; // array of static methods to be called in order to // perform the encoding of absolute links to all the // contents sent to xml private $courseid; // courseid this content belongs to private $unicoderegexp; // to know if the site supports unicode regexp public function __construct($courseid) { $this->absolute_links_encoders = array(); $this->courseid = $courseid; // Check if we support unicode modifiers in regular expressions $this->unicoderegexp = @preg_match('/\pL/u', 'a'); // This will fail silently, returning false, // if regexp libraries don't support unicode // Register all the available content link encoders $this->absolute_links_encoders = $this->register_link_encoders(); } public function process($content) { // Array or object, debug and try our best recursively, shouldn't happen but... if (is_array($content)) { debugging('Backup XML transformer should not process arrays but plain content only', DEBUG_DEVELOPER); foreach($content as $key => $plaincontent) { $content[$key] = $this->process($plaincontent); } return $content; } else if (is_object($content)) { debugging('Backup XML transformer should not process objects but plain content only', DEBUG_DEVELOPER); foreach((array)$content as $key => $plaincontent) { $content[$key] = $this->process($plaincontent); } return (object)$content; } if (is_null($content)) { // Some cases we know we can skip complete processing return '$@NULL@$'; } else if ($content === '') { return ''; } else if (is_numeric($content)) { return $content; } else if (strlen($content) < 32) { // Impossible to have one link in 32cc return $content; // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=) } $content = $this->process_filephp_links($content); // Replace all calls to file.php by $@FILEPHP@$ in a normalised way // Replace all calls to h5p/embed.php by $@H5PEMBED@$. $content = $this->process_h5pembedphp_links($content); $content = $this->encode_absolute_links($content); // Pass the content against all the found encoders return $content; } private function process_filephp_links($content) { global $CFG; if (strpos($content, 'file.php') === false) { // No file.php, nothing to convert return $content; } //First, we check for every call to file.php inside the course $search = array($CFG->wwwroot.'/file.php/' . $this->courseid, $CFG->wwwroot.'/file.php?file=/' . $this->courseid, $CFG->wwwroot.'/file.php?file=%2f' . $this->courseid, $CFG->wwwroot.'/file.php?file=%2F' . $this->courseid); $replace = array('$@FILEPHP@$', '$@FILEPHP@$', '$@FILEPHP@$', '$@FILEPHP@$'); $content = str_replace($search, $replace, $content); // Now we look for any '$@FILEPHP@$' URLs, replacing: // - slashes and %2F by $@SLASH@$ // - &forcedownload=1 &forcedownload=1 and ?forcedownload=1 by $@FORCEDOWNLOAD@$ // This way, backup contents will be neutral and independent of slasharguments configuration. MDL-18799 // Based in $this->unicoderegexp, decide the regular expression to use if ($this->unicoderegexp) { //We can use unicode modifiers $search = '/(\$@FILEPHP@\$)((?:(?:\/|%2f|%2F))(?:(?:\([-;:@#&=\pL0-9\$~_.+!*\',]*?\))|[-;:@#&=\pL0-9\$~_.+!*\',]|%[a-fA-F0-9]{2}|\/)*)?(\?(?:(?:(?:\([-;:@#&=\pL0-9\$~_.+!*\',]*?\))|[-;:@#&=?\pL0-9\$~_.+!*\',]|%[a-fA-F0-9]{2}|\/)*))?(?<![,.;])/u'; } else { //We cannot ue unicode modifiers $search = '/(\$@FILEPHP@\$)((?:(?:\/|%2f|%2F))(?:(?:\([-;:@#&=a-zA-Z0-9\$~_.+!*\',]*?\))|[-;:@#&=a-zA-Z0-9\$~_.+!*\',]|%[a-fA-F0-9]{2}|\/)*)?(\?(?:(?:(?:\([-;:@#&=a-zA-Z0-9\$~_.+!*\',]*?\))|[-;:@#&=?a-zA-Z0-9\$~_.+!*\',]|%[a-fA-F0-9]{2}|\/)*))?(?<![,.;])/'; } $content = preg_replace_callback($search, array('backup_xml_transformer', 'process_filephp_uses'), $content); return $content; } /** * Replace all calls to /h5p/embed.php by $@H5PEMBED@$ * to allow restore the /h5p/embed.php url in * other domains. * * @param string $content * @return string */ private function process_h5pembedphp_links($content) { global $CFG; // No /h5p/embed.php, nothing to convert. if (strpos($content, '/h5p/embed.php') === false) { return $content; } return str_replace($CFG->wwwroot.'/h5p/embed.php', '$@H5PEMBED@$', $content); } private function encode_absolute_links($content) { foreach ($this->absolute_links_encoders as $classname => $methodname) { $content = call_user_func(array($classname, $methodname), $content); } return $content; } private static function process_filephp_uses($matches) { // Replace slashes (plain and encoded) and forcedownload=1 parameter $search = array('/', '%2f', '%2F', '?forcedownload=1', '&forcedownload=1', '&forcedownload=1'); $replace = array('$@SLASH@$', '$@SLASH@$', '$@SLASH@$', '$@FORCEDOWNLOAD@$', '$@FORCEDOWNLOAD@$', '$@FORCEDOWNLOAD@$'); $result = $matches[1] . (isset($matches[2]) ? str_replace($search, $replace, $matches[2]) : '') . (isset($matches[3]) ? str_replace($search, $replace, $matches[3]) : ''); return $result; } /** * Register all available content link encoders * * @return array encoder * @todo MDL-25290 replace LINKS_ENCODERS_CACHE global with MUC code */ private function register_link_encoders() { global $LINKS_ENCODERS_CACHE; // If encoder is linked, then return cached encoder. if (!empty($LINKS_ENCODERS_CACHE)) { return $LINKS_ENCODERS_CACHE; } $encoders = array(); // Add the course encoder $encoders['backup_course_task'] = 'encode_content_links'; // Add the module ones. Each module supporting moodle2 backups MUST have it $mods = core_component::get_plugin_list('mod'); foreach ($mods as $mod => $moddir) { if (plugin_supports('mod', $mod, FEATURE_BACKUP_MOODLE2) && class_exists('backup_' . $mod . '_activity_task')) { $encoders['backup_' . $mod . '_activity_task'] = 'encode_content_links'; } } // Add the block encoders $blocks = core_component::get_plugin_list('block'); foreach ($blocks as $block => $blockdir) { if (class_exists('backup_' . $block . '_block_task')) { $encoders['backup_' . $block . '_block_task'] = 'encode_content_links'; } } // Add the course format encodes // TODO: Same than blocks, need to know how courseformats are going to handle backup // (1.9 was based in backuplib function, see code) // Add local encodes // TODO: Any interest? 1.9 never had that. $LINKS_ENCODERS_CACHE = $encoders; return $encoders; } } moodle2/tests/behat/import_multiple_times.feature 0000644 00000003240 15215711721 0016336 0 ustar 00 @core @core_backup Feature: Import course's content's twice In order to import content from a course more than one As a teacher I need to confirm that errors will not happen Background: Given the following config values are set as admin: | enableglobalsearch | 1 | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | online_users | Course | C1 | course-view-* | site-post | And the following "activities" exist: | activity | name | course | idnumber | | quiz | Test quiz | C1 | quiz1 | And I log in as "teacher1" Scenario: Import course's contents to another course Given I am on "Course 2" course homepage And I should not see "Online users" And I should not see "Test quiz" And I import "Course 1" course into "Course 2" course using this options: And I am on "Course 2" course homepage And I should see "Online users" And I should see "Test quiz" When I import "Course 1" course into "Course 2" course using this options: And I am on "Course 2" course homepage Then I should see "Online users" And I should see "Test quiz" moodle2/tests/behat/backup_restore_question_tags.feature 0000644 00000002151 15215711721 0017665 0 ustar 00 @core @core_backup Feature: Backup and restore of the question that was tagged Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following config values are set as admin: | enableasyncbackup | 0 | @javascript @_file_upload Scenario: Restore the quiz containing the question that was tagged Given I am on the "Course 1" "restore" page logged in as "admin" And I press "Manage course backups" And I upload "backup/moodle2/tests/fixtures/test_tags_backup.mbz" file to "Files" filemanager And I press "Save changes" And I restore "test_tags_backup.mbz" backup into a new course using this options: | Schema | Course name | Course 2 | | Schema | Course short name | C2 | When I am on the "TF1" "core_question > edit" page logged in as admin And I expand all fieldsets Then I should see "Tag1-TF1" And I should see "Tag2-TF1" And I am on the "TF2" "core_question > edit" page logged in as admin And I expand all fieldsets And I should see "Tag1-TF2" moodle2/tests/fixtures/availability_26_format.mbz 0000644 00000021244 15215711721 0016170 0 ustar 00 � �]�㶑��+��<��zD�)ٲ����N.W���'G�fxK�2I��W��| $4���u�T��h�x��?����!�B�N����|��=��G�c�pN_ ��MDz��D���_��R$�!͂�0�H��3{A�O�m��, �8Iu�LU�o��]Zf� ��wmӁ��A��g?n��QOߠ�9α��,V�6"R�cۤ�}D��a������GR��N�0�}�ݘ� �[��pw��~����?.���v�l��o���m�>D�,��-��=k=�U���SF%��a�YL�� ��y�wx�� �OX�G�w-~H����}y�[��%�5�]����'�]�ċ�F�7���e�|Ϟ�R�� �0���?&��|nd�G�A.�)&�9���Ũ,�bڠ�J�b�7�A�˒9���� �4����m�t���t�1�$�%���,X�o�.K3& r��� ���t����#5N^AI��D��R&R��$$�4D^Q�,#H��UD��#�`�r�a�t�MH��3{�N�)�"��c��M���*�#o�x���BB�'a�.�� X��}�3��a�:��3�4�#�%x���@����4��r$��G��c��W�a�R�W�֒4i�� �g�����p���Lp�S9�3X���}�7�L�7�3M&�p�z��vR�wI��i @�O �>��&��_�:�/Y㸍�O�G��2��6��u�a��ƒHɪ��C�Ր���;�oz ������x6�Ե��m�v����I�T�����ȱ��oA��B]�?oF��/�Ld#d�����%`���|"p�@�Wt���C"eH���R}a��9����<@ơ������s�kWd���h���f��|m���̇0 o#��-~ �8Z14����'Y�4�U��"��E����6 j���n��IGy�X�d�U��2��w��?~�k���?ʟ`Y?�I�p���j�!���&��̸!1$�� #I�sX��Ǐ9;��R��P�W��J��e]c e�-�ƵQ�h��0é�$��\����j�a�������M�C-c�D�s�D�c� Mû]iv�%.�����HB��wx������L��O)�o�߉�?���&��������o�߉�?�-�-���&E����Ǵ�z�G���3�������!���\��?���&��0���y�{���C�«-���@f!��a��B�T��FȻ�\1�y�{�F�\��d��I�h�ʷ�4J���@��?����ߎ���~m��\ۄ�_��������_�|��v���F䏋�|�q1�xe�������6�~�S���n� ��J ��p��^�Z�����9�-Շd��]�zބ��X1���4�z��at�D�8��|�����kd~�~��"`���������+��m� �*��l:�忏n�]�{շ��٘�>GH��o��D����� �_-��uS���q�D%�O)������I�ǁ�y�u��M��?�ς���������Y��'�W�T��U��j!������ϵ}��u�Y��/�� �sk�����o�1�N7�ϵ �{M��ǁ���W�w����H�?��R�8�?8��� ��nR�q����B��]7)�� ����`�����,�� �{���=N�of���##( �@��b� ����\��i���Ym��W����?��3�!��vC�f��; �3)�q�`��B��]75��=���O)���Z��&E���-��u�����5�?���D��6����E�o� q��{=�8a�%��I�(DVd���'��UH��������x�٘��� ܥ�I�*d�����He��߲L �@9)�Q��s���:��I��݆��5�o&����Qn� �uB =��5�b���_-��uS��;#��l��t���ǁ���-��u����������I���Hq��j�s�?�?���"�ǚ��<��v=��:���y�ǟp��y������9���UW���� � "SB ?��O��x ���?��� ��2R���`��B��]75��;���O)�������M��?��h!��������B��.����Y��U�^W����b���������_���! �s��Y�CM��`���Cfl�H���i'$з ����(�?�Z��F��F������ E�����?Z��&E���-��u��������Z�����7��y���Y���R���_-t�gI��ׂ�9f��^������i ���Y7��u �{��ǁ���W�w���H�?�?���q�p�G�wݤ��`��� ��nR�Q��j�s�?�?���,���g��zH1�����s :��%��o�P�����.�J�����M� ���8�?��j!�����:���O)�����B��]7)��v��B��?���.:�,�yX�ɐ�sfe�.F � (I����sl�m��-t�7��4����m�șNݩ�]�@���\:2�D�+�2�V�B���6���ڮ,sT�\���9�Zr�g���q�)зr�2�o��w�M��u�?�8a�0�O*F!�" C��<a�� ��y���,�x"�c�^,(r��t>)Y��C��;2���.�d˳���Ȍ7��'�]?�:��9t$��?���u������y��#��6�ߪ����)���9�+��G��]���� �M��ǁ���� ��nj��(�2 ����?��i!������?�Z��&E��#�n��\�:���?�o�/�D��_?���3�����3k��:��J������q� �qg>�>���8���i!�������9�'���A���9������?�M��;�s��W���!�z����?�F�7-��������c�����s�������O���{�:�������?-t�#]�綠?֬��ȗ��;e?�ٽ���O���K(п vL�"�FL�ps�'����:&�'�q�3Wur�f`@�^5���s���_�s�T����<���G5���_�s���#���纩�����2]����?Z����������l�z� h�����@b����?`��Cg�?�om������#����u�a�K˴���q���?V'��1�y%��ǁ� ��� ��n���\�v�������?���B��\75��H�8��� ��nj���a����k�3��� ���Z�}o�[� �����D��_=t�#]��v��]���9����o��� �)HV���1��7!� �Ps� ���纩�/p��y����j�H���� ��nj����?��_�s����?n��۶|��&� �-t�� ����S��z��(tYj��C�?�����&:����i��Ǯ��t�'حs ��]���� ! ��$�'�FM��$����?�MB������F���&�?:�Y���˲����Z�_�G�H���>d�.N��5U�P!��T�i�Q� �2���d����V�h��%;m�\"g�2�T�n�l>RXJ7��y2J��M�(^���8��*/�hu�f��5��dE�r�r�L�*�l��2oXoʅ(K� w��HX}%%�&�9!-����*��2O�ԞL�[�cB�C�mH���*�^!��i�*.� ���.!+!Zh�o*�zD��8!ϯsc(��U�Y[@�(� �A� EP� ��p����*"�x�����_b]"Q�w��x��tW$����_%8Ȫe�\��*��l}�f�Ns�&�e>!��I�+¾ �*j���_5�_�=t2�+M�I1��� ���V�brxi�(^���t�^�A��F��$~���o�Zߠ`�<�����?�_���z*�5�_�yp�S~��k����C����Ps����⿶�7��?Z�,����'㿴��o�5�F�O��o���@zO�n�B6��x���ZEq��8�"�pfE$�H m��>ح#R0���- Ż��Y*�+��I�@�{�3�$��[��C��:�l�*�6X}�OB��,���,]V#~�% ��bĕX���-���uifʃ�D��>)hc�v��!k�r�W�R�UOQ?�Zx�5G�LQ�L� �uv8M��MY�/�o��,�9�LJ�p��r����wx�� R>X��K»�,إ����gD�(O�X��S��+�$?ap�P %��yeo̺�T��a��p�4�jk<I�� �s�ĩ%n�>�B�����y��V,��֍�b?� _�����!�VY���r�'�ݵ��,Ry��ق������$~LIN��PU���*�T<#ff��U���`��y�>\e�\���*�[jFL�<T[��ʰ�FkIDlLBt^)7��.Պ��� 튈"�E��RYp��$�:�Z��j�jQp�G���3R���#Oy�+�v�*��G'���%� � �k*��"RϙV��q��F����?z�\7��J��l���6��:(����eIm�?L[����Z���Hq��x�w���h�I��?��ǯ�t�?3�3�R��!�d�8^Gذn<�*B�=Q� ��,�d=@�q�L��n�����3��ڜ��U1TW.�\s��,9��R���H�"/�2���.N֜|����̧��o�.ӈ�7���&xOJ������6[5 ��X���-L6�!�d�v�����HͰ4��cR�dy��S�Y ��Vk��26x���S��(_�U�w��,�I�4x��{g��Y6w�<���HiB�F�%��" �%�6�L$f���t�#R�8>���L4���Td��D�6��.W��"1���u���#Z8��/������o��'-�u��H>��`aj`ɵ_�\l��!��}�q�ȳ����"� cՠ%�d���bP�&�BJ��+A�[KbE�zڇ�s5H�2�,DάEti2�2%[�D �b�2�$�:���U�Ф����+ia3抴혴�I��̆�����r�J�n�9*9�)��ܦ����r�J�o�MUrӚ��>H�ȹM�frnS�Y��+K��q�l�5?͘�Uۛ/�ܦd�۔l�s���F���dGհ���+��vVj,_��O�Ҡ%�qn}�����!]�w��̈́� ��+ڎ�|�t?C�W�Rm`dZy3ۆ6�mL3�3�6��m��q�-}*Y1:P[� 6�r��Qn��Џ@��L��������pk�x��s�[?�# T��t�^FC�\�EE$�������OIy��ӏ@���_ǃ�_��nb��:~Nm���D�Ǧ�l�?i���?�}0��C'�'*}-\ph;�eK�h�w��C��A]����O�����k�ڡ?� =k+Ӓ�[�A7��{�l�?��H`bM���.w��Q�1�"�/�$W0��'�����=5m��˴$�>����g�ݘ�e9fk��w�à�&�!�nIa���m�G��/jO6��y��j�Ρ,�s(��qQ��q�P|q�(�=/9��ڝD�u�W�,��%���52�Q�+p�ެ�~��ΣJ��p��%IU����*b��/ ��>;�!��\����m'�����y�v\��} �i����/�H�,760�%�-�Mf���6�� �-���ĩ�f���JX1&�RXV�Y���cf�|$c��[�[v�b��^r��v<'��tm�f�����Ih'�9�y��. `=[t���q����/�!��u�w܍��u�#EI� �p�U�ƻ��/�|���y�1I�}�p!��n~��D�(I^�X)�|qq���lۙ�J�Jњow8#o�Sd����h�[E��pC�q"2�BK�#�S��,^�'��hY�Faʯh� ������8��Y��z2a���!�k�`�P�*7����iB�k��'�}���;��eH=o��1Y���3��7��صfضVk2Nط����[���k��k�%wB��Ż�P;�����qǥ��Ey�츔�-�ɽ��(�*�^c�)QDžx$VG=�]>;gd(H�ٚ��B\2eml���o{x�n���5ݬ=sf�ѻ�����Fy��7?K<�W�$e su�s�Ae�b���܀�|��W���XP1�QN� ��]*���T:f�Tzo(YF�s���oQ��{�������0��K{�p��6~��Nɢ�:�&��%#�n�"��ݤ���{��X媰�㺛����>5�ʮߞ��z�]��V&:�_Z�M��Sj��+������y�)HV��{b��}e6T����B��U�ج�Xwg]ݠW{}k��oN�52{�6$�Q��a��k�[��!r�_y�y�z˳�-��R]�jA�K�Mߝ�k��\;�r��k+Lk��3����Z�M;wa���CflH�����6�Ac�H��<Fq���ޛ��io,�t�.�b�{�ڐ>���|RV�SN�i��"#��F~3�N�roJ�Փ>F�U�m��5���%�}��t�n�讒�W,���Y��E�ĕy�:y�S�^N~���'�F2�v�~xz�Svl�<9a�� �H�7�uJg�l�2�YrNM����D{�]�f_>�ɧ�H#�7z6�ᆙg��F��DZ����Q����O�gp�i�����͙�� �,�v���^��!�6�|�!|$�8ZP3��EWq|h ��"Î$�!�/�\@(1���%ܦ�] )�b^�W��`�/� �"�*К�rv8�Ut��r��:�I:R��-�U{u�뻣Uy�\��z��8�ye�C��N秣�NkV�F��0���T��.���O��<�t1� cS�a��D�9L�{:i˥�z��B�o&�\ �b�T�ߖ�劲�$Y�.v,�<~�R�ċ�b�>�g}�S:��~K�����I��-I�_��I�g-I� _��I�)�b�9|9vI��%���FP�9|IZm��FP�9|I�m���¦�D_�N["/�F����K�m��F�B���K�X.0�����K�mՀF�����K�M�E#�˻$r��l[����$rȒ�v4Ke!1hY������v[����9�����y����t~^y���K�t��<�������%= ���G�]��d�p�Wu=�[���oH9�U�:K@'Py��p��\�i��|��������W�;��¿�K"���9�j�W�m3�(�&��|�+aF/ߐ{��D�L.�n}��F~��Q�&��Fz$xm����S�wr&~w��p�d�G�Ǒ����1�?PO���h�L����_��#�hc�ϟϝ晲�<�/Ռz �y�*���,"1���VW�u�r��ʊ�얊u�V�}���2p�J��'�y�J85W<G�PU#-�`�>��S#K����iQ-��==IL�����T`�$��ŞT���ߣ�V���N�h��*ȋ���9*[}AQ=��zSo�cά�el�`�.��ZV,XBN��Z,� �{��y0���;�� K��mkfN��c��vR���ܿI�����?l�"��+ {�1�t�ڑ��v$g�]ɕ�u�It �o�}�'�@%P<�%��PS��.�-Y�/˺X��V֖S�Wr�K�em�#�?C-[�(R�.�a�*��������p�5�}�&Su�c}���;^��=o9��(xR{��ML�`Xt�*z�� �b� yum"��_Z��s�����P�B"������2B�z����d��i��tы?rj��8����y���A'�!~LJt'*�%%�rk�f겋h'5OW�g��ς��l.��RP�ݦ�L=�p�eJ*���B��$�a�b�����P����Z�k���f�m2��ЉT"��7X�w��0�Ϧo:���t����^0M%]y�W��)�^��m�P�:HU�}_�6���]���w<��K�|��[�������?���E���Cu�y��L�-��} ԣIys� ȋx�O�m�<�e{�K�V�W��D���|�~`����z��O*~Rv��%$�2{�ĐD�,�$�y�M���*��Q�O�n�B��dCW�Rxp����R�(�?���u�����<�4�?�_���9��gi�������d}@��|@��}@�D����PO �3�q�>�:HU���6� ��B��]7���f������D���Z�,�ϕ�?����x��Q)<G�C�}�ߟ7��W}�I��f����j+ߪ^1�TB�&x]9q���)vh��<ώQ�AA�^�n��9���xq|YdU94�j)��v�#Z��y�砧<.7!��r��cÌ����bp�x���(��OJ��犿��Wd���E����!��)e��p���X���b��LX�� U��^�Y�����F:y�'�4yX��?� ��l{�"{XPN����_?�A�`��Bg��<i�缸��x��KB�+A4& �Z����c � moodle2/tests/fixtures/question_category_34_format.mbz 0000644 00000013575 15215711721 0017271 0 ustar 00 � �=k�㸑�u�W� � 7mQ�x����d���p@�0d����%G����[�DYr�-�N�� ���E�b������������O?��� Xf�K~48� ��-�v\���7�:�Q�*� GY#��,J�$��}� '�9�4���'�gUZ��x�.�!.cXL�Do�7�ɘU�k`�� ��Q��>$o�o<� �l�`���wy��>.�,b��5� ��,�[Fo�R���ض�=ԇ,���}�R����A}�%5�ӕuT��^h����_�/z"�2�����Ԗ�2�V5����̓�;���9�����1�e��<��]���\0E`Z�8������qGpT��_�:��v4�����T�h([Og��wx4�^�'�ŀ�@H6Y��{/f��UG�\ z ��z���#o�Uh��Du*�7W�W\ע�JX��� \�����8yn���)�n.p���w+`�`[�i�ͱ��K�o��6l;���r��~��iϻӞ����̴m�]���:��V9"�vd��#u�5�K#�%����{^��G��p�o&��r$�+��4����8����z���ѳ�C�|�m������Ϭ�ƀ�w��'0/����[po���4̢8�}���?������VwK��e\�1e_�θ��������H�i*��ǀ���t���d�0t�MS�T;�0^$��H�L}���_u����0��Y��8?.�aE T�����J2Uid7���۬à�;4j� ���tlONGK�A�ȗ������������F���of����<M��۷�� ݤ�`qW�oq�y; ����KB�e��ʂ3N�V�rJ>(.N�<[a�Ŀ1�!(W�y1'%(�̎0]��F|�^a���QG�ć���A=3T+Q�A%�6VAUfE�A�i�O�!<�<�"��G4�<��}��*_E�����S )�fbs���QҠD��,�iu�S6Q a�&AQJd! �X�QA ���D0�Ar�b�4��S����;P�̺P9|��#��j����˩�� ��\X��(V��ٜV��� ��RR�LSQ��qțH!oy�!w0�y�h6��py�ۗAZ<�v���x�w� ѧ��Se??;���A�8�>����bf}P��W�mi� Q�Ȫ[X�d�Gt%�3$4p�8�h�'3gM��!�ŷ��� ƇAQ<fy����s�Ia��y#�ɳ��U��ջ崉⌐O g�}Ih[�mQs�c�"9�aY�d�j�j�M��� N�\l`������}�f6ZW�����P�,�${�P��)�Q�1C�o�mq�&�4m=���#B�ce� �8/�"�ȬP;j�=�D����~��*�idz�Ogx��<.9s�8F�i惧G�1ũ��g> �r@�##�G`$��3�2 �8J�1VL�#3c�|;m"��*�F����%�=�L�ō�)��'U].�F��l�o���6�0T���b�~�F- p,� ��J`���r*!��� �b�Kw�W��*⎬����%|E�Y�bH�H��s+� � Q3%�.��ȇ��7 ������R�/Å%Z&O"�*���IVQ�@�<��*=��u�)�3�Tr��d��)�f~���������wr�&���`T"�ɼ-�B`I{<ql���u6�l���I72 ����M�w+3=|)��]�P=�������8w�g-�[���G0|�r%�wE���w.�K���a�� ��[>� �>�&.ٛ� t�2 _n�<J"�J.���R,�L��ޔ�NT���J2�IC��唤2I)���Rd�|n.�1J���U�$�S!:���p�`ʖ M�S��H%�z��*)��7e��^��8NvL���Dd���]w���nkڦGG ��Dˇy�xׅ/qp�h`D=f9�[�Z�FU(�;��#Z��] �)���W!U�@q�}�$2C�/�w�d̾d�����7�Lњ����B�/;9���"D��f�s�e��}ќӥ`{�̨� $��l���T���Ʈ[�/N�E�T}�6h�9�9����S��_ke�g9�� ��.=��� ,�}������<~��/�@���p�٥��[�ٮ�Z������]�����ޣ���tk������fR�X 9Q,l�Z���4�в�L ������i��� ]��N �H�!Z|�^X����)jT�p�q�W)�^p�ɀ�N���橄�J|��,�pO�L@d;�����mȒ��a���'Լ|]��٩2�4v���O�J� ͧ��>��<�)�c���������\�E�J.ɻV����#M�� ��Z�f��6pd��4+��)A��?|��;�� ��݉%[�-Q|Z��(ô�b=�(YNu�(�:�SՉV|�\�iA:�������H���iĒ�?�p�~�4���?.6�?��?��y��4��_s� s�g0�^7�6�r�4����2�����^�}�� N�_�P=�K��1�{F�cx���b���NW��T/:� ɾ�תh=@���pҠޅ��l��������������)��J���5�?��Q��nPm�^'��_М�-�2��F���OW7kyɱ����.��3���jd���?˷�o�Z3�N�g��=�i�ٶ��7>��r��x��uۋ`W� ��dI;X}S�ʊZ\�_�V �'�����_$�Z�Kv*$D)��ki`B�F w���8J�2ޒ[;�[%*�9,�x{o>s-A[�EI� Iȥ�מ�5��T%d�(�-�����<G�'� ��%�#|��}Ձ�2;�i ��٢�utv����9m:WG��<�צ���fm:_G���:�y�.�y�)�MٮB�mS�+�bU���� �E�N� U:��ێ�b۔�()�M�. Ŷ)�bO�R �Cn�uh�"jl�g�|?�z(c�<&�昸��e:�D���7Zf4à�?��a�����_�`h� �V�h� ~�oԳ��;`�937��h��d�^�����](��4��|�����_�=�}o]@'A����������0Cކ�G s��uC�1�˧����n�:<��:�f�.!+��c���__����<�rah=}�4z��F#@����;�'�����j݅o` ��$�r������w[%��)�w�n��Eu8���;�K��6�UI�~��1bO���;�~*��t[�������wt[���]��K����%�$��E7���%~W�^ka_�7��L�.���� �����U�x�;�w���k'�EL�(���آN���F���������^N�����8˜>�@/���8�Ռvw7�o��ۻ-&�F8�;�,��2��� $l������H�"� �B�U� $�^�l2T�-3U&���3!�f`��u�e�U�2����F��u)���O�w��i���g��Q`(�3�?�e��+��{�@�_�p|c�a0��^7�+�W�z�ߙyM�O���Q�,������M�7�� 7e|��P�U�O�%h��c,�~[�Z�-��r��m�G9���Kh�����H<��^V�Q�~4� <��1�,�E^��d�B �l� �4y"%&`t�v��}S��<�� gh@q����>���,�6�j�} J~��Q(��%��Pt;���Pj<�^�r�_�jd���[4bUe�z1Fu������`�Y#��x0J��Au(cH2�Ճ�� F*��� �� ��i�P��"��R'�0׳�C�m��Ի��gM����(0\���>*�|Q��c���ݺT��!ˢ������&�?`�8�gn�S�'Tm#��B��� �xG��wT 7{��-0��{\�K����W�`�%���E�7��B��\9��=���Uq���� � s:�f��~OF�R���6�Za�6O�2���>d%ķL�HGI�4L*$� x�@��h�8���ź���3o� Y�n�t89M ��v1F?>>�YV��ey|?����>N�iu�#q+�lP���V�F�X�fc;�X����د曍� �p�zah�g��Dn���YhER���4�M��I!k����Q���VU}��ևn�Uu�w��j.uS�c��6u��cc+OS�ڷ�%� $�K�r�'W�)�XZQ��1���h>��̉��#���yp���lfB���'��������X'k�1��,'&!���Y,`[�2~�a%���CC��l*B,�h�U�R=K��um�!���KL5ᶞQ�/��ziON�znD�m0�n��LZ��Q�L�9�yC۲%^Fsߺl�v�z����L�)?Yޏ`p!��)0RC/e�5uv1i��ӹ���8�8>m�[����t�z������2�8�1�܌u��s3f[�3朙1��'��1p:c��2&50��:EPaB|Ks���y�r�'�V����w�c�Rݞ�R" |�� �ZX��~t̍�У�W��g���C%O��c|(���,Z�H���c&�Y�t��j��$�T���b��H\#U�L��DV�ܗЪk���Ѯ�)�SJvc�^G�����0f��l7j%�<ݬz���5�M�~�u���z�hb�Q�L�i'ͥY�n�f$75ɚmEo���(ޠ�x}�uT;�b�%V���e��]/�W����-kR,i�2�nZ��3y���;�~ޙ���P���|^��/���ק�ѕO���~�|�����/�>����S8�ڂr|)��Oe��]zM �~�����33�F����`�nqs�ѡo]$g _f_s ���#��{���4 �U%��߲�DcmO1d�52A3�䣲����v�!�37�|KH���)���6�R���p�̖LD�q�7L�}��h��������� �Ŏ�-�(����U�b��� p�������Ĥ���³�/�zFR_,-�q� B!ZAtY��h�Yg3�C-}C����6��*툳~������QK��q�e��`�'���u���β��j��p����ZQVX7�a���� �M)eG-{G Gym~ �pO�C���(�Q�Z!�H�:DZrq��?��b����G���O�Ic��F8��u�40������v��c�F��]����L�_t�ic�}��7�:��������!� t��>G�w6k�?�<�c�`��� j�ا&���l�����P��yؐ3����O@�ݙ�uع�8 6q�O�,�)-/L?��t}H}�V�h�m���2���k�����<c�m0�����/�5�:�o�F�������H�� ��/A�#�?��k�� �͌�7 ��u���o/��Y���������g��K7/A��|��c���_��g�F#��n������(�,�o�����]И�{�0���k�� �y��(`�� :�����Y���:�������/I��B=��X�|���_���G�|@Lmy�AN�K� �=��{���?�2������e���hg!��,I ���]_2�T�_&�q5)j�pY�2� ��g�ƓdAԈ����'���Fp<&O8"�8�TP\$��\�x��� 1���zR��Q���0�$ʁ�*GC�E�� (�e�v�y�F�Q������O5�'4YNBk�B$C-6����<�, �� �}:J��e�yv�̩���;�<���]=L�9M���TVdo��9��ֵ�0�B;�h t��t��T�T�Uح�%0pe�7 �� }qAv moodle2/tests/fixtures/test_tags_backup.mbz 0000644 00000020257 15215711721 0015164 0 ustar 00 � �=k�㸑�u�� ����ݽ�&������^6���AK��Y���=�_U|���V�my*̠�b�,�U�"�����������矾��~q�f�������q���xǶ��>;&�yA2`e��>@�1MØZ$6�#�V�$��o�o�2)����+�cTD4�|~i���yWF�z��;����i���6�r��t��3��o�s��i��&E.�̜~iv��I "��=ƴ��D&�o{$�F+hVէ^댄wN�X��M�i��8��H%�4�+���٦aS��֛�H��Eיz=RdiL;nIµ�3e��O�;3��r*�Q�� ���'lMC�f^!g2�+R�*ۻ��ȴX�����h��K�@����|��n�� Sp�o��ӇhNT�C��2Me�8w����;����tˬ�Òo˝���Ӳ�.R��}���9v�����Y��{g����孂�Ӏe*ES�ŏ�s43�mvʴ��Z���'r��������>���χ{�1|x�����;�?�|���1����>���(��^���T�8]�^����.�j}����g+���t���5����/���8��3��;������w�t�!��T��e���p8_���`��̦��N�q�?�=l=¸ ���o�����4����o�����}����>�~LįK3>�I�Dz˫�8��Ӻ��78��W��@�P�/����K��*���_��3�G���?��=�E�]<W�u�@LL�dO�n��z} ye����6��Q����B *���Lj>i:o uԨ�"����2��Nc���?��!L��&:r�o� T�&�hm�)�Č��Bpp{� �8�����5���|�!���9y~P_�B��QA�L� ��_�������7�����9 ޝ�.B��T�<![���慅�i���OT�NY<��&<���h>�X`.�B t�(1H�p�_��)��V�]�l!HD��j�.~�������P�&�uk�$ʘ��e�c%%ktVqȧ �[�~�� �9A�8�(J5��*�mm�n���I!6U4�(jG�VCI�m+�4[8�J�I�]\��2�p����]�VR��h� ܊�F�L�D���HA��(�("5�j�4�B�j�Pdt廘<cA�%q�5�BI�M�4��◌���- 1B��#�hKe�-#Jü܅���% �T����D�봠��*j!V��p�SUp��Jj�4j�I�S��Lj(�٦���}>7��j��2������A��o��P������O^�>N8�a��e��W������@_�Qs�3�0ݛïpt�G��0-s�[o传*�e�5��YUVy�l��ٴ���Ru>{wp� �8������l�!���s��Sz&"o�D��� ���-���I���x����mN��T-��i������{�m�1�aX!h)˜�`�&0[*�Ԟ�B�c�G˘M��O �S@���#k��@��q�Sš��i��6b�T]�*���o0���Hfb�� m)�������,�B7���c�;�U>m���{$QL�QS2�kͺr�>��d�N6F�da���) �Q�: �b����X��������w���\')������?���K���I�X\�1���_�Azc1�z_0��o��������V�o��&��R� ->Ci��~m�0�U�J�>�;�:�~WQA��T��@Gۨ��<�#q}/,�$aͶ e����'�8�-tG�( ��*�V(N�/�f ���1�e ̊�o�'-D<u@��� ��u'%�]�?T#ne�IL�B#m&���& �j�^�k��4�=��Z��G7�~�T~w[���H#�Z���l�~b"uB�3n��&��'ؒ�m���t���h��L�F��tM���=e=RO��:3I�'�+#��'@�$q����ne�z��#P�L�; ���G�㫌RT�GC�M�ZŔ��cF5� +�b1I��W(M��5t��/K�Z��;�gm���<�Y� �?%�˄���e�>�5�z,��:F�EL����ihW�]Մ���EAQ G�������KfB�^R�� �A��5��l#Ed&�#�!���)����iū����:J�� �|���T������{�Bg�<�X}��1�ͫ�Ů�,/�8e.f�6�� � UqB���/!�U��{����u��2�ꈖo,�QCF�� o�����%�p�^�S�qh����˛���}�&L��׃a'�'-8���1Z'��"4�7�T�����C��8�i���a���s9Au�.�'���考�?�ym���x�sx�/�#�:ӥ!/]�^���������a�ݹ?�1�w�2^�9�����x��� -7_����_��Ù�F��?>o���e\���ܩ=���������7�f�IژQ�׃{/�>Q�����;� p�σ�օ�*��a/�_?�2^��?����� ��v��̏�jh�+q�2��?L���.�=o�=�l����Nsg�8��oҬ`[��e;e�Ve�(�(�XO� O^n�${��+��i���� � �8ͬ<*,ȫ�F�p3-J�Y$�vQ@�,Gō�J�$M��Ҋ �E��-v8.A�,sk��Y����=�-�vP�"*���� O�VL����k(*J���"��B��+���C�+���zL����ی��E�r{c�H���K`yAe�h]��v�nC2Zd��0Oʮ!���"��@� �eV��Я1��������g�1�U�W�o�ݒ��D�CBf�����Y�ʂ@!�J�4��K^[�������:!��V�ܨ �*؝] r�ˎ�)�&�w%B�,e��UD�_H�V�͠���O(�`���G�D��(/i�5�!ͮ#���U%,z���0(! ʜ�X�qա9� <�{;R�&�Wh� �N���TlU�)�W-���g)�+� d^�8��0Z���V�i������Y��k٥����"V�I� ���PR&A��J��W�&��حI$/ Q}��<Ji�E�4L��k%���^& �V�Q�jpE���5�KV�,w�+��P�X+� iQ�MJ����Fo���r]���%:�`�z�$-���X(���hl0=a$�`�#S��VV��w)��A�&�3��`�Jfr�d�O �[Y�"J�(,AfA�!�g(i+|��e�{Rf![�Ո�j�\IC �!�6�m��,!�AO�b��H(�d x`�P}�k0J�$tp\�<o��DWhl�Ԡ�@[�lϊ���E�3��2~��#*Kt�;���* �"bM���5PɾcZ*C�W�fG�H��@��`储 #�`�Y �%3H!p�-��&Sl<��lc�`%a><�M�(-Q�B�������\���f�A�������t1L@����EB��t�M�� 6��e GdX��Aa�=���_A��� �`�؈���)3.y�����Hce�R&���I"`�R�*.Gj��L2�\-�kP�>�K�����H��C6�C�4C�/chc\ۼ��bҧ�LR�E�;�`BΆ�*Rd��Qk!hZ��1��])�5K�T@)��Al"��GH�&�0`�K���&��1�`]%c�o¼7���س"�x��F�ac�-F�k�1��Zxrh��Y���q=��'�?vVGUJ*��C�Y�'�&���_� �h�8�/�n�J[13�kf��$F���ʚ�d����J��%���ۘ�P�β��K1;��Ԃ3}BHPA���-��æ9X�k��Χ��c�]C@MЦg��C�����+�[ٺ��Kn}��K��Ί�*�Q�צ1��;�bd,Qh��0 �S3 �5����`h�ҥ�R�C�J�a[�S*-��l@B�v-U� CU��Q�(�jG%��v��� �6٫��6��?�t9�!�<�&�)���vCUB�r~#��NX�v���Omv(��" ���O�d�]��-?5+~q��sAs���tM�g�0&�nOJ��v�0������(� �!h����v5_�7o�h"���Pv@�W��]�?ty��o�Hb��%Ϙa��V3����B��t~XNJk�w m���[$i���c��ŕA� �q��[e|�/���~4���W�o���Q��аq(?��1|y��T�ӏ6c�A�N�ԏKr�2�z���杖���ؓ��B�~��]���Q������O��mx�c�@��o�����S^[�u�q�w�S �0��v��Ұq��,�$)I|υ��5�BJn��/C�*g(�Bܒa��K3kj�T�P^c�VD�wQ�\�s#l��6x���ң�Vd�I37Dd��U�j�f��¸�)��XPft�~e�BI�)�)?}��8>DI���A�ѹM:���k�mt~�n�F7m����fM�yݼIw�Fw[�6$k6 �6)�MȱM�f#r�IҠY8C6�E3�I�_y�rl���%�6)����&eS(8VR����ew}��k��c�Ik�a��vѩ��5��f��� L�L�=�����pS��MaN��gd ? �>d�Cv�>d �C��>d��֍�n8['� ;)Y�]z�� b�������O�x��@0���y�����Q����6� p��V�r`H���e��ΐ���G�BƧ B�Oz�c�����a`��������?>�7����y��A��?���������8�7�� F��??oP/��8����b��W|�ox����Q�IQ��L��vRs�{Q#�1�c�w��O[�æ����l,>��b�^nc?�d���&�H�S��1�H�-���o�+����6�z�ߚ�o�P���=w'��i?�������$e�[����<���w��ߐ�&�|�����`��G����2�H���!� >Rh~ V(��x�ɽ���!t �|���@�t�5��/N��_���Wi����U�fNa-���xڢ�!\�JJ���9.�)�]�}�%ęDQ���DU19�!k��H !�M�3���ٵ�Jm˸X� ��#�4��q���LõX� ����ё��LQ}�|���\#AͪaT;�#�!t5�|Pep��sfp���8��r @�Wl��G���K�@!��o�S��0/wr��<���{���jy����i�ZŴP߸&M�䀁�Ѵj�[eҘME*�2��5�T�_;���=~��qgu�������ʉ�~ L)� ���<��6MØ>�p��2�?�}�q����A���0�g_q�����;�R����E&����8��m-ʠ�hLIN�����Z���2�ï,��?TrIɓs.��3����e�k�)��qa�R��"�ܟ�mBH�M�;�q�:��FI�0��v����h=��)�S�P��}�'�����mfq�� 3�u�觧�,M�Ŧ(v_M&Ν{��no\gv�Ͼ��om������Բ�c���&�o�Æ� �4�lz7�]�+���-��n�����ӲmM_+C�@ �~=����jOS;�ݑ�y�Y'I{�uiݱ�[��wS���q�jO���[���+��P<��~k�z��Wb�S�H���%����2�ϼ�o�w���%����'�9�h�?�j��SbK���� 3E��ݸ�V����w�uu<}O���9�&ph!�/��:���h��NA]=�����gU(�7{�}�_e���T�QU�=��\�Ĵ�b|bT�υxDE�|��cu�0·�q����R�K����zMFk��K�ꨪs���e�<d���h�ӇAg?����A���t�a�=%��~�c�Nɠ��A��S28�������?�m�+"�4�� ����o��1*�_�b�+��T!�Bb�H�zXxo���5E7�Mj���kO����� ��A�8��(���:�(�$i����g�e}�ܠV�䐥.q�y��Dk����W��4hO����xΐ�U7d��Uw> Yt����z��o�� g)�� ͋�U��|y{`}U/! Znu-ذ�U��jPu�ɋh�d��gM'�qd�=���wҙ�E��+�'�WN�����F�\���E��q���Z�e����C���~ ����Pڷ7�H�z���>�����Ҿ~?~��}{�����}{�UA�k�z����ۗ�(? �=���vx�W��_��}�H9��=�$�zӆ�t�Fxȕ�s�����Y���=�OG��꿔��9�:^�k�m�^�"�{�j[G\�AL��!�܋�ۊ@s+T.�4��e��S��|��0�cZ�GC��nQ&o��)٤y�[��ln�S{�ۿ?~���bwnw"�8�d�Ǘ���vcg��3US���d���tFjQƱ�=g>H�3�ŐD~:/��_����v!��~���;qO��ʾq�S�vL�w`���T}8dT�V�q���B�lmn������?��Ez�ߛYY�Ќ��~h��^�8M�(�A�����E��.��2������`���Vr܂m&]S��( q�������MZơ�!��b>���4e%������X����@<�����Yv��C�tMH\<� $����v=�ъĸ�❹O�4Y���W��q�ً��0F���M���M�T�:i�D��g��*�$�ypJ-���q���ք�M��5S�2u�?����B�f�:g�2�����h��i�!�w%Z��hh*S��DRK~���a�Ul���=� խI�L*E:�A,��V�&vz5�\[���k�)K��E�XƵ��R����2���� ���= �[O�5�p��+�B���5e����U[Tk�U�Z�����B�ψ (�͋ܥ��^wl�]ϲj�ioa`��jV�{)�+�r�6QG�Rȧ��"�0�6����n�|��Y;�+V�����w����R�.�d}�S�Nݗp��i�욉m�� o$�zT:Ӛވ�V�[f�f�G>è����0چ��0�~3��R|�p�b|S�ջ��a�b�i^6����dSo�b\x��5�kL1�c��8�P�S��O1�#�����M1�O�q�G�&2j��{�����3g��;��wc���xg =5� ��ن����3J��H�]7��{���-��ܴ�]�d��_}�8�PL��k�F��cK����9��_�x�� �W����v5����/u���e��ϧ���gמ۾�?�����<)����Ë�ӱ�����?�p��{^��q�^���|�']�/����x����� ����xj���w���]����vK�g���_�ZW7���+)��_l V�}��hɯ1�?MI�eG��qM~#��Y���ڥ���q����� ]�q�o�����}�Х������0�����?������;1t\�{y�o>�C@W�_��G�?���� ]�q�o|�g8�s �����:^&���<��������� 0��7t�����A�h��3�?o�>N�x���3�C@W�_����}�Х����{�!�h��7�?�>N�xW��;�C@W�_����}�Х����������MG����^���g��/��@_�gr�*�\��N��g7q�>C�ߟ�mM�]�����Og�ڮkM��_9����i���Z��""�r�Ņ��ږ�qL3k��3o��A���tI譿��t�8��UW�!]��Z�Q>=O~��U�x3�S�2��$y]�^���n?cX�%.��_W�ߣ\��Rm�7�rc�3� �*���)3p�^��t��;d*YE�X�!T-\L�8(cRo�8�֔.?�;�;r���Y�����-���{���v�ܣZ��^���[�;�ս�Z��^��[�?���Z�U����y�9�� �E@�w��`�:+���9�g�?�g����逃�����`��s�s��(u�y0�=� �Bϣ�wK}U�^��a�F� �� ��� P moodle2/tests/fixtures/format_test_cs_options.php 0000644 00000005331 15215711721 0016424 0 ustar 00 <?php // This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>. /** * Various fixture course formats for backup unit tests * * @package core_backup * @category test * @copyright 2022 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com} * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/course/format/topics/lib.php'); /** * Fixture course format with one option. */ class format_test_cs_options extends format_topics { /** * Override method format_topics::get_default_section_name to prevent PHPUnit errors related to the nonexistent * format_test_cs_options lang file. * * @param \stdClass $section The section in question. * @return string The section's name for display. */ public function get_default_section_name($section) { if ($section->section == 0) { return parent::get_default_section_name($section); } else { return get_string('sectionname', 'format_topics') . ' ' . $section->section; } } public function section_format_options($foreditform = false) { return array( 'numdaystocomplete' => array( 'type' => PARAM_INT, 'label' => 'Test days', 'element_type' => 'text', 'default' => 0, ), ); } } /** * Fixture course format with 2 options, 1 inherited. */ class format_test_cs2_options extends format_test_cs_options { public function section_format_options($foreditform = false) { return array( 'numdaystocomplete' => array( 'type' => PARAM_INT, 'label' => 'Test days', 'element_type' => 'text', 'default' => 0, ), 'secondparameter' => array( 'type' => PARAM_INT, 'label' => 'Test Parmater', 'element_type' => 'text', 'default' => 0, ), ) + parent::section_format_options($foreditform); } } moodle2/tests/fixtures/question_category_35_format.mbz 0000644 00000013754 15215711721 0017271 0 ustar 00 � �=k��6��:�+��{��i�z{�8�����Y\��EА%�֍,9ztO���"���[�L�0�6�ŷ�X$��?�����?��������"`"���E�k����L�t,�x`����0/S� T�)ʺA�!��am�Gh�H�~�7���zo��.���1�X���o�7�Ș_�䷇�>�)�ⰸ��K߬߸��Z�����o�0�ۤ���%�7�*��y���_FlY����q�BF���c�P�[�X�E�6�P�ֻ����kE~�\*��?08� FG�?�:<�|��j���v�=�1��w�|�Oa��/^�m��E ���P�;�U<O{n;j����;��N���>48�����[*�#˚���,x���}q��U���s�W��z�s��;"�Va���pM�����B}*�(��k K�ע]���-�ٚ+L]�/{?�F$S� �0�� |�`�!^�M�~�16��)��p�(��Q�S��Z|pr��Ż� Z��p {p�íW9$�7D���:8�K��b:V�|�����+ýX�$x�_�.r�2N�8��)`D]?K#�?�q���m9��Dx��s�� `� e�%�c�~�ͷ̢<N���o��?�o�Y�-�#�mя�u�k8����2��*-�wM��?��G��3���n������!i�q�px#�<e��?Z���϶5�O��?�($�O*�cG��� ,f�%��4r���k���$5�:Qr9�c� Ў���у&5\p���܁��1���m��L�����Ͼ�g#��~�:o �?�C��CY��_����[#ʳ ~�0�ݷ����x��p��HL��dU�/1O���.���E� %���0[�����DEi^BGMd�쒊G� ��Q��5܆Y��Y�u��� ,f�8!#��E��8C9H �HJ���� %}L�X���F�"�: 3��ž6.����a��*�z���l��<Kò��B��;XmQC �,H b%�0�� ^Z���P��4r�/U��j���A,f*R&�r�sQ���\:�kqZ/����JI12M�G�Z'"��)��aڛ�'�+�Ͷ �� ���F���s�r�q�`���p��0��G6��B̬����z�N!�V�3jaY�c��~D'P�w�"��%>. �xc��,� I�����m;�TD0>��)/b$g�O^�U�%��A��"*QS`TH.�-fmg�4|��엄��=�?�(�}UuA$T���<�Prb�A�+r&��!��h_�ng��CMC0LX�i����M2ȳ"2��Һ�͵Y$�z6�B�~�d��o9Ŭ����4'�Bh�0ēO���c_�"N;8^~�k�Vr���I�1�֎�ԚE�)�L �S5s_�I�C $,#8H`$��#�3 �8J��%-f�����HΝ� _ϔP����Q@�N�u��N���ࠩ˙���mv�����'�c��Q\� ,� �!�*~�X��i1�w� G�l�B��e7�lVqG�,���>��֜O1$[��љ�� � E3#�.����I��R��:Ϩ�W���2y�Ti̶N�z�����(��I,�h�N��q�RH�$Bo�������������I�4jX]T'�Q��G��v4�%��&i�JR,��j(�;�֠y!*�=i�md&�� ��/�]����������=�u\O�)���a��� V�>���j�o�"��Xx�B�6,K|GH���y���i4 ��L*ЩGB�4|��tJ#J5�$��J)�u.I6�j��{g*�iT�!o������t'i&�uQ�L�a�3F�4I�l�$M)���CS����R�ipJQt����, ��P��}Oy�l� ���Ӻd(����d�fS� �(�놶1��j*щ�I� �t���M�0���-F#���&���ܣ�?�`?E�*��(N�M�JC�H��G� :�_r��Q�7��Ђ���1bC/9�����OQ�^N�|1���t��:5*%�T��m4q�jV����8t�\e���Z�3�������c��Q�¿���ŬGPH�'�l�J&T���ɮ}Wc�]���=h��)���3�1���\Y����^t�o�������ϧe @�$�g���z&�O%��҆��)��-��T�c"�==?5pj#�b�_I0d@`��Y �D���(��⏥j(T�p�dI�U)�Yr�Mɐ/P��깂�J�QaU�іΛ��v �,��>[�Ec�:��h���R �ceTiBU�?Ejq���rp�� ��cا���uEW�M����5���BRtck!�g�h��e)L&z#ډ �O�J���SӼ��Ǧu'/��1s��UKk�w'mM�D��"�Vf]����'��{�њ/��5k#ȇcl�"hzp$ ���4 bA� }xy�~�2���<�; h��I@�y�p���O,c���?���Π�'}��uÈ����1>�����I��?��U =p��y��`���gi��^b�gg�G��� J����d��K54�!�6�T8�J�<e����w�?�����k���s>78��Le��W�s<�����yݠ���L����o�>05�O��?]�<��Ŭ��vs4���E� [�����������}������k{-�ϲ-�'����B9��;$@��L�\�"��.��0]�/��(S�UM�-�_�R �'l����_$���K*$�"�_kiaB�� ����x��*Y�K;JX%��,�xw/�S�6h�@�:LSr�Fĭ'��= �++eG�����(��[m�ipK�x�R�[��!f�wIV%�@v�>:�Kg���]:����ҹ}tn����t~�ߥ��]� �nRl��ۅۥ�v"Ū�1���d��[4A�t؆��!�v)�YRl�����Rv? �=pIMX�sIm�4Qc/<��#�Yo�"�����;������g��������Zj98����` �+�_�jh��BsL�c���=sL�-贠�N�qaB��k�����tg*ct�������9 �x���(�M��� �<4y�2��{�x���5 �_���Ȟ��1�w���p|���K��K��a�2];6���H��ӟyq��̋� �y���(c��-�t�ok�8M�����s�^�(�yQ�]�o�I���a���T��;cةsY�v!bI�L�%a��*�FV�>�J�M���;�*��t������.���wtW�m�M��Cv����#�ۤ{};�?+B���_���e��M=���)�R�=�b� ��+,c�Np�8�Q�)�e�zE��$�ɣ�W[��{1��q��%�pz<K. �l�����T?�ܝ!�nivw;8LH�p�W �[N�]�7��OAH�C7ɕ��~H���a^���{1��P���L�պwghe���Lé�����aL�����������P\���ײ�7}��? �L��Olin���쏽���_�w�`����/���q�:��۞۹����I�/���UU$��oU�Za�l�q����v�c5&����Xd�f��Z��o��M2�9�'��b��u��DJO�,��<�"h>@��&��Y��)�}�)�g;o�g�3i3q Ӈ�&�O��'U;�P�i��,'�@�?1�E�zF�7�G<e�>%��4�,���R��PtG��Pj>�ވ�U��ժ�'<��ӈ��~2�f��J�IOF�/�ɨ�d�M<%�J�AS�*��ȳF�q�X1R��%l��?�iM�c���,C�*M�s�8����@��?�k��~�q8^���IsP�y�qPǾTBv�Vi8vy��?�^ﯲ�<8��������?��mdԬB��t��(���j���]L��2A`ڦ�������7�\���*�$��O�R��°�K�ލ��o��4~o��.RsB��VV.�93]�ZQ �T� �1q�m�FcZ�2X�Ļ���� �i�(i�Ei�T��_�U]"���^�|���)7Y�>���xrZ Z�m�~zz*�Zn�j�~6+��.�ݗ��}����G�>6�Y��wa��{�ߔ��h!�Υ�96�yH�GV�۰�.�Q4����f�+�"��ʉ"{-eۛ�U����#��� ���e\4=�V����Oݶ��H�k�4L՟K���M�zl ���o���3��vrry�ۑ�P� iED�cD��{f�Z�6����`�B���b4)�Jh�{���0�����I>��$�fq�)�6����c"�b?¨�K"�C�Wo*B,�h��=��Mo5�!��ěLep�Ϩ����mV�����V6����H���BE�dqR���?�x��kj�4o�m�X��35��[~��e�#���=��# �B�eSQ�����<\}^ ��d�ۀNEF���H�T-8[�懫f�\����曇�f�Z5�<[��9'W W5�h8��R6i(�)" M%�ft"o�R�#�k:�ƛw<$�o� �BR��%V��u1���Q�P)�nd����h����E�R/�d�+���:��(���Sfy��K~���6՛��b&(\�T���DVM�-���ù ���P^���>3��:�GD�Cs_`�F��f�N��{�Y�"E�y�0�0����u��]����-��;����Hu$����}L�^Şx��}��Uݪ�W���Yj6k_Dq�=o5�ٛb�;TKFpվ<����8(8������jz}:?������jz}*�L��7o�O���-�)8���&��~Z�� 3vP=š";��Y�K�ۖ^�h�����i����I�����`�nI�p�C_�IN ���� ��a{�s�C��= �m�H��(��Z������b�# ���B:�ga��� �t���g��],�&�t��0�ޤ��J��/�lZ�@�6�������v�3��۳���b���͎���KaǬ �|U �����w�G��i�n`0~�B�9�1D�q�D� ���$��~�a,�Ϫ��;�>��|~�t�f&{d��c�Ϲ3=������3y��Dp�>/c'\��1���y]��r/�P]d�=�ᢎ�������I�$�?��Ϡ>~��T'(E66�eUc{ܖc@�(ɘ9����@SJٿY�ۙ"Dܮ �hK�B��4("D��R��u,iɥ�k���p����c��~'�ݟ.����p"���y��=���N}��N <|�W���w����o����t���$p���/��k�^�g�������I�h��~�G�O}8-��"�����t�ܙ���c�K������c���*I��J�[R' ��B`clr�!;+�������$�;�7��yZ�������������yx��������g}N nB�s��7�-�Z�OZ�{������i��I�e�_��@��-�ۺ �����;�W� =��������������|�����= �L���ϜH�����ʴxF��y�����wy���sm-�� ���n������4�2�?�����lA}�)�7(c��m��[�?��M��1��?�y-� �{�|\�~����1�ρ* ɳ��c!^�<Maa�=�r7VLW���?�(e� ��Ҩr���y��٧a6��;���j�� ���g���ʳ��9�<��� ����'=:��h��H���gi�{D;>��K̛>�Z���D+�x�W̓� ͕R��y��A��;�'�C��(����Mg�e�E�����齄��H|��}a=��2T��cC��+�-9�u�5X�f�r Ǵ־}l9/�S:,@v�iI� 4\� �� <�uP moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test 0000644 00000000335 15215711721 0022760 0 ustar 00 <gradebook some_other_value="false" calculations_freeze="20160511" and_another_value="42"> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation 0000644 00000000346 15215711721 0024324 0 ustar 00 <gradebook > <attributes> <calculations_freeze>20160511</calculations_freeze> </attributes> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation 0000644 00000000377 15215711721 0024331 0 ustar 00 <gradebook some_other_value="false" > <attributes> <calculations_freeze>20160511</calculations_freeze> </attributes> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test 0000644 00000000351 15215711721 0022757 0 ustar 00 <gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42"> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebookplugin> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test 0000644 00000000255 15215711721 0022757 0 ustar 00 <gradebook calculations_freeze="20160511"> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test 0000644 00000000306 15215711721 0022755 0 ustar 00 <gradebook some_other_value="false" calculations_freeze="20160511"> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation 0000644 00000000351 15215711721 0024323 0 ustar 00 <gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42"> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebookplugin> moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation 0000644 00000000426 15215711721 0024325 0 ustar 00 <gradebook some_other_value="false" and_another_value="42"> <attributes> <calculations_freeze>20160511</calculations_freeze> </attributes> <grade_categories> <grade_category id="10"> <depth>1</depth> </grade_category> </grade_categories> </gradebook> moodle2/tests/moodle2_test.php 0000644 00000145630 15215711721 0012373 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use backup_setting; use restore_controller; use restore_dbops; 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'); /** * Tests for Moodle 2 format backup operation. * * @package core_backup * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class moodle2_test extends \advanced_testcase { /** * Tests the availability field on modules and sections is correctly * backed up and restored. */ public function test_backup_availability(): void { global $DB, $CFG; $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)); // Backup and restore it. $newcourseid = $this->backup_and_restore($course); // Check settings in new course. $modinfo = get_fast_modinfo($newcourseid); $forums = array_values($modinfo->get_instances_of('forum')); $assigns = array_values($modinfo->get_instances_of('assign')); $newassign = new \assign(\context_module::instance($assigns[0]->id), false, false); $newitem = $newassign->get_grade_item(); $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid)); // Expected availability should have new ID for the forum, grade, and grouping. $newavailability = str_replace( '"grouping","id":' . $grouping->id, '"grouping","id":' . $newgroupingid, str_replace( '"grade","id":' . $item->id, '"grade","id":' . $newitem->id, str_replace( '"cm":' . $forum2->cmid, '"cm":' . $forums[1]->id, $availability))); $this->assertEquals($newavailability, $forums[0]->availability); $this->assertNull($forums[1]->availability); $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability); $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability); } /** * The availability data format was changed in Moodle 2.7. This test * ensures that a Moodle 2.6 backup with this data can still be correctly * restored. */ public function test_restore_legacy_availability(): void { global $DB, $USER, $CFG; require_once($CFG->dirroot . '/grade/querylib.php'); require_once($CFG->libdir . '/completionlib.php'); $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Extract backup file. $backupid = 'abc'; $backuppath = make_backup_temp_directory($backupid); get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath); // Do restore to new course with default settings. $generator = $this->getDataGenerator(); $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); $newcourseid = restore_dbops::create_new_course( 'Test fullname', 'Test shortname', $categoryid); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $thrown = null; try { $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); } catch (Exception $e) { $thrown = $e; // Because of the PHPUnit exception behaviour in this situation, we // will not see this message unless it is explicitly echoed (just // using it in a fail() call or similar will not work). echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' . $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n"; } $this->assertNull($thrown); // Get information about the resulting course and check that it is set // up correctly. $modinfo = get_fast_modinfo($newcourseid); $pages = array_values($modinfo->get_instances_of('page')); $forums = array_values($modinfo->get_instances_of('forum')); $quizzes = array_values($modinfo->get_instances_of('quiz')); $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid)); // FROM date. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}', $pages[1]->availability); // UNTIL date. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}', $pages[2]->availability); // FROM and UNTIL. $this->assertEquals( '{"op":"&","showc":[true,false],"c":[' . '{"type":"date","d":">=","t":1449705600},' . '{"type":"date","d":"<","t":1893456000}' . ']}', $pages[3]->availability); // Grade >= 75%. $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true)); $gradeid = $grades[0]->id; $coursegrade = \grade_item::fetch_course_item($newcourseid); $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}', $pages[4]->availability); // Grade < 25%. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}', $pages[5]->availability); // Grade 90-100%. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}', $pages[6]->availability); // Email contains frog. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}', $pages[7]->availability); // Page marked complete.. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', $pages[8]->availability); // Quiz complete but failed. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}', $pages[9]->availability); // Quiz complete and succeeded. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_COMPLETE_PASS. '}]}', $pages[10]->availability); // Quiz not complete. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id . ',"e":' . COMPLETION_INCOMPLETE . '}]}', $pages[11]->availability); // Grouping. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', $pages[12]->availability); // All the options. $this->assertEquals('{"op":"&",' . '"showc":[false,true,false,true,true,true,true,true,true],' . '"c":[' . '{"type":"grouping","id":' . $grouping->id . '},' . '{"type":"date","d":">=","t":1488585600},' . '{"type":"date","d":"<","t":1709510400},' . '{"type":"profile","op":"contains","sf":"email","v":"@"},' . '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' . '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' . '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' . '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' . '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' . ']}', $pages[13]->availability); // Group members only forum. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"group"}]}', $forums[0]->availability); // Section with lots of conditions. $this->assertEquals( '{"op":"&","showc":[false,false,false,false],"c":[' . '{"type":"date","d":">=","t":1417737600},' . '{"type":"profile","op":"contains","sf":"email","v":"@"},' . '{"type":"grade","id":' . $gradeid . ',"min":20},' . '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}', $modinfo->get_section_info(3)->availability); // Section with grouping. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}', $modinfo->get_section_info(4)->availability); } /** * Tests the backup and restore of single activity to same course (duplicate) * when it contains availability conditions that depend on other items in * course. */ public function test_duplicate_availability(): void { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Create a course with completion enabled and 2 forums. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED)); $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 group and grouping as well. $group = $generator->create_group(array('courseid' => $course->id, 'name' => 'Group!')); $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); // Set the forum to have availability conditions on all those things, // plus some that don't exist or are special values. $availability = '{"op":"|","show":false,"c":[' . '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . '{"type":"completion","cm":99999999,"e":1},' . '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . '{"type":"grade","id":99999998,"min":4,"max":94},' . '{"type":"grouping","id":' . $grouping->id . '},' . '{"type":"grouping","id":99999997},' . '{"type":"group","id":' . $group->id . '},' . '{"type":"group"},' . '{"type":"group","id":99999996}' . ']}'; $DB->set_field('course_modules', 'availability', $availability, array( 'id' => $forum->cmid)); // Duplicate it. $newcmid = $this->duplicate($course, $forum->cmid); // For those which still exist on the course we expect it to keep using // the real ID. For those which do not exist on the course any more // (e.g. simulating backup/restore of single activity between 2 courses) // we expect the IDs to be replaced with marker value: 0 for cmid // and grade, -1 for group/grouping. $expected = str_replace( array('99999999', '99999998', '99999997', '99999996'), array(0, 0, -1, -1), $availability); // Check settings in new activity. $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid)); $this->assertEquals($expected, $actual); } /** * When restoring a course, you can change the start date, which shifts other * dates. This test checks that certain dates are correctly modified. */ public function test_restore_dates(): void { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; // Create a course with specific start date. $generator = $this->getDataGenerator(); $course = $generator->create_course(array( 'startdate' => strtotime('1 Jan 2014 00:00 GMT'), 'enddate' => strtotime('3 Aug 2014 00:00 GMT') )); // Add a forum with conditional availability date restriction, including // one of them nested inside a tree. $availability = '{"op":"&","showc":[true,true],"c":[' . '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' . '{"type":"date","d":"<","t":DATE2}]}'; $before = str_replace( array('DATE1', 'DATE2'), array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')), $availability); $forum = $generator->create_module('forum', array('course' => $course->id, 'availability' => $before)); // Add an assign with defined start date. $assign = $generator->create_module('assign', array('course' => $course->id, 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT'))); // Do backup and restore. $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT')); $newcourse = $DB->get_record('course', array('id' => $newcourseid)); $this->assertEquals(strtotime('5 Aug 2015 00:00 GMT'), $newcourse->enddate); $modinfo = get_fast_modinfo($newcourseid); // Check forum dates are modified by the same amount as the course start. $newforums = $modinfo->get_instances_of('forum'); $newforum = reset($newforums); $after = str_replace( array('DATE1', 'DATE2'), array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')), $availability); $this->assertEquals($after, $newforum->availability); // Check assign date. $newassigns = $modinfo->get_instances_of('assign'); $newassign = reset($newassigns); $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field( 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance))); } /** * Test front page backup/restore and duplicate activities * @return void */ public function test_restore_frontpage(): void { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $frontpage = $DB->get_record('course', array('id' => SITEID)); $forum = $generator->create_module('forum', array('course' => $frontpage->id)); // Activities can be duplicated. $this->duplicate($frontpage, $forum->cmid); $modinfo = get_fast_modinfo($frontpage); $this->assertEquals(2, count($modinfo->get_instances_of('forum'))); // Front page backup. $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $frontpagebackupid = $frontpagebc->get_backupid(); $frontpagebc->execute_plan(); $frontpagebc->destroy(); $course = $generator->create_course(); $newcourseid = restore_dbops::create_new_course( $course->fullname . ' 2', $course->shortname . '_2', $course->category); // Other course backup. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $otherbackupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // We can only restore a front page over the front page. $rc = new restore_controller($frontpagebackupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($frontpagebackupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($frontpagebackupid, $frontpage->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // We can't restore a non-front page course on the front page course. $rc = new restore_controller($otherbackupid, $frontpage->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertFalse($rc->execute_precheck()); $rc->destroy(); $rc = new restore_controller($otherbackupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); } /** * Backs a course up and restores it. * * @param \stdClass $course Course object to backup * @param int $newdate If non-zero, specifies custom date for new course * @param callable|null $inbetween If specified, function that is called before restore * @param bool $userdata Whether the backup/restory must be with user data or not. * @return int ID of newly restored course */ protected function backup_and_restore($course, $newdate = 0, $inbetween = null, bool $userdata = false) { global $USER, $CFG; // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); $bc->get_plan()->get_setting('users')->set_value($userdata); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); if ($inbetween) { $inbetween($backupid); } // Do restore to new course with default settings. $newcourseid = restore_dbops::create_new_course( $course->fullname, $course->shortname . '_2', $course->category); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); if ($newdate) { $rc->get_plan()->get_setting('course_startdate')->set_value($newdate); } $rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); $rc->get_plan()->get_setting('users')->set_value($userdata); if ($userdata) { $rc->get_plan()->get_setting('xapistate')->set_value(true); } $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); return $newcourseid; } /** * Duplicates a single activity within a course. * * This is based on the code from course/modduplicate.php, but reduced for * simplicity. * * @param \stdClass $course Course object * @param int $cmid Activity to duplicate * @return int ID of new activity */ protected function duplicate($course, $cmid) { global $USER; // Do backup. $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Do restore. $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); // Find cmid. $tasks = $rc->get_plan()->get_tasks(); $cmcontext = \context_module::instance($cmid); $newcmid = 0; foreach ($tasks as $task) { if (is_subclass_of($task, 'restore_activity_task')) { if ($task->get_old_contextid() == $cmcontext->id) { $newcmid = $task->get_moduleid(); break; } } } $rc->destroy(); if (!$newcmid) { throw new \coding_exception('Unexpected: failure to find restored cmid'); } return $newcmid; } /** * Help function for enrolment methods backup/restore tests: * * - Creates a course ($course), adds self-enrolment method and a user * - Makes a backup * - Creates a target course (if requested) ($newcourseid) * - Initialises restore controller for this backup file ($rc) * * @param int $target target for restoring: backup::TARGET_NEW_COURSE etc. * @param array $additionalcaps - additional capabilities to give to user * @return array array of original course, new course id, restore controller: [$course, $newcourseid, $rc] */ protected function prepare_for_enrolments_test($target, $additionalcaps = []) { global $CFG, $DB; $this->resetAfterTest(true); // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; $user = $this->getDataGenerator()->create_user(); $roleidcat = create_role('Category role', 'dummyrole1', 'dummy role description'); $course = $this->getDataGenerator()->create_course(); // Enable instance of self-enrolment plugin (it should already be present) and enrol a student with it. $selfplugin = enrol_get_plugin('self'); $selfinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'self')); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $selfplugin->update_status($selfinstance, ENROL_INSTANCE_ENABLED); $selfplugin->enrol_user($selfinstance, $user->id, $studentrole->id); // Give current user capabilities to do backup and restore and assign student role. $categorycontext = \context_course::instance($course->id)->get_parent_context(); $caps = array_merge([ 'moodle/course:view', 'moodle/course:create', 'moodle/backup:backupcourse', 'moodle/backup:configure', 'moodle/backup:backuptargetimport', 'moodle/restore:restorecourse', 'moodle/role:assign', 'moodle/restore:configure', ], $additionalcaps); foreach ($caps as $cap) { assign_capability($cap, CAP_ALLOW, $roleidcat, $categorycontext); } core_role_set_assign_allowed($roleidcat, $studentrole->id); role_assign($roleidcat, $user->id, $categorycontext); accesslib_clear_all_caches_for_unit_testing(); $this->setUser($user); // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id); $backupid = $bc->get_backupid(); $backupbasepath = $bc->get_plan()->get_basepath(); $bc->execute_plan(); $results = $bc->get_results(); $file = $results['backup_destination']; $bc->destroy(); // Restore the backup immediately. // Check if we need to unzip the file because the backup temp dir does not contains backup files. if (!file_exists($backupbasepath . "/moodle_backup.xml")) { $file->extract_to_pathname(get_file_packer('application/vnd.moodle.backup'), $backupbasepath); } if ($target == backup::TARGET_NEW_COURSE) { $newcourseid = restore_dbops::create_new_course($course->fullname . '_2', $course->shortname . '_2', $course->category); } else { $newcourse = $this->getDataGenerator()->create_course(); $newcourseid = $newcourse->id; } $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $user->id, $target); return [$course, $newcourseid, $rc]; } /** * Backup a course with enrolment methods and restore it without user data and without enrolment methods */ public function test_restore_without_users_without_enrolments(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE); // Ensure enrolment methods will not be restored without capability. $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Self-enrolment method was not enabled, users were not restored. $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED])); $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; $enrolments = $DB->get_records_sql($sql, [$newcourseid]); $this->assertEmpty($enrolments); } /** * Backup a course with enrolment methods and restore it without user data with enrolment methods */ public function test_restore_without_users_with_enrolments(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, ['moodle/course:enrolconfig']); // Ensure enrolment methods will be restored. $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(false, $rc->get_plan()->get_setting('users')->get_value()); // Set "Include enrolment methods" to "Always" so they can be restored without users. $rc->get_plan()->get_setting('enrolments')->set_value(backup::ENROL_ALWAYS); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Self-enrolment method was restored (it is enabled), users were not restored. $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED]); $this->assertNotEmpty($enrol); $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; $enrolments = $DB->get_records_sql($sql, [$newcourseid]); $this->assertEmpty($enrolments); } /** * Backup a course with enrolment methods and restore it with user data and without enrolment methods */ public function test_restore_with_users_without_enrolments(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, ['moodle/backup:userinfo', 'moodle/restore:userinfo']); // Ensure enrolment methods will not be restored without capability. $this->assertEquals(backup::ENROL_NEVER, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); global $qwerty; $qwerty = 1; $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); $qwerty = 0; // Self-enrolment method was not restored, student was restored as manual enrolment. $this->assertEmpty($DB->count_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED])); $enrol = $DB->get_record('enrol', ['enrol' => 'manual', 'courseid' => $newcourseid]); $this->assertEquals(1, $DB->count_records('user_enrolments', ['enrolid' => $enrol->id])); } /** * Backup a course with enrolment methods and restore it with user data with enrolment methods */ public function test_restore_with_users_with_enrolments(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_NEW_COURSE, ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); // Ensure enrolment methods will be restored. $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Self-enrolment method was restored (it is enabled), student was restored. $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED]); $this->assertNotEmpty($enrol); $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; $enrolments = $DB->get_records_sql($sql, [$newcourseid]); $this->assertEquals(1, count($enrolments)); $enrolment = reset($enrolments); $this->assertEquals('self', $enrolment->enrol); } /** * Backup a course with enrolment methods and restore it with user data with enrolment methods merging into another course */ public function test_restore_with_users_with_enrolments_merging(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_ADDING, ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); // Ensure enrolment methods will be restored. $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // User was restored with self-enrolment method. $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED]); $this->assertNotEmpty($enrol); $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; $enrolments = $DB->get_records_sql($sql, [$newcourseid]); $this->assertEquals(1, count($enrolments)); $enrolment = reset($enrolments); $this->assertEquals('self', $enrolment->enrol); } /** * Backup a course with enrolment methods and restore it with user data with enrolment methods into another course deleting it's contents */ public function test_restore_with_users_with_enrolments_deleting(): void { global $DB; list($course, $newcourseid, $rc) = $this->prepare_for_enrolments_test(backup::TARGET_EXISTING_DELETING, ['moodle/backup:userinfo', 'moodle/restore:userinfo', 'moodle/course:enrolconfig']); // Ensure enrolment methods will be restored. $this->assertEquals(backup::ENROL_WITHUSERS, $rc->get_plan()->get_setting('enrolments')->get_value()); $this->assertEquals(true, $rc->get_plan()->get_setting('users')->get_value()); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Self-enrolment method was restored (it is enabled), student was restored. $enrol = $DB->get_records('enrol', ['enrol' => 'self', 'courseid' => $newcourseid, 'status' => ENROL_INSTANCE_ENABLED]); $this->assertNotEmpty($enrol); $sql = "select ue.id, ue.userid, e.enrol from {user_enrolments} ue join {enrol} e on ue.enrolid = e.id WHERE e.courseid = ?"; $enrolments = $DB->get_records_sql($sql, [$newcourseid]); $this->assertEquals(1, count($enrolments)); $enrolment = reset($enrolments); $this->assertEquals('self', $enrolment->enrol); } /** * Test the block instance time fields (timecreated, timemodified) through a backup and restore. */ public function test_block_instance_times_backup(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); // Create course and add HTML block. $course = $generator->create_course(); $context = \context_course::instance($course->id); $page = new \moodle_page(); $page->set_context($context); $page->set_course($course); $page->set_pagelayout('standard'); $page->set_pagetype('course-view'); $page->blocks->load_blocks(); $page->blocks->add_block_at_end_of_default_region('html'); // Update (hack in database) timemodified and timecreated to specific values for testing. $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $context->id]); $originalblockid = $blockdata->id; $blockdata->timecreated = 12345; $blockdata->timemodified = 67890; $DB->update_record('block_instances', $blockdata); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); // Confirm that values were transferred correctly into HTML block on new course. $newcontext = \context_course::instance($newcourseid); $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); $this->assertEquals(12345, $blockdata->timecreated); $this->assertEquals(67890, $blockdata->timemodified); // Simulate what happens with an older backup that doesn't have those fields, by removing // them from the backup before doing a restore. $before = time(); $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) { global $CFG; $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' . $originalblockid . '/block.xml'; $xml = file_get_contents($path); $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml); file_put_contents($path, $xml); }); $after = time(); // The fields not specified should default to current time. $newcontext = \context_course::instance($newcourseid); $blockdata = $DB->get_record('block_instances', ['blockname' => 'html', 'parentcontextid' => $newcontext->id]); $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated); $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified); } /** * When you restore a site with global search (or search indexing) turned on, then it should * add entries to the search index requests table so that the data gets indexed. */ public function test_restore_search_index_requests(): void { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableglobalsearch = true; // Create a course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Add a forum. $forum = $generator->create_module('forum', ['course' => $course->id]); // Add a block. $context = \context_course::instance($course->id); $page = new \moodle_page(); $page->set_context($context); $page->set_course($course); $page->set_pagelayout('standard'); $page->set_pagetype('course-view'); $page->blocks->load_blocks(); $page->blocks->add_block_at_end_of_default_region('html'); // Initially there should be no search index requests. $this->assertEquals(0, $DB->count_records('search_index_requests')); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); // Now the course should be requested for index (all search areas). $newcontext = \context_course::instance($newcourseid); $requests = array_values($DB->get_records('search_index_requests')); $this->assertCount(1, $requests); $this->assertEquals($newcontext->id, $requests[0]->contextid); $this->assertEquals('', $requests[0]->searcharea); get_fast_modinfo($newcourseid); // Backup the new course... $CFG->backup_file_logger_level = backup::LOG_NONE; $bc = new backup_controller(backup::TYPE_1COURSE, $newcourseid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore it on top of old course (should duplicate the forum). $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Get the forums now on the old course. $modinfo = get_fast_modinfo($course->id); $forums = $modinfo->get_instances_of('forum'); $this->assertCount(2, $forums); // The newer one will be the one with larger ID. (Safe to assume for unit test.) $biggest = null; foreach ($forums as $forum) { if ($biggest === null || $biggest->id < $forum->id) { $biggest = $forum; } } $restoredforumcontext = \context_module::instance($biggest->id); // Get the HTML blocks now on the old course. $blockdata = array_values($DB->get_records('block_instances', ['blockname' => 'html', 'parentcontextid' => $context->id], 'id DESC')); $restoredblockcontext = \context_block::instance($blockdata[0]->id); // Check that we have requested index update on both the module and the block. $requests = array_values($DB->get_records('search_index_requests', null, 'id')); $this->assertCount(3, $requests); $this->assertEquals($restoredblockcontext->id, $requests[1]->contextid); $this->assertEquals('', $requests[1]->searcharea); $this->assertEquals($restoredforumcontext->id, $requests[2]->contextid); $this->assertEquals('', $requests[2]->searcharea); } /** * Test restoring courses based on the backup plan. Primarily used with * the import functionality */ public function test_restore_course_using_plan_defaults(): void { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableglobalsearch = true; // Set admin config setting so that activities are not restored by default. set_config('restore_general_activities', 0, 'restore'); // Create a course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $course2 = $generator->create_course(); $course3 = $generator->create_course(); // Add a forum. $forum = $generator->create_module('forum', ['course' => $course->id]); // Backup course... $CFG->backup_file_logger_level = backup::LOG_NONE; $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Restore it on top of course2 (should duplicate the forum). $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_EXISTING_ADDING, null, backup::RELEASESESSION_NO); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Get the forums now on the old course. $modinfo = get_fast_modinfo($course2->id); $forums = $modinfo->get_instances_of('forum'); $this->assertCount(0, $forums); } /** * The Question category hierarchical structure was changed in Moodle 3.5. * From 3.5, all question categories in each context are a child of a single top level question category for that context. * This test ensures that both Moodle 3.4 and 3.5 backups can still be correctly restored. */ public function test_restore_question_category_34_35(): void { global $DB, $USER, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); $backupfiles = array('question_category_34_format', 'question_category_35_format'); foreach ($backupfiles as $backupfile) { // Extract backup file. $backupid = $backupfile; $backuppath = make_backup_temp_directory($backupid); get_file_packer('application/vnd.moodle.backup')->extract_to_pathname( __DIR__ . "/fixtures/$backupfile.mbz", $backuppath); // Do restore to new course with default settings. $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}"); $newcourseid = restore_dbops::create_new_course( 'Test fullname', 'Test shortname', $categoryid); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); // Get information about the resulting course and check that it is set up correctly. $modinfo = get_fast_modinfo($newcourseid); $quizzes = array_values($modinfo->get_instances_of('quiz')); $contexts = $quizzes[0]->context->get_parent_contexts(true); $topcategorycount = []; foreach ($contexts as $context) { $cats = $DB->get_records('question_categories', array('contextid' => $context->id), 'parent', 'id, name, parent'); // Make sure all question categories that were inside the backup file were restored correctly. if ($context->contextlevel == CONTEXT_COURSE) { $this->assertEquals(['top', 'Default for C101'], array_column($cats, 'name')); } else if ($context->contextlevel == CONTEXT_MODULE) { $this->assertEquals(['top', 'Default for Q1'], array_column($cats, 'name')); } $topcategorycount[$context->id] = 0; foreach ($cats as $cat) { if (!$cat->parent) { $topcategorycount[$context->id]++; } } // Make sure there is a single top level category in this context. if ($cats) { $this->assertEquals(1, $topcategorycount[$context->id]); } } } } /** * Test the content bank content through a backup and restore. */ public function test_contentbank_content_backup(): void { global $DB, $USER, $CFG; $this->resetAfterTest(); $this->setAdminUser(); $generator = $this->getDataGenerator(); $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); // Create course and add content bank content. $course = $generator->create_course(); $context = \context_course::instance($course->id); $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p'; $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath); $this->assertEquals(2, $DB->count_records('contentbank_content')); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); // Confirm that values were transferred correctly into content bank on new course. $newcontext = \context_course::instance($newcourseid); $this->assertEquals(4, $DB->count_records('contentbank_content')); $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id])); } /** * Test the xAPI state through a backup and restore. * * @covers \backup_xapistate_structure_step * @covers \restore_xapistate_structure_step */ public function test_xapistate_backup(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $this->setUser($user); /** @var \mod_h5pactivity_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); /** @var \core_h5p_generator $h5pgenerator */ $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); // Add an attempt to the H5P activity. $attemptinfo = [ 'userid' => $user->id, 'h5pactivityid' => $activity->id, 'attempt' => 1, 'interactiontype' => 'compound', 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0, ]; $generator->create_attempt($attemptinfo); // Add also a xAPI state to the H5P activity. $filerecord = [ 'contextid' => \context_module::instance($activity->cmid)->id, 'component' => 'mod_h5pactivity', 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; $h5pgenerator->generate_h5p_data(false, $filerecord); // Check the H5P activity exists and the attempt has been created. $this->assertEquals(1, $DB->count_records('h5pactivity')); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Do backup and restore. $this->setAdminUser(); $newcourseid = $this->backup_and_restore($course, 0, null, true); // Confirm that values were transferred correctly into H5P activity on new course. $this->assertEquals(2, $DB->count_records('h5pactivity')); $this->assertEquals(4, $DB->count_records('grade_items')); $this->assertEquals(4, $DB->count_records('grade_grades')); $this->assertEquals(2, $DB->count_records('xapi_states')); $newactivity = $DB->get_record('h5pactivity', ['course' => $newcourseid]); $cm = get_coursemodule_from_instance('h5pactivity', $newactivity->id); $context = \context_module::instance($cm->id); $this->assertEquals(1, $DB->count_records('xapi_states', ['itemid' => $context->id])); } } moodle2/tests/moodle2_course_format_test.php 0000644 00000020522 15215711721 0015313 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use restore_dbops; use restore_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'); require_once($CFG->dirroot . '/course/format/topics/lib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->dirroot . '/backup/moodle2/tests/fixtures/format_test_cs_options.php'); /** * Tests for Moodle 2 course format section_options backup operation. * * @package core_backup * @copyright 2014 Russell Smith * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class moodle2_course_format_test extends \advanced_testcase { /** * Tests a backup and restore adds the required section option data * when the same course format is used. */ public function test_course_format_options_restore(): void { global $DB, $CFG; $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' => 'test_cs_options', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $courseobject = \core_courseformat\base::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1), '*', MUST_EXIST); $data = array('id' => $section->id, 'numdaystocomplete' => 2); $courseobject->update_section_format_options($data); // Backup and restore it. $this->backup_and_restore($course); $sectionoptions = $courseobject->get_format_options(1); $this->assertArrayHasKey('numdaystocomplete', $sectionoptions); $this->assertEquals(2, $sectionoptions['numdaystocomplete']); } /** * Tests an import into the same subject successfully * restores the options without error. */ public function test_course_format_options_import_myself(): void { global $DB, $CFG; $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' => 'test_cs_options', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $courseobject = \core_courseformat\base::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1), '*', MUST_EXIST); $data = array('id' => $section->id, 'numdaystocomplete' => 2); $courseobject->update_section_format_options($data); $this->backup_and_restore($course, $course, backup::TARGET_EXISTING_ADDING); $sectionoptions = $courseobject->get_format_options(1); $this->assertArrayHasKey('numdaystocomplete', $sectionoptions); $this->assertArrayNotHasKey('secondparameter', $sectionoptions); $this->assertEquals(2, $sectionoptions['numdaystocomplete']); } /** * Tests that all section options are copied when the course format is changed. * None of the data is copied. * * It is a future enhancement to copy; * 1. Only the relevant options. * 2. Only the data associated with relevant options. */ public function test_course_format_options_restore_new_format(): void { global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); // Create a source course using the test_cs2_options format. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'test_cs2_options', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); // Create a target course using test_cs_options format. $newcourse = $generator->create_course( array('format' => 'test_cs_options', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); // Set section 2 to have both options, and a name. $courseobject = \core_courseformat\base::instance($course->id); $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2), '*', MUST_EXIST); $data = array('id' => $section->id, 'numdaystocomplete' => 2, 'secondparameter' => 8 ); $courseobject->update_section_format_options($data); $DB->set_field('course_sections', 'name', 'Frogs', array('id' => $section->id)); // Backup and restore to the new course using 'add to existing' so it // keeps the current (test_cs_options) format. $this->backup_and_restore($course, $newcourse, backup::TARGET_EXISTING_ADDING); // Check that the section contains the options suitable for the new // format and that even the one with the same name as from the old format // has NOT been set. $newcourseobject = \core_courseformat\base::instance($newcourse->id); $sectionoptions = $newcourseobject->get_format_options(2); $this->assertArrayHasKey('numdaystocomplete', $sectionoptions); $this->assertArrayNotHasKey('secondparameter', $sectionoptions); $this->assertEquals(0, $sectionoptions['numdaystocomplete']); // However, the name should have been changed, as this does not depend // on the format. $modinfo = get_fast_modinfo($newcourse->id); $section = $modinfo->get_section_info(2); $this->assertEquals('Frogs', $section->name); } /** * Backs a course up and restores it. * * @param \stdClass $srccourse Course object to backup * @param \stdClass $dstcourse Course object to restore into * @param int $target Target course mode (backup::TARGET_xx) * @return int ID of newly restored course */ protected function backup_and_restore($srccourse, $dstcourse = null, $target = backup::TARGET_NEW_COURSE) { global $USER, $CFG; // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new backup_controller(backup::TYPE_1COURSE, $srccourse->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Do restore to new course with default settings. if ($dstcourse !== null) { $newcourseid = $dstcourse->id; } else { $newcourseid = restore_dbops::create_new_course( $srccourse->fullname, $srccourse->shortname . '_2', $srccourse->category); } $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, $target); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); return $newcourseid; } } moodle2/tests/backup_stepslib_test.php 0000644 00000006764 15215711721 0014210 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use backup_section_structure_step; use backup_section_task; /** * Tests for Moodle 2 steplib classes. * * @package core_backup * @copyright 2023 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_stepslib_test extends \advanced_testcase { /** * Setup to include all libraries. */ public static function setUpBeforeClass(): void { 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->dirroot . '/backup/moodle2/backup_stepslib.php'); parent::setUpBeforeClass(); } /** * Test for the section structure step included elements. * * @covers \backup_section_structure_step::define_structure */ public function test_backup_section_structure_step(): void { global $USER; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']); $this->setAdminUser(); $step = new backup_section_structure_step('section_commons', 'section.xml'); // The backup_section_structure_step requires a complex dependency sequence // but it does not have an easy dependency injection system. // We create a real backup plan to get the task dependency sequence ready. $bc = new backup_controller( backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $tasks = $bc->get_plan()->get_tasks(); foreach ($tasks as $task) { // We need only the task to backup section 1. if ($task instanceof backup_section_task && $task->get_name() == "1") { $task->add_step($step); break; } } $reflection = new \ReflectionClass($step); $method = $reflection->getMethod('define_structure'); $structure = $method->invoke($step); $bc->destroy(); $elements = $structure->get_final_elements(); $this->assertArrayHasKey('number', $elements); $this->assertArrayHasKey('name', $elements); $this->assertArrayHasKey('summary', $elements); $this->assertArrayHasKey('summaryformat', $elements); $this->assertArrayHasKey('sequence', $elements); $this->assertArrayHasKey('visible', $elements); $this->assertArrayHasKey('availabilityjson', $elements); $this->assertArrayHasKey('component', $elements); $this->assertArrayHasKey('itemid', $elements); $this->assertArrayHasKey('timemodified', $elements); } } moodle2/tests/restore_stepslib_test.php 0000644 00000012221 15215711721 0014407 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; /** * Tests for Moodle 2 restore steplib classes. * * @package core_backup * @copyright 2023 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_stepslib_test extends \advanced_testcase { /** * Setup to include all libraries. */ public static function setUpBeforeClass(): void { 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->dirroot . '/backup/moodle2/restore_stepslib.php'); parent::setUpBeforeClass(); } /** * Makes a backup of the course. * * @param \stdClass $course The course object. * @return string Unique identifier for this backup. */ protected function backup_course(\stdClass $course): string { global $CFG, $USER; // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new \backup_controller( backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id ); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); return $backupid; } /** * Restores a backup that has been made earlier. * * @param string $backupid The unique identifier of the backup. * @return int The new course id. */ protected function restore_replacing_content(string $backupid): int { global $CFG, $USER; // Create course to restore into, and a user to do the restore. $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do restore to new course with default settings. $rc = new \restore_controller( $backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_DELETING ); $precheck = $rc->execute_precheck(); $this->assertTrue($precheck); $rc->get_plan()->get_setting('role_assignments')->set_value(true); $rc->get_plan()->get_setting('permissions')->set_value(true); $rc->execute_plan(); $rc->destroy(); return $course->id; } /** * Test for delegate section behaviour. * * @covers \restore_section_structure_step::process_section */ public function test_restore_section_structure_step(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(['numsections' => 2, 'format' => 'topics']); // Section 2 has an existing delegate class for component that is not an activity. course_update_section( $course, $DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]), [ 'component' => 'test_component', 'itemid' => 1, ] ); $backupid = $this->backup_course($course); $newcourseid = $this->restore_replacing_content($backupid); $originalsections = get_fast_modinfo($course->id)->get_section_info_all(); $restoredsections = get_fast_modinfo($newcourseid)->get_section_info_all(); // Delegated sections depends on the plugin to be backuped and restored. // In this case, the plugin is not backuped and restored, so the section is not restored. $this->assertEquals(3, count($originalsections)); $this->assertEquals(2, count($restoredsections)); $validatefields = ['name', 'summary', 'summaryformat', 'visible', 'component', 'itemid']; $this->assertEquals($originalsections[1]->name, $restoredsections[1]->name); foreach ($validatefields as $field) { $this->assertEquals($originalsections[0]->$field, $restoredsections[0]->$field); $this->assertEquals($originalsections[1]->$field, $restoredsections[1]->$field); } } } moodle2/tests/restore_stepslib_date_test.php 0000644 00000044752 15215711721 0015422 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use mod_quiz\quiz_attempt; use mod_quiz\quiz_settings; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php"); require_once($CFG->libdir . "/badgeslib.php"); require_once($CFG->dirroot . '/mod/assign/tests/base_test.php'); /** * Restore date tests. * * @package core_backup * @copyright 2017 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_stepslib_date_test extends \restore_date_testcase { /** * Restoring a manual grade item does not result in the timecreated or * timemodified dates being changed. */ public function test_grade_item_date_restore(): void { $course = $this->getDataGenerator()->create_course(['startdate' => time()]); $params = new \stdClass(); $params->courseid = $course->id; $params->fullname = 'unittestgradecalccategory'; $params->aggregation = GRADE_AGGREGATE_MEAN; $params->aggregateonlygraded = 0; $gradecategory = new \grade_category($params, false); $gradecategory->insert(); $gradecategory->load_grade_item(); $gradeitems = new \grade_item(); $gradeitems->courseid = $course->id; $gradeitems->categoryid = $gradecategory->id; $gradeitems->itemname = 'manual grade_item'; $gradeitems->itemtype = 'manual'; $gradeitems->itemnumber = 0; $gradeitems->needsupdate = false; $gradeitems->gradetype = GRADE_TYPE_VALUE; $gradeitems->grademin = 0; $gradeitems->grademax = 10; $gradeitems->iteminfo = 'Manual grade item used for unit testing'; $gradeitems->timecreated = time(); $gradeitems->timemodified = time(); $gradeitems->aggregationcoef = GRADE_AGGREGATE_SUM; $gradeitems->insert(); $gradeitemparams = [ 'itemtype' => 'manual', 'itemname' => $gradeitems->itemname, 'courseid' => $course->id, ]; $gradeitem = \grade_item::fetch($gradeitemparams); // Do backup and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $newgradeitemparams = [ 'itemtype' => 'manual', 'itemname' => $gradeitems->itemname, 'courseid' => $course->id, ]; $newgradeitem = \grade_item::fetch($newgradeitemparams); $this->assertEquals($gradeitem->timecreated, $newgradeitem->timecreated); $this->assertEquals($gradeitem->timemodified, $newgradeitem->timemodified); } /** * The course section timemodified date does not get rolled forward * when the course is restored. */ public function test_course_section_date_restore(): void { global $DB; // Create a course. $course = $this->getDataGenerator()->create_course(['startdate' => time()]); // Get the second course section. $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '1']); // Do a backup and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $newsection = $DB->get_record('course_sections', ['course' => $newcourse->id, 'section' => '1']); // Compare dates. $this->assertEquals($section->timemodified, $newsection->timemodified); } /** * Test that the timecreated and timemodified dates are not rolled forward when restoring * badge data. */ public function test_badge_date_restore(): void { global $DB, $USER; // Create a course. $course = $this->getDataGenerator()->create_course(['startdate' => time()]); // Create a badge. $fordb = new \stdClass(); $fordb->id = null; $fordb->name = "Test badge"; $fordb->description = "Testing badges"; $fordb->timecreated = time(); $fordb->timemodified = time(); $fordb->usercreated = $USER->id; $fordb->usermodified = $USER->id; $fordb->issuername = "Test issuer"; $fordb->issuerurl = "http://issuer-url.domain.co.nz"; $fordb->issuercontact = "issuer@example.com"; $fordb->expiredate = time(); $fordb->expireperiod = null; $fordb->type = BADGE_TYPE_COURSE; $fordb->courseid = $course->id; $fordb->messagesubject = "Test message subject"; $fordb->message = "Test message body"; $fordb->attachment = 1; $fordb->notification = 0; $fordb->status = BADGE_STATUS_INACTIVE; $fordb->nextcron = time(); $DB->insert_record('badge', $fordb, true); // Do a backup and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $badges = badges_get_badges(BADGE_TYPE_COURSE, $newcourseid); // Compare dates. $badge = array_shift($badges); $this->assertEquals($fordb->timecreated, $badge->timecreated); $this->assertEquals($fordb->timemodified, $badge->timemodified); $this->assertEquals($fordb->nextcron, $badge->nextcron); // Expire date should be moved forward. $this->assertNotEquals($fordb->expiredate, $badge->expiredate); } /** * Test that course calendar events timemodified field is not rolled forward * when restoring the course. */ public function test_calendarevents_date_restore(): void { global $USER, $DB; // Create course. $course = $this->getDataGenerator()->create_course(['startdate' => time()]); // Create calendar event. $starttime = time(); $event = [ 'name' => 'Start of assignment', 'description' => '', 'format' => 1, 'courseid' => $course->id, 'groupid' => 0, 'userid' => $USER->id, 'modulename' => 0, 'instance' => 0, 'eventtype' => 'course', 'timestart' => $starttime, 'timeduration' => 86400, 'visible' => 1 ]; $calendarevent = \calendar_event::create($event, false); // Backup and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $newevent = $DB->get_record('event', ['courseid' => $newcourseid, 'eventtype' => 'course']); // Compare dates. $this->assertEquals($calendarevent->timemodified, $newevent->timemodified); $this->assertNotEquals($calendarevent->timestart, $newevent->timestart); } /** * Testing that the timeenrolled, timestarted, and timecompleted fields are not rolled forward / back * when doing a course restore. */ public function test_course_completion_date_restore(): void { global $DB; // Create course with course completion enabled. $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]); // Enrol a user in the course. $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Complete the course with a user. $ccompletion = new \completion_completion(['course' => $course->id, 'userid' => $user->id, 'timeenrolled' => time(), 'timestarted' => time() ]); // Now, mark the course as completed. $ccompletion->mark_complete(); $this->assertEquals('100', \core_completion\progress::get_course_progress_percentage($course, $user->id)); // Back up and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $newcompletion = \completion_completion::fetch(['course' => $newcourseid, 'userid' => $user->id]); // Compare dates. $this->assertEquals($ccompletion->timeenrolled, $newcompletion->timeenrolled); $this->assertEquals($ccompletion->timestarted, $newcompletion->timestarted); $this->assertEquals($ccompletion->timecompleted, $newcompletion->timecompleted); } /** * Testing that the grade grade date information is not changed in the gradebook when a course * restore is performed. */ public function test_grade_grade_date_restore(): void { global $USER, $DB; // Testing the restore of an overridden grade. list($course, $assign) = $this->create_course_and_module('assign', []); $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]); $assignobj = new \mod_assign_testable_assign(\context_module::instance($cm->id), $cm, $course); $submission = $assignobj->get_user_submission($USER->id, true); $grade = $assignobj->get_user_grade($USER->id, true); $grade->grade = 75; $assignobj->update_grade($grade); // Find the grade item. $gradeitemparams = [ 'itemtype' => 'mod', 'iteminstance' => $assign->id, 'itemmodule' => 'assign', 'courseid' => $course->id, ]; $gradeitem = \grade_item::fetch($gradeitemparams); // Next the grade grade. $gradegrade = \grade_grade::fetch(['itemid' => $gradeitem->id, 'userid' => $USER->id]); $gradegrade->set_overridden(true); // Back up and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); // Find assignment. $assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]); // Find grade item. $newgradeitemparams = [ 'itemtype' => 'mod', 'iteminstance' => $assignid, 'itemmodule' => 'assign', 'courseid' => $newcourse->id, ]; $newgradeitem = \grade_item::fetch($newgradeitemparams); // Find grade grade. $newgradegrade = \grade_grade::fetch(['itemid' => $newgradeitem->id, 'userid' => $USER->id]); // Compare dates. $this->assertEquals($gradegrade->timecreated, $newgradegrade->timecreated); $this->assertEquals($gradegrade->timemodified, $newgradegrade->timemodified); $this->assertEquals($gradegrade->overridden, $newgradegrade->overridden); } /** * Checking that the user completion of an activity relating to the timemodified field does not change * when doing a course restore. */ public function test_usercompletion_date_restore(): void { global $USER, $DB; // More completion... $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]); $assign = $this->getDataGenerator()->create_module('assign', [ 'course' => $course->id, 'completion' => COMPLETION_TRACKING_AUTOMATIC, // Show activity as complete when conditions are met. 'completionusegrade' => 1 // Student must receive a grade to complete this activity. ]); $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]); $assignobj = new \mod_assign_testable_assign(\context_module::instance($cm->id), $cm, $course); $submission = $assignobj->get_user_submission($USER->id, true); $grade = $assignobj->get_user_grade($USER->id, true); $grade->grade = 75; $assignobj->update_grade($grade); $coursemodulecompletion = $DB->get_record('course_modules_completion', ['coursemoduleid' => $cm->id]); // Back up and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); // Find assignment. $assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]); $cm = $DB->get_record('course_modules', ['course' => $newcourse->id, 'instance' => $assignid]); $newcoursemodulecompletion = $DB->get_record('course_modules_completion', ['coursemoduleid' => $cm->id]); $this->assertEquals($coursemodulecompletion->timemodified, $newcoursemodulecompletion->timemodified); } /** * Checking that the user completion of an activity relating to the view field does not change * when doing a course restore. * @covers \backup_userscompletion_structure_step * @covers \restore_userscompletion_structure_step */ public function test_usercompletion_view_restore(): void { global $DB; // More completion... $course = $this->getDataGenerator()->create_course(['startdate' => time(), 'enablecompletion' => 1]); $student = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); $assign = $this->getDataGenerator()->create_module('assign', [ 'course' => $course->id, 'completion' => COMPLETION_TRACKING_AUTOMATIC, // Show activity as complete when conditions are met. 'completionview' => 1 ]); $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]); // Mark the activity as completed. $completion = new \completion_info($course); $completion->set_module_viewed($cm, $student->id); $coursemodulecompletion = $DB->get_record('course_modules_viewed', ['coursemoduleid' => $cm->id]); // Back up and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); $assignid = $DB->get_field('assign', 'id', ['course' => $newcourseid]); $cm = $DB->get_record('course_modules', ['course' => $newcourse->id, 'instance' => $assignid]); $newcoursemodulecompletion = $DB->get_record('course_modules_viewed', ['coursemoduleid' => $cm->id]); $this->assertEquals($coursemodulecompletion->timecreated, $newcoursemodulecompletion->timecreated); } /** * Ensuring that the timemodified field of the question attempt steps table does not change when * a course restore is done. */ public function test_question_attempt_steps_date_restore(): void { global $DB; $course = $this->getDataGenerator()->create_course(['startdate' => time()]); // Make a quiz. $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); $quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2)); $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $quiz->id]); // Create a couple of questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); $saq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); $numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); // Add them to the quiz. quiz_add_quiz_question($saq->id, $quiz); quiz_add_quiz_question($numq->id, $quiz); // Make a user to do the quiz. $user1 = $this->getDataGenerator()->create_user(); $quizobj = quiz_settings::create($quiz->id, $user1->id); // Start the attempt. $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); $timenow = time(); $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id); quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); quiz_attempt_save_started($quizobj, $quba, $attempt); // Process some responses from the student. $attemptobj = quiz_attempt::create($attempt->id); $prefix1 = $quba->get_field_prefix(1); $prefix2 = $quba->get_field_prefix(2); $tosubmit = array(1 => array('answer' => 'frog'), 2 => array('answer' => '3.14')); $attemptobj->process_submitted_actions($timenow, false, $tosubmit); // Finish the attempt. $attemptobj = quiz_attempt::create($attempt->id); $attemptobj->process_finish($timenow, false); $questionattemptstepdates = []; $originaliterator = $quba->get_attempt_iterator(); foreach ($originaliterator as $questionattempt) { $questionattemptstepdates[] = ['originaldate' => $questionattempt->get_last_action_time()]; } // Back up and restore. $newcourseid = $this->backup_and_restore($course); $newcourse = get_course($newcourseid); // Get the quiz for this new restored course. $quizdata = $DB->get_record('quiz', ['course' => $newcourseid]); $quizobj = \mod_quiz\quiz_settings::create($quizdata->id, $user1->id); $questionusage = $DB->get_record('question_usages', [ 'component' => 'mod_quiz', 'contextid' => $quizobj->get_context()->id ]); $newquba = \question_engine::load_questions_usage_by_activity($questionusage->id); $restorediterator = $newquba->get_attempt_iterator(); $i = 0; foreach ($restorediterator as $restoredquestionattempt) { $questionattemptstepdates[$i]['restoredate'] = $restoredquestionattempt->get_last_action_time(); $i++; } foreach ($questionattemptstepdates as $dates) { $this->assertEquals($dates['originaldate'], $dates['restoredate']); } } } moodle2/tests/backup_encrypted_content_test.php 0000644 00000013116 15215711721 0016077 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use base_element_struct_exception; use encrypted_final_element; 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->dirroot . '/backup/moodle2/backup_custom_fields.php'); /** * Tests for the handling of encrypted contents in backup and restore. * * @package core_backup * @copyright 2016 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_encrypted_content_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); if (!function_exists('openssl_encrypt')) { $this->markTestSkipped('OpenSSL extension is not loaded.'); } else if (!function_exists('hash_hmac')) { $this->markTestSkipped('Hash extension is not loaded.'); } else if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) { $this->markTestSkipped('Expected cipher not available: ' . backup::CIPHER); } } public function test_encrypted_final_element(): void { $this->resetAfterTest(true); // Some basic verifications. $efe = new encrypted_final_element('test', array('encrypted')); $this->assertInstanceOf('encrypted_final_element', $efe); $this->assertSame('test', $efe->get_name()); $atts = $efe->get_attributes(); $this->assertCount(1, $atts); $att = reset($atts); $this->assertInstanceOf('backup_attribute', $att); $this->assertSame('encrypted', $att->get_name()); // Using a manually defined (incorrect length) key. $efe = new encrypted_final_element('test', array('encrypted')); $key = 'this_in_not_correct_32_byte_key'; try { set_config('backup_encryptkey', base64_encode($key), 'backup'); $efe->set_value('tiny_secret'); $this->fail('Expecting base_element_struct_exception exception, none happened'); } catch (\Exception $e) { $this->assertInstanceOf('base_element_struct_exception', $e); $this->assertEquals('encrypted_final_element incorrect key length', $e->errorcode); } // Using a manually defined (correct length) key. $efe = new encrypted_final_element('test', array('testattr', 'encrypted')); $key = hash('md5', 'Moodle rocks and this is not secure key, who cares, it is a test'); set_config('backup_encryptkey', base64_encode($key), 'backup'); $this->assertEmpty($efe->get_value()); $secret = 'This is a secret message that nobody else will be able to read but me 💩 '; $efe->set_value($secret); $atts = $efe->get_attributes(); $this->assertCount(2, $atts); $this->assertArrayHasKey('encrypted', $atts); // We added it explicitly. $this->assertTrue($atts['encrypted']->is_set()); $this->assertSame('true', $atts['encrypted']->get_value()); $this->assertNotEmpty($efe->get_value()); $this->assertTrue($efe->is_set()); // Get the crypted content and decrypt it manually. $ctext = $efe->get_value(); $hmaclen = 32; // SHA256 is 32 bytes. $ivlen = openssl_cipher_iv_length(backup::CIPHER); list($hmac, $iv, $text) = array_values(unpack("a{$hmaclen}hmac/a{$ivlen}iv/a*text", base64_decode($ctext))); $this->assertSame(hash_hmac('sha256', $iv . $text, $key, true), $hmac); $this->assertSame($secret, openssl_decrypt($text, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv)); // Using the default site-generated key. $efe = new encrypted_final_element('test', array('testattr')); $this->assertEmpty($efe->get_value()); $secret = 'This is a secret message that nobody else will be able to read but me 💩 '; $efe->set_value($secret); $atts = $efe->get_attributes(); $this->assertCount(2, $atts); $this->assertArrayHasKey('encrypted', $atts); // Was added automatcally, we did not specify it. $this->assertTrue($atts['encrypted']->is_set()); $this->assertSame('true', $atts['encrypted']->get_value()); $this->assertNotEmpty($efe->get_value()); $this->assertTrue($efe->is_set()); // Get the crypted content and decrypt it manually. $ctext = $efe->get_value(); $hmaclen = 32; // SHA256 is 32 bytes. $ivlen = openssl_cipher_iv_length(backup::CIPHER); list($hmac, $iv, $text) = array_values(unpack("a{$hmaclen}hmac/a{$ivlen}iv/a*text", base64_decode($ctext))); $key = base64_decode(get_config('backup', 'backup_encryptkey')); $this->assertSame(hash_hmac('sha256', $iv . $text, $key, true), $hmac); $this->assertSame($secret, openssl_decrypt($text, backup::CIPHER, $key, OPENSSL_RAW_DATA, $iv)); } } moodle2/tests/backup_xml_transformer_test.php 0000644 00000010125 15215711721 0015567 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup_xml_transformer; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); /** * Tests for backup_xml_transformer. * * @package core_backup * @subpackage moodle2 * @category test * @copyright 2017 Dmitrii Metelkin (dmitriim@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_xml_transformer_test extends \advanced_testcase { /** * Initial set up. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Data provider for ::test_filephp_links_replace. * * @return array */ public static function filephp_links_replace_data_provider(): array { return array( array('http://test.test/', 'http://test.test/'), array('http://test.test/file.php/1', 'http://test.test/file.php/1'), array('http://test.test/file.php/2/1.jpg', 'http://test.test/file.php/2/1.jpg'), array('http://test.test/file.php/2', 'http://test.test/file.php/2'), array('http://test.test/file.php/1/1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'), array('http://test.test/file.php/1//1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=/1', '$@FILEPHP@$'), array('http://test.test/file.php?file=/2/1.jpg', 'http://test.test/file.php?file=/2/1.jpg'), array('http://test.test/file.php?file=/2', 'http://test.test/file.php?file=/2'), array('http://test.test/file.php?file=/1/1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=/1//1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=%2f1', '$@FILEPHP@$'), array('http://test.test/file.php?file=%2f2%2f1.jpg', 'http://test.test/file.php?file=%2f2%2f1.jpg'), array('http://test.test/file.php?file=%2f2', 'http://test.test/file.php?file=%2f2'), array('http://test.test/file.php?file=%2f1%2f1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=%2f1%2f%2f1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=%2F1', '$@FILEPHP@$'), array('http://test.test/file.php?file=%2F2%2F1.jpg', 'http://test.test/file.php?file=%2F2%2F1.jpg'), array('http://test.test/file.php?file=%2F2', 'http://test.test/file.php?file=%2F2'), array('http://test.test/file.php?file=%2F1%2F1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'), array('http://test.test/file.php?file=%2F1%2F%2F1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'), array('http://test.test/h5p/embed.php?url=testurl', '$@H5PEMBED@$?url=testurl'), ); } /** * Test that backup_xml_transformer replaces file php links to $@FILEPHP@$. * * @dataProvider filephp_links_replace_data_provider * @param string $content Testing content. * @param string $expected Expected result. */ public function test_filephp_links_replace($content, $expected): void { global $CFG; $CFG->wwwroot = 'http://test.test'; $transformer = new backup_xml_transformer(1); $this->assertEquals($expected, $transformer->process($content)); } } moodle2/tests/restore_gradebook_structure_step_test.php 0000644 00000006203 15215711721 0017675 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; defined('MOODLE_INTERNAL') || die(); 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'); /** * Test for restore_stepslib. * * @package core_backup * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_gradebook_structure_step_test extends \advanced_testcase { /** * Provide tests for rewrite_step_backup_file_for_legacy_freeze based upon fixtures. * * @return array */ public static function rewrite_step_backup_file_for_legacy_freeze_provider(): array { $fixturesdir = realpath(__DIR__ . '/fixtures/rewrite_step_backup_file_for_legacy_freeze/'); $tests = []; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($fixturesdir), \RecursiveIteratorIterator::LEAVES_ONLY, ); foreach ($iterator as $sourcefile) { $pattern = '/\.test$/'; if (!preg_match($pattern, $sourcefile)) { continue; } $expectfile = preg_replace($pattern, '.expectation', $sourcefile); $test = array($sourcefile, $expectfile); $tests[basename($sourcefile)] = $test; } return $tests; } /** * @dataProvider rewrite_step_backup_file_for_legacy_freeze_provider * @param string $source The source file to test * @param string $expected The expected result of the transformation */ public function test_rewrite_step_backup_file_for_legacy_freeze($source, $expected): void { $restore = $this->getMockBuilder('\restore_gradebook_structure_step') ->onlyMethods([]) ->disableOriginalConstructor() ->getMock() ; // Copy the file somewhere as the rewrite_step_backup_file_for_legacy_freeze will write the file. $dir = make_request_directory(true); $filepath = $dir . DIRECTORY_SEPARATOR . 'file.xml'; copy($source, $filepath); $rc = new \ReflectionClass('\restore_gradebook_structure_step'); $rcm = $rc->getMethod('rewrite_step_backup_file_for_legacy_freeze'); $rcm->invoke($restore, $filepath); // Check the result. $this->assertFileEquals($expected, $filepath); } } moodle2/restore_settingslib.php 0000644 00000025663 15215711721 0012726 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines classes used to handle restore settings * * @package core_backup * @subpackage moodle2 * @category backup * @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(); // TODO: Reduce these to the minimum because ui/dependencies are 100% separated // Root restore settings /** * root generic setting to store different things without dependencies */ class restore_generic_setting extends root_backup_setting {} /** * root setting to control if restore will create user information * A lot of other settings are dependent of this (module's user info, * grades user info, messages, blogs... */ class restore_users_setting extends restore_generic_setting {} /** * root setting to control if restore will create override permission information by roles */ class restore_permissions_setting extends restore_generic_setting { } /** * root setting to control if restore will create groups/grouping information. Depends on @restore_users_setting * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright 2014 Matt Sammarco */ class restore_groups_setting extends restore_generic_setting { } /** * root setting to control if restore will include custom field information * * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @copyright 2018 Daniel Neis Araujo */ class restore_customfield_setting extends restore_generic_setting { } /** * root setting to control if restore will create role assignments * or no (any level), depends of @restore_users_setting */ class restore_role_assignments_setting extends root_backup_setting {} /** * root setting to control if restore will create activities * A lot of other settings (_included at activity levels) * are dependent of this setting */ class restore_activities_setting extends restore_generic_setting {} /** * root setting to control if restore will create * comments or no, depends of @restore_users_setting * exactly in the same way than @restore_role_assignments_setting so we extend from it */ class restore_comments_setting extends restore_role_assignments_setting {} /** * root setting to control if restore will create badges or not, * depends on @restore_activities_setting */ class restore_badges_setting extends restore_generic_setting {} /** * root setting to control if competencies will also be restored. */ class restore_competencies_setting extends restore_generic_setting { /** * restore_competencies_setting constructor. * @param bool $hascompetencies Flag whether to set the restore setting as checked and unlocked. */ public function __construct($hascompetencies) { $defaultvalue = false; $visibility = base_setting::HIDDEN; $status = base_setting::LOCKED_BY_CONFIG; if (\core_competency\api::is_enabled()) { $visibility = base_setting::VISIBLE; if ($hascompetencies) { $defaultvalue = true; $status = base_setting::NOT_LOCKED; } } parent::__construct('competencies', base_setting::IS_BOOLEAN, $defaultvalue, $visibility, $status); } } /** * root setting to control if restore will create * events or no, depends of @restore_users_setting * exactly in the same way than @restore_role_assignments_setting so we extend from it */ class restore_calendarevents_setting extends restore_role_assignments_setting {} /** * root setting to control if restore will create * completion info or no, depends of @restore_users_setting * exactly in the same way than @restore_role_assignments_setting so we extend from it */ class restore_userscompletion_setting extends restore_role_assignments_setting {} /** * root setting to control if restore will create * logs or no, depends of @restore_users_setting * exactly in the same way than @restore_role_assignments_setting so we extend from it */ class restore_logs_setting extends restore_role_assignments_setting {} /** * root setting to control if restore will create * grade_histories or no, depends of @restore_users_setting * exactly in the same way than @restore_role_assignments_setting so we extend from it */ class restore_grade_histories_setting extends restore_role_assignments_setting {} // Course restore settings /** * generic course setting to pass various settings between tasks and steps */ class restore_course_generic_setting extends course_backup_setting {} /** * Setting to define is we are going to overwrite course configuration */ class restore_course_overwrite_conf_setting extends restore_course_generic_setting {} /** * Setting to switch between current and new course name/startdate * * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_course_defaultcustom_setting extends restore_course_generic_setting { /** * Validates that the value $value has type $vtype * @param int $vtype * @param mixed $value * @return mixed */ public function validate_value($vtype, $value) { if ($value === false) { // Value "false" means default and is allowed for this setting type even if it does not match $vtype. return $value; } return parent::validate_value($vtype, $value); } /** * Special method for this element only. When value is "false" returns the default value. * @return mixed */ public function get_normalized_value() { $value = $this->get_value(); if ($value === false && $this->get_ui() instanceof backup_setting_ui_defaultcustom) { $attributes = $this->get_ui()->get_attributes(); return $attributes['defaultvalue']; } return $value; } } class restore_course_generic_text_setting extends restore_course_generic_setting { public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); $this->set_ui(new backup_setting_ui_text($this, $name)); } } // Section restore settings /** * generic section setting to pass various settings between tasks and steps */ class restore_section_generic_setting extends section_backup_setting {} /** * Setting to define if one section is included or no. Activities _included * settings depend of them if available */ class restore_section_included_setting extends restore_section_generic_setting {} /** * section backup setting to control if section will include * user information or no, depends of @restore_users_setting */ class restore_section_userinfo_setting extends restore_section_generic_setting {} /** * Subsection base class (delegated section). */ class restore_subsection_generic_setting extends restore_section_generic_setting { /** * Class constructor. * * @param string $name Name of the setting * @param string $vtype Type of the setting, for example base_setting::IS_TEXT * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE * @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); $this->level = self::SUBSECTION_LEVEL; } } /** * Setting to define if one subsection is included or no. * * Activities _included settings depend of them if available. */ class restore_subsection_included_setting extends restore_subsection_generic_setting { } /** * Subsection backup setting to control if section will include * user information or no, depends of @restore_users_setting. */ class restore_subsection_userinfo_setting extends restore_subsection_generic_setting { } // Activity backup settings /** * generic activity setting to pass various settings between tasks and steps */ class restore_activity_generic_setting extends activity_backup_setting {} /** * activity backup setting to control if activity will * be included or no, depends of @restore_activities_setting and * optionally parent section included setting */ class restore_activity_included_setting extends restore_activity_generic_setting {} /** * activity backup setting to control if activity will include * user information or no, depends of @restore_users_setting */ class restore_activity_userinfo_setting extends restore_activity_generic_setting {} /** * Generic subactivity setting to pass various settings between tasks and steps */ class restore_subactivity_generic_setting extends restore_activity_generic_setting { /** * Class constructor. * * @param string $name Name of the setting * @param string $vtype Type of the setting, for example base_setting::IS_TEXT * @param mixed $value Value of the setting * @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE * @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED */ public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) { parent::__construct($name, $vtype, $value, $visibility, $status); $this->level = self::SUBACTIVITY_LEVEL; } } /** * Subactivity backup setting to control if activity will be included or no. * * Depends of restore_activities_setting and optionally parent section included setting. */ class restore_subactivity_included_setting extends restore_subactivity_generic_setting { } /** * Subactivity backup setting to control if activity will include user information. * * Depends of restore_users_setting. */ class restore_subactivity_userinfo_setting extends restore_subactivity_generic_setting { } /** * root setting to control if restore will create content bank content or no */ class restore_contentbankcontent_setting extends restore_generic_setting { } /** * Root setting to control if restore will create xAPI states or not. */ class restore_xapistate_setting extends restore_generic_setting { } moodle2/restore_stepslib.php 0000644 00001042167 15215711721 0012223 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines various restore steps that will be used by common tasks in restore * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * delete old directories and conditionally create backup_temp_ids table */ class restore_create_and_clean_temp_stuff extends restore_execution_step { protected function define_execution() { $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally // If the table already exists, it's because restore_prechecks have been executed in the same // request (without problems) and it already contains a bunch of preloaded information (users...) // that we aren't going to execute again if ($exists) { // Inform plan about preloaded information $this->task->set_preloaded_information(); } // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning $itemid = $this->task->get_old_contextid(); $newitemid = context_course::instance($this->get_courseid())->id; restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid); // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning $itemid = $this->task->get_old_system_contextid(); $newitemid = context_system::instance()->id; restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid); // Create the old-course-id to new-course-id mapping, we need that available since the beginning $itemid = $this->task->get_old_courseid(); $newitemid = $this->get_courseid(); restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid); } } /** * Drop temp ids table and delete the temp dir used by backup/restore (conditionally). */ class restore_drop_and_clean_temp_stuff extends restore_execution_step { protected function define_execution() { global $CFG; restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally $progress = $this->task->get_progress(); $progress->start_progress('Deleting backup dir'); backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir $progress->end_progress(); } } } /** * Restore calculated grade items, grade categories etc */ class restore_gradebook_structure_step extends restore_structure_step { /** * To conditionally decide if this step must be executed * Note the "settings" conditions are evaluated in the * corresponding task. Here we check for other conditions * not being restore settings (files, site settings...) */ protected function execute_condition() { global $CFG, $DB; if ($this->get_courseid() == SITEID) { return false; } // No gradebook info found, don't execute $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } // Some module present in backup file isn't available to restore // in this site, don't execute if ($this->task->is_missing_modules()) { return false; } // Some activity has been excluded to be restored, don't execute if ($this->task->is_excluding_activities()) { return false; } // There should only be one grade category (the 1 associated with the course itself) // If other categories already exist we're restoring into an existing course. // Restoring categories into a course with an existing category structure is unlikely to go well $category = new stdclass(); $category->courseid = $this->get_courseid(); $catcount = $DB->count_records('grade_categories', (array)$category); if ($catcount>1) { return false; } $restoretask = $this->get_task(); // On older versions the freeze value has to be converted. // We do this from here as it is happening right before the file is read. // This only targets the backup files that can contain the legacy freeze. if ($restoretask->backup_version_compare(20150618, '>') && $restoretask->backup_release_compare('3.0', '<') || $restoretask->backup_version_compare(20160527, '<')) { $this->rewrite_step_backup_file_for_legacy_freeze($fullpath); } // Arrived here, execute the step return true; } protected function define_structure() { $paths = array(); $userinfo = $this->task->get_setting_value('users'); $paths[] = new restore_path_element('attributes', '/gradebook/attributes'); $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category'); $gradeitem = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item'); $paths[] = $gradeitem; $this->add_plugin_structure('local', $gradeitem); if ($userinfo) { $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade'); } $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter'); $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting'); return $paths; } protected function process_attributes($data) { // For non-merge restore types: // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup. $target = $this->get_task()->get_target(); if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) { set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null); } if (!empty($data['calculations_freeze'])) { if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) { set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']); } } } protected function process_grade_item($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); $data->courseid = $this->get_courseid(); if ($data->itemtype=='manual') { // manual grade items store category id in categoryid $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL); // if mapping failed put in course's grade category if (NULL == $data->categoryid) { $coursecat = grade_category::fetch_course_category($this->get_courseid()); $data->categoryid = $coursecat->id; } } else if ($data->itemtype=='course') { // course grade item stores their category id in iteminstance $coursecat = grade_category::fetch_course_category($this->get_courseid()); $data->iteminstance = $coursecat->id; } else if ($data->itemtype=='category') { // category grade items store their category id in iteminstance $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL); } else { throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype); } $data->scaleid = $this->get_mappingid('scale', $data->scaleid, NULL); $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL); $data->locktime = $this->apply_date_offset($data->locktime); $coursecategory = $newitemid = null; //course grade item should already exist so updating instead of inserting if($data->itemtype=='course') { //get the ID of the already created grade item $gi = new stdclass(); $gi->courseid = $this->get_courseid(); $gi->itemtype = $data->itemtype; //need to get the id of the grade_category that was automatically created for the course $category = new stdclass(); $category->courseid = $this->get_courseid(); $category->parent = null; //course category fullname starts out as ? but may be edited //$category->fullname = '?'; $coursecategory = $DB->get_record('grade_categories', (array)$category); $gi->iteminstance = $coursecategory->id; $existinggradeitem = $DB->get_record('grade_items', (array)$gi); if (!empty($existinggradeitem)) { $data->id = $newitemid = $existinggradeitem->id; $DB->update_record('grade_items', $data); } } else if ($data->itemtype == 'manual') { // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists. $gi = array( 'itemtype' => $data->itemtype, 'courseid' => $data->courseid, 'itemname' => $data->itemname, 'categoryid' => $data->categoryid, ); $newitemid = $DB->get_field('grade_items', 'id', $gi); } if (empty($newitemid)) { //in case we found the course category but still need to insert the course grade item if ($data->itemtype=='course' && !empty($coursecategory)) { $data->iteminstance = $coursecategory->id; } $newitemid = $DB->insert_record('grade_items', $data); $data->id = $newitemid; $gradeitem = new grade_item($data); core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger(); } $this->set_mapping('grade_item', $oldid, $newitemid); } protected function process_grade_grade($data) { global $DB; $data = (object)$data; $oldid = $data->id; $olduserid = $data->userid; $data->itemid = $this->get_new_parentid('grade_item'); $data->userid = $this->get_mappingid('user', $data->userid, null); if (!empty($data->userid)) { $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); $data->locktime = $this->apply_date_offset($data->locktime); $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid)); if ($gradeexists) { $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'"; $this->log($message, backup::LOG_DEBUG); } else { $newitemid = $DB->insert_record('grade_grades', $data); $this->set_mapping('grade_grades', $oldid, $newitemid); } } else { $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; $this->log($message, backup::LOG_DEBUG); } } protected function process_grade_category($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->course = $this->get_courseid(); $data->courseid = $data->course; $newitemid = null; //no parent means a course level grade category. That may have been created when the course was created if(empty($data->parent)) { //parent was being saved as 0 when it should be null $data->parent = null; //get the already created course level grade category $category = new stdclass(); $category->courseid = $this->get_courseid(); $category->parent = null; $coursecategory = $DB->get_record('grade_categories', (array)$category); if (!empty($coursecategory)) { $data->id = $newitemid = $coursecategory->id; $DB->update_record('grade_categories', $data); } } // Add a warning about a removed setting. if (!empty($data->aggregatesubcats)) { set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1); } //need to insert a course category if (empty($newitemid)) { $newitemid = $DB->insert_record('grade_categories', $data); } $this->set_mapping('grade_category', $oldid, $newitemid); } protected function process_grade_letter($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->contextid = context_course::instance($this->get_courseid())->id; $gradeletter = (array)$data; unset($gradeletter['id']); if (!$DB->record_exists('grade_letters', $gradeletter)) { $newitemid = $DB->insert_record('grade_letters', $data); } else { $newitemid = $data->id; } $this->set_mapping('grade_letter', $oldid, $newitemid); } protected function process_grade_setting($data) { global $DB; $data = (object)$data; $oldid = $data->id; $data->courseid = $this->get_courseid(); $target = $this->get_task()->get_target(); if ($data->name == 'minmaxtouse' && ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) { // We never restore minmaxtouse during merge. return; } if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) { $newitemid = $DB->insert_record('grade_settings', $data); } else { $newitemid = $data->id; } if (!empty($oldid)) { // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting. $this->set_mapping('grade_setting', $oldid, $newitemid); } } /** * put all activity grade items in the correct grade category and mark all for recalculation */ protected function after_execute() { global $DB; $conditions = array( 'backupid' => $this->get_restoreid(), 'itemname' => 'grade_item'//, //'itemid' => $itemid ); $rs = $DB->get_recordset('backup_ids_temp', $conditions); // We need this for calculation magic later on. $mappings = array(); if (!empty($rs)) { foreach($rs as $grade_item_backup) { // Store the oldid with the new id. $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid; $updateobj = new stdclass(); $updateobj->id = $grade_item_backup->newitemid; //if this is an activity grade item that needs to be put back in its correct category if (!empty($grade_item_backup->parentitemid)) { $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null); if (!is_null($oldcategoryid)) { $updateobj->categoryid = $oldcategoryid; $DB->update_record('grade_items', $updateobj); } } else { //mark course and category items as needing to be recalculated $updateobj->needsupdate=1; $DB->update_record('grade_items', $updateobj); } } } $rs->close(); // We need to update the calculations for calculated grade items that may reference old // grade item ids using ##gi\d+##. // $mappings can be empty, use 0 if so (won't match ever) list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0); $sql = "SELECT gi.id, gi.calculation FROM {grade_items} gi WHERE gi.id {$sql} AND calculation IS NOT NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $gradeitem) { // Collect all of the used grade item id references if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) { // This calculation doesn't reference any other grade items... EASY! continue; } // For this next bit we are going to do the replacement of id's in two steps: // 1. We will replace all old id references with a special mapping reference. // 2. We will replace all mapping references with id's // Why do we do this? // Because there potentially there will be an overlap of ids within the query and we // we substitute the wrong id.. safest way around this is the two step system $calculationmap = array(); $mapcount = 0; foreach ($matches[1] as $match) { // Check that the old id is known to us, if not it was broken to begin with and will // continue to be broken. if (!array_key_exists($match, $mappings)) { continue; } // Our special mapping key $mapping = '##MAPPING'.$mapcount.'##'; // The old id that exists within the calculation now $oldid = '##gi'.$match.'##'; // The new id that we want to replace the old one with. $newid = '##gi'.$mappings[$match].'##'; // Replace in the special mapping key $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation); // And record the mapping $calculationmap[$mapping] = $newid; $mapcount++; } // Iterate all special mappings for this calculation and replace in the new id's foreach ($calculationmap as $mapping => $newid) { $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation); } // Update the calculation now that its being remapped $DB->update_record('grade_items', $gradeitem); } $rs->close(); // Need to correct the grade category path and parent $conditions = array( 'courseid' => $this->get_courseid() ); $rs = $DB->get_recordset('grade_categories', $conditions); // Get all the parents correct first as grade_category::build_path() loads category parents from the DB foreach ($rs as $gc) { if (!empty($gc->parent)) { $grade_category = new stdClass(); $grade_category->id = $gc->id; $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent); $DB->update_record('grade_categories', $grade_category); } } $rs->close(); // Now we can rebuild all the paths $rs = $DB->get_recordset('grade_categories', $conditions); foreach ($rs as $gc) { $grade_category = new stdClass(); $grade_category->id = $gc->id; $grade_category->path = grade_category::build_path($gc); $grade_category->depth = substr_count($grade_category->path, '/') - 1; $DB->update_record('grade_categories', $grade_category); } $rs->close(); // Check what to do with the minmaxtouse setting. $this->check_minmaxtouse(); // Freeze gradebook calculations if needed. $this->gradebook_calculation_freeze(); // Ensure the module cache is current when recalculating grades. rebuild_course_cache($this->get_courseid(), true); // Restore marks items as needing update. Update everything now. grade_regrade_final_grades($this->get_courseid()); } /** * Freeze gradebook calculation if needed. * * This is similar to various upgrade scripts that check if the freeze is needed. */ protected function gradebook_calculation_freeze() { global $CFG; $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid()); $restoretask = $this->get_task(); // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619). if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=') && $restoretask->backup_version_compare(20150619, '<')) { require_once($CFG->libdir . '/db/upgradelib.php'); upgrade_extra_credit_weightoverride($this->get_courseid()); } // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627). if (!$gradebookcalculationsfreeze && $restoretask->backup_version_compare(20141110, '>=') && $restoretask->backup_version_compare(20150627, '<')) { require_once($CFG->libdir . '/db/upgradelib.php'); upgrade_calculated_grade_items($this->get_courseid()); } // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue. // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should // be checked for this problem. if (!$gradebookcalculationsfreeze && ($restoretask->backup_version_compare(20160518, '<') || $restoretask->backup_release_compare('2.9', '<='))) { require_once($CFG->libdir . '/db/upgradelib.php'); upgrade_course_letter_boundary($this->get_courseid()); } } /** * Checks what should happen with the course grade setting minmaxtouse. * * This is related to the upgrade step at the time the setting was added. * * @see MDL-48618 * @return void */ protected function check_minmaxtouse() { global $CFG, $DB; require_once($CFG->libdir . '/gradelib.php'); $userinfo = $this->task->get_setting_value('users'); $settingname = 'minmaxtouse'; $courseid = $this->get_courseid(); $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname)); $version28start = 2014111000.00; $version28last = 2014111006.05; $version29start = 2015051100.00; $version29last = 2015060400.02; $target = $this->get_task()->get_target(); if ($minmaxtouse === false && ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) { // The setting was not found because this setting did not exist at the time the backup was made. // And we are not restoring as merge, in which case we leave the course as it was. $version = $this->get_task()->get_info()->moodle_version; if ($version < $version28start) { // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them. if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) { grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM); } } else if (($version >= $version28start && $version < $version28last) || ($version >= $version29start && $version < $version29last)) { // They should be using grade_grade when the course has inconsistencies. $sql = "SELECT gi.id FROM {grade_items} gi JOIN {grade_grades} gg ON gg.itemid = gi.id WHERE gi.courseid = ? AND (gi.itemtype != ? AND gi.itemtype != ?) AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)"; // The course can only have inconsistencies when we restore the user info, // we do not need to act on existing grades that were not restored as part of this backup. if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) { // Display the notice as we do during upgrade. set_config('show_min_max_grades_changed_' . $courseid, 1); if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) { // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE. // If they are using the site-wide grade_grade setting, we only want to notice them. grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE); } } } else { // This should never happen because from now on minmaxtouse is always saved in backups. } } } /** * Rewrite step definition to handle the legacy freeze attribute. * * In previous backups the calculations_freeze property was stored as an attribute of the * top level node <gradebook>. The backup API, however, do not process grandparent nodes. * It only processes definitive children, and their parent attributes. * * We had: * * <gradebook calculations_freeze="20160511"> * <grade_categories> * <grade_category id="10"> * <depth>1</depth> * ... * </grade_category> * </grade_categories> * ... * </gradebook> * * And this method will convert it to: * * <gradebook > * <attributes> * <calculations_freeze>20160511</calculations_freeze> * </attributes> * <grade_categories> * <grade_category id="10"> * <depth>1</depth> * ... * </grade_category> * </grade_categories> * ... * </gradebook> * * Note that we cannot just load the XML file in memory as it could potentially be huge. * We can also completely ignore if the node <attributes> is already in the backup * file as it never existed before. * * @param string $filepath The absolute path to the XML file. * @return void */ protected function rewrite_step_backup_file_for_legacy_freeze($filepath) { $foundnode = false; $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml'; $fr = fopen($filepath, 'r'); $fw = fopen($newfile, 'w'); if ($fr && $fw) { while (($line = fgets($fr, 4096)) !== false) { if (!$foundnode && strpos($line, '<gradebook ') === 0) { $foundnode = true; $matches = array(); $pattern = '@calculations_freeze=.([0-9]+).@'; if (preg_match($pattern, $line, $matches)) { $freeze = $matches[1]; $line = preg_replace($pattern, '', $line); $line .= " <attributes>\n <calculations_freeze>$freeze</calculations_freeze>\n </attributes>\n"; } } fputs($fw, $line); } if (!feof($fr)) { throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.'); } fclose($fr); fclose($fw); if (!rename($newfile, $filepath)) { throw new restore_step_exception('Error while attempting to rename the gradebook step file.'); } } else { if ($fr) { fclose($fr); } if ($fw) { fclose($fw); } } } } /** * Step in charge of restoring the grade history of a course. * * The execution conditions are itendical to {@link restore_gradebook_structure_step} because * we do not want to restore the history if the gradebook and its content has not been * restored. At least for now. */ class restore_grade_history_structure_step extends restore_structure_step { protected function execute_condition() { global $CFG, $DB; if ($this->get_courseid() == SITEID) { return false; } // No gradebook info found, don't execute. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } // Some module present in backup file isn't available to restore in this site, don't execute. if ($this->task->is_missing_modules()) { return false; } // Some activity has been excluded to be restored, don't execute. if ($this->task->is_excluding_activities()) { return false; } // There should only be one grade category (the 1 associated with the course itself). $category = new stdclass(); $category->courseid = $this->get_courseid(); $catcount = $DB->count_records('grade_categories', (array)$category); if ($catcount > 1) { return false; } // Arrived here, execute the step. return true; } protected function define_structure() { $paths = array(); // Settings to use. $userinfo = $this->get_setting_value('users'); $history = $this->get_setting_value('grade_histories'); if ($userinfo && $history) { $paths[] = new restore_path_element('grade_grade', '/grade_history/grade_grades/grade_grade'); } return $paths; } protected function process_grade_grade($data) { global $DB; $data = (object)($data); $olduserid = $data->userid; unset($data->id); $data->userid = $this->get_mappingid('user', $data->userid, null); if (!empty($data->userid)) { // Do not apply the date offsets as this is history. $data->itemid = $this->get_mappingid('grade_item', $data->itemid); $data->oldid = $this->get_mappingid('grade_grades', $data->oldid); $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); $DB->insert_record('grade_grades_history', $data); } else { $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; $this->log($message, backup::LOG_DEBUG); } } } /** * decode all the interlinks present in restored content * relying 100% in the restore_decode_processor that handles * both the contents to modify and the rules to be applied */ class restore_decode_interlinks extends restore_execution_step { protected function define_execution() { // Get the decoder (from the plan) /** @var restore_decode_processor $decoder */ $decoder = $this->task->get_decoder(); restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules // And launch it, everything will be processed $decoder->execute(); } } /** * first, ensure that we have no gaps in section numbers * and then, rebuid the course cache */ class restore_rebuild_course_cache extends restore_execution_step { protected function define_execution() { global $DB; // Although there is some sort of auto-recovery of missing sections // present in course/formats... here we check that all the sections // from 0 to MAX(section->section) exist, creating them if necessary $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid())); // Iterate over all sections for ($i = 0; $i <= $maxsection; $i++) { // If the section $i doesn't exist, create it if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) { $sectionrec = array( 'course' => $this->get_courseid(), 'section' => $i, 'timemodified' => time()); $DB->insert_record('course_sections', $sectionrec); // missing section created } } // Rebuild cache now that all sections are in place rebuild_course_cache($this->get_courseid()); cache_helper::purge_by_event('changesincourse'); cache_helper::purge_by_event('changesincoursecat'); } } /** * Review all the tasks having one after_restore method * executing it to perform some final adjustments of information * not available when the task was executed. */ class restore_execute_after_restore extends restore_execution_step { protected function define_execution() { // Simply call to the execute_after_restore() method of the task // that always is the restore_final_task $this->task->launch_execute_after_restore(); } } /** * Review all the (pending) block positions in backup_ids, matching by * contextid, creating positions as needed. This is executed by the * final task, once all the contexts have been created */ class restore_review_pending_block_positions extends restore_execution_step { protected function define_execution() { global $DB; // Get all the block_position objects pending to match $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position'); $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info'); // Process block positions, creating them or accumulating for final step foreach($rs as $posrec) { // Get the complete position object out of the info field. $position = backup_controller_dbops::decode_backup_temp_info($posrec->info); // If position is for one already mapped (known) contextid // process it now, creating the position, else nothing to // do, position finally discarded if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) { $position->contextid = $newctx->newitemid; // Create the block position $DB->insert_record('block_positions', $position); } } $rs->close(); } } /** * Updates the availability data for course modules and sections. * * Runs after the restore of all course modules, sections, and grade items has * completed. This is necessary in order to update IDs that have changed during * restore. * * @package core_backup * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_update_availability extends restore_execution_step { protected function define_execution() { global $CFG, $DB; // Note: This code runs even if availability is disabled when restoring. // That will ensure that if you later turn availability on for the site, // there will be no incorrect IDs. (It doesn't take long if the restored // data does not contain any availability information.) // Get modinfo with all data after resetting cache. rebuild_course_cache($this->get_courseid(), true); $modinfo = get_fast_modinfo($this->get_courseid()); // Get the date offset for this restore. $dateoffset = $this->apply_date_offset(1) - 1; // Update all sections that were restored. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section'); $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid'); $sectionsbyid = null; foreach ($rs as $rec) { if (is_null($sectionsbyid)) { $sectionsbyid = array(); foreach ($modinfo->get_section_info_all() as $section) { $sectionsbyid[$section->id] = $section; } } if (!array_key_exists($rec->newitemid, $sectionsbyid)) { // If the section was not fully restored for some reason // (e.g. due to an earlier error), skip it. $this->get_logger()->process('Section not fully restored: id ' . $rec->newitemid, backup::LOG_WARNING); continue; } $section = $sectionsbyid[$rec->newitemid]; if (!is_null($section->availability)) { $info = new \core_availability\info_section($section); $info->update_after_restore($this->get_restoreid(), $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task); } } $rs->close(); // Update all modules that were restored. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module'); $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid'); foreach ($rs as $rec) { if (!array_key_exists($rec->newitemid, $modinfo->cms)) { // If the module was not fully restored for some reason // (e.g. due to an earlier error), skip it. $this->get_logger()->process('Module not fully restored: id ' . $rec->newitemid, backup::LOG_WARNING); continue; } $cm = $modinfo->get_cm($rec->newitemid); if (!is_null($cm->availability)) { $info = new \core_availability\info_module($cm); $info->update_after_restore($this->get_restoreid(), $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task); } } $rs->close(); } } /** * Process legacy module availability records in backup_ids. * * Matches course modules and grade item id once all them have been already restored. * Only if all matchings are satisfied the availability condition will be created. * At the same time, it is required for the site to have that functionality enabled. * * This step is included only to handle legacy backups (2.6 and before). It does not * do anything for newer backups. * * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class restore_process_course_modules_availability extends restore_execution_step { protected function define_execution() { global $CFG, $DB; // Site hasn't availability enabled if (empty($CFG->enableavailability)) { return; } // Do both modules and sections. foreach (array('module', 'section') as $table) { // Get all the availability objects to process. $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability'); $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info'); // Process availabilities, creating them if everything matches ok. foreach ($rs as $availrec) { $allmatchesok = true; // Get the complete legacy availability object. $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info); // Note: This code used to update IDs, but that is now handled by the // current code (after restore) instead of this legacy code. // Get showavailability option. $thingid = ($table === 'module') ? $availability->coursemoduleid : $availability->coursesectionid; $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(), $table . '_showavailability', $thingid); if (!$showrec) { // Should not happen. throw new coding_exception('No matching showavailability record'); } $show = $showrec->info->showavailability; // The $availability object is now in the format used in the old // system. Interpret this and convert to new system. $currentvalue = $DB->get_field('course_' . $table . 's', 'availability', array('id' => $thingid), MUST_EXIST); $newvalue = \core_availability\info::add_legacy_availability_condition( $currentvalue, $availability, $show); $DB->set_field('course_' . $table . 's', 'availability', $newvalue, array('id' => $thingid)); } $rs->close(); } } } /* * Execution step that, *conditionally* (if there isn't preloaded information) * will load the inforef files for all the included course/section/activity tasks * to backup_temp_ids. They will be stored with "xxxxref" as itemname */ class restore_load_included_inforef_records extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do return; } // Get all the included tasks $tasks = restore_dbops::get_included_tasks($this->get_restoreid()); $progress = $this->task->get_progress(); $progress->start_progress($this->get_name(), count($tasks)); foreach ($tasks as $task) { // Load the inforef.xml file if exists $inforefpath = $task->get_taskbasepath() . '/inforef.xml'; if (file_exists($inforefpath)) { // Load each inforef file to temp_ids. restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress); } } $progress->end_progress(); } } /* * Execution step that will load all the needed files into backup_files_temp * - info: contains the whole original object (times, names...) * (all them being original ids as loaded from xml) */ class restore_load_included_files extends restore_structure_step { protected function define_structure() { $file = new restore_path_element('file', '/files/file'); return array($file); } /** * Process one <file> element from files.xml * * @param array $data the element data */ public function process_file($data) { $data = (object)$data; // handy // load it if needed: // - it it is one of the annotated inforef files (course/section/activity/block) // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever) // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use, // but then we'll need to change it to load plugins itself (because this is executed too early in restore) $isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id); $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' || $data->component == 'grouping' || $data->component == 'grade' || $data->component == 'question' || substr($data->component, 0, 5) == 'qtype'); if ($isfileref || $iscomponent) { restore_dbops::set_backup_files_record($this->get_restoreid(), $data); } } } /** * Execution step that, *conditionally* (if there isn't preloaded information), * will load all the needed roles to backup_temp_ids. They will be stored with * "role" itemname. Also it will perform one automatic mapping to roles existing * in the target site, based in permissions of the user performing the restore, * archetypes and other bits. At the end, each original role will have its associated * target role or 0 if it's going to be skipped. Note we wrap everything over one * restore_dbops method, as far as the same stuff is going to be also executed * by restore prechecks */ class restore_load_and_map_roles extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded return; } $file = $this->get_basepath() . '/roles.xml'; // Load needed toles to temp_ids restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file); // Process roles, mapping/skipping. Any error throws exception // Note we pass controller's info because it can contain role mapping information // about manual mappings performed by UI restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_info()->role_mappings); } } /** * Execution step that, *conditionally* (if there isn't preloaded information * and users have been selected in settings, will load all the needed users * to backup_temp_ids. They will be stored with "user" itemname and with * their original contextid as paremitemid */ class restore_load_included_users extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do return; } if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do return; } $file = $this->get_basepath() . '/users.xml'; // Load needed users to temp_ids. restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress()); } } /** * Execution step that, *conditionally* (if there isn't preloaded information * and users have been selected in settings, will process all the needed users * in order to decide and perform any action with them (create / map / error) * Note: Any error will cause exception, as far as this is the same processing * than the one into restore prechecks (that should have stopped process earlier) */ class restore_process_included_users extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do return; } if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do return; } restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress()); } } /** * Execution step that will create all the needed users as calculated * by @restore_process_included_users (those having newiteind = 0) */ class restore_create_included_users extends restore_execution_step { protected function define_execution() { restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(), $this->task->get_userid(), $this->task->get_progress(), $this->task->get_courseid()); } } /** * Structure step that will create all the needed groups and groupings * by loading them from the groups.xml file performing the required matches. * Note group members only will be added if restoring user info */ class restore_groups_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); // Add paths here // Do not include group/groupings information if not requested. $groupinfo = $this->get_setting_value('groups'); if ($groupinfo) { $paths[] = new restore_path_element('group', '/groups/group'); $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping'); $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group'); // Custom fields. if ($this->get_setting_value('customfield')) { $paths[] = new restore_path_element('groupcustomfield', '/groups/groupcustomfields/groupcustomfield'); $paths[] = new restore_path_element('groupingcustomfield', '/groups/groupings/groupingcustomfields/groupingcustomfield'); } } return $paths; } // Processing functions go here public function process_group($data) { global $DB; $data = (object)$data; // handy $data->courseid = $this->get_courseid(); // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by // another a group in the same course $context = context_course::instance($data->courseid); if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) { if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) { unset($data->idnumber); } } else { unset($data->idnumber); } $oldid = $data->id; // need this saved for later $restorefiles = false; // Only if we end creating the group // This is for backwards compatibility with old backups. If the backup data for a group contains a non-empty value of // hidepicture, then we'll exclude this group's picture from being restored. if (!empty($data->hidepicture)) { // Exclude the group picture from being restored if hidepicture is set to 1 in the backup data. unset($data->picture); } // Search if the group already exists (by name & description) in the target course $description_clause = ''; $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name); if (!empty($data->description)) { $description_clause = ' AND ' . $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description'); $params['description'] = $data->description; } if (!$groupdb = $DB->get_record_sql("SELECT * FROM {groups} WHERE courseid = :courseid AND name = :grname $description_clause", $params)) { // group doesn't exist, create $newitemid = $DB->insert_record('groups', $data); $restorefiles = true; // We'll restore the files } else { // group exists, use it $newitemid = $groupdb->id; } // Save the id mapping $this->set_mapping('group', $oldid, $newitemid, $restorefiles); // Add the related group picture file if it's available at this point. if (!empty($data->picture)) { $this->add_related_files('group', 'icon', 'group', null, $oldid); } // Invalidate the course group data cache just in case. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); } /** * Restore group custom field values. * @param array $data data for group custom field * @return void */ public function process_groupcustomfield($data) { $newgroup = $this->get_mapping('group', $data['groupid']); if ($newgroup && $newgroup->newitemid) { $data['groupid'] = $newgroup->newitemid; $handler = \core_group\customfield\group_handler::create(); $handler->restore_instance_data_from_backup($this->task, $data); } } public function process_grouping($data) { global $DB; $data = (object)$data; // handy $data->courseid = $this->get_courseid(); // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by // another a grouping in the same course $context = context_course::instance($data->courseid); if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) { if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) { unset($data->idnumber); } } else { unset($data->idnumber); } $oldid = $data->id; // need this saved for later $restorefiles = false; // Only if we end creating the grouping // Search if the grouping already exists (by name & description) in the target course $description_clause = ''; $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name); if (!empty($data->description)) { $description_clause = ' AND ' . $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description'); $params['description'] = $data->description; } if (!$groupingdb = $DB->get_record_sql("SELECT * FROM {groupings} WHERE courseid = :courseid AND name = :grname $description_clause", $params)) { // grouping doesn't exist, create $newitemid = $DB->insert_record('groupings', $data); $restorefiles = true; // We'll restore the files } else { // grouping exists, use it $newitemid = $groupingdb->id; } // Save the id mapping $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles); // Invalidate the course group data cache just in case. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid)); } /** * Restore grouping custom field values. * @param array $data data for grouping custom field * @return void */ public function process_groupingcustomfield($data) { $newgrouping = $this->get_mapping('grouping', $data['groupingid']); if ($newgrouping && $newgrouping->newitemid) { $data['groupingid'] = $newgrouping->newitemid; $handler = \core_group\customfield\grouping_handler::create(); $handler->restore_instance_data_from_backup($this->task, $data); } } public function process_grouping_group($data) { global $CFG; require_once($CFG->dirroot.'/group/lib.php'); $data = (object)$data; groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded); } protected function after_execute() { // Add group related files, matching with "group" mappings. $this->add_related_files('group', 'description', 'group'); // Add grouping related files, matching with "grouping" mappings $this->add_related_files('grouping', 'description', 'grouping'); // Invalidate the course group data. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid())); } } /** * Structure step that will create all the needed group memberships * by loading them from the groups.xml file performing the required matches. */ class restore_groups_members_structure_step extends restore_structure_step { protected $plugins = null; protected function define_structure() { $paths = array(); // Add paths here if ($this->get_setting_value('groups') && $this->get_setting_value('users')) { $paths[] = new restore_path_element('group', '/groups/group'); $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member'); } return $paths; } public function process_group($data) { $data = (object)$data; // handy // HACK ALERT! // Not much to do here, this groups mapping should be already done from restore_groups_structure_step. // Let's fake internal state to make $this->get_new_parentid('group') work. $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id)); } public function process_member($data) { global $DB, $CFG; require_once("$CFG->dirroot/group/lib.php"); // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled. $data = (object)$data; // handy // get parent group->id $data->groupid = $this->get_new_parentid('group'); // map user newitemid and insert if not member already if ($data->userid = $this->get_mappingid('user', $data->userid)) { if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) { // Check the component, if any, exists. if (empty($data->component)) { groups_add_member($data->groupid, $data->userid); } else if ((strpos($data->component, 'enrol_') === 0)) { // Deal with enrolment groups - ignore the component and just find out the instance via new id, // it is possible that enrolment was restored using different plugin type. if (!isset($this->plugins)) { $this->plugins = enrol_get_plugins(true); } if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) { if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { if (isset($this->plugins[$instance->enrol])) { $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid); } } } } else { $dir = core_component::get_component_directory($data->component); if ($dir and is_dir($dir)) { if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) { return; } } // Bad luck, plugin could not restore the data, let's add normal membership. groups_add_member($data->groupid, $data->userid); $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead."; $this->log($message, backup::LOG_WARNING); } } } } } /** * Structure step that will create all the needed scales * by loading them from the scales.xml */ class restore_scales_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); // Add paths here $paths[] = new restore_path_element('scale', '/scales_definition/scale'); return $paths; } protected function process_scale($data) { global $DB; $data = (object)$data; $restorefiles = false; // Only if we end creating the group $oldid = $data->id; // need this saved for later // Look for scale (by 'scale' both in standard (course=0) and current course // with priority to standard scales (ORDER clause) // scale is not course unique, use get_record_sql to suppress warning // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides $compare_scale_clause = $DB->sql_compare_text('scale') . ' = ' . $DB->sql_compare_text(':scaledesc'); $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale); if (!$scadb = $DB->get_record_sql("SELECT * FROM {scale} WHERE courseid IN (0, :courseid) AND $compare_scale_clause ORDER BY courseid", $params, IGNORE_MULTIPLE)) { // Remap the user if possible, defaut to user performing the restore if not $userid = $this->get_mappingid('user', $data->userid); $data->userid = $userid ? $userid : $this->task->get_userid(); // Remap the course if course scale $data->courseid = $data->courseid ? $this->get_courseid() : 0; // If global scale (course=0), check the user has perms to create it // falling to course scale if not $systemctx = context_system::instance(); if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) { $data->courseid = $this->get_courseid(); } // scale doesn't exist, create $newitemid = $DB->insert_record('scale', $data); $restorefiles = true; // We'll restore the files } else { // scale exists, use it $newitemid = $scadb->id; } // Save the id mapping (with files support at system context) $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid()); } protected function after_execute() { // Add scales related files, matching with "scale" mappings $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid()); } } /** * Structure step that will create all the needed outocomes * by loading them from the outcomes.xml */ class restore_outcomes_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); // Add paths here $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome'); return $paths; } protected function process_outcome($data) { global $DB; $data = (object)$data; $restorefiles = false; // Only if we end creating the group $oldid = $data->id; // need this saved for later // Look for outcome (by shortname both in standard (courseid=null) and current course // with priority to standard outcomes (ORDER clause) // outcome is not course unique, use get_record_sql to suppress warning $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname); if (!$outdb = $DB->get_record_sql('SELECT * FROM {grade_outcomes} WHERE shortname = :shortname AND (courseid = :courseid OR courseid IS NULL) ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) { // Remap the user $userid = $this->get_mappingid('user', $data->usermodified); $data->usermodified = $userid ? $userid : $this->task->get_userid(); // Remap the scale $data->scaleid = $this->get_mappingid('scale', $data->scaleid); // Remap the course if course outcome $data->courseid = $data->courseid ? $this->get_courseid() : null; // If global outcome (course=null), check the user has perms to create it // falling to course outcome if not $systemctx = context_system::instance(); if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) { $data->courseid = $this->get_courseid(); } // outcome doesn't exist, create $newitemid = $DB->insert_record('grade_outcomes', $data); $restorefiles = true; // We'll restore the files } else { // scale exists, use it $newitemid = $outdb->id; } // Set the corresponding grade_outcomes_courses record $outcourserec = new stdclass(); $outcourserec->courseid = $this->get_courseid(); $outcourserec->outcomeid = $newitemid; if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) { $DB->insert_record('grade_outcomes_courses', $outcourserec); } // Save the id mapping (with files support at system context) $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid()); } protected function after_execute() { // Add outcomes related files, matching with "outcome" mappings $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid()); } } /** * Execution step that, *conditionally* (if there isn't preloaded information * will load all the question categories and questions (header info only) * to backup_temp_ids. They will be stored with "question_category" and * "question" itemnames and with their original contextid and question category * id as paremitemids */ class restore_load_categories_and_questions extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do return; } $file = $this->get_basepath() . '/questions.xml'; restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file); } } /** * Execution step that, *conditionally* (if there isn't preloaded information) * will process all the needed categories and questions * in order to decide and perform any action with them (create / map / error) * Note: Any error will cause exception, as far as this is the same processing * than the one into restore prechecks (that should have stopped process earlier) */ class restore_process_categories_and_questions extends restore_execution_step { protected function define_execution() { if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do return; } restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite()); } } /** * Structure step that will read the section.xml creating/updating sections * as needed, rebuilding course cache and other friends */ class restore_section_structure_step extends restore_structure_step { /** @var array Cache: Array of id => course format */ private static $courseformats = array(); /** * Resets a static cache of course formats. Required for unit testing. */ public static function reset_caches() { self::$courseformats = array(); } protected function define_structure() { global $CFG; $paths = array(); $section = new restore_path_element('section', '/section'); $paths[] = $section; if ($CFG->enableavailability) { $paths[] = new restore_path_element('availability', '/section/availability'); $paths[] = new restore_path_element('availability_field', '/section/availability_field'); } $paths[] = new restore_path_element('course_format_options', '/section/course_format_options'); // Apply for 'format' plugins optional paths at section level $this->add_plugin_structure('format', $section); // Apply for 'local' plugins optional paths at section level $this->add_plugin_structure('local', $section); return $paths; } public function process_section($data) { global $CFG, $DB; $data = (object)$data; $oldid = $data->id; // We'll need this later $restorefiles = false; // Look for the section $section = new stdclass(); $section->course = $this->get_courseid(); $section->section = $data->number; $section->timemodified = $data->timemodified ?? 0; $section->component = null; $section->itemid = null; $secrec = $DB->get_record( 'course_sections', ['course' => $this->get_courseid(), 'section' => $data->number, 'component' => null] ); $createsection = empty($secrec); // Delegated sections are always restored as new sections. if (!empty($data->component)) { $section->itemid = $this->get_delegated_section_mapping($data->component, $data->itemid); // If the delegate component does not set the mapping id, the section must be converted // into a regular section. Otherwise, it won't be accessible. $createsection = $createsection || $section->itemid !== null; $section->component = ($section->itemid !== null) ? $data->component : null; // The section number will be always the last of the course, no matter the case. $section->section = $this->get_last_section_number($this->get_courseid()) + 1; } // Section doesn't exist, create it with all the info from backup if ($createsection) { $section->name = $data->name; $section->summary = $data->summary; $section->summaryformat = $data->summaryformat; $section->sequence = ''; $section->visible = $data->visible; if (empty($CFG->enableavailability)) { // Process availability information only if enabled. $section->availability = null; } else { $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null; // Include legacy [<2.7] availability data if provided. if (is_null($section->availability)) { $section->availability = \core_availability\info::convert_legacy_fields( $data, true); } } // Delegated sections should be always after the normal sections. $this->displace_delegated_sections_after($section->section); $newitemid = $DB->insert_record('course_sections', $section); $section->id = $newitemid; core\event\course_section_created::create_from_section($section)->trigger(); $restorefiles = true; // Section exists, update non-empty information } else { $section->id = $secrec->id; if ((string)$secrec->name === '') { $section->name = $data->name; } if (empty($secrec->summary)) { $section->summary = $data->summary; $section->summaryformat = $data->summaryformat; $restorefiles = true; } // Don't update availability (I didn't see a useful way to define // whether existing or new one should take precedence). $DB->update_record('course_sections', $section); $newitemid = $secrec->id; // Trigger an event for course section update. $event = \core\event\course_section_updated::create( array( 'objectid' => $section->id, 'courseid' => $section->course, 'context' => context_course::instance($section->course), 'other' => array('sectionnum' => $section->section) ) ); $event->trigger(); } // Annotate the section mapping, with restorefiles option if needed $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles); // set the new course_section id in the task $this->task->set_sectionid($newitemid); // If there is the legacy showavailability data, store this for later use. // (This data is not present when restoring 'new' backups.) if (isset($data->showavailability)) { // Cache the showavailability flag using the backup_ids data field. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'section_showavailability', $newitemid, 0, null, (object)array('showavailability' => $data->showavailability)); } // Commented out. We never modify course->numsections as far as that is used // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x) // Note: We keep the code here, to know about and because of the possibility of making this // optional based on some setting/attribute in the future // If needed, adjust course->numsections //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) { // if ($numsections < $section->section) { // $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid())); // } //} } /** * Process the legacy availability table record. This table does not exist * in Moodle 2.7+ but we still support restore. * * @param stdClass $data Record data */ public function process_availability($data) { $data = (object)$data; // Simply going to store the whole availability record now, we'll process // all them later in the final task (once all activities have been restored) // Let's call the low level one to be able to store the whole object. $data->coursesectionid = $this->task->get_sectionid(); restore_dbops::set_backup_ids_record($this->get_restoreid(), 'section_availability', $data->id, 0, null, $data); } /** * Process the legacy availability fields table record. This table does not * exist in Moodle 2.7+ but we still support restore. * * @param stdClass $data Record data */ public function process_availability_field($data) { global $DB, $CFG; require_once($CFG->dirroot.'/user/profile/lib.php'); $data = (object)$data; // Mark it is as passed by default $passed = true; $customfieldid = null; // If a customfield has been used in order to pass we must be able to match an existing // customfield by name (data->customfield) and type (data->customfieldtype) if (is_null($data->customfield) xor is_null($data->customfieldtype)) { // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both. // If one is null but the other isn't something clearly went wrong and we'll skip this condition. $passed = false; } else if (!is_null($data->customfield)) { $field = profile_get_custom_field_data_by_shortname($data->customfield); $passed = $field && $field->datatype == $data->customfieldtype; } if ($passed) { // Create the object to insert into the database $availfield = new stdClass(); $availfield->coursesectionid = $this->task->get_sectionid(); $availfield->userfield = $data->userfield; $availfield->customfieldid = $customfieldid; $availfield->operator = $data->operator; $availfield->value = $data->value; // Get showavailability option. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'section_showavailability', $availfield->coursesectionid); if (!$showrec) { // Should not happen. throw new coding_exception('No matching showavailability record'); } $show = $showrec->info->showavailability; // The $availfield object is now in the format used in the old // system. Interpret this and convert to new system. $currentvalue = $DB->get_field('course_sections', 'availability', array('id' => $availfield->coursesectionid), MUST_EXIST); $newvalue = \core_availability\info::add_legacy_availability_field_condition( $currentvalue, $availfield, $show); $section = new stdClass(); $section->id = $availfield->coursesectionid; $section->availability = $newvalue; $section->timemodified = time(); $DB->update_record('course_sections', $section); } } public function process_course_format_options($data) { global $DB; $courseid = $this->get_courseid(); if (!array_key_exists($courseid, self::$courseformats)) { // It is safe to have a static cache of course formats because format can not be changed after this point. self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid)); } $data = (array)$data; if (self::$courseformats[$courseid] === $data['format']) { // Import section format options only if both courses (the one that was backed up // and the one we are restoring into) have same formats. $params = array( 'courseid' => $this->get_courseid(), 'sectionid' => $this->task->get_sectionid(), 'format' => $data['format'], 'name' => $data['name'] ); if ($record = $DB->get_record('course_format_options', $params, 'id, value')) { // Do not overwrite existing information. $newid = $record->id; } else { $params['value'] = $data['value']; $newid = $DB->insert_record('course_format_options', $params); } $this->set_mapping('course_format_options', $data['id'], $newid); } } protected function after_execute() { // Add section related files, with 'course_section' itemid to match $this->add_related_files('course', 'section', 'course_section'); } /** * Create a delegate section mapping. * * @param string $component the component name (frankenstyle) * @param int $oldsectionid The old section id. * @return int|null The new section id or null if not found. */ protected function get_delegated_section_mapping($component, $oldsectionid): ?int { $result = $this->get_mappingid("course_section::$component", $oldsectionid, null); return $result; } /** * Displace delegated sections after the given section number. * * @param int $sectionnum The section number. */ protected function displace_delegated_sections_after(int $sectionnum): void { global $DB; $sectionstomove = $DB->get_records_select( 'course_sections', 'course = ? AND component IS NOT NULL', [$this->get_courseid()], 'section DESC', 'id, section' ); // Here we add the new section to the end of the list so we make sure that all delegated sections are really // all located after the normal sections. We can have case where delegated sections are located before the // normal sections, so we need to move them to the end (mostly in the restore process more than in the duplicate // process in which the order sections => delegated section is mostly there). $sectionnum = $sectionnum + count($sectionstomove); foreach ($sectionstomove as $section) { $section->section = $sectionnum--; $DB->update_record('course_sections', $section); } } /** * Get the last section number in the course. * * @param int $courseid The course id. * @param bool $includedelegated If true, include delegated sections in the count. * @return int The last section number. */ protected function get_last_section_number(int $courseid, bool $includedelegated = false): int { global $DB; $delegtadefilter = $includedelegated ? '' : ' AND component IS NULL'; return (int) $DB->get_field_sql( 'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter, [$courseid] ); } } /** * Structure step that will read the course.xml file, loading it and performing * various actions depending of the site/restore settings. Note that target * course always exist before arriving here so this step will be updating * the course record (never inserting) */ class restore_course_structure_step extends restore_structure_step { /** * @var bool this gets set to true by {@link process_course()} if we are * restoring an old coures that used the legacy 'module security' feature. * If so, we have to do more work in {@link after_execute()}. */ protected $legacyrestrictmodules = false; /** * @var array Used when {@link $legacyrestrictmodules} is true. This is an * array with array keys the module names ('forum', 'quiz', etc.). These are * the modules that are allowed according to the data in the backup file. * In {@link after_execute()} we then have to prevent adding of all the other * types of activity. */ protected $legacyallowedmodules = array(); protected function define_structure() { $paths = []; $course = new restore_path_element('course', '/course'); $paths[] = $course; $paths[] = new restore_path_element('category', '/course/category'); $paths[] = new restore_path_element('tag', '/course/tags/tag'); $paths[] = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption'); $paths[] = new restore_path_element('allowed_module', '/course/allowed_modules/module'); // Custom fields. if ($this->get_setting_value('customfield')) { $paths[] = new restore_path_element('customfield', '/course/customfields/customfield'); } // Apply for 'format' plugins optional paths at course level $this->add_plugin_structure('format', $course); // Apply for 'theme' plugins optional paths at course level $this->add_plugin_structure('theme', $course); // Apply for 'report' plugins optional paths at course level $this->add_plugin_structure('report', $course); // Apply for 'course report' plugins optional paths at course level $this->add_plugin_structure('coursereport', $course); // Apply for plagiarism plugins optional paths at course level $this->add_plugin_structure('plagiarism', $course); // Apply for local plugins optional paths at course level $this->add_plugin_structure('local', $course); // Apply for admin tool plugins optional paths at course level. $this->add_plugin_structure('tool', $course); return $paths; } /** * Processing functions go here * * @global moodledatabase $DB * @param stdClass $data */ public function process_course($data) { global $CFG, $DB; $context = context::instance_by_id($this->task->get_contextid()); $userid = $this->task->get_userid(); $target = $this->get_task()->get_target(); $isnewcourse = $target == backup::TARGET_NEW_COURSE; // When restoring to a new course we can set all the things except for the ID number. $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid); $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid); $canforcelanguage = has_capability('moodle/course:setforcedlanguage', $context, $userid); $data = (object)$data; $data->id = $this->get_courseid(); // Calculate final course names, to avoid dupes. $fullname = $this->get_setting_value('course_fullname'); $shortname = $this->get_setting_value('course_shortname'); list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname === false ? $data->fullname : $fullname, $shortname === false ? $data->shortname : $shortname); // Do not modify the course names at all when merging and user selected to keep the names (or prohibited by cap). if (!$isnewcourse && $fullname === false) { unset($data->fullname); } if (!$isnewcourse && $shortname === false) { unset($data->shortname); } // Unset summary if user can't change it. if (!$canchangesummary) { unset($data->summary); unset($data->summaryformat); } // Unset lang if user can't change it. if (!$canforcelanguage) { unset($data->lang); } // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by // another course on this site. if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) { // Do not reset idnumber. } else if (!$isnewcourse) { // Prevent override when restoring as merge. unset($data->idnumber); } else { $data->idnumber = ''; } // If we restore a course from this site, let's capture the original course id. if ($isnewcourse && $this->get_task()->is_samesite()) { $data->originalcourseid = $this->get_task()->get_old_courseid(); } // Any empty value for course->hiddensections will lead to 0 (default, show collapsed). // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532 if (empty($data->hiddensections)) { $data->hiddensections = 0; } // Set legacyrestrictmodules to true if the course was resticting modules. If so // then we will need to process restricted modules after execution. $this->legacyrestrictmodules = !empty($data->restrictmodules); $data->startdate= $this->apply_date_offset($data->startdate); if (isset($data->enddate)) { $data->enddate = $this->apply_date_offset($data->enddate); } if ($data->defaultgroupingid) { $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid); } $courseconfig = get_config('moodlecourse'); if (empty($CFG->enablecompletion)) { // Completion is disabled globally. $data->enablecompletion = 0; $data->completionstartonenrol = 0; $data->completionnotify = 0; $data->showcompletionconditions = null; } else { $showcompletionconditionsdefault = ($courseconfig->showcompletionconditions ?? null); $data->showcompletionconditions = $data->showcompletionconditions ?? $showcompletionconditionsdefault; } $showactivitydatesdefault = ($courseconfig->showactivitydates ?? null); $data->showactivitydates = $data->showactivitydates ?? $showactivitydatesdefault; $pdffontdefault = ($courseconfig->pdfexportfont ?? null); $data->pdfexportfont = $data->pdfexportfont ?? $pdffontdefault; $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search if (isset($data->lang) && !array_key_exists($data->lang, $languages)) { $data->lang = ''; } $themes = get_list_of_themes(); // Get themes for quick search later if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) { $data->theme = ''; } // Check if this is an old SCORM course format. if ($data->format == 'scorm') { $data->format = 'singleactivity'; $data->activitytype = 'scorm'; } // Course record ready, update it $DB->update_record('course', $data); // Apply any course format options that may be saved against the course // entity in earlier-version backups. course_get_format($data)->update_course_format_options($data); // Role name aliases restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid()); } public function process_category($data) { // Nothing to do with the category. UI sets it before restore starts } public function process_tag($data) { global $CFG, $DB; $data = (object)$data; core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(), context_course::instance($this->get_courseid()), $data->rawname); } /** * Process custom fields * * @param array $data */ public function process_customfield($data) { $handler = core_course\customfield\course_handler::create(); $newid = $handler->restore_instance_data_from_backup($this->task, $data); if ($newid) { $handler->restore_define_structure($this, $newid, $data['id']); } } /** * Processes a course format option. * * @param array $data The record being restored. * @throws base_step_exception * @throws dml_exception */ public function process_course_format_option(array $data): void { global $DB; if ($data['sectionid']) { // Ignore section-level format options saved course-level in earlier-version backups. return; } $courseid = $this->get_courseid(); $record = $DB->get_record('course_format_options', [ 'courseid' => $courseid, 'name' => $data['name'], 'format' => $data['format'], 'sectionid' => 0 ], 'id'); if ($record !== false) { $DB->update_record('course_format_options', (object) [ 'id' => $record->id, 'value' => $data['value'] ]); } else { $data['courseid'] = $courseid; $DB->insert_record('course_format_options', (object) $data); } } public function process_allowed_module($data) { $data = (object)$data; // Backwards compatiblity support for the data that used to be in the // course_allowed_modules table. if ($this->legacyrestrictmodules) { $this->legacyallowedmodules[$data->modulename] = 1; } } protected function after_execute() { global $DB; // Add course related files, without itemid to match $this->add_related_files('course', 'summary', null); $this->add_related_files('course', 'overviewfiles', null); // Deal with legacy allowed modules. if ($this->legacyrestrictmodules) { $context = context_course::instance($this->get_courseid()); list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities'); list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config'); foreach ($managerroleids as $roleid) { unset($roleids[$roleid]); } foreach (core_component::get_plugin_list('mod') as $modname => $notused) { if (isset($this->legacyallowedmodules[$modname])) { // Module is allowed, no worries. continue; } $capability = 'mod/' . $modname . ':addinstance'; if (!get_capability_info($capability)) { $this->log("Capability '{$capability}' was not found!", backup::LOG_WARNING); continue; } foreach ($roleids as $roleid) { assign_capability($capability, CAP_PREVENT, $roleid, $context); } } } } } /** * Execution step that will migrate legacy files if present. */ class restore_course_legacy_files_step extends restore_execution_step { public function define_execution() { global $DB; // Do a check for legacy files and skip if there are none. $sql = 'SELECT count(*) FROM {backup_files_temp} WHERE backupid = ? AND contextid = ? AND component = ? AND filearea = ?'; $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy'); if ($DB->count_records_sql($sql, $params)) { $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid())); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course', 'legacy', $this->task->get_old_contextid(), $this->task->get_userid()); } } } /* * Structure step that will read the roles.xml file (at course/activity/block levels) * containing all the role_assignments and overrides for that context. If corresponding to * one mapped role, they will be applied to target context. Will observe the role_assignments * setting to decide if ras are restored. * * Note: this needs to be executed after all users are enrolled. */ class restore_ras_and_caps_structure_step extends restore_structure_step { protected $plugins = null; protected function define_structure() { $paths = array(); // Observe the role_assignments setting if ($this->get_setting_value('role_assignments')) { $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment'); } if ($this->get_setting_value('permissions')) { $paths[] = new restore_path_element('override', '/roles/role_overrides/override'); } return $paths; } /** * Assign roles * * This has to be called after enrolments processing. * * @param mixed $data * @return void */ public function process_assignment($data) { global $DB; $data = (object)$data; // Check roleid, userid are one of the mapped ones if (!$newroleid = $this->get_mappingid('role', $data->roleid)) { return; } if (!$newuserid = $this->get_mappingid('user', $data->userid)) { return; } if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) { // Only assign roles to not deleted users return; } if (!$contextid = $this->task->get_contextid()) { return; } if (empty($data->component)) { // assign standard manual roles // TODO: role_assign() needs one userid param to be able to specify our restore userid role_assign($newroleid, $newuserid, $contextid); } else if ((strpos($data->component, 'enrol_') === 0)) { // Deal with enrolment roles - ignore the component and just find out the instance via new id, // it is possible that enrolment was restored using different plugin type. if (!isset($this->plugins)) { $this->plugins = enrol_get_plugins(true); } if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) { if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { if (isset($this->plugins[$instance->enrol])) { $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid); } } } } else { $data->roleid = $newroleid; $data->userid = $newuserid; $data->contextid = $contextid; $dir = core_component::get_component_directory($data->component); if ($dir and is_dir($dir)) { if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) { return; } } // Bad luck, plugin could not restore the data, let's add normal membership. role_assign($data->roleid, $data->userid, $data->contextid); $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead."; $this->log($message, backup::LOG_WARNING); } } public function process_override($data) { $data = (object)$data; // Check roleid is one of the mapped ones $newrole = $this->get_mapping('role', $data->roleid); $newroleid = $newrole->newitemid ?? false; $userid = $this->task->get_userid(); // If newroleid and context are valid assign it via API (it handles dupes and so on) if ($newroleid && $this->task->get_contextid()) { if (!$capability = get_capability_info($data->capability)) { $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING); } else { $context = context::instance_by_id($this->task->get_contextid()); $overrideableroles = get_overridable_roles($context, ROLENAME_SHORT); $safecapability = is_safe_capability($capability); // Check if the new role is an overrideable role AND if the user performing the restore has the // capability to assign the capability. if (in_array($newrole->info['shortname'], $overrideableroles) && (has_capability('moodle/role:override', $context, $userid) || ($safecapability && has_capability('moodle/role:safeoverride', $context, $userid))) ) { assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid()); } else { $this->log("Insufficient capability to assign capability '{$data->capability}' to role!", backup::LOG_WARNING); } } } } } /** * If no instances yet add default enrol methods the same way as when creating new course in UI. */ class restore_default_enrolments_step extends restore_execution_step { public function define_execution() { global $DB; // No enrolments in front page. if ($this->get_courseid() == SITEID) { return; } $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST); // Return any existing course enrolment instances. $enrolinstances = enrol_get_instances($course->id, false); if ($enrolinstances) { // Something already added instances. // Get the existing enrolment methods in the course. $enrolmethods = array_map(function($enrolinstance) { return $enrolinstance->enrol; }, $enrolinstances); $plugins = enrol_get_plugins(true); foreach ($plugins as $pluginname => $plugin) { // Make sure all default enrolment methods exist in the course. if (!in_array($pluginname, $enrolmethods)) { $plugin->course_updated(true, $course, null); } $plugin->restore_sync_course($course); } } else { // Looks like a newly created course. enrol_course_updated(true, $course, null); } } } /** * This structure steps restores the enrol plugins and their underlying * enrolments, performing all the mappings and/or movements required */ class restore_enrolments_structure_step extends restore_structure_step { protected $enrolsynced = false; protected $plugins = null; protected $originalstatus = array(); /** * Conditionally decide if this step should be executed. * * This function checks the following parameter: * * 1. the course/enrolments.xml file exists * * @return bool true is safe to execute, false otherwise */ protected function execute_condition() { if ($this->get_courseid() == SITEID) { return false; } // Check it is included in the backup $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore enrolments info return false; } return true; } protected function define_structure() { $userinfo = $this->get_setting_value('users'); $paths = []; $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol'); if ($userinfo) { $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment'); } // Attach local plugin stucture to enrol element. $this->add_plugin_structure('enrol', $enrol); return $paths; } /** * Create enrolment instances. * * This has to be called after creation of roles * and before adding of role assignments. * * @param mixed $data * @return void */ public function process_enrol($data) { global $DB; $data = (object)$data; $oldid = $data->id; // We'll need this later. unset($data->id); $this->originalstatus[$oldid] = $data->status; if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) { $this->set_mapping('enrol', $oldid, 0); return; } if (!isset($this->plugins)) { $this->plugins = enrol_get_plugins(true); } if (!$this->enrolsynced) { // Make sure that all plugin may create instances and enrolments automatically // before the first instance restore - this is suitable especially for plugins // that synchronise data automatically using course->idnumber or by course categories. foreach ($this->plugins as $plugin) { $plugin->restore_sync_course($courserec); } $this->enrolsynced = true; } // Map standard fields - plugin has to process custom fields manually. $data->roleid = $this->get_mappingid('role', $data->roleid); $data->courseid = $courserec->id; if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) { $converttomanual = true; } else { $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER); } if ($converttomanual) { // Restore enrolments as manual enrolments. unset($data->sortorder); // Remove useless sortorder from <2.4 backups. if (!enrol_is_enabled('manual')) { $this->set_mapping('enrol', $oldid, 0); return; } if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) { $instance = reset($instances); $this->set_mapping('enrol', $oldid, $instance->id); } else { if ($data->enrol === 'manual') { $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data); } else { $instanceid = $this->plugins['manual']->add_default_instance($courserec); } $this->set_mapping('enrol', $oldid, $instanceid); } } else { if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) { $this->set_mapping('enrol', $oldid, 0); $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods"; $this->log($message, backup::LOG_WARNING); return; } if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) { // Let's keep the sortorder in old backups. } else { // Prevent problems with colliding sortorders in old backups, // new 2.4 backups do not need sortorder because xml elements are ordered properly. unset($data->sortorder); } // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type. $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid); } } /** * Create user enrolments. * * This has to be called after creation of enrolment instances * and before adding of role assignments. * * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards. * * @param mixed $data * @return void */ public function process_enrolment($data) { global $DB; if (!isset($this->plugins)) { $this->plugins = enrol_get_plugins(true); } $data = (object)$data; // Process only if parent instance have been mapped. if ($enrolid = $this->get_new_parentid('enrol')) { $oldinstancestatus = ENROL_INSTANCE_ENABLED; $oldenrolid = $this->get_old_parentid('enrol'); if (isset($this->originalstatus[$oldenrolid])) { $oldinstancestatus = $this->originalstatus[$oldenrolid]; } if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) { // And only if user is a mapped one. if ($userid = $this->get_mappingid('user', $data->userid)) { if (isset($this->plugins[$instance->enrol])) { $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus); } } } } } } /** * Make sure the user restoring the course can actually access it. */ class restore_fix_restorer_access_step extends restore_execution_step { protected function define_execution() { global $CFG, $DB; if (!$userid = $this->task->get_userid()) { return; } if (empty($CFG->restorernewroleid)) { // Bad luck, no fallback role for restorers specified return; } $courseid = $this->get_courseid(); $context = context_course::instance($courseid); if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) { // Current user may access the course (admin, category manager or restored teacher enrolment usually) return; } // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled role_assign($CFG->restorernewroleid, $userid, $context); if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) { // Extra role is enough, yay! return; } // The last chance is to create manual enrol if it does not exist and and try to enrol the current user, // hopefully admin selected suitable $CFG->restorernewroleid ... if (!enrol_is_enabled('manual')) { return; } if (!$enrol = enrol_get_plugin('manual')) { return; } if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) { $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0)); $enrol->add_instance($course, $fields); } enrol_try_internal_enrol($courseid, $userid); } } /** * This structure steps restores the filters and their configs */ class restore_filters_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active'); $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config'); return $paths; } public function process_active($data) { $data = (object)$data; if (strpos($data->filter, 'filter/') === 0) { $data->filter = substr($data->filter, 7); } else if (strpos($data->filter, '/') !== false) { // Unsupported old filter. return; } if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do return; } filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active); } public function process_config($data) { $data = (object)$data; if (strpos($data->filter, 'filter/') === 0) { $data->filter = substr($data->filter, 7); } else if (strpos($data->filter, '/') !== false) { // Unsupported old filter. return; } if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do return; } filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value); } } /** * This structure steps restores the comments * Note: Cannot use the comments API because defaults to USER->id. * That should change allowing to pass $userid */ class restore_comments_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); $paths[] = new restore_path_element('comment', '/comments/comment'); return $paths; } public function process_comment($data) { global $DB; $data = (object)$data; // First of all, if the comment has some itemid, ask to the task what to map $mapping = false; if ($data->itemid) { $mapping = $this->task->get_comment_mapping_itemname($data->commentarea); $data->itemid = $this->get_mappingid($mapping, $data->itemid); } // Only restore the comment if has no mapping OR we have found the matching mapping if (!$mapping || $data->itemid) { // Only if user mapping and context $data->userid = $this->get_mappingid('user', $data->userid); if ($data->userid && $this->task->get_contextid()) { $data->contextid = $this->task->get_contextid(); // Only if there is another comment with same context/user/timecreated $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated); if (!$DB->record_exists('comments', $params)) { $DB->insert_record('comments', $data); } } } } } /** * This structure steps restores the badges and their configs */ class restore_badges_structure_step extends restore_structure_step { /** * Conditionally decide if this step should be executed. * * This function checks the following parameters: * * 1. Badges and course badges are enabled on the site. * 2. The course/badges.xml file exists. * 3. All modules are restorable. * 4. All modules are marked for restore. * * @return bool True is safe to execute, false otherwise */ protected function execute_condition() { global $CFG; // First check is badges and course level badges are enabled on this site. if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) { // Disabled, don't restore course badges. return false; } // Check if badges.xml is included in the backup. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore course badges. return false; } // Check we are able to restore all backed up modules. if ($this->task->is_missing_modules()) { return false; } // Finally check all modules within the backup are being restored. if ($this->task->is_excluding_activities()) { return false; } return true; } protected function define_structure() { $paths = array(); $paths[] = new restore_path_element('badge', '/badges/badge'); $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion'); $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter'); $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement'); $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment'); $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge'); $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award'); $paths[] = new restore_path_element('tag', '/badges/badge/tags/tag'); return $paths; } public function process_badge($data) { global $DB, $CFG; require_once($CFG->libdir . '/badgeslib.php'); $data = (object)$data; $data->usercreated = $this->get_mappingid('user', $data->usercreated); if (empty($data->usercreated)) { $data->usercreated = $this->task->get_userid(); } $data->usermodified = $this->get_mappingid('user', $data->usermodified); if (empty($data->usermodified)) { $data->usermodified = $this->task->get_userid(); } // We'll restore the badge image. $restorefiles = true; $courseid = $this->get_courseid(); $params = array( 'name' => $data->name, 'description' => $data->description, 'timecreated' => $data->timecreated, 'timemodified' => $data->timemodified, 'usercreated' => $data->usercreated, 'usermodified' => $data->usermodified, 'issuername' => $data->issuername, 'issuerurl' => $data->issuerurl, 'issuercontact' => $data->issuercontact, 'expiredate' => $this->apply_date_offset($data->expiredate), 'expireperiod' => $data->expireperiod, 'type' => BADGE_TYPE_COURSE, 'courseid' => $courseid, 'message' => $data->message, 'messagesubject' => $data->messagesubject, 'attachment' => $data->attachment, 'notification' => $data->notification, 'status' => BADGE_STATUS_INACTIVE, 'nextcron' => $data->nextcron, 'version' => $data->version, 'language' => $data->language, 'imageauthorname' => $data->imageauthorname, 'imageauthoremail' => $data->imageauthoremail, 'imageauthorurl' => $data->imageauthorurl, 'imagecaption' => $data->imagecaption ); $newid = $DB->insert_record('badge', $params); $this->set_mapping('badge', $data->id, $newid, $restorefiles); } /** * Create an endorsement for a badge. * * @param mixed $data * @return void */ public function process_endorsement($data) { global $DB; $data = (object)$data; $params = [ 'badgeid' => $this->get_new_parentid('badge'), 'issuername' => $data->issuername, 'issuerurl' => $data->issuerurl, 'issueremail' => $data->issueremail, 'claimid' => $data->claimid, 'claimcomment' => $data->claimcomment, 'dateissued' => $this->apply_date_offset($data->dateissued) ]; $newid = $DB->insert_record('badge_endorsement', $params); $this->set_mapping('endorsement', $data->id, $newid); } /** * Link to related badges for a badge. This relies on post processing in after_execute(). * * @param mixed $data * @return void */ public function process_relatedbadge($data) { global $DB; $data = (object)$data; $relatedbadgeid = $data->relatedbadgeid; if ($relatedbadgeid) { // Only backup and restore related badges if they are contained in the backup file. $params = array( 'badgeid' => $this->get_new_parentid('badge'), 'relatedbadgeid' => $relatedbadgeid ); $newid = $DB->insert_record('badge_related', $params); } } /** * Link to an alignment for a badge. * * @param mixed $data * @return void */ public function process_alignment($data) { global $DB; $data = (object)$data; $params = array( 'badgeid' => $this->get_new_parentid('badge'), 'targetname' => $data->targetname, 'targeturl' => $data->targeturl, 'targetdescription' => $data->targetdescription, 'targetframework' => $data->targetframework, 'targetcode' => $data->targetcode ); $newid = $DB->insert_record('badge_alignment', $params); $this->set_mapping('alignment', $data->id, $newid); } public function process_criterion($data) { global $DB; $data = (object)$data; $params = array( 'badgeid' => $this->get_new_parentid('badge'), 'criteriatype' => $data->criteriatype, 'method' => $data->method, 'description' => isset($data->description) ? $data->description : '', 'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0, ); $newid = $DB->insert_record('badge_criteria', $params); $this->set_mapping('criterion', $data->id, $newid); } public function process_parameter($data) { global $DB, $CFG; require_once($CFG->libdir . '/badgeslib.php'); $data = (object)$data; $criteriaid = $this->get_new_parentid('criterion'); // Parameter array that will go to database. $params = array(); $params['critid'] = $criteriaid; $oldparam = explode('_', $data->name); if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) { $module = $this->get_mappingid('course_module', $oldparam[1]); $params['name'] = $oldparam[0] . '_' . $module; $params['value'] = $oldparam[0] == 'module' ? $module : $data->value; } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) { $params['name'] = $oldparam[0] . '_' . $this->get_courseid(); $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value; } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) { $role = $this->get_mappingid('role', $data->value); if (!empty($role)) { $params['name'] = 'role_' . $role; $params['value'] = $role; } else { return; } } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) { $competencyid = $this->get_mappingid('competency', $data->value); if (!empty($competencyid)) { $params['name'] = 'competency_' . $competencyid; $params['value'] = $competencyid; } else { return; } } if (!$DB->record_exists('badge_criteria_param', $params)) { $DB->insert_record('badge_criteria_param', $params); } } public function process_manual_award($data) { global $DB; $data = (object)$data; $role = $this->get_mappingid('role', $data->issuerrole); if (!empty($role)) { $award = array( 'badgeid' => $this->get_new_parentid('badge'), 'recipientid' => $this->get_mappingid('user', $data->recipientid), 'issuerid' => $this->get_mappingid('user', $data->issuerid), 'issuerrole' => $role, 'datemet' => $this->apply_date_offset($data->datemet) ); // Skip the manual award if recipient or issuer can not be mapped to. if (empty($award['recipientid']) || empty($award['issuerid'])) { return; } $DB->insert_record('badge_manual_award', $award); } } /** * Process tag. * * @param array $data The data. * @throws base_step_exception */ public function process_tag(array $data): void { $data = (object)$data; $badgeid = $this->get_new_parentid('badge'); if (!empty($data->rawname)) { core_tag_tag::add_item_tag('core_badges', 'badge', $badgeid, context_course::instance($this->get_courseid()), $data->rawname); } } protected function after_execute() { global $DB; // Add related files. $this->add_related_files('badges', 'badgeimage', 'badge'); $badgeid = $this->get_new_parentid('badge'); // Remap any related badges. // We do this in the DB directly because this is backup/restore it is not valid to call into // the component API. $params = array('badgeid' => $badgeid); $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid FROM {badge_related} br WHERE (br.badgeid = :badgeid)"; $relatedbadges = $DB->get_records_sql($query, $params); $newrelatedids = []; foreach ($relatedbadges as $relatedbadge) { $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid); $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid; $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params); if ($relatedid) { $newrelatedids[] = $relatedid; } } if (!empty($newrelatedids)) { $relatedbadges = []; foreach ($newrelatedids as $relatedid) { $relatedbadge = new stdClass(); $relatedbadge->badgeid = $badgeid; $relatedbadge->relatedbadgeid = $relatedid; $relatedbadges[] = $relatedbadge; } $DB->insert_records('badge_related', $relatedbadges); } } } /** * This structure steps restores the calendar events */ class restore_calendarevents_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); $paths[] = new restore_path_element('calendarevents', '/events/event'); return $paths; } public function process_calendarevents($data) { global $DB, $SITE, $USER; $data = (object)$data; $oldid = $data->id; $restorefiles = true; // We'll restore the files // If this is a new action event, it will automatically be populated by the adhoc task. // Nothing to do here. if (isset($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION) { return; } // User overrides for activities are identified by having a courseid of zero with // both a modulename and instance value set. $isuseroverride = !$data->courseid && $data->modulename && $data->instance; // If we don't want to include user data and this record is a user override event // for an activity then we should not create it. (Only activity events can be user override events - which must have this // setting). if ($isuseroverride && $this->task->setting_exists('userinfo') && !$this->task->get_setting_value('userinfo')) { return; } // Find the userid and the groupid associated with the event. $data->userid = $this->get_mappingid('user', $data->userid); if ($data->userid === false) { // Blank user ID means that we are dealing with module generated events such as quiz starting times. // Use the current user ID for these events. $data->userid = $USER->id; } if (!empty($data->groupid)) { $data->groupid = $this->get_mappingid('group', $data->groupid); if ($data->groupid === false) { return; } } // Handle events with empty eventtype //MDL-32827 if(empty($data->eventtype)) { if ($data->courseid == $SITE->id) { // Site event $data->eventtype = "site"; } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) { // Course assingment event $data->eventtype = "due"; } else if ($data->courseid != 0 && $data->groupid == 0) { // Course event $data->eventtype = "course"; } else if ($data->groupid) { // Group event $data->eventtype = "group"; } else if ($data->userid) { // User event $data->eventtype = "user"; } else { return; } } $params = array( 'name' => $data->name, 'description' => $data->description, 'format' => $data->format, // User overrides in activities use a course id of zero. All other event types // must use the mapped course id. 'courseid' => $data->courseid ? $this->get_courseid() : 0, 'groupid' => $data->groupid, 'userid' => $data->userid, 'repeatid' => $this->get_mappingid('event', $data->repeatid), 'modulename' => $data->modulename, 'type' => isset($data->type) ? $data->type : 0, 'eventtype' => $data->eventtype, 'timestart' => $this->apply_date_offset($data->timestart), 'timeduration' => $data->timeduration, 'timesort' => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null, 'visible' => $data->visible, 'uuid' => $data->uuid, 'sequence' => $data->sequence, 'timemodified' => $data->timemodified, 'priority' => isset($data->priority) ? $data->priority : null, 'location' => isset($data->location) ? $data->location : null); if ($this->name == 'activity_calendar') { $params['instance'] = $this->task->get_activityid(); } else { $params['instance'] = 0; } $sql = "SELECT id FROM {event} WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . " AND courseid = ? AND modulename = ? AND instance = ? AND timestart = ? AND timeduration = ? AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255); $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['instance'], $params['timestart'], $params['timeduration'], $params['description']); $result = $DB->record_exists_sql($sql, $arg); if (empty($result)) { $newitemid = $DB->insert_record('event', $params); $this->set_mapping('event', $oldid, $newitemid); $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles); } // With repeating events, each event has the repeatid pointed at the first occurrence. // Since the repeatid will be empty when the first occurrence is restored, // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence. // Then keep a list of repeatids so we only perform this update once. static $repeatids = array(); if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) { // This entry is repeated so the repeatid field must be set. $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid'])); $repeatids[] = $params['repeatid']; } } protected function after_execute() { // Add related files $this->add_related_files('calendar', 'event_description', 'event_description'); } } class restore_course_completion_structure_step extends restore_structure_step { /** * Conditionally decide if this step should be executed. * * This function checks parameters that are not immediate settings to ensure * that the enviroment is suitable for the restore of course completion info. * * This function checks the following four parameters: * * 1. Course completion is enabled on the site * 2. The backup includes course completion information * 3. All modules are restorable * 4. All modules are marked for restore. * 5. No completion criteria already exist for the course. * * @return bool True is safe to execute, false otherwise */ protected function execute_condition() { global $CFG, $DB; // First check course completion is enabled on this site if (empty($CFG->enablecompletion)) { // Disabled, don't restore course completion return false; } // No course completion on the front page. if ($this->get_courseid() == SITEID) { return false; } // Check it is included in the backup $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore course completion return false; } // Check we are able to restore all backed up modules if ($this->task->is_missing_modules()) { return false; } // Check all modules within the backup are being restored. if ($this->task->is_excluding_activities()) { return false; } // Check that no completion criteria is already set for the course. if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) { return false; } return true; } /** * Define the course completion structure * * @return array Array of restore_path_element */ protected function define_structure() { // To know if we are including user completion info $userinfo = $this->get_setting_value('userscompletion'); $paths = array(); $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria'); $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd'); if ($userinfo) { $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl'); $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions'); } return $paths; } /** * Process course completion criteria * * @global moodle_database $DB * @param stdClass $data */ public function process_course_completion_criteria($data) { global $DB; $data = (object)$data; $data->course = $this->get_courseid(); // Apply the date offset to the time end field $data->timeend = $this->apply_date_offset($data->timeend); // Map the role from the criteria if (isset($data->role) && $data->role != '') { // Newer backups should include roleshortname, which makes this much easier. if (!empty($data->roleshortname)) { $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname)); if (!$roleinstanceid) { $this->log( 'Could not match the role shortname in course_completion_criteria, so skipping', backup::LOG_DEBUG ); return; } $data->role = $roleinstanceid; } else { $data->role = $this->get_mappingid('role', $data->role); } // Check we have an id, otherwise it causes all sorts of bugs. if (!$data->role) { $this->log( 'Could not match role in course_completion_criteria, so skipping', backup::LOG_DEBUG ); return; } } // If the completion criteria is for a module we need to map the module instance // to the new module id. if (!empty($data->moduleinstance) && !empty($data->module)) { $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance); if (empty($data->moduleinstance)) { $this->log( 'Could not match the module instance in course_completion_criteria, so skipping', backup::LOG_DEBUG ); return; } } else { $data->module = null; $data->moduleinstance = null; } // We backup the course shortname rather than the ID so that we can match back to the course if (!empty($data->courseinstanceshortname)) { $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname)); if (!$courseinstanceid) { $this->log( 'Could not match the course instance in course_completion_criteria, so skipping', backup::LOG_DEBUG ); return; } } else { $courseinstanceid = null; } $data->courseinstance = $courseinstanceid; $params = array( 'course' => $data->course, 'criteriatype' => $data->criteriatype, 'enrolperiod' => $data->enrolperiod, 'courseinstance' => $data->courseinstance, 'module' => $data->module, 'moduleinstance' => $data->moduleinstance, 'timeend' => $data->timeend, 'gradepass' => $data->gradepass, 'role' => $data->role ); $newid = $DB->insert_record('course_completion_criteria', $params); $this->set_mapping('course_completion_criteria', $data->id, $newid); } /** * Processes course compltion criteria complete records * * @global moodle_database $DB * @param stdClass $data */ public function process_course_completion_crit_compl($data) { global $DB; $data = (object)$data; // This may be empty if criteria could not be restored $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid); $data->course = $this->get_courseid(); $data->userid = $this->get_mappingid('user', $data->userid); if (!empty($data->criteriaid) && !empty($data->userid)) { $params = array( 'userid' => $data->userid, 'course' => $data->course, 'criteriaid' => $data->criteriaid, 'timecompleted' => $data->timecompleted ); if (isset($data->gradefinal)) { $params['gradefinal'] = $data->gradefinal; } if (isset($data->unenroled)) { $params['unenroled'] = $data->unenroled; } $DB->insert_record('course_completion_crit_compl', $params); } } /** * Process course completions * * @global moodle_database $DB * @param stdClass $data */ public function process_course_completions($data) { global $DB; $data = (object)$data; $data->course = $this->get_courseid(); $data->userid = $this->get_mappingid('user', $data->userid); if (!empty($data->userid)) { $params = array( 'userid' => $data->userid, 'course' => $data->course, 'timeenrolled' => $data->timeenrolled, 'timestarted' => $data->timestarted, 'timecompleted' => $data->timecompleted, 'reaggregate' => $data->reaggregate ); $existing = $DB->get_record('course_completions', array( 'userid' => $data->userid, 'course' => $data->course )); // MDL-46651 - If cron writes out a new record before we get to it // then we should replace it with the Truth data from the backup. // This may be obsolete after MDL-48518 is resolved if ($existing) { $params['id'] = $existing->id; $DB->update_record('course_completions', $params); } else { $DB->insert_record('course_completions', $params); } } } /** * Process course completion aggregate methods * * @global moodle_database $DB * @param stdClass $data */ public function process_course_completion_aggr_methd($data) { global $DB; $data = (object)$data; $data->course = $this->get_courseid(); // Only create the course_completion_aggr_methd records if // the target course has not them defined. MDL-28180 if (!$DB->record_exists('course_completion_aggr_methd', array( 'course' => $data->course, 'criteriatype' => $data->criteriatype))) { $params = array( 'course' => $data->course, 'criteriatype' => $data->criteriatype, 'method' => $data->method, 'value' => $data->value, ); $DB->insert_record('course_completion_aggr_methd', $params); } } } /** * This structure step restores course logs (cmid = 0), delegating * the hard work to the corresponding {@link restore_logs_processor} passing the * collection of {@link restore_log_rule} rules to be observed as they are defined * by the task. Note this is only executed based in the 'logs' setting. * * NOTE: This is executed by final task, to have all the activities already restored * * NOTE: Not all course logs are being restored. For now only 'course' and 'user' * records are. There are others like 'calendar' and 'upload' that will be handled * later. * * NOTE: All the missing actions (not able to be restored) are sent to logs for * debugging purposes */ class restore_course_logs_structure_step extends restore_structure_step { /** * Conditionally decide if this step should be executed. * * This function checks the following parameter: * * 1. the course/logs.xml file exists * * @return bool true is safe to execute, false otherwise */ protected function execute_condition() { // Check it is included in the backup $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore course logs return false; } return true; } protected function define_structure() { $paths = array(); // Simple, one plain level of information contains them $paths[] = new restore_path_element('log', '/logs/log'); return $paths; } protected function process_log($data) { global $DB; $data = (object)($data); // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961. $data->userid = $this->get_mappingid('user', $data->userid); $data->course = $this->get_courseid(); $data->cmid = 0; // For any reason user wasn't remapped ok, stop processing this if (empty($data->userid)) { return; } // Everything ready, let's delegate to the restore_logs_processor // Set some fixed values that will save tons of DB requests $values = array( 'course' => $this->get_courseid()); // Get instance and process log record $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data); // If we have data, insert it, else something went wrong in the restore_logs_processor if ($data) { if (empty($data->url)) { $data->url = ''; } if (empty($data->info)) { $data->info = ''; } // Store the data in the legacy log table if we are still using it. $manager = get_log_manager(); if (method_exists($manager, 'legacy_add_to_log')) { $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url, $data->info, $data->cmid, $data->userid, $data->ip, $data->time); } } } } /** * This structure step restores activity logs, extending {@link restore_course_logs_structure_step} * sharing its same structure but modifying the way records are handled */ class restore_activity_logs_structure_step extends restore_course_logs_structure_step { protected function process_log($data) { global $DB; $data = (object)($data); // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961. $data->userid = $this->get_mappingid('user', $data->userid); $data->course = $this->get_courseid(); $data->cmid = $this->task->get_moduleid(); // For any reason user wasn't remapped ok, stop processing this if (empty($data->userid)) { return; } // Everything ready, let's delegate to the restore_logs_processor // Set some fixed values that will save tons of DB requests $values = array( 'course' => $this->get_courseid(), 'course_module' => $this->task->get_moduleid(), $this->task->get_modulename() => $this->task->get_activityid()); // Get instance and process log record $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data); // If we have data, insert it, else something went wrong in the restore_logs_processor if ($data) { if (empty($data->url)) { $data->url = ''; } if (empty($data->info)) { $data->info = ''; } // Store the data in the legacy log table if we are still using it. $manager = get_log_manager(); if (method_exists($manager, 'legacy_add_to_log')) { $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url, $data->info, $data->cmid, $data->userid, $data->ip, $data->time); } } } } /** * Structure step in charge of restoring the logstores.xml file for the course logs. * * This restore step will rebuild the logs for all the enabled logstore subplugins supporting * it, for logs belonging to the course level. */ class restore_course_logstores_structure_step extends restore_structure_step { /** * Conditionally decide if this step should be executed. * * This function checks the following parameter: * * 1. the logstores.xml file exists * * @return bool true is safe to execute, false otherwise */ protected function execute_condition() { // Check it is included in the backup. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore logstores.xml information. return false; } return true; } /** * Return the elements to be processed on restore of logstores. * * @return restore_path_element[] array of elements to be processed on restore. */ protected function define_structure() { $paths = array(); $logstore = new restore_path_element('logstore', '/logstores/logstore'); $paths[] = $logstore; // Add logstore subplugin support to the 'logstore' element. $this->add_subplugin_structure('logstore', $logstore, 'tool', 'log'); return array($logstore); } /** * Process the 'logstore' element, * * Note: This is empty by definition in backup, because stores do not share any * data between them, so there is nothing to process here. * * @param array $data element data */ protected function process_logstore($data) { return; } } /** * Structure step in charge of restoring the loglastaccess.xml file for the course logs. * * This restore step will rebuild the table for user_lastaccess table. */ class restore_course_loglastaccess_structure_step extends restore_structure_step { /** * Conditionally decide if this step should be executed. * * This function checks the following parameter: * * 1. the loglastaccess.xml file exists * * @return bool true is safe to execute, false otherwise */ protected function execute_condition() { // Check it is included in the backup. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { // Not found, can't restore loglastaccess.xml information. return false; } return true; } /** * Return the elements to be processed on restore of loglastaccess. * * @return restore_path_element[] array of elements to be processed on restore. */ protected function define_structure() { $paths = array(); // To know if we are including userinfo. $userinfo = $this->get_setting_value('users'); if ($userinfo) { $paths[] = new restore_path_element('lastaccess', '/lastaccesses/lastaccess'); } // Return the paths wrapped. return $paths; } /** * Process the 'lastaccess' elements. * * @param array $data element data */ protected function process_lastaccess($data) { global $DB; $data = (object)$data; $data->courseid = $this->get_courseid(); if (!$data->userid = $this->get_mappingid('user', $data->userid)) { return; // Nothing to do, not able to find the user to set the lastaccess time. } // Check if record does exist. $exists = $DB->get_record('user_lastaccess', array('courseid' => $data->courseid, 'userid' => $data->userid)); if ($exists) { // If the time of last access of the restore is newer, then replace and update. if ($exists->timeaccess < $data->timeaccess) { $exists->timeaccess = $data->timeaccess; $DB->update_record('user_lastaccess', $exists); } } else { $DB->insert_record('user_lastaccess', $data); } } } /** * Structure step in charge of restoring the logstores.xml file for the activity logs. * * Note: Activity structure is completely equivalent to the course one, so just extend it. */ class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step { } /** * Restore course competencies structure step. */ class restore_course_competencies_structure_step extends restore_structure_step { /** * Returns the structure. * * @return array */ protected function define_structure() { $userinfo = $this->get_setting_value('users'); $paths = array( new restore_path_element('course_competency', '/course_competencies/competencies/competency'), new restore_path_element('course_competency_settings', '/course_competencies/settings'), ); if ($userinfo) { $paths[] = new restore_path_element('user_competency_course', '/course_competencies/user_competencies/user_competency'); } return $paths; } /** * Process a course competency settings. * * @param array $data The data. */ public function process_course_competency_settings($data) { global $DB; $data = (object) $data; // We do not restore the course settings during merge. $target = $this->get_task()->get_target(); if ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) { return; } $courseid = $this->task->get_courseid(); $exists = \core_competency\course_competency_settings::record_exists_select('courseid = :courseid', array('courseid' => $courseid)); // Strangely the course settings already exist, let's just leave them as is then. if ($exists) { $this->log('Course competency settings not restored, existing settings have been found.', backup::LOG_WARNING); return; } $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans); $settings = new \core_competency\course_competency_settings(0, $data); $settings->create(); } /** * Process a course competency. * * @param array $data The data. */ public function process_course_competency($data) { $data = (object) $data; // Mapping the competency by ID numbers. $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber)); if (!$framework) { return; } $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber, 'competencyframeworkid' => $framework->get('id'))); if (!$competency) { return; } $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get('id')); $params = array( 'competencyid' => $competency->get('id'), 'courseid' => $this->task->get_courseid() ); $query = 'competencyid = :competencyid AND courseid = :courseid'; $existing = \core_competency\course_competency::record_exists_select($query, $params); if (!$existing) { // Sortorder is ignored by precaution, anyway we should walk through the records in the right order. $record = (object) $params; $record->ruleoutcome = $data->ruleoutcome; $coursecompetency = new \core_competency\course_competency(0, $record); $coursecompetency->create(); } } /** * Process the user competency course. * * @param array $data The data. */ public function process_user_competency_course($data) { global $USER, $DB; $data = (object) $data; $data->competencyid = $this->get_mappingid(\core_competency\competency::TABLE, $data->competencyid); if (!$data->competencyid) { // This is strange, the competency does not belong to the course. return; } else if ($data->grade === null) { // We do not need to do anything when there is no grade. return; } $data->userid = $this->get_mappingid('user', $data->userid); $shortname = $DB->get_field('course', 'shortname', array('id' => $this->task->get_courseid()), MUST_EXIST); // The method add_evidence also sets the course rating. \core_competency\api::add_evidence($data->userid, $data->competencyid, $this->task->get_contextid(), \core_competency\evidence::ACTION_OVERRIDE, 'evidence_courserestored', 'core_competency', $shortname, false, null, $data->grade, $USER->id); } /** * Execute conditions. * * @return bool */ protected function execute_condition() { // Do not execute if competencies are not included. if (!$this->get_setting_value('competencies')) { return false; } // Do not execute if the competencies XML file is not found. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } return true; } } /** * Restore activity competencies structure step. */ class restore_activity_competencies_structure_step extends restore_structure_step { /** * Defines the structure. * * @return array */ protected function define_structure() { $paths = array( new restore_path_element('course_module_competency', '/course_module_competencies/competencies/competency') ); return $paths; } /** * Process a course module competency. * * @param array $data The data. */ public function process_course_module_competency($data) { $data = (object) $data; // Mapping the competency by ID numbers. $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber)); if (!$framework) { return; } $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber, 'competencyframeworkid' => $framework->get('id'))); if (!$competency) { return; } $params = array( 'competencyid' => $competency->get('id'), 'cmid' => $this->task->get_moduleid() ); $query = 'competencyid = :competencyid AND cmid = :cmid'; $existing = \core_competency\course_module_competency::record_exists_select($query, $params); if (!$existing) { // Sortorder is ignored by precaution, anyway we should walk through the records in the right order. $record = (object) $params; $record->ruleoutcome = $data->ruleoutcome; $record->overridegrade = $data->overridegrade ?? 0; $coursemodulecompetency = new \core_competency\course_module_competency(0, $record); $coursemodulecompetency->create(); } } /** * Execute conditions. * * @return bool */ protected function execute_condition() { // Do not execute if competencies are not included. if (!$this->get_setting_value('competencies')) { return false; } // Do not execute if the competencies XML file is not found. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } return true; } } /** * Defines the restore step for advanced grading methods attached to the activity module */ class restore_activity_grading_structure_step extends restore_structure_step { /** * This step is executed only if the grading file is present */ protected function execute_condition() { if ($this->get_courseid() == SITEID) { return false; } $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } return true; } /** * Declares paths in the grading.xml file we are interested in */ protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $area = new restore_path_element('grading_area', '/areas/area'); $paths[] = $area; // attach local plugin stucture to $area element $this->add_plugin_structure('local', $area); $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition'); $paths[] = $definition; $this->add_plugin_structure('gradingform', $definition); // attach local plugin stucture to $definition element $this->add_plugin_structure('local', $definition); if ($userinfo) { $instance = new restore_path_element('grading_instance', '/areas/area/definitions/definition/instances/instance'); $paths[] = $instance; $this->add_plugin_structure('gradingform', $instance); // attach local plugin stucture to $intance element $this->add_plugin_structure('local', $instance); } return $paths; } /** * Processes one grading area element * * @param array $data element data */ protected function process_grading_area($data) { global $DB; $task = $this->get_task(); $data = (object)$data; $oldid = $data->id; $data->component = 'mod_'.$task->get_modulename(); $data->contextid = $task->get_contextid(); $newid = $DB->insert_record('grading_areas', $data); $this->set_mapping('grading_area', $oldid, $newid); } /** * Processes one grading definition element * * @param array $data element data */ protected function process_grading_definition($data) { global $DB; $task = $this->get_task(); $data = (object)$data; $oldid = $data->id; $data->areaid = $this->get_new_parentid('grading_area'); $data->copiedfromid = null; $data->timecreated = time(); $data->usercreated = $task->get_userid(); $data->timemodified = $data->timecreated; $data->usermodified = $data->usercreated; $newid = $DB->insert_record('grading_definitions', $data); $this->set_mapping('grading_definition', $oldid, $newid, true); } /** * Processes one grading form instance element * * @param array $data element data */ protected function process_grading_instance($data) { global $DB; $data = (object)$data; // new form definition id $newformid = $this->get_new_parentid('grading_definition'); // get the name of the area we are restoring to $sql = "SELECT ga.areaname FROM {grading_definitions} gd JOIN {grading_areas} ga ON gd.areaid = ga.id WHERE gd.id = ?"; $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST); // get the mapped itemid - the activity module is expected to define the mappings // for each gradable area $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid); $oldid = $data->id; $data->definitionid = $newformid; $data->raterid = $this->get_mappingid('user', $data->raterid); $data->itemid = $newitemid; $newid = $DB->insert_record('grading_instances', $data); $this->set_mapping('grading_instance', $oldid, $newid); } /** * Final operations when the database records are inserted */ protected function after_execute() { // Add files embedded into the definition description $this->add_related_files('grading', 'description', 'grading_definition'); } } /** * This structure step restores the grade items associated with one activity * All the grade items are made child of the "course" grade item but the original * categoryid is saved as parentitemid in the backup_ids table, so, when restoring * the complete gradebook (categories and calculations), that information is * available there */ class restore_activity_grades_structure_step extends restore_structure_step { /** * No grades in front page. * @return bool */ protected function execute_condition() { return ($this->get_courseid() != SITEID); } protected function define_structure() { $paths = array(); $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item'); $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter'); if ($userinfo) { $paths[] = new restore_path_element('grade_grade', '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade'); } return $paths; } protected function process_grade_item($data) { global $DB; $data = (object)($data); $oldid = $data->id; // We'll need these later $oldparentid = $data->categoryid; $courseid = $this->get_courseid(); $idnumber = null; if (!empty($data->idnumber)) { // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop) // so the best is to keep the ones already in the gradebook // Potential problem: duplicates if same items are restored more than once. :-( // This needs to be fixed in some way (outcomes & activities with multiple items) // $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber; // In any case, verify always for uniqueness $sql = "SELECT cm.id FROM {course_modules} cm WHERE cm.course = :courseid AND cm.idnumber = :idnumber AND cm.id <> :cmid"; $params = array( 'courseid' => $courseid, 'idnumber' => $data->idnumber, 'cmid' => $this->task->get_moduleid() ); if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) { $idnumber = $data->idnumber; } } if (!empty($data->categoryid)) { // If the grade category id of the grade item being restored belongs to this course // then it is a fair assumption that this is the correct grade category for the activity // and we should leave it in place, if not then unset it. // TODO MDL-34790 Gradebook does not import if target course has gradebook categories. $conditions = array('id' => $data->categoryid, 'courseid' => $courseid); if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) { unset($data->categoryid); } } unset($data->id); $data->courseid = $this->get_courseid(); $data->iteminstance = $this->task->get_activityid(); $data->idnumber = $idnumber; $data->scaleid = $this->get_mappingid('scale', $data->scaleid); $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid); $gradeitem = new grade_item($data, false); $gradeitem->insert('restore'); //sortorder is automatically assigned when inserting. Re-instate the previous sortorder $gradeitem->sortorder = $data->sortorder; $gradeitem->update('restore'); // Set mapping, saving the original category id into parentitemid // gradebook restore (final task) will need it to reorganise items $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid); } protected function process_grade_grade($data) { global $CFG; require_once($CFG->libdir . '/grade/constants.php'); $data = (object)($data); $olduserid = $data->userid; $oldid = $data->id; unset($data->id); $data->itemid = $this->get_new_parentid('grade_item'); $data->userid = $this->get_mappingid('user', $data->userid, null); if (!empty($data->userid)) { $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); $grade = new grade_grade($data, false); $grade->insert('restore'); $this->set_mapping('grade_grades', $oldid, $grade->id, true); $this->add_related_files( GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, 'grade_grades', null, $oldid ); } else { debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"); } } /** * process activity grade_letters. Note that, while these are possible, * because grade_letters are contextid based, in practice, only course * context letters can be defined. So we keep here this method knowing * it won't be executed ever. gradebook restore will restore course letters. */ protected function process_grade_letter($data) { global $DB; $data['contextid'] = $this->task->get_contextid(); $gradeletter = (object)$data; // Check if it exists before adding it unset($data['id']); if (!$DB->record_exists('grade_letters', $data)) { $newitemid = $DB->insert_record('grade_letters', $gradeletter); } // no need to save any grade_letter mapping } public function after_restore() { // Fix grade item's sortorder after restore, as it might have duplicates. $courseid = $this->get_task()->get_courseid(); grade_item::fix_duplicate_sortorder($courseid); } } /** * Step in charge of restoring the grade history of an activity. * * This step is added to the task regardless of the setting 'grade_histories'. * The reason is to allow for a more flexible step in case the logic needs to be * split accross different settings to control the history of items and/or grades. */ class restore_activity_grade_history_structure_step extends restore_structure_step { /** * This step is executed only if the grade history file is present. */ protected function execute_condition() { if ($this->get_courseid() == SITEID) { return false; } $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } return true; } protected function define_structure() { $paths = array(); // Settings to use. $userinfo = $this->get_setting_value('userinfo'); $history = $this->get_setting_value('grade_histories'); if ($userinfo && $history) { $paths[] = new restore_path_element('grade_grade', '/grade_history/grade_grades/grade_grade'); } return $paths; } protected function process_grade_grade($data) { global $CFG, $DB; require_once($CFG->libdir . '/grade/constants.php'); $data = (object) $data; $oldhistoryid = $data->id; $olduserid = $data->userid; unset($data->id); $data->userid = $this->get_mappingid('user', $data->userid, null); if (!empty($data->userid)) { // Do not apply the date offsets as this is history. $data->itemid = $this->get_mappingid('grade_item', $data->itemid); $data->oldid = $this->get_mappingid('grade_grades', $data->oldid); $data->usermodified = $this->get_mappingid('user', $data->usermodified, null); $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid); $newhistoryid = $DB->insert_record('grade_grades_history', $data); $this->set_mapping('grade_grades_history', $oldhistoryid, $newhistoryid, true); $this->add_related_files( GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA, 'grade_grades_history', null, $oldhistoryid ); } else { $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'"; $this->log($message, backup::LOG_DEBUG); } } } /** * This structure steps restores the content bank content */ class restore_contentbankcontent_structure_step extends restore_structure_step { /** * Define structure for content bank step */ protected function define_structure() { $paths = []; $paths[] = new restore_path_element('contentbankcontent', '/contents/content'); return $paths; } /** * Define data processed for content bank * * @param mixed $data */ public function process_contentbankcontent($data) { global $DB; $data = (object)$data; $oldid = $data->id; $params = [ 'name' => $data->name, 'contextid' => $this->task->get_contextid(), 'contenttype' => $data->contenttype, 'instanceid' => $data->instanceid, 'timecreated' => $data->timecreated, ]; $exists = $DB->record_exists('contentbank_content', $params); if (!$exists) { $params['configdata'] = $data->configdata; $params['timemodified'] = time(); // Trying to map users. Users cannot always be mapped, e.g. when copying. $params['usercreated'] = $this->get_mappingid('user', $data->usercreated); if (!$params['usercreated']) { // Leave the content creator unchanged when we are restoring the same site. // Otherwise use current user id. if ($this->task->is_samesite()) { $params['usercreated'] = $data->usercreated; } else { $params['usercreated'] = $this->task->get_userid(); } } $params['usermodified'] = $this->get_mappingid('user', $data->usermodified); if (!$params['usermodified']) { // Leave the content modifier unchanged when we are restoring the same site. // Otherwise use current user id. if ($this->task->is_samesite()) { $params['usermodified'] = $data->usermodified; } else { $params['usermodified'] = $this->task->get_userid(); } } $newitemid = $DB->insert_record('contentbank_content', $params); $this->set_mapping('contentbank_content', $oldid, $newitemid, true); } } /** * Define data processed after execute for content bank */ protected function after_execute() { // Add related files. $this->add_related_files('contentbank', 'public', 'contentbank_content'); } } /** * This structure steps restores the xAPI states. */ class restore_xapistate_structure_step extends restore_structure_step { /** * Define structure for xAPI state step */ protected function define_structure() { return [new restore_path_element('xapistate', '/states/state')]; } /** * Define data processed for xAPI state. * * @param array|stdClass $data */ public function process_xapistate($data) { global $DB; $data = (object)$data; $oldid = $data->id; $exists = false; $params = [ 'component' => $data->component, 'itemid' => $this->task->get_contextid(), // Set stateid to 'restored', to let plugins identify the origin of this state is a backup. 'stateid' => 'restored', 'statedata' => $data->statedata, 'registration' => $data->registration, 'timecreated' => $data->timecreated, 'timemodified' => time(), ]; // Trying to map users. Users cannot always be mapped, for instance, when copying. $params['userid'] = $this->get_mappingid('user', $data->userid); if (!$params['userid']) { // Leave the userid unchanged when we are restoring the same site. if ($this->task->is_samesite()) { $params['userid'] = $data->userid; } $filter = $params; unset($filter['statedata']); $exists = $DB->record_exists('xapi_states', $filter); } if (!$exists && $params['userid']) { // Only insert the record if the user exists or can be mapped. $newitemid = $DB->insert_record('xapi_states', $params); $this->set_mapping('xapi_states', $oldid, $newitemid, true); } } } /** * This structure steps restores one instance + positions of one block * Note: Positions corresponding to one existing context are restored * here, but all the ones having unknown contexts are sent to backup_ids * for a later chance to be restored at the end (final task) */ class restore_block_instance_structure_step extends restore_structure_step { protected function define_structure() { $paths = array(); $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position'); return $paths; } public function process_block($data) { global $DB, $CFG; $data = (object)$data; // Handy $oldcontextid = $data->contextid; $oldid = $data->id; $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array(); // Look for the parent contextid if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) { // Parent contextid does not exist, ignore this block. return false; } // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple() // If there is already one block of that type in the parent context // and the block is not multiple, stop processing // Use blockslib loader / method executor if (!$bi = block_instance($data->blockname)) { return false; } if (!$bi->instance_allow_multiple()) { // The block cannot be added twice, so we will check if the same block is already being // displayed on the same page. For this, rather than mocking a page and using the block_manager // we use a similar query to the one in block_manager::load_blocks(), this will give us // a very good idea of the blocks already displayed in the context. $params = array( 'blockname' => $data->blockname ); // Context matching test. $context = context::instance_by_id($data->parentcontextid); $contextsql = 'bi.parentcontextid = :contextid'; $params['contextid'] = $context->id; $parentcontextids = $context->get_parent_context_ids(); if ($parentcontextids) { list($parentcontextsql, $parentcontextparams) = $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED); $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))"; $params = array_merge($params, $parentcontextparams); } // Page type pattern test. $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern); list($pagetypepatternsql, $pagetypepatternparams) = $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED); $params = array_merge($params, $pagetypepatternparams); // Sub page pattern test. $subpagepatternsql = 'bi.subpagepattern IS NULL'; if ($data->subpagepattern !== null) { $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)"; $params['subpagepattern'] = $data->subpagepattern; } $existingblock = $DB->get_records_sql("SELECT bi.id FROM {block_instances} bi JOIN {block} b ON b.name = bi.blockname WHERE bi.blockname = :blockname AND $contextsql AND bi.pagetypepattern $pagetypepatternsql AND $subpagepatternsql", $params); if (!empty($existingblock)) { // Save the context mapping in case something else is linking to this block's context. $newcontext = context_block::instance(reset($existingblock)->id); $this->set_mapping('context', $oldcontextid, $newcontext->id); // There is at least one very similar block visible on the page where we // are trying to restore the block. In these circumstances the block API // would not allow the user to add another instance of the block, so we // apply the same rule here. return false; } } // If there is already one block of that type in the parent context // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata // stop processing $params = array( 'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid, 'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern, 'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion); if ($birecs = $DB->get_records('block_instances', $params)) { foreach($birecs as $birec) { if ($birec->configdata == $data->configdata) { // Save the context mapping in case something else is linking to this block's context. $newcontext = context_block::instance($birec->id); $this->set_mapping('context', $oldcontextid, $newcontext->id); return false; } } } // Set task old contextid, blockid and blockname once we know them $this->task->set_old_contextid($oldcontextid); $this->task->set_old_blockid($oldid); $this->task->set_blockname($data->blockname); // Let's look for anything within configdata neededing processing // (nulls and uses of legacy file.php) if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) { $configdata = array_filter( (array) unserialize_object(base64_decode($data->configdata)), static function($value): bool { return !($value instanceof __PHP_Incomplete_Class); } ); foreach ($configdata as $attribute => $value) { if (in_array($attribute, $attrstotransform)) { $configdata[$attribute] = $this->contentprocessor->process_cdata($value); } } $data->configdata = base64_encode(serialize((object)$configdata)); } // Set timecreated, timemodified if not included (older backup). if (empty($data->timecreated)) { $data->timecreated = time(); } if (empty($data->timemodified)) { $data->timemodified = $data->timecreated; } // Create the block instance $newitemid = $DB->insert_record('block_instances', $data); // Save the mapping (with restorefiles support) $this->set_mapping('block_instance', $oldid, $newitemid, true); // Create the block context $newcontextid = context_block::instance($newitemid)->id; // Save the block contexts mapping and sent it to task $this->set_mapping('context', $oldcontextid, $newcontextid); $this->task->set_contextid($newcontextid); $this->task->set_blockid($newitemid); // Restore block fileareas if declared $component = 'block_' . $this->task->get_blockname(); foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed $this->add_related_files($component, $filearea, null); } // Process block positions, creating them or accumulating for final step foreach($positions as $position) { $position = (object)$position; $position->blockinstanceid = $newitemid; // The instance is always the restored one // If position is for one already mapped (known) contextid // process it now, creating the position if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) { $position->contextid = $newpositionctxid; // Create the block position $DB->insert_record('block_positions', $position); // The position belongs to an unknown context, send it to backup_ids // to process them as part of the final steps of restore. We send the // whole $position object there, hence use the low level method. } else { restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position); } } } } /** * Structure step to restore common course_module information * * This step will process the module.xml file for one activity, in order to restore * the corresponding information to the course_modules table, skipping various bits * of information based on CFG settings (groupings, completion...) in order to fullfill * all the reqs to be able to create the context to be used by all the rest of steps * in the activity restore task */ class restore_module_structure_step extends restore_structure_step { protected function define_structure() { global $CFG; $paths = array(); $module = new restore_path_element('module', '/module'); $paths[] = $module; if ($CFG->enableavailability) { $paths[] = new restore_path_element('availability', '/module/availability_info/availability'); $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field'); } $paths[] = new restore_path_element('tag', '/module/tags/tag'); // Apply for 'format' plugins optional paths at module level $this->add_plugin_structure('format', $module); // Apply for 'report' plugins optional paths at module level. $this->add_plugin_structure('report', $module); // Apply for 'plagiarism' plugins optional paths at module level $this->add_plugin_structure('plagiarism', $module); // Apply for 'local' plugins optional paths at module level $this->add_plugin_structure('local', $module); // Apply for 'admin tool' plugins optional paths at module level. $this->add_plugin_structure('tool', $module); return $paths; } protected function process_module($data) { global $CFG, $DB; $data = (object)$data; $oldid = $data->id; $this->task->set_old_moduleversion($data->version); $data->course = $this->task->get_courseid(); $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename)); // Map section (first try by course_section mapping match. Useful in course and section restores) $data->section = $this->get_mappingid('course_section', $data->sectionid); if (!$data->section) { // mapping failed, try to get section by sectionnumber matching $params = array( 'course' => $this->get_courseid(), 'section' => $data->sectionnumber); $data->section = $DB->get_field('course_sections', 'id', $params); } if (!$data->section) { // sectionnumber failed, try to get first section in course $params = array( 'course' => $this->get_courseid()); $data->section = $DB->get_field('course_sections', 'MIN(id)', $params); } if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1 $sectionrec = array( 'course' => $this->get_courseid(), 'section' => 0, 'timemodified' => time()); $DB->insert_record('course_sections', $sectionrec); // section 0 $sectionrec = array( 'course' => $this->get_courseid(), 'section' => 1, 'timemodified' => time()); $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1 } $data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) { // idnumber uniqueness $data->idnumber = ''; } if (empty($CFG->enablecompletion)) { // completion $data->completion = 0; $data->completiongradeitemnumber = null; $data->completionview = 0; $data->completionexpected = 0; } else { $data->completionexpected = $this->apply_date_offset($data->completionexpected); } if (empty($CFG->enableavailability)) { $data->availability = null; } // Backups that did not include showdescription, set it to default 0 // (this is not totally necessary as it has a db default, but just to // be explicit). if (!isset($data->showdescription)) { $data->showdescription = 0; } $data->instance = 0; // Set to 0 for now, going to create it soon (next step) if (empty($data->availability)) { // If there are legacy availablility data fields (and no new format data), // convert the old fields. $data->availability = \core_availability\info::convert_legacy_fields( $data, false); } else if (!empty($data->groupmembersonly)) { // There is current availability data, but it still has groupmembersonly // as well (2.7 backups), convert just that part. require_once($CFG->dirroot . '/lib/db/upgradelib.php'); $data->availability = upgrade_group_members_only($data->groupingid, $data->availability); } if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($data->course))) { unset($data->lang); } // course_module record ready, insert it $newitemid = $DB->insert_record('course_modules', $data); // save mapping $this->set_mapping('course_module', $oldid, $newitemid); // set the new course_module id in the task $this->task->set_moduleid($newitemid); // we can now create the context safely $ctxid = context_module::instance($newitemid)->id; // set the new context id in the task $this->task->set_contextid($ctxid); // update sequence field in course_section if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) { $sequence .= ',' . $newitemid; } else { $sequence = $newitemid; } $updatesection = new \stdClass(); $updatesection->id = $data->section; $updatesection->sequence = $sequence; $updatesection->timemodified = time(); $DB->update_record('course_sections', $updatesection); // If there is the legacy showavailability data, store this for later use. // (This data is not present when restoring 'new' backups.) if (isset($data->showavailability)) { // Cache the showavailability flag using the backup_ids data field. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_showavailability', $newitemid, 0, null, (object)array('showavailability' => $data->showavailability)); } } /** * Fetch all the existing because tag_set() deletes them * so everything must be reinserted on each call. * * @param stdClass $data Record data */ protected function process_tag($data) { global $CFG; $data = (object)$data; if (core_tag_tag::is_enabled('core', 'course_modules')) { $modcontext = context::instance_by_id($this->task->get_contextid()); $instanceid = $this->task->get_moduleid(); core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname); } } /** * Process the legacy availability table record. This table does not exist * in Moodle 2.7+ but we still support restore. * * @param stdClass $data Record data */ protected function process_availability($data) { $data = (object)$data; // Simply going to store the whole availability record now, we'll process // all them later in the final task (once all activities have been restored) // Let's call the low level one to be able to store the whole object $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data); } /** * Process the legacy availability fields table record. This table does not * exist in Moodle 2.7+ but we still support restore. * * @param stdClass $data Record data */ protected function process_availability_field($data) { global $DB, $CFG; require_once($CFG->dirroot.'/user/profile/lib.php'); $data = (object)$data; // Mark it is as passed by default $passed = true; $customfieldid = null; // If a customfield has been used in order to pass we must be able to match an existing // customfield by name (data->customfield) and type (data->customfieldtype) if (!empty($data->customfield) xor !empty($data->customfieldtype)) { // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both. // If one is null but the other isn't something clearly went wrong and we'll skip this condition. $passed = false; } else if (!empty($data->customfield)) { $field = profile_get_custom_field_data_by_shortname($data->customfield); $passed = $field && $field->datatype == $data->customfieldtype; } if ($passed) { // Create the object to insert into the database $availfield = new stdClass(); $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid $availfield->userfield = $data->userfield; $availfield->customfieldid = $customfieldid; $availfield->operator = $data->operator; $availfield->value = $data->value; // Get showavailability option. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'module_showavailability', $availfield->coursemoduleid); if (!$showrec) { // Should not happen. throw new coding_exception('No matching showavailability record'); } $show = $showrec->info->showavailability; // The $availfieldobject is now in the format used in the old // system. Interpret this and convert to new system. $currentvalue = $DB->get_field('course_modules', 'availability', array('id' => $availfield->coursemoduleid), MUST_EXIST); $newvalue = \core_availability\info::add_legacy_availability_field_condition( $currentvalue, $availfield, $show); $DB->set_field('course_modules', 'availability', $newvalue, array('id' => $availfield->coursemoduleid)); } } /** * This method will be executed after the rest of the restore has been processed. * * Update old tag instance itemid(s). */ protected function after_restore() { global $DB; $contextid = $this->task->get_contextid(); $instanceid = $this->task->get_activityid(); $olditemid = $this->task->get_old_activityid(); $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid)); } } /** * Structure step that will process the user activity completion * information if all these conditions are met: * - Target site has completion enabled ($CFG->enablecompletion) * - Activity includes completion info (file_exists) */ class restore_userscompletion_structure_step extends restore_structure_step { /** * To conditionally decide if this step must be executed * Note the "settings" conditions are evaluated in the * corresponding task. Here we check for other conditions * not being restore settings (files, site settings...) */ protected function execute_condition() { global $CFG; // Completion disabled in this site, don't execute if (empty($CFG->enablecompletion)) { return false; } // No completion on the front page. if ($this->get_courseid() == SITEID) { return false; } // No user completion info found, don't execute $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } // Arrived here, execute the step return true; } protected function define_structure() { $paths = array(); // Restore completion. $paths[] = new restore_path_element('completion', '/completions/completion'); // Restore completion view. $paths[] = new restore_path_element('completionview', '/completions/completionviews/completionview'); return $paths; } protected function process_completion($data) { global $DB; $data = (object)$data; $data->coursemoduleid = $this->task->get_moduleid(); $data->userid = $this->get_mappingid('user', $data->userid); // Find the existing record $existing = $DB->get_record('course_modules_completion', array( 'coursemoduleid' => $data->coursemoduleid, 'userid' => $data->userid), 'id, timemodified'); // Check we didn't already insert one for this cmid and userid // (there aren't supposed to be duplicates in that field, but // it was possible until MDL-28021 was fixed). if ($existing) { // Update it to these new values, but only if the time is newer if ($existing->timemodified < $data->timemodified) { $data->id = $existing->id; $DB->update_record('course_modules_completion', $data); } } else { // Normal entry where it doesn't exist already $DB->insert_record('course_modules_completion', $data); } // Add viewed to course_modules_viewed. if (isset($data->viewed) && $data->viewed) { $dataview = clone($data); unset($dataview->id); unset($dataview->viewed); $dataview->timecreated = $data->timemodified; $DB->insert_record('course_modules_viewed', $dataview); } } /** * Process the completioinview data. * @param array $data The data from the XML file. */ protected function process_completionview(array $data) { global $DB; $data = (object)$data; $data->coursemoduleid = $this->task->get_moduleid(); $data->userid = $this->get_mappingid('user', $data->userid); $DB->insert_record('course_modules_viewed', $data); } } /** * Abstract structure step, parent of all the activity structure steps. Used to support * the main <activity ...> tag and process it. */ abstract class restore_activity_structure_step extends restore_structure_step { /** * Adds support for the 'activity' path that is common to all the activities * and will be processed globally here */ protected function prepare_activity_structure($paths) { $paths[] = new restore_path_element('activity', '/activity'); return $paths; } /** * Process the activity path, informing the task about various ids, needed later */ protected function process_activity($data) { $data = (object)$data; $this->task->set_old_contextid($data->contextid); // Save old contextid in task $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping $this->task->set_old_activityid($data->id); // Save old activityid in task } /** * This must be invoked immediately after creating the "module" activity record (forum, choice...) * and will adjust the new activity id (the instance) in various places */ protected function apply_activity_instance($newitemid) { global $DB; $this->task->set_activityid($newitemid); // Save activity id in task // Apply the id to course_sections->instanceid $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid())); // Do the mapping for modulename, preparing it for files by oldcontext $modulename = $this->task->get_modulename(); $oldid = $this->task->get_old_activityid(); $this->set_mapping($modulename, $oldid, $newitemid, true); } /** * Create a delegate section mapping. * * @param string $component The component name (frankenstyle) * @param int $olditemid The old section id. * @param int $newitemid The new section id. */ protected function set_delegated_section_mapping($component, $olditemid, $newitemid) { $this->set_mapping("course_section::$component", $olditemid, $newitemid); } } /** * Structure step in charge of creating/mapping all the qcats and qs * by parsing the questions.xml file and checking it against the * results calculated by {@link restore_process_categories_and_questions} * and stored in backup_ids_temp. */ class restore_create_categories_and_questions extends restore_structure_step { /** @var array $cachedcategory store a question category */ protected $cachedcategory = null; /** @var stdClass the last question_bank_entry seen during the restore. Processed when we get to a question. */ protected $latestqbe; /** @var stdClass the last question_version seen during the restore. Processed when we get to a question. */ protected $latestversion; protected function define_structure() { // Check if the backup is a pre 4.0 one. $restoretask = $this->get_task(); $before40 = $restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<'); // Start creating the path, category should be the first one. $paths = []; $paths [] = new restore_path_element('question_category', '/question_categories/question_category'); // For the backups done before 4.0. if ($before40) { // This path is to recreate the bank entry and version for the legacy question objets. $question = new restore_path_element('question', '/question_categories/question_category/questions/question'); // Apply for 'qtype' plugins optional paths at question level. $this->add_plugin_structure('qtype', $question); // Apply for 'local' plugins optional paths at question level. $this->add_plugin_structure('local', $question); $paths [] = $question; $paths [] = new restore_path_element('question_hint', '/question_categories/question_category/questions/question/question_hints/question_hint'); $paths [] = new restore_path_element('tag', '/question_categories/question_category/questions/question/tags/tag'); } else { // For all the new backups. $paths [] = new restore_path_element('question_bank_entry', '/question_categories/question_category/question_bank_entries/question_bank_entry'); $paths [] = new restore_path_element('question_versions', '/question_categories/question_category/'. 'question_bank_entries/question_bank_entry/question_version/question_versions'); $question = new restore_path_element('question', '/question_categories/question_category/'. 'question_bank_entries/question_bank_entry/question_version/question_versions/questions/question'); // Apply for 'qtype' plugins optional paths at question level. $this->add_plugin_structure('qtype', $question); // Apply for 'qbank' plugins optional paths at question level. $this->add_plugin_structure('qbank', $question); // Apply for 'local' plugins optional paths at question level. $this->add_plugin_structure('local', $question); $paths [] = $question; $paths [] = new restore_path_element('question_hint', '/question_categories/question_category/question_bank_entries/'. 'question_bank_entry/question_version/question_versions/questions/question/question_hints/question_hint'); $paths [] = new restore_path_element('tag', '/question_categories/question_category/question_bank_entries/'. 'question_bank_entry/question_version/question_versions/questions/question/tags/tag'); } return $paths; } /** * Process question category restore. * * @param array $data the data from the XML file. */ protected function process_question_category($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Check we have one mapping for this category. if (!$mapping = $this->get_mapping('question_category', $oldid)) { return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped } // Check we have to create the category (newitemid = 0). if ($mapping->newitemid) { // By performing this set_mapping() we make get_old/new_parentid() to work for all the // children elements of the 'question_category' one. $this->set_mapping('question_category', $oldid, $mapping->newitemid); return; // newitemid != 0, this category is going to be mapped. Nothing to do } // Arrived here, newitemid = 0, we need to create the category // we'll do it at parentitemid context, but for CONTEXT_MODULE // categories, that will be created at CONTEXT_COURSE and moved // to module context later when the activity is created. if ($mapping->info->contextlevel == CONTEXT_MODULE) { $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid()); } $data->contextid = $mapping->parentitemid; // Before 3.5, question categories could be created at top level. // From 3.5 onwards, all question categories should be a child of a special category called the "top" category. $restoretask = $this->get_task(); $before35 = $restoretask->backup_release_compare('3.5', '<') || $restoretask->backup_version_compare(20180205, '<'); if (empty($mapping->info->parent) && $before35) { $top = question_get_top_category($data->contextid, true); $data->parent = $top->id; } if (empty($data->parent)) { if (!$top = question_get_top_category($data->contextid)) { $top = question_get_top_category($data->contextid, true); $this->set_mapping('question_category_created', $oldid, $top->id, false, null, $data->contextid); } $this->set_mapping('question_category', $oldid, $top->id); } else { // Before 3.1, the 'stamp' field could be erroneously duplicated. // From 3.1 onwards, there's a unique index of (contextid, stamp). // If we encounter a duplicate in an old restore file, just generate a new stamp. // This is the same as what happens during an upgrade to 3.1+ anyway. if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) { $data->stamp = make_unique_id_code(); } // The idnumber if it exists also needs to be unique within a context or reset it to null. if (!empty($data->idnumber) && $DB->record_exists('question_categories', ['idnumber' => $data->idnumber, 'contextid' => $data->contextid])) { unset($data->idnumber); } // Let's create the question_category and save mapping. $newitemid = $DB->insert_record('question_categories', $data); $this->set_mapping('question_category', $oldid, $newitemid); // Also annotate them as question_category_created, we need // that later when remapping parents. $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid); } } /** * Set up date to allow restore of questions from pre-4.0 backups. * * @param stdClass $data the data from the XML file. */ protected function process_question_legacy_data($data) { $this->latestqbe = (object) [ 'id' => $data->id, 'questioncategoryid' => $data->category, 'ownerid' => $data->createdby, 'idnumber' => $data->idnumber ?? null, ]; $this->latestversion = (object) [ 'id' => $data->id, 'version' => 1, 'status' => $data->hidden ? \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN : \core_question\local\bank\question_version_status::QUESTION_STATUS_READY, ]; } /** * Process question bank entry data. * * @param array $data the data from the XML file. */ protected function process_question_bank_entry($data) { // We can only determine the right way to process this once we get to // process_question and have more information, so for now just store. $this->latestqbe = (object) $data; } /** * Process question versions. * * @param array $data the data from the XML file. */ protected function process_question_versions($data) { // We can only determine the right way to process this once we get to // process_question and have more information, so for now just store. $this->latestversion = (object) $data; } /** * Process the actual question. * * @param array $data the data from the XML file. */ protected function process_question($data) { global $DB; $data = (object) $data; $oldid = $data->id; // Check we have one mapping for this question. if (!$questionmapping = $this->get_mapping('question', $oldid)) { // No mapping = this question doesn't need to be created/mapped. return; } // Check if this is a pre 4.0 backup, then there will not be a question bank entry // or question version in the file. So, we need to set up that data ready to be used below. $restoretask = $this->get_task(); if ($restoretask->backup_release_compare('4.0', '<') || $restoretask->backup_version_compare(20220202, '<')) { // Get the mapped category (cannot use get_new_parentid() because not // all the categories have been created, so it is not always available // Instead we get the mapping for the question->parentitemid because // we have loaded qcatids there for all parsed questions. $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid); $this->process_question_legacy_data($data); } // In the past, there were some very sloppy values of penalty. Fix them. if ($data->penalty >= 0.33 && $data->penalty <= 0.34) { $data->penalty = 0.3333333; } if ($data->penalty >= 0.66 && $data->penalty <= 0.67) { $data->penalty = 0.6666667; } if ($data->penalty >= 1) { $data->penalty = 1; } $userid = $this->get_mappingid('user', $data->createdby); if ($userid) { // The question creator is included in the backup, so we can use their mapping id. $data->createdby = $userid; } else { // Leave the question creator unchanged when we are restoring the same site. // Otherwise use current user id. if (!$this->task->is_samesite()) { $data->createdby = $this->task->get_userid(); } } $userid = $this->get_mappingid('user', $data->modifiedby); if ($userid) { // The question modifier is included in the backup, so we can use their mapping id. $data->modifiedby = $userid; } else { // Leave the question modifier unchanged when we are restoring the same site. // Otherwise use current user id. if (!$this->task->is_samesite()) { $data->modifiedby = $this->task->get_userid(); } } // With newitemid = 0, let's create the question. if (!$questionmapping->newitemid) { // Now we know we are inserting a question, we may need to insert the questionbankentry. if (empty($this->latestqbe->newid)) { $this->latestqbe->oldid = $this->latestqbe->id; $this->latestqbe->questioncategoryid = $this->get_new_parentid('question_category'); $userid = $this->get_mappingid('user', $this->latestqbe->ownerid); if ($userid) { $this->latestqbe->ownerid = $userid; } else { if (!$this->task->is_samesite()) { $this->latestqbe->ownerid = $this->task->get_userid(); } } // The idnumber if it exists also needs to be unique within a category or reset it to null. if (!empty($this->latestqbe->idnumber) && $DB->record_exists('question_bank_entries', ['idnumber' => $this->latestqbe->idnumber, 'questioncategoryid' => $this->latestqbe->questioncategoryid])) { unset($this->latestqbe->idnumber); } $this->latestqbe->newid = $DB->insert_record('question_bank_entries', $this->latestqbe); $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid); } // Now store the question. $newitemid = $DB->insert_record('question', $data); $this->set_mapping('question', $oldid, $newitemid); // Also annotate them as question_created, we need // that later when remapping parents (keeping the old categoryid as parentid). $parentcatid = $this->get_old_parentid('question_category'); $this->set_mapping('question_created', $oldid, $newitemid, false, null, $parentcatid); // Also insert this question_version. $oldqvid = $this->latestversion->id; $this->latestversion->questionbankentryid = $this->latestqbe->newid; $this->latestversion->questionid = $newitemid; $newqvid = $DB->insert_record('question_versions', $this->latestversion); $this->set_mapping('question_versions', $oldqvid, $newqvid); } else { // By performing this set_mapping() we make get_old/new_parentid() to work for all the // children elements of the 'question' one (so qtype plugins will know the question they belong to). $this->set_mapping('question', $oldid, $questionmapping->newitemid); // Also create the question_bank_entry and version mappings, if required. $newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]); $this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id); if (empty($this->latestqbe->newid)) { $this->latestqbe->oldid = $this->latestqbe->id; $this->latestqbe->newid = $newquestionversion->questionbankentryid; $this->set_mapping('question_bank_entry', $this->latestqbe->oldid, $this->latestqbe->newid); } } // Note, we don't restore any question files yet // as far as the CONTEXT_MODULE categories still // haven't their contexts to be restored to // The {@see restore_create_question_files}, executed in the final // step will be in charge of restoring all the question files. } protected function process_question_hint($data) { global $DB; $data = (object)$data; $oldid = $data->id; // Detect if the question is created or mapped $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question has been created by restore, we need to create its question_answers too if ($questioncreated) { // Adjust some columns $data->questionid = $newquestionid; // Insert record $newitemid = $DB->insert_record('question_hints', $data); // The question existed, we need to map the existing question_hints } else { // Look in question_hints by hint text matching $sql = 'SELECT id FROM {question_hints} WHERE questionid = ? AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255); $params = array($newquestionid, $data->hint); $newitemid = $DB->get_field_sql($sql, $params); // Not able to find the hint, let's try cleaning the hint text // of all the question's hints in DB as slower fallback. MDL-33863. if (!$newitemid) { $potentialhints = $DB->get_records('question_hints', array('questionid' => $newquestionid), '', 'id, hint'); foreach ($potentialhints as $potentialhint) { $cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars. $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending. if ($cleanhint === $data->hint) { $newitemid = $data->id; } } } // If we haven't found the newitemid, something has gone really wrong, question in DB // is missing hints, exception if (!$newitemid) { $info = new stdClass(); $info->filequestionid = $oldquestionid; $info->dbquestionid = $newquestionid; $info->hint = $data->hint; throw new restore_step_exception('error_question_hint_missing_in_db', $info); } } // Create mapping (I'm not sure if this is really needed?) $this->set_mapping('question_hint', $oldid, $newitemid); } protected function process_tag($data) { global $DB; $data = (object)$data; $newquestion = $this->get_new_parentid('question'); $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question')); if (!$questioncreated) { // This question already exists in the question bank. Nothing for us to do. return; } if (core_tag_tag::is_enabled('core_question', 'question')) { $tagname = $data->rawname; if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) { $tagcontextid = $newcontextid; } else { // Get the category, so we can then later get the context. $categoryid = $this->get_new_parentid('question_category'); if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) { $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid)); } $tagcontextid = $this->cachedcategory->contextid; } // Add the tag to the question. core_tag_tag::add_item_tag('core_question', 'question', $newquestion, context::instance_by_id($tagcontextid), $tagname); } } protected function after_execute() { global $DB; // First of all, recode all the created question_categories->parent fields $qcats = $DB->get_records('backup_ids_temp', array( 'backupid' => $this->get_restoreid(), 'itemname' => 'question_category_created')); foreach ($qcats as $qcat) { $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid)); // Get new parent (mapped or created, so we look in quesiton_category mappings) if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( 'backupid' => $this->get_restoreid(), 'itemname' => 'question_category', 'itemid' => $dbcat->parent))) { // contextids must match always, as far as we always include complete qbanks, just check it $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent)); if ($dbcat->contextid == $newparentctxid) { $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id)); } else { $newparent = 0; // No ctx match for both cats, no parent relationship } } // Here with $newparent empty, problem with contexts or remapping, set it to top cat if (!$newparent && $dbcat->parent) { $topcat = question_get_top_category($dbcat->contextid, true); if ($dbcat->parent != $topcat->id) { $DB->set_field('question_categories', 'parent', $topcat->id, array('id' => $dbcat->id)); } } } // Now, recode all the created question->parent fields $qs = $DB->get_records('backup_ids_temp', array( 'backupid' => $this->get_restoreid(), 'itemname' => 'question_created')); foreach ($qs as $q) { $dbq = $DB->get_record('question', array('id' => $q->newitemid)); // Get new parent (mapped or created, so we look in question mappings) if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array( 'backupid' => $this->get_restoreid(), 'itemname' => 'question', 'itemid' => $dbq->parent))) { $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id)); } } // Note, we don't restore any question files yet // as far as the CONTEXT_MODULE categories still // haven't their contexts to be restored to // The {@link restore_create_question_files}, executed in the final step // step will be in charge of restoring all the question files } } /** * Execution step that will move all the CONTEXT_MODULE question categories * created at early stages of restore in course context (because modules weren't * created yet) to their target module (matching by old-new-contextid mapping) */ class restore_move_module_questions_categories extends restore_execution_step { protected function define_execution() { global $DB; $after35 = $this->task->backup_release_compare('3.5', '>=') && $this->task->backup_version_compare(20180205, '>'); $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE); foreach ($contexts as $contextid => $contextlevel) { // Only if context mapping exists (i.e. the module has been restored) if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) { // Update all the qcats having their parentitemid set to the original contextid $modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info FROM {backup_ids_temp} WHERE backupid = ? AND itemname = 'question_category' AND parentitemid = ?", array($this->get_restoreid(), $contextid)); $top = question_get_top_category($newcontext->newitemid, true); $oldtopid = 0; $categoryids = []; foreach ($modulecats as $modulecat) { // Before 3.5, question categories could be created at top level. // From 3.5 onwards, all question categories should be a child of a special category called the "top" category. $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info); if ($after35 && empty($info->parent)) { $oldtopid = $modulecat->newitemid; $modulecat->newitemid = $top->id; } else { $cat = new stdClass(); $cat->id = $modulecat->newitemid; $cat->contextid = $newcontext->newitemid; if (empty($info->parent)) { $cat->parent = $top->id; } $DB->update_record('question_categories', $cat); $categoryids[] = (int)$cat->id; } // And set new contextid (and maybe update newitemid) also in question_category mapping (will be // used by {@link restore_create_question_files} later. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid); } // Update the context id of any tags applied to any questions in these categories. if ($categoryids) { [$categorysql, $categoryidparams] = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED); $sqlupdate = "UPDATE {tag_instance} SET contextid = :newcontext WHERE component = :component AND itemtype = :itemtype AND itemid IN (SELECT DISTINCT bi.newitemid as questionid FROM {backup_ids_temp} bi JOIN {question} q ON q.id = bi.newitemid JOIN {question_versions} qv ON qv.questionid = q.id JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid WHERE bi.backupid = :backupid AND bi.itemname = 'question_created' AND qbe.questioncategoryid {$categorysql}) "; $params = [ 'newcontext' => $newcontext->newitemid, 'component' => 'core_question', 'itemtype' => 'question', 'backupid' => $this->get_restoreid(), ]; $params += $categoryidparams; $DB->execute($sqlupdate, $params); // As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()} // question_set_references relating to random questions restored from old backups, // which pick from context_module question_categores, will have been restored with the wrong questioncontextid. // So, now, we need to find those, and updated the questioncontextid. // We can only find them by picking apart the filter conditions, and seeign which categories they refer to. // We need to check all the question_set_references belonging to this context_module. $references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]); foreach ($references as $reference) { $filtercondition = json_decode($reference->filtercondition); if (!empty($filtercondition->questioncategoryid) && in_array($filtercondition->questioncategoryid, $categoryids)) { // This is one of ours, update the questionscontextid. $DB->set_field('question_set_references', 'questionscontextid', $newcontext->newitemid, ['id' => $reference->id]); } } } // Now set the parent id for the question categories that were in the top category in the course context // and have been moved now. if ($oldtopid) { $DB->set_field('question_categories', 'parent', $top->id, array('contextid' => $newcontext->newitemid, 'parent' => $oldtopid)); } } } } } /** * Execution step that will create all the question/answers/qtype-specific files for the restored * questions. It must be executed after {@link restore_move_module_questions_categories} * because only then each question is in its final category and only then the * contexts can be determined. */ class restore_create_question_files extends restore_execution_step { /** @var array Question-type specific component items cache. */ private $qtypecomponentscache = array(); /** * Preform the restore_create_question_files step. */ protected function define_execution() { global $DB; // Track progress, as this task can take a long time. $progress = $this->task->get_progress(); $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE); // Parentitemids of question_createds in backup_ids_temp are the category it is in. // MUST use a recordset, as there is no unique key in the first (or any) column. $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype FROM {backup_ids_temp} bi JOIN {question} q ON q.id = bi.newitemid WHERE bi.backupid = ? AND bi.itemname = 'question_created' ORDER BY categoryid ASC", array($this->get_restoreid())); $currentcatid = -1; foreach ($catqtypes as $categoryid => $row) { $qtype = $row->qtype; // Check if we are in a new category. if ($currentcatid !== $categoryid) { // Report progress for each category. $progress->progress(); if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'question_category', $categoryid)) { // Something went really wrong, cannot find the question_category for the question_created records. debugging('Error fetching target context for question', DEBUG_DEVELOPER); continue; } // Calculate source and target contexts. $oldctxid = $qcatmapping->info->contextid; $newctxid = $qcatmapping->parentitemid; $this->send_common_files($oldctxid, $newctxid, $progress); $currentcatid = $categoryid; } $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress); } $catqtypes->close(); $progress->end_progress(); } /** * Send the common question files to a new context. * * @param int $oldctxid Old context id. * @param int $newctxid New context id. * @param \core\progress\base $progress Progress object to use. */ private function send_common_files($oldctxid, $newctxid, $progress) { // Add common question files (question and question_answer ones). restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext', $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback', $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer', $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback', $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint', $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback', $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback', $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress); restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback', $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress); } /** * Send the question type specific files to a new context. * * @param text $qtype The qtype name to send. * @param int $oldctxid Old context id. * @param int $newctxid New context id. * @param \core\progress\base $progress Progress object to use. */ private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) { if (!isset($this->qtypecomponentscache[$qtype])) { $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype); } $components = $this->qtypecomponentscache[$qtype]; foreach ($components as $component => $fileareas) { foreach ($fileareas as $filearea => $mapping) { restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea, $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress); } } } } /** * Try to restore aliases and references to external files. * * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}. * We expect that all regular (non-alias) files have already been restored. Make sure * there is no restore step executed after this one that would call send_files_to_pool() again. * * You may notice we have hardcoded support for Server files, Legacy course files * and user Private files here at the moment. This could be eventually replaced with a set of * callbacks in the future if needed. * * @copyright 2012 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_process_file_aliases_queue extends restore_execution_step { /** @var array internal cache for {@link choose_repository()} */ private $cachereposbyid = array(); /** @var array internal cache for {@link choose_repository()} */ private $cachereposbytype = array(); /** * What to do when this step is executed. */ protected function define_execution() { global $DB; $fs = get_file_storage(); // Load the queue. $aliascount = $DB->count_records('backup_ids_temp', ['backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue']); $rs = $DB->get_recordset('backup_ids_temp', ['backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'], '', 'info'); $this->log('processing file aliases queue. ' . $aliascount . ' entries.', backup::LOG_DEBUG); $progress = $this->task->get_progress(); $progress->start_progress('Processing file aliases queue', $aliascount); // Iterate over aliases in the queue. foreach ($rs as $record) { $progress->increment_progress(); $info = backup_controller_dbops::decode_backup_temp_info($record->info); // Try to pick a repository instance that should serve the alias. $repository = $this->choose_repository($info); if (is_null($repository)) { $this->notify_failure($info, 'unable to find a matching repository instance'); continue; } if ($info->oldfile->repositorytype === 'local' || $info->oldfile->repositorytype === 'coursefiles' || $info->oldfile->repositorytype === 'contentbank') { // Aliases to Server files and Legacy course files may refer to a file // contained in the backup file or to some existing file (if we are on the // same site). try { $reference = file_storage::unpack_reference($info->oldfile->reference); } catch (Exception $e) { $this->notify_failure($info, 'invalid reference field format'); continue; } // Let's see if the referred source file was also included in the backup. $candidates = $DB->get_recordset('backup_files_temp', array( 'backupid' => $this->get_restoreid(), 'contextid' => $reference['contextid'], 'component' => $reference['component'], 'filearea' => $reference['filearea'], 'itemid' => $reference['itemid'], ), '', 'info, newcontextid, newitemid'); $source = null; foreach ($candidates as $candidate) { $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info); if ($candidateinfo->filename === $reference['filename'] and $candidateinfo->filepath === $reference['filepath'] and !is_null($candidate->newcontextid) and !is_null($candidate->newitemid) ) { $source = $candidateinfo; $source->contextid = $candidate->newcontextid; $source->itemid = $candidate->newitemid; break; } } $candidates->close(); if ($source) { // We have an alias that refers to another file also included in // the backup. Let us change the reference field so that it refers // to the restored copy of the original file. $reference = file_storage::pack_reference($source); // Send the new alias to the filepool. $fs->create_file_from_reference($info->newfile, $repository->id, $reference); $this->notify_success($info); continue; } else { // This is a reference to some moodle file that was not contained in the backup // file. If we are restoring to the same site, keep the reference untouched // and restore the alias as is if the referenced file exists. if ($this->task->is_samesite()) { if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'], $reference['itemid'], $reference['filepath'], $reference['filename'])) { $reference = file_storage::pack_reference($reference); $fs->create_file_from_reference($info->newfile, $repository->id, $reference); $this->notify_success($info); continue; } else { $this->notify_failure($info, 'referenced file not found'); continue; } // If we are at other site, we can't restore this alias. } else { $this->notify_failure($info, 'referenced file not included'); continue; } } } else if ($info->oldfile->repositorytype === 'user') { if ($this->task->is_samesite()) { // For aliases to user Private files at the same site, we have a chance to check // if the referenced file still exists. try { $reference = file_storage::unpack_reference($info->oldfile->reference); } catch (Exception $e) { $this->notify_failure($info, 'invalid reference field format'); continue; } if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'], $reference['itemid'], $reference['filepath'], $reference['filename'])) { $reference = file_storage::pack_reference($reference); $fs->create_file_from_reference($info->newfile, $repository->id, $reference); $this->notify_success($info); continue; } else { $this->notify_failure($info, 'referenced file not found'); continue; } // If we are at other site, we can't restore this alias. } else { $this->notify_failure($info, 'restoring at another site'); continue; } } else { // This is a reference to some external file such as dropbox. // If we are restoring to the same site, keep the reference untouched and // restore the alias as is. if ($this->task->is_samesite()) { $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference); $this->notify_success($info); continue; // If we are at other site, we can't restore this alias. } else { $this->notify_failure($info, 'restoring at another site'); continue; } } } $progress->end_progress(); $rs->close(); } /** * Choose the repository instance that should handle the alias. * * At the same site, we can rely on repository instance id and we just * check it still exists. On other site, try to find matching Server files or * Legacy course files repository instance. Return null if no matching * repository instance can be found. * * @param stdClass $info * @return repository|null */ private function choose_repository(stdClass $info) { global $DB, $CFG; require_once($CFG->dirroot.'/repository/lib.php'); if ($this->task->is_samesite()) { // We can rely on repository instance id. if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) { return $this->cachereposbyid[$info->oldfile->repositoryid]; } $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1); try { $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID); return $this->cachereposbyid[$info->oldfile->repositoryid]; } catch (Exception $e) { $this->cachereposbyid[$info->oldfile->repositoryid] = null; return null; } } else { // We can rely on repository type only. if (empty($info->oldfile->repositorytype)) { return null; } if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) { return $this->cachereposbytype[$info->oldfile->repositorytype]; } $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1); // Both Server files and Legacy course files repositories have a single // instance at the system context to use. Let us try to find it. if ($info->oldfile->repositorytype === 'local' || $info->oldfile->repositorytype === 'coursefiles' || $info->oldfile->repositorytype === 'contentbank') { $sql = "SELECT ri.id FROM {repository} r JOIN {repository_instances} ri ON ri.typeid = r.id WHERE r.type = ? AND ri.contextid = ?"; $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID)); if (empty($ris)) { return null; } $repoids = array_keys($ris); $repoid = reset($repoids); try { $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID); return $this->cachereposbytype[$info->oldfile->repositorytype]; } catch (Exception $e) { $this->cachereposbytype[$info->oldfile->repositorytype] = null; return null; } } $this->cachereposbytype[$info->oldfile->repositorytype] = null; return null; } } /** * Let the user know that the given alias was successfully restored * * @param stdClass $info */ private function notify_success(stdClass $info) { $filedesc = $this->describe_alias($info); $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1); } /** * Let the user know that the given alias can't be restored * * @param stdClass $info * @param string $reason detailed reason to be logged */ private function notify_failure(stdClass $info, $reason = '') { $filedesc = $this->describe_alias($info); if ($reason) { $reason = ' ('.$reason.')'; } $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1); $this->add_result_item('file_aliases_restore_failures', $filedesc); } /** * Return a human readable description of the alias file * * @param stdClass $info * @return string */ private function describe_alias(stdClass $info) { $filedesc = $this->expected_alias_location($info->newfile); if (!is_null($info->oldfile->source)) { $filedesc .= ' ('.$info->oldfile->source.')'; } return $filedesc; } /** * Return the expected location of a file * * Please note this may and may not work as a part of URL to pluginfile.php * (depends on how the given component/filearea deals with the itemid). * * @param stdClass $filerecord * @return string */ private function expected_alias_location($filerecord) { $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea; if (!is_null($filerecord->itemid)) { $filedesc .= '/'.$filerecord->itemid; } $filedesc .= $filerecord->filepath.$filerecord->filename; return $filedesc; } /** * Append a value to the given resultset * * @param string $name name of the result containing a list of values * @param mixed $value value to add as another item in that result */ private function add_result_item($name, $value) { $results = $this->task->get_results(); if (isset($results[$name])) { if (!is_array($results[$name])) { throw new coding_exception('Unable to append a result item into a non-array structure.'); } $current = $results[$name]; $current[] = $value; $this->task->add_result(array($name => $current)); } else { $this->task->add_result(array($name => array($value))); } } } /** * Helper code for use by any plugin that stores question attempt data that it needs to back up. */ trait restore_questions_attempt_data_trait { /** @var array question_attempt->id to qtype. */ protected $qtypes = array(); /** @var array question_attempt->id to questionid. */ protected $newquestionids = array(); /** * Attach below $element (usually attempts) the needed restore_path_elements * to restore question_usages and all they contain. * * If you use the $nameprefix parameter, then you will need to implement some * extra methods in your class, like * * protected function process_{nameprefix}question_attempt($data) { * $this->restore_question_usage_worker($data, '{nameprefix}'); * } * protected function process_{nameprefix}question_attempt($data) { * $this->restore_question_attempt_worker($data, '{nameprefix}'); * } * protected function process_{nameprefix}question_attempt_step($data) { * $this->restore_question_attempt_step_worker($data, '{nameprefix}'); * } * * @param restore_path_element $element the parent element that the usages are stored inside. * @param array $paths the paths array that is being built. * @param string $nameprefix should match the prefix passed to the corresponding * backup_questions_activity_structure_step::add_question_usages call. */ protected function add_question_usages($element, &$paths, $nameprefix = '') { // Check $element is restore_path_element if (! $element instanceof restore_path_element) { throw new restore_step_exception('element_must_be_restore_path_element', $element); } // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $paths[] = new restore_path_element($nameprefix . 'question_usage', $element->get_path() . "/{$nameprefix}question_usage"); $paths[] = new restore_path_element($nameprefix . 'question_attempt', $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt"); $paths[] = new restore_path_element($nameprefix . 'question_attempt_step', $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step", true); $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data', $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable"); } /** * Process question_usages */ public function process_question_usage($data) { $this->restore_question_usage_worker($data, ''); } /** * Process question_attempts */ public function process_question_attempt($data) { $this->restore_question_attempt_worker($data, ''); } /** * Process question_attempt_steps */ public function process_question_attempt_step($data) { $this->restore_question_attempt_step_worker($data, ''); } /** * This method does the actual work for process_question_usage or * process_{nameprefix}_question_usage. * @param array $data the data from the XML file. * @param string $nameprefix the element name prefix. */ protected function restore_question_usage_worker($data, $nameprefix) { global $DB; // Clear our caches. $this->qtypes = array(); $this->newquestionids = array(); $data = (object)$data; $oldid = $data->id; $data->contextid = $this->task->get_contextid(); // Everything ready, insert (no mapping needed) $newitemid = $DB->insert_record('question_usages', $data); $this->inform_new_usage_id($newitemid); $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false); } /** * When process_question_usage creates the new usage, it calls this method * to let the activity link to the new usage. For example, the quiz uses * this method to set quiz_attempts.uniqueid to the new usage id. * @param integer $newusageid */ abstract protected function inform_new_usage_id($newusageid); /** * This method does the actual work for process_question_attempt or * process_{nameprefix}_question_attempt. * @param array $data the data from the XML file. * @param string $nameprefix the element name prefix. */ protected function restore_question_attempt_worker($data, $nameprefix) { global $DB; $data = (object)$data; $oldid = $data->id; $questioncreated = $this->get_mappingid('question_created', $data->questionid) ? true : false; $question = $this->get_mapping('question', $data->questionid); if ($questioncreated) { $data->questionid = $question->newitemid; } $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage'); if (!property_exists($data, 'variant')) { $data->variant = 1; } if (!property_exists($data, 'maxfraction')) { $data->maxfraction = 1; } $newitemid = $DB->insert_record('question_attempts', $data); $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid); if (isset($question->info->qtype)) { $qtype = $question->info->qtype; } else { $qtype = $DB->get_record('question', ['id' => $data->questionid])->qtype; } $this->qtypes[$newitemid] = $qtype; $this->newquestionids[$newitemid] = $data->questionid; } /** * This method does the actual work for process_question_attempt_step or * process_{nameprefix}_question_attempt_step. * @param array $data the data from the XML file. * @param string $nameprefix the element name prefix. */ protected function restore_question_attempt_step_worker($data, $nameprefix) { global $DB; $data = (object)$data; $oldid = $data->id; // Pull out the response data. $response = array(); if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) { foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) { $response[$variable['name']] = $variable['value']; } } unset($data->response); $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt'); $data->userid = $this->get_mappingid('user', $data->userid); // Everything ready, insert and create mapping (needed by question_sessions) $newitemid = $DB->insert_record('question_attempt_steps', $data); $this->set_mapping('question_attempt_step', $oldid, $newitemid, true); // Now process the response data. $response = $this->questions_recode_response_data( $this->qtypes[$data->questionattemptid], $this->newquestionids[$data->questionattemptid], $data->sequencenumber, $response); foreach ($response as $name => $value) { $row = new stdClass(); $row->attemptstepid = $newitemid; $row->name = $name; $row->value = $value; $DB->insert_record('question_attempt_step_data', $row, false); } } /** * Recode the respones data for a particular step of an attempt at at particular question. * @param string $qtype the question type. * @param int $newquestionid the question id. * @param int $sequencenumber the sequence number. * @param array $response the response data to recode. */ public function questions_recode_response_data( $qtype, $newquestionid, $sequencenumber, array $response) { $qtyperestorer = $this->get_qtype_restorer($qtype); if ($qtyperestorer) { $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response); } return $response; } /** * Given a list of question->ids, separated by commas, returns the * recoded list, with all the restore question mappings applied. * Note: Used by quiz->questions and quiz_attempts->layout * Note: 0 = page break (unconverted) */ protected function questions_recode_layout($layout) { // Extracts question id from sequence if ($questionids = explode(',', $layout)) { foreach ($questionids as $id => $questionid) { if ($questionid) { // If it is zero then this is a pagebreak, don't translate $newquestionid = $this->get_mappingid('question', $questionid); $questionids[$id] = $newquestionid; } } } return implode(',', $questionids); } /** * Get the restore_qtype_plugin subclass for a specific question type. * @param string $qtype e.g. multichoice. * @return restore_qtype_plugin instance. */ protected function get_qtype_restorer($qtype) { // Build one static cache to store {@link restore_qtype_plugin} // while we are needing them, just to save zillions of instantiations // or using static stuff that will break our nice API static $qtypeplugins = array(); if (!isset($qtypeplugins[$qtype])) { $classname = 'restore_qtype_' . $qtype . '_plugin'; if (class_exists($classname)) { $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this); } else { $qtypeplugins[$qtype] = null; } } return $qtypeplugins[$qtype]; } protected function after_execute() { parent::after_execute(); // Restore any files belonging to responses. foreach (question_engine::get_all_response_file_areas() as $filearea) { $this->add_related_files('question', $filearea, 'question_attempt_step'); } } } /** * Helper trait to restore question reference data. */ trait restore_question_reference_data_trait { /** * Attach the question reference data to the restore. * * @param restore_path_element $element the parent element. (E.g. a quiz attempt.) * @param array $paths the paths array that is being built to describe the structure. */ protected function add_question_references($element, &$paths) { // Check $element is restore_path_element. if (! $element instanceof restore_path_element) { throw new restore_step_exception('element_must_be_restore_path_element', $element); } // Check $paths is one array. if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $paths[] = new restore_path_element('question_reference', $element->get_path() . '/question_reference'); } /** * Process question references which replaces the direct connection to quiz slots to question. * * @param array $data the data from the XML file. */ public function process_question_reference($data) { global $DB; $data = (object) $data; $data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid); $data->itemid = $this->get_new_parentid('quiz_question_instance'); if ($entry = $this->get_mappingid('question_bank_entry', $data->questionbankentryid)) { $data->questionbankentryid = $entry; } $DB->insert_record('question_references', $data); } } /** * Helper trait to restore question set reference data. */ trait restore_question_set_reference_data_trait { /** * Attach the question reference data to the restore. * * @param restore_path_element $element the parent element. (E.g. a quiz attempt.) * @param array $paths the paths array that is being built to describe the structure. */ protected function add_question_set_references($element, &$paths) { // Check $element is restore_path_element. if (! $element instanceof restore_path_element) { throw new restore_step_exception('element_must_be_restore_path_element', $element); } // Check $paths is one array. if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $paths[] = new restore_path_element('question_set_reference', $element->get_path() . '/question_set_reference'); } /** * Process question set references data which replaces the random qtype. * * @param array $data the data from the XML file. */ public function process_question_set_reference($data) { global $DB; $data = (object) $data; $data->usingcontextid = $this->get_mappingid('context', $data->usingcontextid); $data->itemid = $this->get_new_parentid('quiz_question_instance'); $filtercondition = json_decode($data->filtercondition, true); if (!isset($filtercondition['filter'])) { // Pre-4.3, convert the old filtercondition format to the new format. $filtercondition = \core_question\question_reference_manager::convert_legacy_set_reference_filter_condition( $filtercondition); } // Map category id used for category filter condition and corresponding context id. $oldcategoryid = $filtercondition['filter']['category']['values'][0]; $newcategoryid = $this->get_mappingid('question_category', $oldcategoryid); $filtercondition['filter']['category']['values'][0] = $newcategoryid; if ($context = $this->get_mappingid('context', $data->questionscontextid)) { $data->questionscontextid = $context; } else { $this->log('question_set_reference with old id ' . $data->id . ' referenced question context ' . $data->questionscontextid . ' which was not included in the backup. Therefore, this has been ' . ' restored with the old questionscontextid.', backup::LOG_WARNING); } $filtercondition['cat'] = implode(',', [ $filtercondition['filter']['category']['values'][0], $data->questionscontextid, ]); $data->filtercondition = json_encode($filtercondition); $DB->insert_record('question_set_references', $data); } } /** * Abstract structure step to help activities that store question attempt data. * * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_questions_activity_structure_step extends restore_activity_structure_step { use restore_questions_attempt_data_trait; use restore_question_reference_data_trait; use restore_question_set_reference_data_trait; /** @var \question_engine_attempt_upgrader manages upgrading all the question attempts. */ private $attemptupgrader; /** * Attach below $element (usually attempts) the needed restore_path_elements * to restore question attempt data from Moodle 2.0. * * When using this method, the parent element ($element) must be defined with * $grouped = true. Then, in that elements process method, you must call * {@link process_legacy_attempt_data()} with the groupded data. See, for * example, the usage of this method in {@link restore_quiz_activity_structure_step}. * @param restore_path_element $element the parent element. (E.g. a quiz attempt.) * @param array $paths the paths array that is being built to describe the * structure. */ protected function add_legacy_question_attempt_data($element, &$paths) { global $CFG; require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php'); // Check $element is restore_path_element if (!($element instanceof restore_path_element)) { throw new restore_step_exception('element_must_be_restore_path_element', $element); } // Check $paths is one array if (!is_array($paths)) { throw new restore_step_exception('paths_must_be_array', $paths); } $paths[] = new restore_path_element('question_state', $element->get_path() . '/states/state'); $paths[] = new restore_path_element('question_session', $element->get_path() . '/sessions/session'); } protected function get_attempt_upgrader() { if (empty($this->attemptupgrader)) { $this->attemptupgrader = new question_engine_attempt_upgrader(); $this->attemptupgrader->prepare_to_restore(); } return $this->attemptupgrader; } /** * Process the attempt data defined by {@link add_legacy_question_attempt_data()}. * @param object $data contains all the grouped attempt data to process. * @param object $quiz data about the activity the attempts belong to. Required * fields are (basically this only works for the quiz module): * oldquestions => list of question ids in this activity - using old ids. * preferredbehaviour => the behaviour to use for questionattempts. */ protected function process_legacy_quiz_attempt_data($data, $quiz) { global $DB; $upgrader = $this->get_attempt_upgrader(); $data = (object)$data; $layout = explode(',', $data->layout); $newlayout = $layout; // Convert each old question_session into a question_attempt. $qas = array(); foreach (explode(',', $quiz->oldquestions) as $questionid) { if ($questionid == 0) { continue; } $newquestionid = $this->get_mappingid('question', $questionid); if (!$newquestionid) { throw new restore_step_exception('questionattemptreferstomissingquestion', $questionid, $questionid); } $question = $upgrader->load_question($newquestionid, $quiz->id); foreach ($layout as $key => $qid) { if ($qid == $questionid) { $newlayout[$key] = $newquestionid; } } list($qsession, $qstates) = $this->find_question_session_and_states( $data, $questionid); if (empty($qsession) || empty($qstates)) { throw new restore_step_exception('questionattemptdatamissing', $questionid, $questionid); } list($qsession, $qstates) = $this->recode_legacy_response_data( $question, $qsession, $qstates); $data->layout = implode(',', $newlayout); $qas[$newquestionid] = $upgrader->convert_question_attempt( $quiz, $data, $question, $qsession, $qstates); } // Now create a new question_usage. $usage = new stdClass(); $usage->component = 'mod_quiz'; $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid()); $usage->preferredbehaviour = $quiz->preferredbehaviour; $usage->id = $DB->insert_record('question_usages', $usage); $this->inform_new_usage_id($usage->id); $data->uniqueid = $usage->id; $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas, $this->questions_recode_layout($quiz->oldquestions)); } protected function find_question_session_and_states($data, $questionid) { $qsession = null; foreach ($data->sessions['session'] as $session) { if ($session['questionid'] == $questionid) { $qsession = (object) $session; break; } } $qstates = array(); foreach ($data->states['state'] as $state) { if ($state['question'] == $questionid) { // It would be natural to use $state['seq_number'] as the array-key // here, but it seems that buggy behaviour in 2.0 and early can // mean that that is not unique, so we use id, which is guaranteed // to be unique. $qstates[$state['id']] = (object) $state; } } ksort($qstates); $qstates = array_values($qstates); return array($qsession, $qstates); } /** * Recode any ids in the response data * @param object $question the question data * @param object $qsession the question sessions. * @param array $qstates the question states. */ protected function recode_legacy_response_data($question, $qsession, $qstates) { $qsession->questionid = $question->id; foreach ($qstates as &$state) { $state->question = $question->id; $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype); } return array($qsession, $qstates); } /** * Recode the legacy answer field. * @param object $state the state to recode the answer of. * @param string $qtype the question type. */ public function restore_recode_legacy_answer($state, $qtype) { $restorer = $this->get_qtype_restorer($qtype); if ($restorer) { return $restorer->recode_legacy_state_answer($state); } else { return $state->answer; } } } /** * Restore completion defaults for each module type * * @package core_backup * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_completion_defaults_structure_step extends restore_structure_step { /** * To conditionally decide if this step must be executed. */ protected function execute_condition() { // No completion on the front page. if ($this->get_courseid() == SITEID) { return false; } // No default completion info found, don't execute. $fullpath = $this->task->get_taskbasepath(); $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; if (!file_exists($fullpath)) { return false; } // Arrived here, execute the step. return true; } /** * Function that will return the structure to be processed by this restore_step. * * @return restore_path_element[] */ protected function define_structure() { return [new restore_path_element('completion_defaults', '/course_completion_defaults/course_completion_default')]; } /** * Processor for path element 'completion_defaults' * * @param stdClass|array $data */ protected function process_completion_defaults($data) { global $DB; $data = (array)$data; $oldid = $data['id']; unset($data['id']); // Find the module by name since id may be different in another site. if (!$mod = $DB->get_record('modules', ['name' => $data['modulename']])) { return; } unset($data['modulename']); // Find the existing record. $newid = $DB->get_field('course_completion_defaults', 'id', ['course' => $this->task->get_courseid(), 'module' => $mod->id]); if (!$newid) { $newid = $DB->insert_record('course_completion_defaults', ['course' => $this->task->get_courseid(), 'module' => $mod->id] + $data); } else { $DB->update_record('course_completion_defaults', ['id' => $newid] + $data); } // Save id mapping for restoring associated events. $this->set_mapping('course_completion_defaults', $oldid, $newid); } } /** * Index course after restore. * * @package core_backup * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_course_search_index extends restore_execution_step { /** * When this step is executed, we add the course context to the queue for reindexing. */ protected function define_execution() { $context = \context_course::instance($this->task->get_courseid()); \core_search\manager::request_index($context); } } /** * Index activity after restore (when not restoring whole course). * * @package core_backup * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_activity_search_index extends restore_execution_step { /** * When this step is executed, we add the activity context to the queue for reindexing. */ protected function define_execution() { $context = \context::instance_by_id($this->task->get_contextid()); \core_search\manager::request_index($context); } } /** * Index block after restore (when not restoring whole course). * * @package core_backup * @copyright 2017 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_block_search_index extends restore_execution_step { /** * When this step is executed, we add the block context to the queue for reindexing. */ protected function define_execution() { // A block in the restore list may be skipped because a duplicate is detected. // In this case, there is no new blockid (or context) to get. if (!empty($this->task->get_blockid())) { $context = \context_block::instance($this->task->get_blockid()); \core_search\manager::request_index($context); } } } /** * Restore action events. * * @package core_backup * @copyright 2017 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_calendar_action_events extends restore_execution_step { /** * What to do when this step is executed. */ protected function define_execution() { // We just queue the task here rather trying to recreate everything manually. // The task will automatically populate all data. $task = new \core\task\refresh_mod_calendar_events_task(); $task->set_custom_data(array('courseid' => $this->get_courseid())); \core\task\manager::queue_adhoc_task($task, true); } } moodle2/backup_report_plugin.class.php 0000644 00000002426 15215711721 0014146 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); /** * Base class for report backup plugins. * * NOTE: When you back up a course, it potentially may run backup for all * reports. In order to control whether a particular report gets * backed up, a report should make use of the second and third * parameters in get_plugin_element(). * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_report_plugin extends backup_plugin { // Use default parent behaviour } moodle2/backup_qbank_plugin.class.php 0000644 00000002516 15215711721 0013727 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_qbank_plugin class. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Base class for qbank backup plugins. * * @package core_backup * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_qbank_plugin extends backup_plugin { // Use default parent behaviour. } moodle2/restore_qtype_extrafields_plugin.class.php 0000644 00000010457 15215711721 0016610 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_qtype_extrafields_plugin class * * @package core_backup * @copyright 2012 Oleg Sychev, Volgograd State Technical University * @author Valeriy Streltsov <vostreltsov@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/question/engine/bank.php'); /** * Class extending restore_qtype_plugin in order to use extra fields method * * See qtype_shortanswer for an example * * @copyright 2012 Oleg Sychev, Volgograd State Technical University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class restore_qtype_extrafields_plugin extends restore_qtype_plugin { /** * Question type class for a particular question type * @var question_type */ protected $qtypeobj; /** * Constructor * * @param string $plugintype plugin type * @param string $pluginname plugin name * @param restore_step $step step */ public function __construct($plugintype, $pluginname, $step) { parent::__construct($plugintype, $pluginname, $step); $this->qtypeobj = question_bank::get_qtype($this->pluginname); } /** * Returns the paths to be handled by the plugin at question level. */ protected function define_question_plugin_structure() { $paths = array(); // This qtype uses question_answers, add them. $this->add_question_question_answers($paths); // Add own qtype stuff. $elepath = $this->get_pathfor('/' . $this->qtypeobj->name()); $paths[] = new restore_path_element($this->qtypeobj->name(), $elepath); $elepath = $this->get_pathfor('/answers/answer/extraanswerdata'); $paths[] = new restore_path_element('extraanswerdata', $elepath); return $paths; } /** * Processes the extra answer data * * @param array $data extra answer data */ public function process_extraanswerdata($data) { global $DB; $extra = $this->qtypeobj->extra_answer_fields(); $tablename = array_shift($extra); $oldquestionid = $this->get_old_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; if ($questioncreated) { $data['answerid'] = $this->get_mappingid('question_answer', $data['id']); $DB->insert_record($tablename, $data); } else { $DB->update_record($tablename, $data); } } /** * Process the qtype/... element. * * @param array $data question data */ public function really_process_extra_question_fields($data) { global $DB; $oldid = $data['id']; // Detect if the question is created or mapped. $oldquestionid = $this->get_old_parentid('question'); $newquestionid = $this->get_new_parentid('question'); $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false; // If the question has been created by restore, we need to create its qtype_... too. if ($questioncreated) { $extraquestionfields = $this->qtypeobj->extra_question_fields(); $tablename = array_shift($extraquestionfields); // Adjust some columns. $qtfield = $this->qtypeobj->questionid_column_name(); $data[$qtfield] = $newquestionid; // Insert record. $newitemid = $DB->insert_record($tablename, $data); // Create mapping. $this->set_mapping($tablename, $oldid, $newitemid); } } } moodle2/restore_subplugin.class.php 0000644 00000020076 15215711721 0013504 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_subplugin class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Class implementing the subplugins support for moodle2 restore * * TODO: Finish phpdocs * TODO: Make this subclass of restore_plugin * TODO: Add support for declaring decode_contents (not decode_rules) */ abstract class restore_subplugin { /** @var string */ protected $subplugintype; /** @var string */ protected $subpluginname; /** @var restore_path_element */ protected $connectionpoint; /** @var restore_step */ protected $step; /** @var restore_task */ protected $task; public function __construct($subplugintype, $subpluginname, $step) { $this->subplugintype = $subplugintype; $this->subpluginname = $subpluginname; $this->step = $step; $this->task = $step->get_task(); $this->connectionpoint = ''; } public function define_subplugin_structure($connectionpoint) { if (!$connectionpoint instanceof restore_path_element) { throw new restore_step_exception('restore_path_element_required', $connectionpoint); } $paths = array(); $this->connectionpoint = $connectionpoint; $methodname = 'define_' . basename($this->connectionpoint->get_path()) . '_subplugin_structure'; if (method_exists($this, $methodname)) { if ($subbluginpaths = $this->$methodname()) { foreach ($subbluginpaths as $path) { $path->set_processing_object($this); $paths[] = $path; } } } return $paths; } /** * after_execute dispatcher for any restore_subplugin class * * This method will dispatch execution to the corresponding * after_execute_xxx() method when available, with xxx * being the connection point of the instance, so subplugin * classes with multiple connection points will support * multiple after_execute methods, one for each connection point */ public function launch_after_execute_methods() { // Check if the after_execute method exists and launch it $afterexecute = 'after_execute_' . basename($this->connectionpoint->get_path()); if (method_exists($this, $afterexecute)) { $this->$afterexecute(); } } /** * The after_restore dispatcher for any restore_subplugin class. * * This method will dispatch execution to the corresponding * after_restore_xxx() method when available, with xxx * being the connection point of the instance, so subplugin * classes with multiple connection points will support * multiple after_restore methods, one for each connection point. */ public function launch_after_restore_methods() { // Check if the after_restore method exists and launch it. $afterestore = 'after_restore_' . basename($this->connectionpoint->get_path()); if (method_exists($this, $afterestore)) { $this->$afterestore(); } } // Protected API starts here // restore_step/structure_step/task wrappers protected function get_restoreid() { if (is_null($this->task)) { throw new restore_step_exception('not_specified_restore_task'); } return $this->task->get_restoreid(); } /** * To send ids pairs to backup_ids_table and to store them into paths * * This method will send the given itemname and old/new ids to the * backup_ids_temp table, and, at the same time, will save the new id * into the corresponding restore_path_element for easier access * by children. Also will inject the known old context id for the task * in case it's going to be used for restoring files later */ protected function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) { $this->step->set_mapping($itemname, $oldid, $newid, $restorefiles, $filesctxid, $parentid); } /** * Returns the latest (parent) old id mapped by one pathelement */ protected function get_old_parentid($itemname) { return $this->step->get_old_parentid($itemname); } /** * Returns the latest (parent) new id mapped by one pathelement */ protected function get_new_parentid($itemname) { return $this->step->get_new_parentid($itemname); } /** * Return the new id of a mapping for the given itemname * * @param string $itemname the type of item * @param int $oldid the item ID from the backup * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false */ protected function get_mappingid($itemname, $oldid, $ifnotfound = false) { return $this->step->get_mappingid($itemname, $oldid, $ifnotfound); } /** * Return the complete mapping from the given itemname, itemid */ protected function get_mapping($itemname, $oldid) { return $this->step->get_mapping($itemname, $oldid); } /** * Add all the existing file, given their component and filearea and one backup_ids itemname to match with */ protected function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { $this->step->add_related_files($component, $filearea, $mappingitemname, $filesctxid, $olditemid); } /** * Apply course startdate offset based in original course startdate and course_offset_startdate setting * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple * executions in the same request */ protected function apply_date_offset($value) { return $this->step->apply_date_offset($value); } /** * Call the log function from the step. */ public function log($message, $level, $a = null, $depth = null, $display = false) { return $this->step->log($message, $level, $a, $depth, $display); } /** * Returns the value of one (task/plan) setting */ protected function get_setting_value($name) { if (is_null($this->task)) { throw new restore_step_exception('not_specified_restore_task'); } return $this->task->get_setting_value($name); } // end of restore_step/structure_step/task wrappers /** * Simple helper function that returns the name for the restore_path_element * It's not mandatory to use it but recommended ;-) */ protected function get_namefor($name = '') { $name = $name !== '' ? '_' . $name : ''; return $this->subplugintype . '_' . $this->subpluginname . $name; } /** * Simple helper function that returns the base (prefix) of the path for the restore_path_element * Useful if we used get_recommended_name() in backup. It's not mandatory to use it but recommended ;-) */ protected function get_pathfor($path = '') { $path = trim($path, '/') !== '' ? '/' . trim($path, '/') : ''; return $this->connectionpoint->get_path() . '/' . 'subplugin_' . $this->subplugintype . '_' . $this->subpluginname . '_' . basename($this->connectionpoint->get_path()) . $path; } } moodle2/backup_activity_task.class.php 0000644 00000043065 15215711721 0014137 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_activity_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Provides all the settings and steps to perform one complete backup of the activity * * Activities are supposed to provide the subclass of this class in their file * mod/MODULENAME/backup/moodle2/backup_MODULENAME_activity_task.class.php * The expected name of the subclass is backup_MODULENAME_activity_task */ abstract class backup_activity_task extends backup_task { protected $moduleid; protected $sectionid; /** @var stdClass the section object */ protected $section; protected $modulename; protected $activityid; protected $contextid; /** * Constructor - instantiates one object of this class * * @param string $name the task identifier * @param int $moduleid course module id (id in course_modules table) * @param backup_plan|null $plan the backup plan instance this task is part of */ public function __construct($name, $moduleid, $plan = null) { global $DB; // Check moduleid exists if (!$coursemodule = get_coursemodule_from_id(false, $moduleid)) { throw new backup_task_exception('activity_task_coursemodule_not_found', $moduleid); } // Check activity supports this moodle2 backup format if (!plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) { throw new backup_task_exception('activity_task_activity_lacks_moodle2_backup_support', $coursemodule->modname); } $this->moduleid = $moduleid; $this->sectionid = $coursemodule->section; $this->modulename = $coursemodule->modname; $this->activityid = $coursemodule->instance; $this->contextid = context_module::instance($this->moduleid)->id; $this->section = $DB->get_record('course_sections', ['id' => $this->sectionid]); parent::__construct($name, $plan); } /** * @return int the course module id (id in the course_modules table) */ public function get_moduleid() { return $this->moduleid; } /** * @return int the course section id (id in the course_sections table) */ public function get_sectionid() { return $this->sectionid; } /** * @return string the name of the module, eg 'workshop' (from the modules table) */ public function get_modulename() { return $this->modulename; } /** * Return if the activity is inside a subsection. * * @return bool */ public function is_in_subsection(): bool { return !empty($this->section->component); } /** * @return int the id of the activity instance (id in the activity's instances table) */ public function get_activityid() { return $this->activityid; } /** * @return int the id of the associated CONTEXT_MODULE instance */ public function get_contextid() { return $this->contextid; } /** * @return string full path to the directory where this task writes its files */ public function get_taskbasepath() { return $this->get_basepath() . '/activities/' . $this->modulename . '_' . $this->moduleid; } /** * Create all the steps that will be part of this task */ public function build() { // If we have decided not to backup activities, prevent anything to be built if (!$this->get_setting_value('activities')) { $this->built = true; return; } // Add some extra settings that related processors are going to need $this->add_section_setting(backup::VAR_MODID, base_setting::IS_INTEGER, $this->moduleid); $this->add_section_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid()); $this->add_section_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid); $this->add_section_setting(backup::VAR_MODNAME, base_setting::IS_FILENAME, $this->modulename); $this->add_section_setting(backup::VAR_ACTIVITYID, base_setting::IS_INTEGER, $this->activityid); $this->add_section_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $this->contextid); // Create the activity directory $this->add_step(new create_taskbasepath_directory('create_activity_directory')); // Generate the module.xml file, containing general information for the // activity and from its related course_modules record and availability $this->add_step(new backup_module_structure_step('module_info', 'module.xml')); // Annotate the groups used in already annotated groupings if groups are to be backed up. if ($this->get_setting_value('groups')) { $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups')); } // Here we add all the common steps for any activity and, in the point of interest // we call to define_my_steps() is order to get the particular ones inserted in place. $this->define_my_steps(); // Generate the roles file (optionally role assignments and always role overrides) $this->add_step(new backup_roles_structure_step('activity_roles', 'roles.xml')); // Generate the filter file (conditionally) if ($this->get_setting_value('filters')) { $this->add_step(new backup_filters_structure_step('activity_filters', 'filters.xml')); } // Generate the comments file (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new backup_comments_structure_step('activity_comments', 'comments.xml')); } // Generate the userscompletion file (conditionally) if ($this->get_setting_value('userscompletion')) { $this->add_step(new backup_userscompletion_structure_step('activity_userscompletion', 'completion.xml')); } // Generate the logs file (conditionally) if ($this->get_setting_value('logs')) { // Legacy logs. $this->add_step(new backup_activity_logs_structure_step('activity_logs', 'logs.xml')); // New log stores. $this->add_step(new backup_activity_logstores_structure_step('activity_logstores', 'logstores.xml')); } // Generate the calendar events file (conditionally) if ($this->get_setting_value('calendarevents')) { $this->add_step(new backup_calendarevents_structure_step('activity_calendar', 'calendar.xml')); } // Fetch all the activity grade items and put them to backup_ids $this->add_step(new backup_activity_grade_items_to_ids('fetch_activity_grade_items')); // Generate the grades file $this->add_step(new backup_activity_grades_structure_step('activity_grades', 'grades.xml')); // Generate the grading file (conditionally) $this->add_step(new backup_activity_grading_structure_step('activity_grading', 'grading.xml')); // Generate the grade history file. The setting 'grade_histories' is handled in the step. $this->add_step(new backup_activity_grade_history_structure_step('activity_grade_history', 'grade_history.xml')); // Generate the competency file. $this->add_step(new backup_activity_competencies_structure_step('activity_competencies', 'competencies.xml')); // Annotate the scales used in already annotated outcomes $this->add_step(new backup_annotate_scales_from_outcomes('annotate_scales')); // NOTE: Historical grade information is saved completely at course level only (see 1.9) // not per activity nor per selected activities (all or nothing). // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) $this->add_step(new backup_inforef_structure_step('activity_inforef', 'inforef.xml')); // Migrate the already exported inforef entries to final ones $this->add_step(new move_inforef_annotations_to_final('migrate_inforef')); // Generate the xAPI state file (conditionally). if ($this->get_setting_value('xapistate')) { $this->add_step(new backup_xapistate_structure_step('activity_xapistate', 'xapistate.xml')); } // At the end, mark it as built $this->built = true; } /** * Exceptionally override the execute method, so, based in the activity_included setting, we are able * to skip the execution of one task completely */ public function execute() { // Find activity_included_setting if (!$this->get_setting_value('included')) { $this->log('activity skipped by _included setting', backup::LOG_DEBUG, $this->name); $this->plan->set_excluding_activities(); } else { // Setting tells us it's ok to execute parent::execute(); } } /** * Tries to look for the instance specific setting value, task specific setting value or the * common plan setting value - in that order * * @param string $name the name of the setting * @return mixed|null the value of the setting or null if not found */ public function get_setting($name) { $namewithprefix = $this->modulename . '_' . $this->moduleid . '_' . $name; $result = null; foreach ($this->settings as $key => $setting) { if ($setting->get_name() == $namewithprefix) { if ($result != null) { throw new base_task_exception('multiple_settings_by_name_found', $namewithprefix); } else { $result = $setting; } } } if ($result) { return $result; } else { // Fallback to parent return parent::get_setting($name); } } // Protected API starts here /** * Defines the common setting that any backup activity will have. */ protected function define_settings() { global $CFG; require_once($CFG->libdir.'/questionlib.php'); // All the settings related to this activity will include this prefix. $settingprefix = $this->modulename . '_' . $this->moduleid . '_'; // All these are common settings to be shared by all activities. $activityincluded = $this->add_activity_included_setting($settingprefix); if (question_module_uses_questions($this->modulename)) { $questionbank = $this->plan->get_setting('questionbank'); $questionbank->add_dependency($activityincluded); } $this->add_activity_userinfo_setting($settingprefix, $activityincluded); // End of common activity settings, let's add the particular ones. $this->define_my_settings(); } /** * Add a setting to the task. This method is used to add a setting to the task * * @param int|string $identifier the identifier of the setting * @param string $type the type of the setting * @param string|int $value the value of the setting * @return section_backup_setting the setting added */ protected function add_section_setting(int|string $identifier, string $type, string|int $value): activity_backup_setting { if ($this->is_in_subsection()) { $setting = new backup_subactivity_generic_setting($identifier, $type, $value); } else { $setting = new backup_activity_generic_setting($identifier, $type, $value); } $this->add_setting($setting); return $setting; } /** * Add the section include setting to the task. * * @param string $settingprefix the identifier of the setting * @return activity_backup_setting the setting added */ protected function add_activity_included_setting(string $settingprefix): activity_backup_setting { // Define activity_include (to decide if the whole task must be really executed) // Dependent of: // - activities root setting. // - sectionincluded setting (if exists). $settingname = $settingprefix . 'included'; if ($this->is_in_subsection()) { $activityincluded = new backup_subactivity_generic_setting($settingname, base_setting::IS_BOOLEAN, true); } else { $activityincluded = new backup_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true); } $activityincluded->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename), $this->modulename, array('class' => 'ms-1'))); $this->add_setting($activityincluded); // Look for "activities" root setting. $activities = $this->plan->get_setting('activities'); $activities->add_dependency($activityincluded); // Look for "sectionincluded" section setting (if exists). $settingname = 'section_' . $this->sectionid . '_included'; if ($this->plan->setting_exists($settingname)) { $sectionincluded = $this->plan->get_setting($settingname); $sectionincluded->add_dependency($activityincluded); } return $activityincluded; } /** * Add the section userinfo setting to the task. * * @param string $settingprefix the identifier of the setting * @param activity_backup_setting $includefield the setting to depend on * @return activity_backup_setting the setting added */ protected function add_activity_userinfo_setting( string $settingprefix, activity_backup_setting $includefield ): activity_backup_setting { // Define activity_userinfo. Dependent of: // - users root setting. // - sectionuserinfo setting (if exists). // - includefield setting. $settingname = $settingprefix . 'userinfo'; if ($this->is_in_subsection()) { $activityuserinfo = new backup_subactivity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true); } else { $activityuserinfo = new backup_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true); } $activityuserinfo->get_ui()->set_label('-'); $activityuserinfo->get_ui()->set_visually_hidden_label( get_string('includeuserinfo_instance', 'core_backup', $this->name) ); $this->add_setting($activityuserinfo); // Look for "users" root setting. $users = $this->plan->get_setting('users'); $users->add_dependency($activityuserinfo); // Look for "sectionuserinfo" section setting (if exists). $settingname = 'section_' . $this->sectionid . '_userinfo'; if ($this->plan->setting_exists($settingname)) { $sectionuserinfo = $this->plan->get_setting($settingname); $sectionuserinfo->add_dependency($activityuserinfo); } $includefield->add_dependency($activityuserinfo); return $activityuserinfo; } /** * Defines activity specific settings to be added to the common ones * * This method is called from {@link self::define_settings()}. The activity module * author may use it to define additional settings that influence the execution of * the backup. * * Most activities just leave the method empty. * * @see self::define_settings() for the example how to define own settings */ abstract protected function define_my_settings(); /** * Defines activity specific steps for this task * * This method is called from {@link self::build()}. Activities are supposed * to call {self::add_step()} in it to include their specific steps in the * backup plan. */ abstract protected function define_my_steps(); /** * Encodes URLs to the activity instance's scripts into a site-independent form * * The current instance of the activity may be referenced from other places in * the course by URLs like http://my.moodle.site/mod/workshop/view.php?id=42 * Obvisouly, such URLs are not valid any more once the course is restored elsewhere. * For this reason the backup file does not store the original URLs but encodes them * into a transportable form. During the restore, the reverse process is applied and * the encoded URLs are replaced with the new ones valid for the target site. * * Every plugin must override this method in its subclass. * * @see backup_xml_transformer class that actually runs the transformation * @param string $content some HTML text that eventually contains URLs to the activity instance scripts * @return string the content with the URLs encoded */ public static function encode_content_links($content) { throw new coding_exception('encode_content_links() method needs to be overridden in each subclass of backup_activity_task'); } } moodle2/backup_custom_fields.php 0000644 00000026361 15215711721 0013015 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines various element classes used in specific areas * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Implementation of backup_final_element that provides one interceptor for anonymization of data * * This class overwrites the standard set_value() method, in order to get (by name) * functions from backup_anonymizer_helper executed, producing anonymization of information * to happen in a clean way * * TODO: Finish phpdocs */ class anonymizer_final_element extends backup_final_element { public function set_value($value) { // Get parent name $pname = $this->get_parent()->get_name(); // Get my name $myname = $this->get_name(); // Define class and function name $classname = 'backup_anonymizer_helper'; $methodname= 'process_' . $pname . '_' . $myname; // Invoke the interception method $result = call_user_func(array($classname, $methodname), $value); // Finally set it parent::set_value($result); } } /** * Implementation of backup_final_element that provides special handling of mnethosturl * * This class overwrites the standard set_value() method, in order to decide, * based on various config options, what to do with the field. * * TODO: Finish phpdocs */ class mnethosturl_final_element extends backup_final_element { public function set_value($value) { global $CFG; $localhostwwwroot = backup_plan_dbops::get_mnet_localhost_wwwroot(); // If user wwwroot matches mnet local host one or if // there isn't associated wwwroot, skip sending it to file if ($localhostwwwroot == $value || empty($value)) { // Do nothing } else { parent::set_value($value); } } } /** * Implementation of {@link backup_final_element} that provides base64 encoding. * * This final element transparently encodes with base64_encode() contents that * normally are not safe for being stored in utf-8 xml files (binaries, serialized * data...). */ class base64_encode_final_element extends backup_final_element { /** * Set the value for the final element, encoding it as utf-8/xml safe base64. * * @param string $value Original value coming from backup step source, usually db. */ public function set_value($value) { // Avoid null being passed to base64_encode. $value = $value ?? ''; parent::set_value(base64_encode($value)); } } /** * Implementation of {@link backup_final_element} that provides symmetric-key AES-256 encryption of contents. * * This final element transparently encrypts, for secure storage and transport, any content * that shouldn't be shown normally in plain text. Usually, passwords or keys that cannot use * hashing algorithms, although potentially can encrypt any content. All information is encoded * using base64. * * Features: * - requires openssl extension to work. Without it contents are completely omitted. * - automatically creates an appropriate default key for the site and stores it into backup_encryptkey config (bas64 encoded). * - uses a different appropriate init vector for every operation, which is transmited with the encrypted contents. * - all generated data is base64 encoded for safe transmission. * - automatically adds "encrypted" attribute for easier detection. * - implements HMAC for providing integrity. * * @copyright 2017 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class encrypted_final_element extends backup_final_element { /** @var string cypher appropiate raw key for backups in the site. Defaults to backup_encryptkey config. */ protected $key = null; /** * Constructor - instantiates a encrypted_final_element, specifying its basic info. * * Overridden to automatically add the 'encrypted' attribute if missing. * * @param string $name name of the element * @param array $attributes attributes this element will handle (optional, defaults to null) */ public function __construct($name, $attributes = null) { parent::__construct($name, $attributes); if (! $this->get_attribute('encrypted')) { $this->add_attributes('encrypted'); } } /** * Set the encryption key manually, overriding default backup_encryptkey config. * * @param string $key key to be used for encrypting. Required to be 256-bit key. * Use a safe generation technique. See self::generate_encryption_random_key() below. */ protected function set_key($key) { $bytes = strlen($key); // Get key length in bytes. // Only accept keys with the expected (backup::CIPHERKEYLEN) key length. There are a number of hashing, // random generators to achieve this esasily, like the one shown below to create the default // site encryption key and ivs. if ($bytes !== backup::CIPHERKEYLEN) { $info = (object)array('expected' => backup::CIPHERKEYLEN, 'found' => $bytes); throw new base_element_struct_exception('encrypted_final_element incorrect key length', $info); } // Everything went ok, store the key. $this->key = $key; } /** * Set the value of the field. * * This method sets the value of the element, encrypted using the specified key for it, * defaulting to (and generating) backup_encryptkey config. HMAC is used for integrity. * * @param string $value plain-text content the will be stored encrypted and encoded. */ public function set_value($value) { // No openssl available, skip this field completely. if (!function_exists('openssl_encrypt')) { return; } // No hmac available, skip this field completely. if (!function_exists('hash_hmac')) { return; } // Cypher not available, skip this field completely. if (!in_array(backup::CIPHER, openssl_get_cipher_methods())) { return; } // Ensure we have a good key, manual or default. if (empty($this->key)) { // The key has not been set manually, look for it at config (base64 encoded there). $enckey = get_config('backup', 'backup_encryptkey'); if ($enckey === false) { // Has not been set, calculate and save an appropiate random key automatically. $enckey = base64_encode(self::generate_encryption_random_key(backup::CIPHERKEYLEN)); set_config('backup_encryptkey', $enckey, 'backup'); } $this->set_key(base64_decode($enckey)); } // Now we need an iv for this operation. $iv = self::generate_encryption_random_key(openssl_cipher_iv_length(backup::CIPHER)); // Everything is ready, let's encrypt and prepend the 1-shot iv. $value = $iv . openssl_encrypt($value ?? '', backup::CIPHER, $this->key, OPENSSL_RAW_DATA, $iv); // Calculate the hmac of the value (iv + encrypted) and prepend it. $hmac = hash_hmac('sha256', $value, $this->key, true); $value = $hmac . $value; // Ready, set the encoded value. parent::set_value(base64_encode($value)); // Finally, if the field has an "encrypted" attribute, set it to true. if ($att = $this->get_attribute('encrypted')) { $att->set_value('true'); } } /** * Generate an appropiate random key to be used for encrypting backup information. * * Normally used as site default encryption key (backup_encryptkey config) and also * for calculating the init vectors. * * Note that until PHP 5.6.12 openssl_random_pseudo_bytes() did NOT * use a "cryptographically strong algorithm" {@link https://bugs.php.net/bug.php?id=70014} * But it's beyond my crypto-knowledge when it's worth finding a *real* better alternative. * * @param int $bytes Number of bytes to determine the key length expected. */ protected static function generate_encryption_random_key($bytes) { return openssl_random_pseudo_bytes($bytes); } } /** * Implementation of backup_nested_element that provides special handling of files * * This class overwrites the standard fill_values() method, so it gets intercepted * for each file record being set to xml, in order to copy, at the same file, the * physical file from moodle file storage to backup file storage * * TODO: Finish phpdocs */ class file_nested_element extends backup_nested_element { protected $backupid; public function process($processor) { // Get current backupid from processor, we'll need later if (is_null($this->backupid)) { $this->backupid = $processor->get_var(backup::VAR_BACKUPID); } return parent::process($processor); } public function fill_values($values) { // Fill values parent::fill_values($values); // Do our own tasks (copy file from moodle to backup) try { backup_file_manager::copy_file_moodle2backup($this->backupid, $values); } catch (file_exception $e) { $this->add_result(array('missing_files_in_pool' => true)); // Build helpful log message with all information necessary to identify // file location. $context = context::instance_by_id($values->contextid, IGNORE_MISSING); $contextname = ''; if ($context) { $contextname = ' \'' . $context->get_context_name() . '\''; } $message = 'Missing file in pool: ' . $values->filepath . $values->filename . ' (context ' . $values->contextid . $contextname . ', component ' . $values->component . ', filearea ' . $values->filearea . ', itemid ' . $values->itemid . ') [' . $e->debuginfo . ']'; $this->add_log($message, backup::LOG_WARNING); } } } /** * Implementation of backup_optigroup_element to be used by plugins stuff. * Split just for better separation and future specialisation */ class backup_plugin_element extends backup_optigroup_element { } /** * Implementation of backup_optigroup_element to be used by subplugins stuff. * Split just for better separation and future specialisation */ class backup_subplugin_element extends backup_optigroup_element { } moodle2/backup_default_block_task.class.php 0000644 00000003236 15215711721 0015075 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_default_block_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Default block task to backup blocks that haven't own DB structures to be added * when one block is being backup * * TODO: Finish phpdocs */ class backup_default_block_task extends backup_block_task { // Nothing to do, it's just the backup_block_task in action // with required methods doing nothing special protected function define_my_settings() { } protected function define_my_steps() { } public function get_fileareas() { return array(); } public function get_configdata_encoded_attributes() { return array(); } public static function encode_content_links($content) { return $content; } } moodle2/backup_plagiarism_plugin.class.php 0000644 00000003233 15215711721 0014760 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_plagiarism_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard backup_plugin in order to implement some * helper methods related with the plagiarism plugins (plagiarism plugin) * * TODO: Finish phpdocs */ abstract class backup_plagiarism_plugin extends backup_plugin { public function define_plugin_structure($connectionpoint) { global $CFG; require_once($CFG->libdir . '/plagiarismlib.php'); //check if enabled at site level and plugin is enabled. $enabledplugins = plagiarism_load_available_plugins(); if (!array_key_exists($this->pluginname, $enabledplugins)) { return; } parent::define_plugin_structure($connectionpoint); } } moodle2/restore_course_task.class.php 0000644 00000026204 15215711721 0014015 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_course_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * course task that provides all the properties and common steps to be performed * when one course is being restored * * TODO: Finish phpdocs */ class restore_course_task extends restore_task { protected $info; // info related to course gathered from backup file protected $contextid; // course context id /** * Constructor - instantiates one object of this class */ public function __construct($name, $info, $plan = null) { $this->info = $info; parent::__construct($name, $plan); } /** * Course tasks have their own directory to read files */ public function get_taskbasepath() { return $this->get_basepath() . '/course'; } public function get_contextid() { return $this->contextid; } /** * Create all the steps that will be part of this task */ public function build() { // Define the task contextid (the course one) $this->contextid = context_course::instance($this->get_courseid())->id; // Executed conditionally if restoring to new course or if overwrite_conf setting is enabled if ($this->get_target() == backup::TARGET_NEW_COURSE || $this->get_setting_value('overwrite_conf') == true) { $this->add_step(new restore_course_structure_step('course_info', 'course.xml')); // Search reindexing (if enabled). if (\core_search\manager::is_indexing_enabled()) { $this->add_step(new restore_course_search_index('course_search_index')); } } if ($this->get_setting_value('legacyfiles')) { $this->add_step(new restore_course_legacy_files_step('legacy_files')); } // Deal with enrolment methods and user enrolments. if ($this->plan->get_mode() == backup::MODE_IMPORT) { // No need to do anything with enrolments. } else if (!$this->get_setting_value('users') or $this->plan->get_mode() == backup::MODE_HUB) { if ($this->get_setting_value('enrolments') == backup::ENROL_ALWAYS && $this->plan->get_mode() != backup::MODE_HUB) { // Restore enrolment methods. $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml')); } else if ($this->get_target() == backup::TARGET_CURRENT_ADDING or $this->get_target() == backup::TARGET_EXISTING_ADDING) { // Keep current enrolments unchanged. } else { // If no instances yet add default enrol methods the same way as when creating new course in UI. $this->add_step(new restore_default_enrolments_step('default_enrolments')); } } else { // Restore course enrolment data. $this->add_step(new restore_enrolments_structure_step('course_enrolments', 'enrolments.xml')); } // Populate groups, this must be done after enrolments because only enrolled users may be in groups. $this->add_step(new restore_groups_members_structure_step('create_groups_members', '../groups.xml')); // Restore course role assignments and overrides (internally will observe the role_assignments setting), // this must be done after all users are enrolled. $this->add_step(new restore_ras_and_caps_structure_step('course_ras_and_caps', 'roles.xml')); // Restore course filters (conditionally) if ($this->get_setting_value('filters')) { $this->add_step(new restore_filters_structure_step('course_filters', 'filters.xml')); } // Restore course comments (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new restore_comments_structure_step('course_comments', 'comments.xml')); } // Calendar events (conditionally) if ($this->get_setting_value('calendarevents')) { $this->add_step(new restore_calendarevents_structure_step('course_calendar', 'calendar.xml')); } // Course competencies. $this->add_step(new restore_course_competencies_structure_step('course_competencies', 'competencies.xml')); // Activity completion defaults. $this->add_step(new restore_completion_defaults_structure_step('course_completion_defaults', 'completiondefaults.xml')); // Content bank content (conditionally). if ($this->get_setting_value('contentbankcontent')) { $this->add_step(new restore_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml')); } // At the end, mark it as built $this->built = true; } /** * Define the contents in the course that must be * processed by the link decoder */ public static function define_decode_contents() { $contents = array(); $contents[] = new restore_decode_content('course', 'summary'); $contents[] = new restore_decode_content('event', 'description'); return $contents; } /** * Define the decoding rules for links belonging * to the course to be executed by the link decoder */ public static function define_decode_rules() { $rules = array(); // Link to the course main page (it also covers "&topic=xx" and "&week=xx" // because they don't become transformed (section number) in backup/restore. $rules[] = new restore_decode_rule('COURSEVIEWBYID', '/course/view.php?id=$1', 'course'); $rules[] = new restore_decode_rule('COURSESECTIONBYID', '/course/section.php?id=$1', 'course_section'); // A few other key course links. $rules[] = new restore_decode_rule('GRADEINDEXBYID', '/grade/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('GRADEREPORTINDEXBYID', '/grade/report/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/index.php?type=2&id=$1', 'course'); $rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context'); $rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/$1', 'context', true); return $rules; } // Protected API starts here /** * Define the common setting that any restore course will have */ protected function define_settings() { // Define overwrite_conf to decide if course configuration will be restored over existing one. $overwrite = new restore_course_overwrite_conf_setting('overwrite_conf', base_setting::IS_BOOLEAN, false); $overwrite->set_ui(new backup_setting_ui_select($overwrite, $overwrite->get_name(), array(1 => get_string('yes'), 0 => get_string('no')))); $overwrite->get_ui()->set_label(get_string('setting_overwrite_conf', 'backup')); if ($this->get_target() == backup::TARGET_NEW_COURSE) { $overwrite->set_value(true); $overwrite->set_status(backup_setting::LOCKED_BY_CONFIG); $overwrite->set_visibility(backup_setting::HIDDEN); $course = (object)['fullname' => null, 'shortname' => null, 'startdate' => null]; } else { $course = get_course($this->get_courseid()); } $this->add_setting($overwrite); $fullnamedefaultvalue = $this->get_info()->original_course_fullname; $fullname = new restore_course_defaultcustom_setting('course_fullname', base_setting::IS_TEXT, $fullnamedefaultvalue); $fullname->set_ui(new backup_setting_ui_defaultcustom($fullname, get_string('setting_course_fullname', 'backup'), ['customvalue' => $fullnamedefaultvalue, 'defaultvalue' => $course->fullname])); $this->add_setting($fullname); $shortnamedefaultvalue = $this->get_info()->original_course_shortname; $shortname = new restore_course_defaultcustom_setting('course_shortname', base_setting::IS_TEXT, $shortnamedefaultvalue); $shortname->set_ui(new backup_setting_ui_defaultcustom($shortname, get_string('setting_course_shortname', 'backup'), ['customvalue' => $shortnamedefaultvalue, 'defaultvalue' => $course->shortname])); $this->add_setting($shortname); $startdatedefaultvalue = $this->get_info()->original_course_startdate; $startdate = new restore_course_defaultcustom_setting('course_startdate', base_setting::IS_INTEGER, $startdatedefaultvalue); $startdate->set_ui(new backup_setting_ui_defaultcustom($startdate, get_string('setting_course_startdate', 'backup'), ['customvalue' => $startdatedefaultvalue, 'defaultvalue' => $course->startdate, 'type' => 'date_time_selector'])); $this->add_setting($startdate); $keep_enrols = new restore_course_generic_setting('keep_roles_and_enrolments', base_setting::IS_BOOLEAN, false); $keep_enrols->set_ui(new backup_setting_ui_select($keep_enrols, $keep_enrols->get_name(), array(1=>get_string('yes'), 0=>get_string('no')))); $keep_enrols->get_ui()->set_label(get_string('setting_keep_roles_and_enrolments', 'backup')); if ($this->get_target() != backup::TARGET_CURRENT_DELETING and $this->get_target() != backup::TARGET_EXISTING_DELETING) { $keep_enrols->set_value(false); $keep_enrols->set_status(backup_setting::LOCKED_BY_CONFIG); $keep_enrols->set_visibility(backup_setting::HIDDEN); } $this->add_setting($keep_enrols); $keep_groups = new restore_course_generic_setting('keep_groups_and_groupings', base_setting::IS_BOOLEAN, false); $keep_groups->set_ui(new backup_setting_ui_select($keep_groups, $keep_groups->get_name(), array(1=>get_string('yes'), 0=>get_string('no')))); $keep_groups->get_ui()->set_label(get_string('setting_keep_groups_and_groupings', 'backup')); if ($this->get_target() != backup::TARGET_CURRENT_DELETING and $this->get_target() != backup::TARGET_EXISTING_DELETING) { $keep_groups->set_value(false); $keep_groups->set_status(backup_setting::LOCKED_BY_CONFIG); $keep_groups->set_visibility(backup_setting::HIDDEN); } $this->add_setting($keep_groups); } } moodle2/restore_coursereport_plugin.class.php 0000644 00000002067 15215711721 0015606 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); /** * Restore for course plugin: course report. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_coursereport_plugin extends restore_plugin { // Use default parent behaviour } moodle2/backup_qtype_extrafields_plugin.class.php 0000644 00000006445 15215711721 0016374 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_qtype_extrafields_plugin class * * @package core_backup * @copyright 2012 Oleg Sychev, Volgograd State Technical University * @author Valeriy Streltsov <vostreltsov@gmail.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/question/engine/bank.php'); /** * Class extending backup_qtype_plugin in order to use extra fields method * * See qtype_shortanswer for an example * * @copyright 2012 Oleg Sychev, Volgograd State Technical University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class backup_qtype_extrafields_plugin extends backup_qtype_plugin { /** * Returns the qtype information to attach to question element. */ protected function define_question_plugin_structure() { $qtypeobj = question_bank::get_qtype($this->pluginname); // Define the virtual plugin element with the condition to fulfill. $plugin = $this->get_plugin_element(null, '../../qtype', $qtypeobj->name()); // Create one standard named plugin element (the visible container). $pluginwrapper = new backup_nested_element($this->get_recommended_name()); // Connect the visible container ASAP. $plugin->add_child($pluginwrapper); // This qtype uses standard question_answers, add them here // to the tree before any other information that will use them. $this->add_question_question_answers($pluginwrapper); $answers = $pluginwrapper->get_child('answers'); $answer = $answers->get_child('answer'); // Extra question fields. $extraquestionfields = $qtypeobj->extra_question_fields(); if (!empty($extraquestionfields)) { $tablename = array_shift($extraquestionfields); $child = new backup_nested_element($qtypeobj->name(), array('id'), $extraquestionfields); $pluginwrapper->add_child($child); $child->set_source_table($tablename, array($qtypeobj->questionid_column_name() => backup::VAR_PARENTID)); } // Extra answer fields. $extraanswerfields = $qtypeobj->extra_answer_fields(); if (!empty($extraanswerfields)) { $tablename = array_shift($extraanswerfields); $child = new backup_nested_element('extraanswerdata', array('id'), $extraanswerfields); $answer->add_child($child); $child->set_source_table($tablename, array('answerid' => backup::VAR_PARENTID)); } // Don't need to annotate ids nor files. return $plugin; } } moodle2/backup_course_task.class.php 0000644 00000022337 15215711721 0013602 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_course_task * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * course task that provides all the properties and common steps to be performed * when one course is being backup * * TODO: Finish phpdocs */ class backup_course_task extends backup_task { protected $courseid; protected $contextid; /** * Constructor - instantiates one object of this class */ public function __construct($name, $courseid, $plan = null) { $this->courseid = $courseid; $this->contextid = context_course::instance($this->courseid)->id; parent::__construct($name, $plan); } public function get_contextid() { return $this->contextid; } /** * Course tasks have their own directory to write files */ public function get_taskbasepath() { return $this->get_basepath() . '/course'; } /** * Create all the steps that will be part of this task */ public function build() { // Add some extra settings that related processors are going to need $this->add_setting(new backup_activity_generic_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid())); $this->add_setting(new backup_activity_generic_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $this->contextid)); // Create the course directory $this->add_step(new create_taskbasepath_directory('create_course_directory')); // Create the course.xml file with course & category information // annotating some bits, tags and module restrictions $this->add_step(new backup_course_structure_step('course_info', 'course.xml')); // Generate the enrolment file (conditionally, prevent it in any IMPORT/HUB operation) if ($this->plan->get_mode() != backup::MODE_IMPORT && $this->plan->get_mode() != backup::MODE_HUB) { $this->add_step(new backup_enrolments_structure_step('course_enrolments', 'enrolments.xml')); } // Annotate enrolment custom fields. $this->add_step(new backup_enrolments_execution_step('annotate_enrol_custom_fields')); // Annotate all the groups and groupings belonging to the course. This can be optional. if ($this->get_setting_value('groups')) { $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups')); } // Annotate the groups used in already annotated groupings (note this may be // unnecessary now that we are annotating all the course groups and groupings in the // step above). This is here to support course->defaultgroupingid. // This may not be required to annotate if groups are not being backed up. if ($this->get_setting_value('groups')) { $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings')); } // Annotate the question_categories belonging to the course context (conditionally). if ($this->get_setting_value('questionbank')) { $this->add_step(new backup_calculate_question_categories('course_question_categories')); } // Generate the roles file (optionally role assignments and always role overrides) $this->add_step(new backup_roles_structure_step('course_roles', 'roles.xml')); // Generate the filter file (conditionally) if ($this->get_setting_value('filters')) { $this->add_step(new backup_filters_structure_step('course_filters', 'filters.xml')); } // Generate the comments file (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new backup_comments_structure_step('course_comments', 'comments.xml')); } // Generate the calender events file (conditionally) if ($this->get_setting_value('calendarevents')) { $this->add_step(new backup_calendarevents_structure_step('course_calendar', 'calendar.xml')); } // Generate the logs file (conditionally) if ($this->get_setting_value('logs')) { // Legacy logs. $this->add_step(new backup_course_logs_structure_step('course_logs', 'logs.xml')); // New log stores. $this->add_step(new backup_course_logstores_structure_step('course_logstores', 'logstores.xml')); // Last access to course logs. $this->add_step(new backup_course_loglastaccess_structure_step('course_loglastaccess', 'loglastaccess.xml')); } // Generate the course competencies. $this->add_step(new backup_course_competencies_structure_step('course_competencies', 'competencies.xml')); // Annotate activity completion defaults. $this->add_step(new backup_completion_defaults_structure_step('course_completion_defaults', 'completiondefaults.xml')); // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) $this->add_step(new backup_inforef_structure_step('course', 'inforef.xml')); // Migrate the already exported inforef entries to final ones $this->add_step(new move_inforef_annotations_to_final('migrate_inforef')); // Generate the content bank file (conditionally). if ($this->get_setting_value('contentbankcontent')) { $this->add_step(new backup_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml')); } // At the end, mark it as built $this->built = true; } /** * Code the transformations to perform in the course in * order to get transportable (encoded) links * @param string $content content in which to encode links. * @return string content with links encoded. */ public static function encode_content_links($content) { // Link to the course main page (it also covers "&topic=xx" and "&week=xx" // because they don't become transformed (section number) in backup/restore. $content = self::encode_links_helper($content, 'COURSEVIEWBYID', '/course/view.php?id='); $content = self::encode_links_helper($content, 'COURSESECTIONBYID', '/course/section.php?id='); // A few other key course links. $content = self::encode_links_helper($content, 'GRADEINDEXBYID', '/grade/index.php?id='); $content = self::encode_links_helper($content, 'GRADEREPORTINDEXBYID', '/grade/report/index.php?id='); $content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/index.php?type=2&id='); $content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id='); $content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/'); $content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/', true); return $content; } /** * Helper method, used by encode_content_links. * @param string $content content in which to encode links. * @param string $name the name of this type of encoded link. * @param string $path the path that identifies this type of link, up * to the ?paramname= bit. * @param bool $urlencoded whether to use urlencode() before replacing the path. * @return string content with one type of link encoded. */ private static function encode_links_helper(string $content, string $name, string $path, bool $urlencoded = false) { global $CFG; // We want to convert both http and https links. $root = $CFG->wwwroot; $httpsroot = str_replace('http://', 'https://', $root); $httproot = str_replace('https://', 'http://', $root); $httpsbase = $httpsroot . $path; $httpbase = $httproot . $path; if ($urlencoded) { $httpsbase = urlencode($httpsbase); $httpbase = urlencode($httpbase); } $httpsbase = preg_quote($httpsbase, '/'); $httpbase = preg_quote($httpbase, '/'); $return = preg_replace('/(' . $httpsbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $content); $return = preg_replace('/(' . $httpbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $return); return $return; } // Protected API starts here /** * Define the common setting that any backup section will have */ protected function define_settings() { // Nothing to add, sections doesn't have common settings (for now) } } moodle2/backup_tool_plugin.class.php 0000644 00000002366 15215711721 0013613 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Admin tool backup plugin base. * * @package core_backup * @subpackage moodle2 * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Admin tool backup plugin base class. * * @package core_backup * @subpackage moodle2 * @copyright 2015 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class backup_tool_plugin extends backup_plugin { // Use default parent behaviour. } moodle2/restore_qbank_plugin.class.php 0000644 00000002522 15215711721 0014142 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_qbank_plugin class. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Base class for qbank backup plugins. * * @package core_backup * @copyright 2021 Catalyst IT Australia Pty Ltd * @author Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_qbank_plugin extends restore_plugin { // Use default parent behaviour. } moodle2/backup_final_task.class.php 0000644 00000020411 15215711721 0013362 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_final_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Final task that provides all the final steps necessary in order to finish one * backup (mainly gathering references and creating the main xml) apart from * some final cleaning * * TODO: Finish phpdocs */ class backup_final_task extends backup_task { /** * Create all the steps that will be part of this task */ public function build() { global $CFG; // Set the backup::VAR_CONTEXTID setting to course context as far as next steps require that $coursectxid = context_course::instance($this->get_courseid())->id; $this->add_setting(new backup_activity_generic_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $coursectxid)); // Set the backup::VAR_COURSEID setting to course, we'll need that in some steps $courseid = $this->get_courseid(); $this->add_setting(new backup_activity_generic_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $courseid)); // Generate the groups file with the final annotated groups and groupings // including membership based on setting $this->add_step(new backup_groups_structure_step('groups', 'groups.xml')); // Generate the questions file with the final annotated question_categories $this->add_step(new backup_questions_structure_step('questions', 'questions.xml')); // Annotate all the question files for the already annotated question // categories (this is performed here and not in the structure step because // it involves multiple contexts and as far as we are always backup-ing // complete question banks we don't need to restrict at all and can be // done in a single pass $this->add_step(new backup_annotate_all_question_files('question_files')); // Annotate all the user files (conditionally) (profile and icon files) // Because each user has its own context, we need a separate/specialised step here // This step also ensures that the contexts for all the users exist, so next // step can be safely executed (join between users and contexts) // Not executed if backup is without users of anonymized if (($this->get_setting_value('users') || !empty($this->get_kept_roles())) && !$this->get_setting_value('anonymize')) { $this->add_step(new backup_annotate_all_user_files('user_files')); } // Generate the users file (conditionally) with the final annotated users // including custom profile fields, preferences, tags, role assignments and // overrides if ($this->get_setting_value('users') || !empty($this->get_kept_roles())) { $this->add_step(new backup_users_structure_step('users', 'users.xml')); } // Generate the top roles file with all the final annotated roles // that have been detected along the whole process. It's just // the list of role definitions (no assignments nor permissions) $this->add_step(new backup_final_roles_structure_step('roleslist', 'roles.xml')); // Generate the gradebook file with categories and course grade items. Do it conditionally, using // execute_condition() so only will be excuted if ALL module grade_items in course have been exported $this->add_step(new backup_gradebook_structure_step('course_gradebook','gradebook.xml')); // Generate the grade history file, conditionally. $this->add_step(new backup_grade_history_structure_step('course_grade_history','grade_history.xml')); // Generate the course completion $this->add_step(new backup_course_completion_structure_step('course_completion', 'completion.xml')); // Conditionally generate the badges file. if ($this->get_setting_value('badges')) { $this->add_step(new backup_badges_structure_step('course_badges', 'badges.xml')); } // Generate the scales file with all the (final) annotated scales $this->add_step(new backup_final_scales_structure_step('scaleslist', 'scales.xml')); // Generate the outcomes file with all the (final) annotated outcomes $this->add_step(new backup_final_outcomes_structure_step('outcomeslist', 'outcomes.xml')); // Migrate the pending annotations to final (prev steps may have added some files) // This must be executed before backup files $this->add_step(new move_inforef_annotations_to_final('migrate_inforef')); // Generate the files.xml file with all the (final) annotated files. At the same // time copy all the files from moodle storage to backup storage (uses custom // backup_nested_element for that) $this->add_step(new backup_final_files_structure_step('fileslist', 'files.xml')); // Write the main moodle_backup.xml file, with all the information related // to the backup, settings, license, versions and other useful information $this->add_step(new backup_main_structure_step('mainfile', 'moodle_backup.xml')); require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); // Look for converter steps only in type course and mode general backup operations. $conversion = false; if ($this->plan->get_type() == backup::TYPE_1COURSE and $this->plan->get_mode() == backup::MODE_GENERAL) { $converters = convert_helper::available_converters(false); foreach ($converters as $value) { if ($this->get_setting_value($value)) { // Zip class. $zip_contents = "{$value}_zip_contents"; $store_backup_file = "{$value}_store_backup_file"; $convert = "{$value}_backup_convert"; $this->add_step(new $convert("package_convert_{$value}")); $this->add_step(new $zip_contents("zip_contents_{$value}")); $this->add_step(new $store_backup_file("save_backupfile_{$value}")); if (!$conversion) { $conversion = true; } } } } // On backup::MODE_IMPORT, we don't have to zip nor store the the file, skip these steps if (($this->plan->get_mode() != backup::MODE_IMPORT) && !$conversion) { // Generate the zip file (mbz extension) $this->add_step(new backup_zip_contents('zip_contents')); // Copy the generated zip (.mbz) file to final destination $this->add_step(new backup_store_backup_file('save_backupfile')); } // Clean the temp dir (conditionally) and drop temp tables $cleanstep = new drop_and_clean_temp_stuff('drop_and_clean_temp_stuff'); // Decide about to delete the temp dir (based on backup::MODE_IMPORT) $cleanstep->skip_cleaning_temp_dir($this->plan->get_mode() == backup::MODE_IMPORT); $this->add_step($cleanstep); $this->built = true; } public function get_weight() { // The final task takes ages, so give it 20 times the weight of a normal task. return 20; } // Protected API starts here /** * Define the common setting that any backup type will have */ protected function define_settings() { // This task has not settings (could have them, like destination or so in the future, let's see) } } moodle2/backup_block_task.class.php 0000644 00000017645 15215711721 0013402 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * abstract block task that provides all the properties and common steps to be performed * when one block is being backup * * TODO: Finish phpdocs */ abstract class backup_block_task extends backup_task { protected $blockid; protected $blockname; protected $contextid; protected $moduleid; protected $modulename; protected $parentcontextid; /** * Constructor - instantiates one object of this class */ public function __construct($name, $blockid, $moduleid = null, $plan = null) { global $DB; // Check blockid exists if (!$block = $DB->get_record('block_instances', array('id' => $blockid))) { throw new backup_task_exception('block_task_block_instance_not_found', $blockid); } $this->blockid = $blockid; $this->blockname = clean_param($block->blockname, PARAM_PLUGIN); $this->contextid = context_block::instance($this->blockid)->id; $this->moduleid = $moduleid; $this->modulename = null; $this->parentcontextid = null; // If moduleid passed, check exists, supports moodle2 format and save info // Check moduleid exists if (!empty($moduleid)) { if (!$coursemodule = get_coursemodule_from_id(false, $moduleid)) { throw new backup_task_exception('block_task_coursemodule_not_found', $moduleid); } // Check activity supports this moodle2 backup format if (!plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) { throw new backup_task_exception('block_task_activity_lacks_moodle2_backup_support', $coursemodule->modname); } $this->moduleid = $moduleid; $this->modulename = $coursemodule->modname; $this->parentcontextid = context_module::instance($this->moduleid)->id; } parent::__construct($name, $plan); } public function get_blockid() { return $this->blockid; } public function get_blockname() { return $this->blockname; } public function get_moduleid() { return $this->moduleid; } public function get_modulename() { return $this->modulename; } public function get_contextid() { return $this->contextid; } public function get_parentcontextid() { return $this->parentcontextid; } /** * Block tasks have their own directory to write files */ public function get_taskbasepath() { $basepath = $this->get_basepath(); // Module blocks are under module dir if (!empty($this->moduleid)) { $basepath .= '/activities/' . $this->modulename . '_' . $this->moduleid . '/blocks/' . $this->blockname . '_' . $this->blockid; // Course blocks are under course dir } else { $basepath .= '/course/blocks/' . $this->blockname . '_' . $this->blockid; } return $basepath; } /** * Create all the steps that will be part of this task */ public function build() { // If we have decided not to backup blocks, prevent anything to be built if (!$this->get_setting_value('blocks')) { $this->built = true; return; } // If "child" of activity task and it has been excluded, nothing to do if (!empty($this->moduleid)) { $includedsetting = $this->modulename . '_' . $this->moduleid . '_included'; if (!$this->get_setting_value($includedsetting)) { $this->built = true; return; } } // Add some extra settings that related processors are going to need $this->add_setting(new backup_activity_generic_setting(backup::VAR_BLOCKID, base_setting::IS_INTEGER, $this->blockid)); $this->add_setting(new backup_activity_generic_setting(backup::VAR_BLOCKNAME, base_setting::IS_FILENAME, $this->blockname)); $this->add_setting(new backup_activity_generic_setting(backup::VAR_MODID, base_setting::IS_INTEGER, $this->moduleid)); $this->add_setting(new backup_activity_generic_setting(backup::VAR_MODNAME, base_setting::IS_FILENAME, $this->modulename)); $this->add_setting(new backup_activity_generic_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid())); $this->add_setting(new backup_activity_generic_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $this->contextid)); // Create the block directory $this->add_step(new create_taskbasepath_directory('create_block_directory')); // Create the block.xml common file (instance + positions) $this->add_step(new backup_block_instance_structure_step('block_commons', 'block.xml')); // Here we add all the common steps for any block and, in the point of interest // we call to define_my_steps() in order to get the particular ones inserted in place. $this->define_my_steps(); // Generate the roles file (optionally role assignments and always role overrides) $this->add_step(new backup_roles_structure_step('block_roles', 'roles.xml')); // Generate the comments file (conditionally) if ($this->get_setting_value('comments')) { $this->add_step(new backup_comments_structure_step('block_comments', 'comments.xml')); } // Generate the inforef file (must be after ALL steps gathering annotations of ANY type) $this->add_step(new backup_inforef_structure_step('block_inforef', 'inforef.xml')); // Migrate the already exported inforef entries to final ones $this->add_step(new move_inforef_annotations_to_final('migrate_inforef')); // At the end, mark it as built $this->built = true; } // Protected API starts here /** * Define the common setting that any backup block will have */ protected function define_settings() { // Nothing to add, blocks doesn't have common settings (for now) // End of common activity settings, let's add the particular ones $this->define_my_settings(); } /** * Define (add) particular settings that each block can have */ abstract protected function define_my_settings(); /** * Define (add) particular steps that each block can have */ abstract protected function define_my_steps(); /** * Define one array() of fileareas that each block controls */ abstract public function get_fileareas(); /** * Define one array() of configdata attributes * that need to be processed by the contenttransformer */ abstract public function get_configdata_encoded_attributes(); /** * Code the transformations to perform in the block in * order to get transportable (encoded) links */ public static function encode_content_links($content) { throw new coding_exception('encode_content_links() method needs to be overridden in each subclass of backup_block_task'); } } moodle2/backup_root_task.class.php 0000644 00000024151 15215711721 0013261 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_root_task class * * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Start task that provides all the settings common to all backups and some initialization steps * * TODO: Finish phpdocs */ class backup_root_task extends backup_task { /** * Create all the steps that will be part of this task */ public function build() { // Add all the steps needed to prepare any moodle2 backup to work $this->add_step(new create_and_clean_temp_stuff('create_and_clean_temp_stuff')); $this->built = true; } // Protected API starts here protected function converter_deps($main_setting, $converters) { foreach ($this->settings as $setting) { $name = $setting->get_name(); if (in_array($name, $converters)) { $setvalue = convert_helper::export_converter_dependencies($name, $main_setting->get_name()); if ($setvalue !== false) { $setting->add_dependency($main_setting, $setvalue, array('value' => $name)); } } } } /** * Define the common setting that any backup type will have */ protected function define_settings() { global $CFG; require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); // Define filename setting $filename = new backup_filename_setting('filename', base_setting::IS_FILENAME, 'backup.mbz'); $filename->set_ui_filename(get_string('filename', 'backup'), 'backup.mbz', array('size'=>50)); $this->add_setting($filename); // Present converter settings only in type course and mode general backup operations. $converters = array(); if ($this->plan->get_type() == backup::TYPE_1COURSE and $this->plan->get_mode() == backup::MODE_GENERAL) { $converters = convert_helper::available_converters(false); foreach ($converters as $cnv) { $formatcnv = new backup_users_setting($cnv, base_setting::IS_BOOLEAN, false); $formatcnv->set_ui(new backup_setting_ui_checkbox($formatcnv, get_string('backupformat'.$cnv, 'backup'))); $this->add_setting($formatcnv); } } // Define users setting (keeping it on hand to define dependencies) $users = new backup_users_setting('users', base_setting::IS_BOOLEAN, true); $users->set_ui(new backup_setting_ui_checkbox($users, get_string('rootsettingusers', 'backup'))); $this->add_setting($users); $this->converter_deps($users, $converters); // Define anonymize (dependent of users) $anonymize = new backup_anonymize_setting('anonymize', base_setting::IS_BOOLEAN, false); $anonymize->set_ui(new backup_setting_ui_checkbox($anonymize, get_string('rootsettinganonymize', 'backup'))); $this->add_setting($anonymize); $users->add_dependency($anonymize); // Define role_assignments (dependent of users) $roleassignments = new backup_role_assignments_setting('role_assignments', base_setting::IS_BOOLEAN, true); $roleassignments->set_ui(new backup_setting_ui_checkbox($roleassignments, get_string('rootsettingroleassignments', 'backup'))); $this->add_setting($roleassignments); $users->add_dependency($roleassignments); // Define permission. if ($this->plan->get_mode() == backup::MODE_IMPORT) { $permissions = new backup_permissions_setting('permissions', base_setting::IS_BOOLEAN, false); $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup'))); $this->add_setting($permissions); } // Define activities $activities = new backup_activities_setting('activities', base_setting::IS_BOOLEAN, true); $activities->set_ui(new backup_setting_ui_checkbox($activities, get_string('rootsettingactivities', 'backup'))); $this->add_setting($activities); // Define blocks $blocks = new backup_generic_setting('blocks', base_setting::IS_BOOLEAN, true); $blocks->set_ui(new backup_setting_ui_checkbox($blocks, get_string('rootsettingblocks', 'backup'))); $this->add_setting($blocks); $this->converter_deps($blocks, $converters); // Define files. $files = new backup_generic_setting('files', base_setting::IS_BOOLEAN, true); $files->set_ui(new backup_setting_ui_checkbox($files, get_string('rootsettingfiles', 'backup'))); $this->add_setting($files); $this->converter_deps($files, $converters); // Define filters $filters = new backup_generic_setting('filters', base_setting::IS_BOOLEAN, true); $filters->set_ui(new backup_setting_ui_checkbox($filters, get_string('rootsettingfilters', 'backup'))); $this->add_setting($filters); $this->converter_deps($filters, $converters); // Define comments (dependent of users) $comments = new backup_comments_setting('comments', base_setting::IS_BOOLEAN, true); $comments->set_ui(new backup_setting_ui_checkbox($comments, get_string('rootsettingcomments', 'backup'))); $this->add_setting($comments); $users->add_dependency($comments); // Define badges (dependent of activities). $badges = new backup_badges_setting('badges', base_setting::IS_BOOLEAN, true); $badges->set_ui(new backup_setting_ui_checkbox($badges, get_string('rootsettingbadges', 'backup'))); $this->add_setting($badges); // Define calendar events. $events = new backup_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, true); $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup'))); $this->add_setting($events); // Define completion (dependent of users) $completion = new backup_userscompletion_setting('userscompletion', base_setting::IS_BOOLEAN, true); $completion->set_ui(new backup_setting_ui_checkbox($completion, get_string('rootsettinguserscompletion', 'backup'))); $this->add_setting($completion); $users->add_dependency($completion); // Define logs (dependent of users) $logs = new backup_logs_setting('logs', base_setting::IS_BOOLEAN, true); $logs->set_ui(new backup_setting_ui_checkbox($logs, get_string('rootsettinglogs', 'backup'))); $this->add_setting($logs); $users->add_dependency($logs); // Define grade_histories (dependent of users) $gradehistories = new backup_generic_setting('grade_histories', base_setting::IS_BOOLEAN, true); $gradehistories->set_ui(new backup_setting_ui_checkbox($gradehistories, get_string('rootsettinggradehistories', 'backup'))); $this->add_setting($gradehistories); $users->add_dependency($gradehistories); // The restore does not process the grade histories when some activities are ignored. // So let's define a dependency to prevent false expectations from our users. $activities->add_dependency($gradehistories); // Define question bank inclusion setting. $questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true); $questionbank->set_ui(new backup_setting_ui_checkbox($questionbank, get_string('rootsettingquestionbank', 'backup'))); $this->add_setting($questionbank); $groups = new backup_groups_setting('groups', base_setting::IS_BOOLEAN, true); $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup'))); $this->add_setting($groups); // Define competencies inclusion setting if competencies are enabled. $competencies = new backup_competencies_setting(); $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup'))); $this->add_setting($competencies); // Define custom fields inclusion setting if custom fields are used. $customfields = new backup_customfield_setting('customfield', base_setting::IS_BOOLEAN, true); $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup'))); $this->add_setting($customfields); // Define content bank content inclusion setting. $contentbank = new backup_contentbankcontent_setting('contentbankcontent', base_setting::IS_BOOLEAN, true); $contentbank->set_ui(new backup_setting_ui_checkbox($contentbank, get_string('rootsettingcontentbankcontent', 'backup'))); $this->add_setting($contentbank); // Define xAPI state inclusion setting. $xapistate = new backup_xapistate_setting('xapistate', base_setting::IS_BOOLEAN, true); $xapistate->set_ui(new backup_setting_ui_checkbox($xapistate, get_string('rootsettingxapistate', 'backup'))); $this->add_setting($xapistate); $users->add_dependency($xapistate); // Define legacy file inclusion setting. $legacyfiles = new backup_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, true); $legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup'))); $this->add_setting($legacyfiles); } } moodle2/restore_root_task.class.php 0000644 00000042701 15215711721 0013500 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_root_task class * @package core_backup * @subpackage moodle2 * @category backup * @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(); /** * Start task that provides all the settings common to all restores and other initial steps * * TODO: Finish phpdocs */ class restore_root_task extends restore_task { /** * Create all the steps that will be part of this task */ public function build() { // Conditionally create the temp table (can exist from prechecks) and delete old stuff $this->add_step(new restore_create_and_clean_temp_stuff('create_and_clean_temp_stuff')); // Now make sure the user that is running the restore can actually access the course // before executing any other step (potentially performing permission checks) $this->add_step(new restore_fix_restorer_access_step('fix_restorer_access')); // If we haven't preloaded information, load all the included inforef records to temp_ids table $this->add_step(new restore_load_included_inforef_records('load_inforef_records')); // Load all the needed files to temp_ids table $this->add_step(new restore_load_included_files('load_file_records', 'files.xml')); // If we haven't preloaded information, load all the needed roles to temp_ids_table $this->add_step(new restore_load_and_map_roles('load_and_map_roles')); // If we haven't preloaded information and are restoring user info, load all the needed users to temp_ids table $this->add_step(new restore_load_included_users('load_user_records')); // If we haven't preloaded information and are restoring user info, process all those needed users // marking for create/map them as needed. Any problem here will cause exception as far as prechecks have // performed the same process so, it's not possible to have errors here $this->add_step(new restore_process_included_users('process_user_records')); // Unconditionally, create all the needed users calculated in the previous step $this->add_step(new restore_create_included_users('create_users')); // Unconditionally, load create all the needed groups and groupings $this->add_step(new restore_groups_structure_step('create_groups_and_groupings', 'groups.xml')); // Unconditionally, load create all the needed scales $this->add_step(new restore_scales_structure_step('create_scales', 'scales.xml')); // Unconditionally, load create all the needed outcomes $this->add_step(new restore_outcomes_structure_step('create_scales', 'outcomes.xml')); // If we haven't preloaded information, load all the needed categories and questions (reduced) to temp_ids_table $this->add_step(new restore_load_categories_and_questions('load_categories_and_questions')); // If we haven't preloaded information, process all the loaded categories and questions // marking them for creation/mapping as needed. Any problem here will cause exception // because this same process has been executed and reported by restore prechecks, so // it is not possible to have errors here. $this->add_step(new restore_process_categories_and_questions('process_categories_and_questions')); // Unconditionally, create and map all the categories and questions $this->add_step(new restore_create_categories_and_questions('create_categories_and_questions', 'questions.xml')); // At the end, mark it as built $this->built = true; } // Protected API starts here /** * Define the common setting that any restore type will have */ protected function define_settings() { // Load all the root settings found in backup file from controller $rootsettings = $this->get_info()->root_settings; // Define users setting (keeping it on hand to define dependencies) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['users']) && $rootsettings['users']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $users = new restore_users_setting('users', base_setting::IS_BOOLEAN, $defaultvalue); $users->set_ui(new backup_setting_ui_checkbox($users, get_string('rootsettingusers', 'backup'))); $users->get_ui()->set_changeable($changeable); $this->add_setting($users); // Restore enrolment methods. if ($changeable) { $options = [ backup::ENROL_NEVER => get_string('rootsettingenrolments_never', 'backup'), backup::ENROL_WITHUSERS => get_string('rootsettingenrolments_withusers', 'backup'), backup::ENROL_ALWAYS => get_string('rootsettingenrolments_always', 'backup'), ]; $enroldefault = backup::ENROL_WITHUSERS; } else { // Users can not be restored, simplify the dropdown. $options = [ backup::ENROL_NEVER => get_string('no'), backup::ENROL_ALWAYS => get_string('yes') ]; $enroldefault = backup::ENROL_NEVER; } $enrolments = new restore_users_setting('enrolments', base_setting::IS_INTEGER, $enroldefault); $enrolments->set_ui(new backup_setting_ui_select($enrolments, get_string('rootsettingenrolments', 'backup'), $options)); $this->add_setting($enrolments); // Define role_assignments (dependent of users) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['role_assignments']) && $rootsettings['role_assignments']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $roleassignments = new restore_role_assignments_setting('role_assignments', base_setting::IS_BOOLEAN, $defaultvalue); $roleassignments->set_ui(new backup_setting_ui_checkbox($roleassignments,get_string('rootsettingroleassignments', 'backup'))); $roleassignments->get_ui()->set_changeable($changeable); $this->add_setting($roleassignments); $users->add_dependency($roleassignments); // Define permissions. $defaultvalue = false; // Safer default. $changeable = false; // Enable when available, or key doesn't exist (backward compatibility). if (!array_key_exists('permissions', $rootsettings) || !empty($rootsettings['permissions'])) { $defaultvalue = true; $changeable = true; } $permissions = new restore_permissions_setting('permissions', base_setting::IS_BOOLEAN, $defaultvalue); $permissions->set_ui(new backup_setting_ui_checkbox($permissions, get_string('rootsettingpermissions', 'backup'))); $permissions->get_ui()->set_changeable($changeable); $this->add_setting($permissions); // Define activitites $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['activities']) && $rootsettings['activities']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $activities = new restore_activities_setting('activities', base_setting::IS_BOOLEAN, $defaultvalue); $activities->set_ui(new backup_setting_ui_checkbox($activities, get_string('rootsettingactivities', 'backup'))); $activities->get_ui()->set_changeable($changeable); $this->add_setting($activities); // Define blocks $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['blocks']) && $rootsettings['blocks']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $blocks = new restore_generic_setting('blocks', base_setting::IS_BOOLEAN, $defaultvalue); $blocks->set_ui(new backup_setting_ui_checkbox($blocks, get_string('rootsettingblocks', 'backup'))); $blocks->get_ui()->set_changeable($changeable); $this->add_setting($blocks); // Define filters $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['filters']) && $rootsettings['filters']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $filters = new restore_generic_setting('filters', base_setting::IS_BOOLEAN, $defaultvalue); $filters->set_ui(new backup_setting_ui_checkbox($filters, get_string('rootsettingfilters', 'backup'))); $filters->get_ui()->set_changeable($changeable); $this->add_setting($filters); // Define comments (dependent of users) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['comments']) && $rootsettings['comments']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $comments = new restore_comments_setting('comments', base_setting::IS_BOOLEAN, $defaultvalue); $comments->set_ui(new backup_setting_ui_checkbox($comments, get_string('rootsettingcomments', 'backup'))); $comments->get_ui()->set_changeable($changeable); $this->add_setting($comments); $users->add_dependency($comments); // Define badges (dependent of activities). $defaultvalue = false; // Safer default. $changeable = false; if (isset($rootsettings['badges']) && $rootsettings['badges']) { // Only enabled when available. $defaultvalue = true; $changeable = true; } $badges = new restore_badges_setting('badges', base_setting::IS_BOOLEAN, $defaultvalue); $badges->set_ui(new backup_setting_ui_checkbox($badges, get_string('rootsettingbadges', 'backup'))); $badges->get_ui()->set_changeable($changeable); $this->add_setting($badges); $activities->add_dependency($badges); $users->add_dependency($badges); // Define Calendar events. $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['calendarevents']) && $rootsettings['calendarevents']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $events = new restore_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, $defaultvalue); $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup'))); $events->get_ui()->set_changeable($changeable); $this->add_setting($events); // Define completion (dependent of users) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['userscompletion']) && $rootsettings['userscompletion']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $completion = new restore_userscompletion_setting('userscompletion', base_setting::IS_BOOLEAN, $defaultvalue); $completion->set_ui(new backup_setting_ui_checkbox($completion, get_string('rootsettinguserscompletion', 'backup'))); $completion->get_ui()->set_changeable($changeable); $this->add_setting($completion); $users->add_dependency($completion); // Define logs (dependent of users) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['logs']) && $rootsettings['logs']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $logs = new restore_logs_setting('logs', base_setting::IS_BOOLEAN, $defaultvalue); $logs->set_ui(new backup_setting_ui_checkbox($logs, get_string('rootsettinglogs', 'backup'))); $logs->get_ui()->set_changeable($changeable); $this->add_setting($logs); $users->add_dependency($logs); // Define grade_histories (dependent of users) $defaultvalue = false; // Safer default $changeable = false; if (isset($rootsettings['grade_histories']) && $rootsettings['grade_histories']) { // Only enabled when available $defaultvalue = true; $changeable = true; } $gradehistories = new restore_grade_histories_setting('grade_histories', base_setting::IS_BOOLEAN, $defaultvalue); $gradehistories->set_ui(new backup_setting_ui_checkbox($gradehistories, get_string('rootsettinggradehistories', 'backup'))); $gradehistories->get_ui()->set_changeable($changeable); $this->add_setting($gradehistories); $users->add_dependency($gradehistories); // The restore does not process the grade histories when some activities are ignored. // So let's define a dependency to prevent false expectations from our users. $activities->add_dependency($gradehistories); // Define groups and groupings. $defaultvalue = false; $changeable = false; if (isset($rootsettings['groups']) && $rootsettings['groups']) { // Only enabled when available. $defaultvalue = true; $changeable = true; } else if (!isset($rootsettings['groups'])) { // It is likely this is an older backup that does not contain information on the group setting, // in which case groups should be restored and this setting can be changed. $defaultvalue = true; $changeable = true; } $groups = new restore_groups_setting('groups', base_setting::IS_BOOLEAN, $defaultvalue); $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup'))); $groups->get_ui()->set_changeable($changeable); $this->add_setting($groups); // Competencies restore setting. Show when competencies is enabled and the setting is available. $hascompetencies = !empty($rootsettings['competencies']); $competencies = new restore_competencies_setting($hascompetencies); $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup'))); $this->add_setting($competencies); // Custom fields. $defaultvalue = false; $changeable = false; if (isset($rootsettings['customfield']) && $rootsettings['customfield']) { // Only enabled when available. $defaultvalue = true; $changeable = true; } $customfields = new restore_customfield_setting('customfield', base_setting::IS_BOOLEAN, $defaultvalue); $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup'))); $customfields->get_ui()->set_changeable($changeable); $this->add_setting($customfields); // Define Content bank content. $defaultvalue = false; $changeable = false; if (isset($rootsettings['contentbankcontent']) && $rootsettings['contentbankcontent']) { // Only enabled when available. $defaultvalue = true; $changeable = true; } $contents = new restore_contentbankcontent_setting('contentbankcontent', base_setting::IS_BOOLEAN, $defaultvalue); $contents->set_ui(new backup_setting_ui_checkbox($contents, get_string('rootsettingcontentbankcontent', 'backup'))); $contents->get_ui()->set_changeable($changeable); $this->add_setting($contents); // Define xAPI states. $defaultvalue = false; $changeable = false; if (isset($rootsettings['xapistate']) && $rootsettings['xapistate']) { // Only enabled when available. $defaultvalue = true; $changeable = true; } $xapistate = new restore_xapistate_setting('xapistate', base_setting::IS_BOOLEAN, $defaultvalue); $xapistate->set_ui(new backup_setting_ui_checkbox($xapistate, get_string('rootsettingxapistate', 'backup'))); $xapistate->get_ui()->set_changeable($changeable); $this->add_setting($xapistate); // Include legacy files. $defaultvalue = true; $changeable = true; $legacyfiles = new restore_generic_setting('legacyfiles', base_setting::IS_BOOLEAN, $defaultvalue); $legacyfiles->set_ui(new backup_setting_ui_checkbox($legacyfiles, get_string('rootsettinglegacyfiles', 'backup'))); $legacyfiles->get_ui()->set_changeable($changeable); $this->add_setting($legacyfiles); } } moodle2/restore_local_plugin.class.php 0000644 00000002242 15215711721 0014137 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_local_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 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(); /** * Class extending standard restore_plugin in order to implement some * helper methods related with local plugins */ abstract class restore_local_plugin extends restore_plugin {} moodle2/restore_enrol_plugin.class.php 0000644 00000002412 15215711721 0014163 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines restore_enrol_plugin class. * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2014 University of Wisconsin * @author Matt petro * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Base class for enrol backup plugins. * * @package core_backup * @copyright 2014 University of Wisconsin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class restore_enrol_plugin extends restore_plugin { // Use default parent behaviour. } moodle2/backup_theme_plugin.class.php 0000644 00000006002 15215711721 0013727 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Defines backup_theme_plugin class * * @package core_backup * @subpackage moodle2 * @category backup * @copyright 2011 onwards The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Base class for theme backup plugins. * * NOTE: When you back up a course, it runs backup for ALL themes - not just * the currently selected one. * * That means that if, for example, a course was once in theme A, and theme A * had some data settings, but it is then changed to theme B, the data settings * will still be included in the backup and restore. With the restored course, * if you ever change it back to theme A, the settings will be ready. * * It also means that other themes which are not the one set up for the course, * but might be seen by some users (eg user themes, session themes, mnet themes) * can store data. * * If this behaviour is not desired for a particular theme's data, the subclass * can call is_current_theme('myname') to check. */ abstract class backup_theme_plugin extends backup_plugin { /** * @var string Current theme for course (may not be the same as plugin). */ protected $coursetheme; /** * @param string $plugintype Plugin type (always 'theme') * @param string $pluginname Plugin name (name of theme) * @param backup_optigroup $optigroup Group that will contain this data * @param backup_course_structure_step $step Backup step that this is part of */ public function __construct($plugintype, $pluginname, $optigroup, $step) { parent::__construct($plugintype, $pluginname, $optigroup, $step); $this->coursetheme = backup_plan_dbops::get_theme_from_courseid( $this->task->get_courseid()); } /** * Return condition for whether this theme should be backed up (= if it * is the same theme as the one used in this course). This condition has * the theme used in the course. It will be compared against the name * of the theme, by use of third parameter in get_plugin_element; in * subclass, you should do: * $plugin = $this->get_plugin_element(null, $this->get_theme_condition(), 'mytheme'); */ protected function get_theme_condition() { return array('sqlparam' => $this->coursetheme); } } tests/backup_restore_base_testcase.php 0000644 00000010031 15215711721 0014311 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Backup restore base tests. * * @package core_backup * @copyright Tomo Tsuyuki <tomotsuyuki@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/includes/restore_includes.php'); /** * Basic testcase class for backup / restore functionality. */ abstract class core_backup_backup_restore_base_testcase extends advanced_testcase { /** * Setup test data. */ protected function setUp(): void { $this->resetAfterTest(); $this->setAdminUser(); } /** * Backup the course by general mode. * * @param stdClass $course Course for backup. * @return string Hash string ID from the backup. * @throws coding_exception * @throws moodle_exception */ protected function perform_backup($course): string { global $CFG, $USER; $coursecontext = context_course::instance($course->id); // Start backup process. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id); $bc->execute_plan(); $backupid = $bc->get_backupid(); $bc->destroy(); // Get the backup file. $fs = get_file_storage(); $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC'); $backupfile = reset($files); // Extract backup file. $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupid; $fp = get_file_packer('application/vnd.moodle.backup'); $fp->extract_to_pathname($backupfile, $path); return $backupid; } /** * Restore from backupid to course. * * @param string $backupid Hash string ID from backup. * @param stdClass $course Course which is restored for. * @throws restore_controller_exception */ protected function perform_restore($backupid, $course): void { global $USER; // Set up restore. $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_EXISTING_ADDING); // Execute restore. $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); } /** * Import course from course1 to course2. * * @param stdClass $course1 Course to be backuped up. * @param stdClass $course2 Course to be restored. * @throws restore_controller_exception */ protected function perform_import($course1, $course2): void { global $USER; // Start backup process. $bc = new backup_controller(backup::TYPE_1COURSE, $course1->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Set up restore. $rc = new restore_controller($backupid, $course2->id, backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_EXISTING_ADDING); // Execute restore. $rc->execute_precheck(); $rc->execute_plan(); $rc->destroy(); } } tests/quiz_restore_decode_links_test.php 0000644 00000011616 15215711721 0014723 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. global $CFG; require_once($CFG->dirroot . '/course/lib.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->dirroot . '/question/engine/tests/helpers.php'); /** * Decode links quiz restore tests. * * @package core_backup * @copyright 2020 Ilya Tregubov <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class quiz_restore_decode_links_test extends \advanced_testcase { /** * Test restore_decode_rule class */ public function test_restore_quiz_decode_links(): void { global $DB, $CFG, $USER; $this->resetAfterTest(true); $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course( array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $quiz = $generator->create_module('quiz', array( 'course' => $course->id)); // Create questions. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $context = \context_course::instance($course->id); $cat = $questiongenerator->create_question_category(array('contextid' => $context->id)); $question = $questiongenerator->create_question('multichoice', null, array('category' => $cat->id)); // Add to the quiz. quiz_add_quiz_question($question->id, $quiz); \mod_quiz\external\submit_question_version::execute( $DB->get_field('quiz_slots', 'id', ['quizid' => $quiz->id, 'slot' => 1]), 1); $questiondata = \question_bank::load_question_data($question->id); $DB->set_field('question', 'questiontext', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, ['id' => $question->id]); $firstanswer = array_shift($questiondata->options->answers); $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/course/view.php?id=' . $course->id, ['id' => $firstanswer->id]); $secondanswer = array_shift($questiondata->options->answers); $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, ['id' => $secondanswer->id]); $thirdanswer = array_shift($questiondata->options->answers); $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, ['id' => $thirdanswer->id]); $fourthanswer = array_shift($questiondata->options->answers); $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, ['id' => $fourthanswer->id]); $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid)); $quizquestions = \mod_quiz\question\bank\qbank_helper::get_question_structure( $newcm->instance, \context_module::instance($newcm->id)); $questionids = []; foreach ($quizquestions as $quizquestion) { if ($quizquestion->questionid) { $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $quizquestion->questiontext); $questionids[] = $quizquestion->questionid; } } list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid'); $condition = 'WHERE qa.question ' . $condition; $sql = "SELECT qa.id, qa.answer FROM {question_answers} qa $condition"; $answers = $DB->get_records_sql($sql, $param); $this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[$firstanswer->id]->answer); $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[$secondanswer->id]->answer); $this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[$thirdanswer->id]->answer); $this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[$fourthanswer->id]->answer); } } tests/privacy/provider_test.php 0000644 00000047533 15215711721 0013003 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Privacy provider tests. * * @package core_backup * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_backup\privacy; use core_backup\privacy\provider; use core_privacy\local\request\approved_userlist; defined('MOODLE_INTERNAL') || die(); /** * Privacy provider tests class. * * @copyright 2018 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata); $contextlist = provider::get_contexts_for_userid($user->id); $this->assertCount(1, $contextlist); $contextforuser = $contextlist->current(); $context = \context_course::instance($course->id); $this->assertEquals($context->id, $contextforuser->id); } /** * Test for provider::export_user_data(). */ public function test_export_for_context(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata1 = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata1); // Create another user who will perform a backup operation. $user2 = $this->getDataGenerator()->create_user(); $bcdata2 = (object) [ 'backupid' => 2, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user2->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata2); // Create another backup_controllers record. $bcdata3 = (object) [ 'backupid' => 3, 'operation' => 'backup', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time() + DAYSECS, 'timemodified' => time() + DAYSECS, 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata3); $coursecontext = \context_course::instance($course->id); // Export all of the data for the context. $this->export_context_data_for_user($user1->id, $coursecontext, 'core_backup'); $writer = \core_privacy\local\request\writer::with_context($coursecontext); $this->assertTrue($writer->has_any_data()); $data = (array) $writer->get_data([get_string('backup'), $course->id]); $this->assertCount(2, $data); $bc1 = array_shift($data); $this->assertEquals('restore', $bc1['operation']); $bc2 = array_shift($data); $this->assertEquals('backup', $bc2['operation']); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata1 = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata1); // Create another user who will perform a backup operation. $user2 = $this->getDataGenerator()->create_user(); $bcdata2 = (object) [ 'backupid' => 2, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user2->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata2); // Before deletion, we should have 2 operations. $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(2, $count); // Delete data based on context. $coursecontext = \context_course::instance($course->id); provider::delete_data_for_all_users_in_context($coursecontext); // After deletion, the operations for that course should have been deleted. $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(0, $count); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); $user1 = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata1 = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata1); // Create another user who will perform a backup operation. $user2 = $this->getDataGenerator()->create_user(); $bcdata2 = (object) [ 'backupid' => 2, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user2->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata2); // Before deletion, we should have 2 operations. $count = $DB->count_records('backup_controllers', ['itemid' => $course->id]); $this->assertEquals(2, $count); $coursecontext = \context_course::instance($course->id); $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_backup', [\context_system::instance()->id, $coursecontext->id]); provider::delete_data_for_user($contextlist); // After deletion, the backup operation for the user should have been deleted. $count = $DB->count_records('backup_controllers', ['itemid' => $course->id, 'userid' => $user1->id]); $this->assertEquals(0, $count); // Confirm we still have the other users record. $bcs = $DB->get_records('backup_controllers'); $this->assertCount(1, $bcs); $lastsubmission = reset($bcs); $this->assertNotEquals($user1->id, $lastsubmission->userid); } /** * Test that only users with a course and module context are fetched. */ public function test_get_users_in_context(): void { global $DB; $this->resetAfterTest(); $component = 'core_backup'; $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('chat', ['course' => $course->id]); $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $coursecontext = \context_course::instance($course->id); $activitycontext = \context_module::instance($activity->cmid); // The list of users for course context should return the user. $userlist = new \core_privacy\local\request\userlist($coursecontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); // Create a course backup. // Just insert directly into the 'backup_controllers' table. $bcdata = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata); // The list of users for the course context should return user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // Create an activity backup. // Just insert directly into the 'backup_controllers' table. $bcdata = (object) [ 'backupid' => 2, 'operation' => 'restore', 'type' => 'activity', 'itemid' => $activity->cmid, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user2->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata); // The list of users for the course context should return user2. $userlist = new \core_privacy\local\request\userlist($activitycontext, $component); provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user2->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { global $DB; $this->resetAfterTest(); $component = 'core_backup'; // Create course1. $course1 = $this->getDataGenerator()->create_course(); $coursecontext = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(); $coursecontext2 = \context_course::instance($course2->id); // Create an activity. $activity = $this->getDataGenerator()->create_module('chat', ['course' => $course1->id]); $activitycontext = \context_module::instance($activity->cmid); // Create user1. $user1 = $this->getDataGenerator()->create_user(); // Create user2. $user2 = $this->getDataGenerator()->create_user(); // Create user2. $user3 = $this->getDataGenerator()->create_user(); // Just insert directly into the 'backup_controllers' table. $bcdata1 = (object) [ 'backupid' => 1, 'operation' => 'restore', 'type' => 'course', 'itemid' => $course1->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user1->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata1); // Just insert directly into the 'backup_controllers' table. $bcdata2 = (object) [ 'backupid' => 2, 'operation' => 'backup', 'type' => 'course', 'itemid' => $course1->id, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user2->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata2); // Just insert directly into the 'backup_controllers' table. $bcdata3 = (object) [ 'backupid' => 3, 'operation' => 'restore', 'type' => 'activity', 'itemid' => $activity->cmid, 'format' => 'moodle2', 'interactive' => 1, 'purpose' => 10, 'userid' => $user3->id, 'status' => 1000, 'execution' => 1, 'executiontime' => 0, 'checksum' => 'checksumyolo', 'timecreated' => time(), 'timemodified' => time(), 'controller' => '' ]; $DB->insert_record('backup_controllers', $bcdata3); // The list of users for coursecontext should return user1 and user2. $userlist1 = new \core_privacy\local\request\userlist($coursecontext, $component); provider::get_users_in_context($userlist1); $this->assertCount(2, $userlist1); $expected = [$user1->id, $user2->id]; $actual = $userlist1->get_userids(); $this->assertEqualsCanonicalizing($expected, $actual); // The list of users for coursecontext2 should not return users. $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(0, $userlist2); // The list of users for activitycontext should return user3. $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); $expected = [$user3->id]; $actual = $userlist3->get_userids(); $this->assertEquals($expected, $actual); // Add user1 to the approved user list. $approvedlist = new approved_userlist($coursecontext, $component, [$user1->id]); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in coursecontext - The user list should now return only user2. $userlist1 = new \core_privacy\local\request\userlist($coursecontext, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user2->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // Re-fetch users in activitycontext - The user list should not be empty (user3). $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); provider::get_users_in_context($userlist3); $this->assertCount(1, $userlist3); // Add user1 to the approved user list. $approvedlist = new approved_userlist($activitycontext, $component, [$user3->id]); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in activitycontext - The user list should not return any users. $userlist3 = new \core_privacy\local\request\userlist($activitycontext, $component); provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); // User data should be only removed in the course context and module context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($coursecontext, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } tests/externallib_test.php 0000644 00000015661 15215711721 0012002 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use core_backup_external; use core_external\external_api; use externallib_advanced_testcase; 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->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/backup/externallib.php'); /** * Backup webservice 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 */ final class externallib_test extends externallib_advanced_testcase { /** * Set up tasks for all tests. */ protected function setUp(): void { global $CFG; parent::setUp(); $this->resetAfterTest(true); // 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 getting course copy progress. */ public function test_get_copy_progress(): void { global $USER; $this->setAdminUser(); // Create a course with some availability data set. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $courseid = $course->id; // Mock up the form data for use in tests. $formdata = new \stdClass; $formdata->courseid = $courseid; $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); $copydetails = \copy_helper::create_copy($copydata); $copydetails['operation'] = \backup::OPERATION_BACKUP; $params = array('copies' => $copydetails); $returnvalue = core_backup_external::get_copy_progress($params); // We need to execute the return values cleaning process to simulate the web service server. $returnvalue = external_api::clean_returnvalue(core_backup_external::get_copy_progress_returns(), $returnvalue); $this->assertEquals(\backup::STATUS_AWAITING, $returnvalue[0]['status']); $this->assertEquals(0, $returnvalue[0]['progress']); $this->assertEquals($copydetails['backupid'], $returnvalue[0]['backupid']); $this->assertEquals(\backup::OPERATION_BACKUP, $returnvalue[0]['operation']); // We are expecting trace output during this test. $this->expectOutputRegex("/$courseid/"); // Execute adhoc task and create the copy. $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); // Check the copy progress now. $params = array('copies' => $copydetails); $returnvalue = core_backup_external::get_copy_progress($params); $returnvalue = external_api::clean_returnvalue(core_backup_external::get_copy_progress_returns(), $returnvalue); $this->assertEquals(\backup::STATUS_FINISHED_OK, $returnvalue[0]['status']); $this->assertEquals(1, $returnvalue[0]['progress']); $this->assertEquals($copydetails['restoreid'], $returnvalue[0]['backupid']); $this->assertEquals(\backup::OPERATION_RESTORE, $returnvalue[0]['operation']); } /** * Test ajax submission of course copy process. */ public function test_submit_copy_form(): void { global $DB; $this->setAdminUser(); // Create a course with some availability data set. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $courseid = $course->id; // Moodle form requires this for validation. $sesskey = sesskey(); $_POST['sesskey'] = $sesskey; // Mock up the form data for use in tests. $formdata = new \stdClass; $formdata->courseid = $courseid; $formdata->returnto = ''; $formdata->returnurl = ''; $formdata->sesskey = $sesskey; $formdata->_qf__core_backup_output_copy_form = 1; $formdata->fullname = 'foo'; $formdata->shortname = 'bar'; $formdata->category = 1; $formdata->visible = 1; $formdata->startdate = array('day' => 5, 'month' => 5, 'year' => 2020, 'hour' => 0, 'minute' => 0); $formdata->idnumber = 123; $formdata->userdata = 1; $formdata->role_1 = 1; $formdata->role_3 = 3; $formdata->role_5 = 5; $urlform = http_build_query($formdata, '', '&'); // Take the form data and url encode it. $jsonformdata = json_encode($urlform); // Take form string and JSON encode. $returnvalue = core_backup_external::submit_copy_form($jsonformdata); $returnjson = external_api::clean_returnvalue(core_backup_external::submit_copy_form_returns(), $returnvalue); $copyids = json_decode($returnjson, true); $backuprec = $DB->get_record('backup_controllers', array('backupid' => $copyids['backupid'])); $restorerec = $DB->get_record('backup_controllers', array('backupid' => $copyids['restoreid'])); // Check backup was completed successfully. $this->assertEquals(backup::STATUS_AWAITING, $backuprec->status); $this->assertEquals(0, $backuprec->progress); $this->assertEquals('backup', $backuprec->operation); // Check restore was completed successfully. $this->assertEquals(backup::STATUS_REQUIRE_CONV, $restorerec->status); $this->assertEquals(0, $restorerec->progress); $this->assertEquals('restore', $restorerec->operation); } } tests/async_backup_test.php 0000644 00000026455 15215711721 0012136 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; 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'); require_once($CFG->libdir . '/completionlib.php'); /** * Asyncronhous backup tests. * * @package core_backup * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class async_backup_test extends \advanced_testcase { /** * Tests the asynchronous backup. */ public function test_async_backup(): void { global $CFG, $DB, $USER; $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)); // Create a teacher user to call the backup. $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); // 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)); // Enable logging. $this->preventResetByRollback(); set_config('enabled_stores', 'logstore_standard', 'tool_log'); set_config('buffersize', 0, 'logstore_standard'); get_log_manager(true); // Case 1: Make a course backup without users. $this->setUser($teacher->id); // 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(); $backupid = $bc->get_backupid(); $bc->destroy(); $prebackuprec = $DB->get_record('backup_controllers', array('backupid' => $backupid)); // Check the initial backup controller was created correctly. $this->assertEquals(backup::STATUS_AWAITING, $prebackuprec->status); $this->assertEquals(2, $prebackuprec->execution); // Create the adhoc task. $asynctask = new \core\task\asynchronous_backup_task(); $asynctask->set_custom_data(['backupid' => $backupid]); $asynctask->set_userid($USER->id); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting trace output during this test. $this->expectOutputRegex("/$backupid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]); // Check backup was created successfully. $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); $this->assertEquals(1.0, $postbackuprec->progress); $this->assertEquals($teacher->id, $postbackuprec->userid); // Check that the backupid was logged correctly. $logrec = $DB->get_record('logstore_standard_log', ['userid' => $postbackuprec->userid, 'target' => 'course_backup'], '*', MUST_EXIST); $otherdata = json_decode($logrec->other); $this->assertEquals($backupid, $otherdata->backupid); // Check backup was stored in correct area. $usercontextid = $DB->get_field('context', 'id', ['contextlevel' => CONTEXT_USER, 'instanceid' => $teacher->id]); $this->assertEquals(1, $DB->count_records('files', ['contextid' => $usercontextid, 'component' => 'user', 'filearea' => 'backup', 'filename' => 'backup.mbz'])); // Case 2: Make a second backup with users and not anonymised. $this->setAdminUser(); $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id); $bc->get_plan()->get_setting('users')->set_status(\backup_setting::NOT_LOCKED); $bc->get_plan()->get_setting('users')->set_value(true); $bc->get_plan()->get_setting('anonymize')->set_value(false); $bc->finish_ui(); $backupid = $bc->get_backupid(); $bc->destroy(); // Create the adhoc task. $asynctask = new \core\task\asynchronous_backup_task(); $asynctask->set_custom_data(['backupid' => $backupid]); \core\task\manager::queue_adhoc_task($asynctask); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postbackuprec = $DB->get_record('backup_controllers', ['backupid' => $backupid]); // Check backup was created successfully. $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); $this->assertEquals(1.0, $postbackuprec->progress); $this->assertEquals($USER->id, $postbackuprec->userid); // Check that the backupid was logged correctly. $logrec = $DB->get_record('logstore_standard_log', ['userid' => $postbackuprec->userid, 'target' => 'course_backup'], '*', MUST_EXIST); $otherdata = json_decode($logrec->other); $this->assertEquals($backupid, $otherdata->backupid); // Check backup was stored in correct area. $coursecontextid = $DB->get_field('context', 'id', ['contextlevel' => CONTEXT_COURSE, 'instanceid' => $course->id]); $this->assertEquals(1, $DB->count_records('files', ['contextid' => $coursecontextid, 'component' => 'backup', 'filearea' => 'course', 'filename' => 'backup.mbz'])); } /** * Tests the asynchronous backup will resolve in duplicate cases. */ public function test_complete_async_backup(): void { global $CFG, $DB, $USER; $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(); $backupid = $bc->get_backupid(); $bc->destroy(); // Now hack the record to remove the controller and set the status fields to complete. // This emulates a duplicate run for an already finished controller. $id = $DB->get_field('backup_controllers', 'id', ['backupid' => $backupid]); $data = [ 'id' => $id, 'controller' => '', 'progress' => 1.0, 'status' => backup::STATUS_FINISHED_OK ]; $DB->update_record('backup_controllers', $data); // Now queue an adhoc task and check it handles and completes gracefully. $asynctask = new \core\task\asynchronous_backup_task(); $asynctask->set_custom_data(array('backupid' => $backupid)); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting a specific message output during this test. $this->expectOutputRegex('/invalid controller/'); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); // Check the task record is removed. $this->assertEquals(0, $DB->count_records('task_adhoc')); } } tests/backup_restore_permission_test.php 0000644 00000013210 15215711721 0014735 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use core_backup_backup_restore_base_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once('backup_restore_base_testcase.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Backup restore permission tests. * * @package core_backup * @copyright Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_restore_permission_test extends core_backup_backup_restore_base_testcase { /** @var stdClass A test course which is restored/imported from. */ protected $course1; /** @var stdClass A test course which is restored/imported to. */ protected $course2; /** @var stdClass A user for using in this test. */ protected $user; /** @var string Capability name for using in this test. */ protected $capabilityname; /** @var context_course Context instance for course1. */ protected $course1context; /** @var context_course Context instance for course2. */ protected $course2context; /** * Setup test data. */ protected function setUp(): void { global $DB; parent::setUp(); // Create a course with some availability data set. $generator = $this->getDataGenerator(); $this->course1 = $generator->create_course(); $this->course1context = \context_course::instance($this->course1->id); $this->course2 = $generator->create_course(); $this->course2context = \context_course::instance($this->course2->id); $this->capabilityname = 'enrol/manual:enrol'; $this->user = $generator->create_user(); // Set additional permission for course 1. $teacherrole = $DB->get_record('role', ['shortname' => 'teacher'], '*', MUST_EXIST); role_change_permission($teacherrole->id, $this->course1context, $this->capabilityname, CAP_ALLOW); // Enrol to the courses. $generator->enrol_user($this->user->id, $this->course1->id, $teacherrole->id); $generator->enrol_user($this->user->id, $this->course2->id, $teacherrole->id); } /** * Test having settings. */ public function test_having_settings(): void { $this->assertEquals(0, get_config('backup', 'backup_import_permissions')); $this->assertEquals(1, get_config('restore', 'restore_general_permissions')); } /** * Test for restore with permission. */ public function test_backup_restore_with_permission(): void { // Set default setting to restore with permission. set_config('restore_general_permissions', 1, 'restore'); // Confirm course1 has the capability for the user. $this->assertTrue(has_capability($this->capabilityname, $this->course1context, $this->user)); // Confirm course2 does not have the capability for the user. $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user)); // Perform backup and restore. $backupid = $this->perform_backup($this->course1); $this->perform_restore($backupid, $this->course2); // Confirm course2 has the capability for the user. $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user)); } /** * Test for backup / restore without restore permission. */ public function test_backup_restore_without_permission(): void { // Set default setting to restore without permission. set_config('restore_general_permissions', 0, 'restore'); // Perform backup and restore. $backupid = $this->perform_backup($this->course1); $this->perform_restore($backupid, $this->course2); // Confirm course2 does not have the capability for the user. $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user)); } /** * Test for import with permission. */ public function test_backup_import_with_permission(): void { // Set default setting to restore with permission. set_config('backup_import_permissions', 1, 'backup'); // Perform import. $this->perform_import($this->course1, $this->course2); // Confirm course2 does not have the capability for the user. $this->assertTrue(has_capability($this->capabilityname, $this->course2context, $this->user)); } /** * Test for import without permission. */ public function test_backup_import_without_permission(): void { // Set default setting to restore without permission. set_config('backup_import_permissions', 0, 'backup'); // Perform import. $this->perform_import($this->course1, $this->course2); // Confirm course2 does not have the capability for the user. $this->assertFalse(has_capability($this->capabilityname, $this->course2context, $this->user)); } } tests/async_restore_test.php 0000644 00000025575 15215711721 0012356 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use backup; use backup_controller; use restore_controller; use restore_dbops; 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'); /** * Asyncronhous restore tests. * * @package core_backup * @copyright 2018 Matt Porritt <mattp@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class async_restore_test extends \advanced_testcase { /** * Tests the asynchronous backup. */ public function test_async_restore(): void { global $CFG, $USER, $DB; $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)); // Backup the course. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_GENERAL, $USER->id); $bc->finish_ui(); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Get the backup file. $coursecontext = \context_course::instance($course->id); $fs = get_file_storage(); $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC'); $backupfile = reset($files); // Extract backup file. $backupdir = "restore_" . uniqid(); $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir; $fp = get_file_packer('application/vnd.moodle.backup'); $fp->extract_to_pathname($backupfile, $path); // Create restore controller. $newcourseid = restore_dbops::create_new_course( $course->fullname, $course->shortname . '_2', $course->category); $rc = new restore_controller($backupdir, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_ASYNC, $USER->id, backup::TARGET_NEW_COURSE); $this->assertTrue($rc->execute_precheck()); $restoreid = $rc->get_restoreid(); $prerestorerec = $DB->get_record('backup_controllers', array('backupid' => $restoreid)); $prerestorerec->controller = ''; $rc->destroy(); // Create the adhoc task. $asynctask = new \core\task\asynchronous_restore_task(); $asynctask->set_custom_data(array('backupid' => $restoreid)); $asynctask->set_userid($USER->id); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting trace output during this test. $this->expectOutputRegex("/$restoreid/"); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); $postrestorerec = $DB->get_record('backup_controllers', array('backupid' => $restoreid)); // Check backup was created successfully. $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status); $this->assertEquals(1.0, $postrestorerec->progress); $this->assertEquals($USER->id, $postrestorerec->userid); } /** * Tests the asynchronous restore will resolve in duplicate cases where the controller is already removed. */ public function test_async_restore_missing_controller(): void { global $CFG, $USER, $DB; $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)); // Backup the course. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_GENERAL, $USER->id); $bc->finish_ui(); $bc->execute_plan(); $bc->destroy(); // Get the backup file. $coursecontext = \context_course::instance($course->id); $fs = get_file_storage(); $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC'); $backupfile = reset($files); // Extract backup file. $backupdir = "restore_" . uniqid(); $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir; $fp = get_file_packer('application/vnd.moodle.backup'); $fp->extract_to_pathname($backupfile, $path); // Create restore controller. $newcourseid = restore_dbops::create_new_course( $course->fullname, $course->shortname . '_2', $course->category); $rc = new restore_controller($backupdir, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_ASYNC, $USER->id, backup::TARGET_NEW_COURSE); $restoreid = $rc->get_restoreid(); $controllerid = $DB->get_field('backup_controllers', 'id', ['backupid' => $restoreid]); // Now hack the record to remove the controller and set the status fields to complete. // This emulates a duplicate run for an already finished controller. $data = [ 'id' => $controllerid, 'controller' => '', 'progress' => 1.0, 'status' => backup::STATUS_FINISHED_OK ]; $DB->update_record('backup_controllers', $data); $rc->destroy(); // Create the adhoc task. $asynctask = new \core\task\asynchronous_restore_task(); $asynctask->set_custom_data(['backupid' => $restoreid]); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting a specific message output during this test. $this->expectOutputRegex('/invalid controller/'); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); // Check the task record is removed. $this->assertEquals(0, $DB->count_records('task_adhoc')); // Now delete the record and confirm an entirely missing controller is handled. $DB->delete_records('backup_controllers'); // Create the adhoc task. $asynctask = new \core\task\asynchronous_restore_task(); $asynctask->set_custom_data(['backupid' => $restoreid]); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting a specific message output during this test. $this->expectOutputRegex('/Unable to find restore controller/'); // Execute adhoc task. $now = time(); $task = \core\task\manager::get_next_adhoc_task($now); $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); $task->execute(); \core\task\manager::adhoc_task_complete($task); // Check the task record is removed. $this->assertEquals(0, $DB->count_records('task_adhoc')); } } tests/backup_cleanup_task_test.php 0000644 00000012545 15215711721 0013465 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); /** * Tests for the \core\task\backup_cleanup_task scheduled task. * * @package core_backup * @copyright 2021 Mikhail Golenkov <mikhailgolenkov@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_cleanup_task_test extends \advanced_testcase { /** * Set up tasks for all tests. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Take a backup of the course provided and return backup id. * * @param int $courseid Course id to be backed up. * @return string Backup id. */ private function backup_course(int $courseid): string { // Backup the course. $user = get_admin(); $controller = new \backup_controller( \backup::TYPE_1COURSE, $courseid, \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_AUTOMATED, $user->id ); $controller->execute_plan(); $controller->destroy(); // Unset all structures, close files... return $controller->get_backupid(); } /** * Test the task idle run. Nothing should explode. */ public function test_backup_cleanup_task_idle(): void { $task = new \core\task\backup_cleanup_task(); $task->execute(); } /** * Test the task exits when backup | loglifetime setting is not set. */ public function test_backup_cleanup_task_exits(): void { set_config('loglifetime', 0, 'backup'); $task = new \core\task\backup_cleanup_task(); ob_start(); $task->execute(); $output = ob_get_contents(); ob_end_clean(); $this->assertStringContainsString('config is not set', $output); } /** * Test the task deletes records from DB. */ public function test_backup_cleanup_task_deletes_records(): void { global $DB; // Create a course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Take two backups of the course. $backupid1 = $this->backup_course($course->id); $backupid2 = $this->backup_course($course->id); // Emulate the first backup to be done 31 days ago. $bcrecord = $DB->get_record('backup_controllers', ['backupid' => $backupid1]); $bcrecord->timecreated -= DAYSECS * 31; $DB->update_record('backup_controllers', $bcrecord); // Run the task. $task = new \core\task\backup_cleanup_task(); $task->execute(); // There should be no records related to the first backup. $this->assertEquals(0, $DB->count_records('backup_controllers', ['backupid' => $backupid1])); $this->assertEquals(0, $DB->count_records('backup_logs', ['backupid' => $backupid1])); // Records related to the second backup should remain. $this->assertGreaterThan(0, $DB->count_records('backup_controllers', ['backupid' => $backupid2])); $this->assertGreaterThanOrEqual(0, $DB->count_records('backup_logs', ['backupid' => $backupid2])); } /** * Test the task deletes files from file system. */ public function test_backup_cleanup_task_deletes_files(): void { global $CFG; // Create a course. $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Take two backups of the course and get their logs. $backupid1 = $this->backup_course($course->id); $backupid2 = $this->backup_course($course->id); $filepath1 = $CFG->backuptempdir . '/' . $backupid1 . '.log'; $filepath2 = $CFG->backuptempdir . '/' . $backupid2 . '.log'; // Create a subdirectory. $subdir = $CFG->backuptempdir . '/subdir'; make_writable_directory($subdir); // Both logs and the dir should exist. $this->assertTrue(file_exists($filepath1)); $this->assertTrue(file_exists($filepath2)); $this->assertTrue(file_exists($subdir)); // Change modification time of the first log and the sub dir to be 8 days ago. touch($filepath1, time() - 8 * DAYSECS); touch($subdir, time() - 8 * DAYSECS); // Run the task. $task = new \core\task\backup_cleanup_task(); $task->execute(); // Files and directories older than a week are supposed to be removed. $this->assertFalse(file_exists($filepath1)); $this->assertFalse(file_exists($subdir)); $this->assertTrue(file_exists($filepath2)); } } tests/backup_restore_group_test.php 0000644 00000010427 15215711721 0013710 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_backup; use core_backup_backup_restore_base_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once('backup_restore_base_testcase.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Backup restore permission tests. * * @package core_backup * @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net> * @copyright 2023 Catalyst IT Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class backup_restore_group_test extends core_backup_backup_restore_base_testcase { /** * Test for backup/restore with customfields. * @covers \backup_groups_structure_step * @covers \restore_groups_structure_step */ public function test_backup_restore_group_with_customfields(): void { $course1 = self::getDataGenerator()->create_course(); $course2 = self::getDataGenerator()->create_course(); $groupfieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'group', ]); $groupcustomfield = self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupcustomfield1', 'type' => 'text', 'categoryid' => $groupfieldcategory->get('id'), ]); $groupingfieldcategory = self::getDataGenerator()->create_custom_field_category([ 'component' => 'core_group', 'area' => 'grouping', ]); $groupingcustomfield = self::getDataGenerator()->create_custom_field([ 'shortname' => 'testgroupingcustomfield1', 'type' => 'text', 'categoryid' => $groupingfieldcategory->get('id'), ]); $group1 = self::getDataGenerator()->create_group([ 'courseid' => $course1->id, 'name' => 'Test group 1', 'customfield_testgroupcustomfield1' => 'Custom input for group1', ]); $grouping1 = self::getDataGenerator()->create_grouping([ 'courseid' => $course1->id, 'name' => 'Test grouping 1', 'customfield_testgroupingcustomfield1' => 'Custom input for grouping1', ]); // Perform backup and restore. $backupid = $this->perform_backup($course1); $this->perform_restore($backupid, $course2); // Test group. $groups = groups_get_all_groups($course2->id); $this->assertCount(1, $groups); $group = reset($groups); // Confirm the group is not same group as original one. $this->assertNotEquals($group1->id, $group->id); $this->assertEquals($group1->name, $group->name); // Confirm custom field is restored in the new group. $grouphandler = \core_group\customfield\group_handler::create(); $data = $grouphandler->export_instance_data_object($group->id); $this->assertSame('Custom input for group1', $data->testgroupcustomfield1); // Test grouping. $groupings = groups_get_all_groupings($course2->id); $this->assertCount(1, $groupings); $grouping = reset($groupings); // Confirm this is not same grouping as original one. $this->assertNotEquals($grouping1->id, $grouping->id); // Confirm custom field is restored in the new grouping. $groupinghandler = \core_group\customfield\grouping_handler::create(); $data = $groupinghandler->export_instance_data_object($grouping->id); $this->assertSame('Custom input for grouping1', $data->testgroupingcustomfield1); } } tests/roles_backup_restore_test.php 0000644 00000015625 15215711721 0013705 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. defined('MOODLE_INTERNAL') || die(); // Include all the needed stuff. global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Unit tests for how backup and restore handles role-related things. * * @package core_backup * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class roles_backup_restore_test extends advanced_testcase { /** * Create a course where the (non-editing) Teacher role is overridden * to have 'moodle/user:loginas' and 'moodle/site:accessallgroups'. * * @return stdClass the new course. */ protected function create_course_with_role_overrides(): stdClass { $generator = $this->getDataGenerator(); $course = $generator->create_course(); $teacher = $generator->create_user(); $context = context_course::instance($course->id); $generator->enrol_user($teacher->id, $course->id, 'teacher'); $editingteacherrole = $this->get_role('teacher'); role_change_permission($editingteacherrole->id, $context, 'moodle/user:loginas', CAP_ALLOW); role_change_permission($editingteacherrole->id, $context, 'moodle/site:accessallgroups', CAP_ALLOW); return $course; } /** * Get the role id from a shortname. * * @param string $shortname the role shortname. * @return stdClass the role from the DB. */ protected function get_role(string $shortname): stdClass { global $DB; return $DB->get_record('role', ['shortname' => $shortname]); } /** * Get an array capability => CAP_... constant for all the orverrides set for a given role on a given context. * * @param string $shortname role shortname. * @param context $context context. * @return array the overrides set here. */ protected function get_overrides_for_role_on_context(string $shortname, context $context): array { $overridedata = get_capabilities_from_role_on_context($this->get_role($shortname), $context); $overrides = []; foreach ($overridedata as $override) { $overrides[$override->capability] = $override->permission; } return $overrides; } /** * Makes a backup of the course. * * @param stdClass $course The course object. * @return string Unique identifier for this backup. */ protected function backup_course(\stdClass $course): string { global $CFG, $USER; // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new \backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); return $backupid; } /** * Restores a backup that has been made earlier. * * @param string $backupid The unique identifier of the backup. * @param string $asroleshortname Which role in the new cousre the restorer should have. * @return int The new course id. */ protected function restore_adding_to_course(string $backupid, string $asroleshortname): int { global $CFG, $USER; // Create course to restore into, and a user to do the restore. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $restorer = $generator->create_user(); $generator->enrol_user($restorer->id, $course->id, $asroleshortname); $this->setUser($restorer); // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do restore to new course with default settings. $rc = new \restore_controller($backupid, $course->id, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_CURRENT_ADDING); $precheck = $rc->execute_precheck(); $this->assertTrue($precheck); $rc->get_plan()->get_setting('role_assignments')->set_value(true); $rc->get_plan()->get_setting('permissions')->set_value(true); $rc->execute_plan(); $rc->destroy(); return $course->id; } public function test_restore_role_overrides_as_manager(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and back it up. $course = $this->create_course_with_role_overrides(); $backupid = $this->backup_course($course); // When manager restores, both role overrides should be restored. $newcourseid = $this->restore_adding_to_course($backupid, 'manager'); // Verify. $overrides = $this->get_overrides_for_role_on_context('teacher', context_course::instance($newcourseid)); $this->assertArrayHasKey('moodle/user:loginas', $overrides); $this->assertEquals(CAP_ALLOW, $overrides['moodle/user:loginas']); $this->assertArrayHasKey('moodle/site:accessallgroups', $overrides); $this->assertEquals(CAP_ALLOW, $overrides['moodle/site:accessallgroups']); } public function test_restore_role_overrides_as_teacher(): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course and back it up. $course = $this->create_course_with_role_overrides(); $backupid = $this->backup_course($course); // When teacher restores, only the safe override should be restored. $newcourseid = $this->restore_adding_to_course($backupid, 'editingteacher'); // Verify. $overrides = $this->get_overrides_for_role_on_context('teacher', context_course::instance($newcourseid)); $this->assertArrayNotHasKey('moodle/user:loginas', $overrides); $this->assertArrayHasKey('moodle/site:accessallgroups', $overrides); $this->assertEquals(CAP_ALLOW, $overrides['moodle/site:accessallgroups']); } } tests/automated_backup_test.php 0000644 00000035315 15215711721 0012777 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Automated backup tests. * * @package core_backup * @copyright 2019 John Yao <johnyao@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_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->libdir . '/completionlib.php'); /** * Automated backup tests. * * @package core_backup * @copyright 2019 John Yao <johnyao@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class automated_backup_test extends \advanced_testcase { /** * @var \backup_cron_automated_helper */ protected $backupcronautomatedhelper; /** * @var \stdClass $course */ protected $course; protected function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(true); $this->setAdminUser(); $CFG->enableavailability = true; $CFG->enablecompletion = true; // Getting a testable backup_cron_automated_helper class. $this->backupcronautomatedhelper = new test_backup_cron_automated_helper(); $generator = $this->getDataGenerator(); $this->course = $generator->create_course( array('format' => 'topics', 'numsections' => 3, 'enablecompletion' => COMPLETION_ENABLED), array('createsections' => true)); $forum = $generator->create_module('forum', array( 'course' => $this->course->id)); $forum2 = $generator->create_module('forum', array( 'course' => $this->course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); // We need a grade, easiest is to add an assignment. $assignrow = $generator->create_module('assign', array( 'course' => $this->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' => $this->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' => $this->course->id, 'section' => 1)); } /** * Tests the automated backup run when the there is course backup should be skipped. */ public function test_automated_backup_skipped_run(): void { global $DB; // Enable automated back up. set_config('backup_auto_active', true, 'backup'); set_config('backup_auto_weekdays', '1111111', 'backup'); // Start backup process. $admin = get_admin(); // Backup entry should not exist. $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); $this->assertFalse($backupcourse); $this->assertInstanceOf( backup_cron_automated_helper::class, $this->backupcronautomatedhelper->return_this() ); $classobject = $this->backupcronautomatedhelper->return_this(); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'get_courses'); $courses = $method->invoke($classobject); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'check_and_push_automated_backups'); $emailpending = $method->invokeArgs($classobject, [$courses, $admin]); $this->expectOutputRegex('/Skipping course id ' . $this->course->id . ': Not scheduled for backup until/'); $this->assertFalse($emailpending); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); $this->assertNotNull($backupcourse->laststatus); } /** * Tests the automated backup run when the there is course backup can be pushed to adhoc task. */ public function test_automated_backup_push_run(): void { global $DB; // Enable automated back up. set_config('backup_auto_active', true, 'backup'); set_config('backup_auto_weekdays', '1111111', 'backup'); $admin = get_admin(); $classobject = $this->backupcronautomatedhelper->return_this(); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'get_courses'); $courses = $method->invoke($classobject); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); // We now manually trigger a backup pushed to adhoc task. // Make sure is in the past, which means should run now. $backupcourse->nextstarttime = time() - 10; $DB->update_record('backup_courses', $backupcourse); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'check_and_push_automated_backups'); $emailpending = $method->invokeArgs($classobject, [$courses, $admin]); $this->assertTrue($emailpending); $this->expectOutputRegex('/Putting backup of course id ' . $this->course->id. ' in adhoc task queue/'); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); // Now this backup course status should be queued. $this->assertEquals(backup_cron_automated_helper::BACKUP_STATUS_QUEUED, $backupcourse->laststatus); } /** * Tests the automated backup inactive run. */ public function test_inactive_run(): void { backup_cron_automated_helper::run_automated_backup(); $this->expectOutputString("Checking automated backup status...INACTIVE\n"); } /** * Tests the invisible course being skipped. */ public function test_should_skip_invisible_course(): void { global $DB; set_config('backup_auto_active', true, 'backup'); set_config('backup_auto_skip_hidden', true, 'backup'); set_config('backup_auto_weekdays', '1111111', 'backup'); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; // This is the status we believe last run was OK. $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); $this->assertTrue(course_change_visibility($this->course->id, false)); $course = $DB->get_record('course', array('id' => $this->course->id)); $this->assertEquals('0', $course->visible); $classobject = $this->backupcronautomatedhelper->return_this(); $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time()); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup'); $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]); $this->assertTrue($skipped); $this->expectOutputRegex('/Skipping course id ' . $this->course->id. ': Not visible/'); } /** * Tests the not modified course being skipped. */ public function test_should_skip_not_modified_course_in_days(): void { global $DB; set_config('backup_auto_active', true, 'backup'); // Skip if not modified in two days. set_config('backup_auto_skip_modif_days', 2, 'backup'); set_config('backup_auto_weekdays', '1111111', 'backup'); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; // This is the status we believe last run was OK. $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED; $backupcourse->laststarttime = time() - 2 * DAYSECS; $backupcourse->lastendtime = time() - 1 * DAYSECS; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); $course = $DB->get_record('course', array('id' => $this->course->id)); $course->timemodified = time() - 2 * DAYSECS - 1; $classobject = $this->backupcronautomatedhelper->return_this(); $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time()); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup'); $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]); $this->assertTrue($skipped); $this->expectOutputRegex('/Skipping course id ' . $this->course->id . ': Not modified in the past 2 days/'); } /** * Tests the backup not modified course being skipped. */ public function test_should_skip_not_modified_course_since_prev(): void { global $DB; set_config('backup_auto_active', true, 'backup'); // Skip if not modified in two days. set_config('backup_auto_skip_modif_prev', 2, 'backup'); set_config('backup_auto_weekdays', '1111111', 'backup'); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; // This is the status we believe last run was OK. $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED; $backupcourse->laststarttime = time() - 2 * DAYSECS; $backupcourse->lastendtime = time() - 1 * DAYSECS; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', array('courseid' => $this->course->id)); $course = $DB->get_record('course', array('id' => $this->course->id)); $course->timemodified = time() - 2 * DAYSECS - 1; $classobject = $this->backupcronautomatedhelper->return_this(); $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup(null, time()); $method = new \ReflectionMethod('\backup_cron_automated_helper', 'should_skip_course_backup'); $skipped = $method->invokeArgs($classobject, [$backupcourse, $course, $nextstarttime]); $this->assertTrue($skipped); $this->expectOutputRegex('/Skipping course id ' . $this->course->id . ': Not modified since previous backup/'); } /** * Test the task completes when coureid is missing. */ public function test_task_complete_when_courseid_is_missing(): void { global $DB; $admin = get_admin(); $classobject = $this->backupcronautomatedhelper->return_this(); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]); // Create a backup task. $method = new \ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task'); $method->invokeArgs($classobject, [$backupcourse, $admin]); // Delete course for this test. delete_course($this->course->id, false); $task = \core\task\manager::get_next_adhoc_task(time()); ob_start(); $task->execute(); $output = ob_get_clean(); $this->assertStringContainsString('Invalid course id: ' . $this->course->id . ', task aborted.', $output); \core\task\manager::adhoc_task_complete($task); } /** * Test the task completes when backup course is missing. */ public function test_task_complete_when_backup_course_is_missing(): void { global $DB; $admin = get_admin(); $classobject = $this->backupcronautomatedhelper->return_this(); // Create this backup course. $backupcourse = new \stdClass; $backupcourse->courseid = $this->course->id; $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN; $DB->insert_record('backup_courses', $backupcourse); $backupcourse = $DB->get_record('backup_courses', ['courseid' => $this->course->id]); // Create a backup task. $method = new \ReflectionMethod('\backup_cron_automated_helper', 'push_course_backup_adhoc_task'); $method->invokeArgs($classobject, [$backupcourse, $admin]); // Delete backup course for this test. $DB->delete_records('backup_courses', ['courseid' => $this->course->id]); $task = \core\task\manager::get_next_adhoc_task(time()); ob_start(); $task->execute(); $output = ob_get_clean(); $this->assertStringContainsString('Automated backup for course: ' . $this->course->fullname . ' encounters an error.', $output); \core\task\manager::adhoc_task_complete($task); } } /** * New backup_cron_automated_helper class for testing. * * This class extends the helper backup_cron_automated_helper class * in order to utilise abstract class for testing. * * @package core * @copyright 2019 John Yao <johnyao@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_backup_cron_automated_helper extends backup_cron_automated_helper { /** * Returning this for testing. */ public function return_this() { return $this; } } import.php 0000644 00000023730 15215711721 0006576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * This script is used to configure and execute the import proccess. * * @package core * @subpackage backup * @copyright Moodle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); // Require both the backup and restore libs require_once('../config.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); require_once($CFG->dirroot . '/backup/util/ui/import_extensions.php'); // The courseid we are importing to $courseid = required_param('id', PARAM_INT); // The id of the course we are importing FROM (will only be set if past first stage $importcourseid = optional_param('importid', false, PARAM_INT); // We just want to check if a search has been run. True if anything is there. $searchcourses = optional_param('searchcourses', false, PARAM_BOOL); // The target method for the restore (adding or deleting) $restoretarget = optional_param('target', backup::TARGET_CURRENT_ADDING, PARAM_INT); // Load the course and context $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $context = context_course::instance($courseid); // Must pass login require_login($course); // Must hold restoretargetimport in the current course require_capability('moodle/restore:restoretargetimport', $context); // Set up the page $PAGE->set_title($course->shortname . ': ' . get_string('import')); $PAGE->set_heading($course->fullname); $PAGE->set_url(new moodle_url('/backup/import.php', array('id'=>$courseid))); $PAGE->set_context($context); $PAGE->set_pagelayout('incourse'); // Prepare the backup renderer $renderer = $PAGE->get_renderer('core','backup'); // Check if we already have a import course id if ($importcourseid === false || $searchcourses) { // Obviously not... show the selector so one can be chosen $url = new moodle_url('/backup/import.php', array('id'=>$courseid)); $search = new import_course_search(array('url'=>$url)); // show the course selector echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('import'); echo html_writer::tag('div', get_string('importinfo'), ['class' => 'pb-3']); $backup = new import_ui(false, array()); echo $renderer->progress_bar($backup->get_progress_bar()); $html = $renderer->import_course_selector($url, $search); echo $html; echo $OUTPUT->footer(); die(); } // Load the course +context to import from $importcourse = $DB->get_record('course', array('id'=>$importcourseid), '*', MUST_EXIST); $importcontext = context_course::instance($importcourseid); // Make sure the user can backup from that course require_capability('moodle/backup:backuptargetimport', $importcontext); // Attempt to load the existing backup controller (backupid will be false if there isn't one) $backupid = optional_param('backup', false, PARAM_ALPHANUM); if (!($bc = backup_ui::load_controller($backupid))) { $bc = new backup_controller(backup::TYPE_1COURSE, $importcourse->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_IMPORT, $USER->id); $bc->get_plan()->get_setting('users')->set_status(backup_setting::LOCKED_BY_CONFIG); $settings = $bc->get_plan()->get_settings(); // For the initial stage we want to hide all locked settings and if there are // no visible settings move to the next stage $visiblesettings = false; foreach ($settings as $setting) { if ($setting->get_status() !== backup_setting::NOT_LOCKED) { $setting->set_visibility(backup_setting::HIDDEN); } else { $visiblesettings = true; } } import_ui::skip_current_stage(!$visiblesettings); } // Prepare the import UI $backup = new import_ui($bc, array('importid'=>$importcourse->id, 'target'=>$restoretarget)); // Process the current stage $backup->process(); // If this is the confirmation stage remove the filename setting if ($backup->get_stage() == backup_ui::STAGE_CONFIRMATION) { $backup->get_setting('filename')->set_visibility(backup_setting::HIDDEN); } // If it's the final stage process the import if ($backup->get_stage() == backup_ui::STAGE_FINAL) { echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('import'); echo html_writer::tag('div', get_string('importinfo'), ['class' => 'pb-3']); // Display an extra progress bar so that we can show the current stage. echo html_writer::start_div('', array('id' => 'executionprogress')); echo $renderer->progress_bar($backup->get_progress_bar()); // Start the progress display - we split into 2 chunks for backup and restore. $progress = new \core\progress\display(); $progress->start_progress('', 2); $backup->get_controller()->set_progress($progress); // Prepare logger for backup. $logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO); $backup->get_controller()->add_logger($logger); // First execute the backup $backup->execute(); // Before destroying the backup object, we still need to generate the progress bar. $progressbar = $renderer->progress_bar($backup->get_progress_bar()); $backup->destroy(); unset($backup); // Note that we've done that progress. $progress->progress(1); // Check whether the backup directory still exists. If missing, something // went really wrong in backup, throw error. Note that backup::MODE_IMPORT // backups don't store resulting files ever $tempdestination = make_backup_temp_directory($backupid, false); if (!file_exists($tempdestination) || !is_dir($tempdestination)) { throw new \moodle_exception('unknownbackupexporterror'); // Shouldn't happen ever. } // Prepare the restore controller. We don't need a UI here as we will just use what // ever the restore has (the user has just chosen). $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_YES, backup::MODE_IMPORT, $USER->id, $restoretarget); // Start a progress section for the restore, which will consist of 2 steps // (the precheck and then the actual restore). $progress->start_progress('Restore process', 2); $rc->set_progress($progress); // Set logger for restore. $rc->add_logger($logger); // Convert the backup if required.... it should NEVER happed if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) { $rc->convert(); } // Mark the UI finished. $rc->finish_ui(); // Execute prechecks $warnings = false; if (!$rc->execute_precheck()) { $precheckresults = $rc->get_precheck_results(); if (is_array($precheckresults)) { if (!empty($precheckresults['errors'])) { // If errors are found, terminate the import. fulldelete($tempdestination); echo $renderer->precheck_notices($precheckresults); echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id))); echo $OUTPUT->footer(); die(); } if (!empty($precheckresults['warnings'])) { // If warnings are found, go ahead but display warnings later. $warnings = $precheckresults['warnings']; } } } if ($restoretarget == backup::TARGET_CURRENT_DELETING || $restoretarget == backup::TARGET_EXISTING_DELETING) { restore_dbops::delete_course_content($course->id); } // Execute the restore. $rc->execute_plan(); // Delete the temp directory now fulldelete($tempdestination); // End restore section of progress tracking (restore/precheck). $progress->end_progress(); // All progress complete. Hide progress area. $progress->end_progress(); echo html_writer::end_div(); echo html_writer::script('document.getElementById("executionprogress").style.display = "none";'); // Display a notification and a continue button if ($warnings) { echo $OUTPUT->box_start(); echo $OUTPUT->notification(get_string('warning'), 'notifyproblem'); echo html_writer::start_tag('ul', array('class'=>'list')); foreach ($warnings as $warning) { echo html_writer::tag('li', $warning); } echo html_writer::end_tag('ul'); echo $OUTPUT->box_end(); } echo $progressbar; echo $OUTPUT->notification(get_string('importsuccess', 'backup'), 'notifysuccess'); echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id))); // Get and display log data if there was any. $loghtml = $logger->get_html(); if ($loghtml != '') { echo $renderer->log_display($loghtml); } echo $OUTPUT->footer(); die(); } else { // Otherwise save the controller and progress $backup->save_controller(); } // Display the current stage. echo $OUTPUT->header(); \backup_helper::print_coursereuse_selector('import'); echo html_writer::tag('div', get_string('importinfo'), ['class' => 'pb-3']); if ($backup->enforce_changed_dependencies()) { debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER); } echo $renderer->progress_bar($backup->get_progress_bar()); echo $backup->display($renderer); $backup->destroy(); unset($backup); echo $OUTPUT->footer(); backup.class.php 0000644 00000014553 15215711721 0007640 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup * @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 = 'activity'; const TYPE_1SECTION = 'section'; const TYPE_1COURSE = 'course'; // Backup format const FORMAT_MOODLE = 'moodle2'; const FORMAT_MOODLE1 = 'moodle1'; const FORMAT_IMSCC1 = 'imscc1'; const FORMAT_IMSCC11 = 'imscc11'; const FORMAT_UNKNOWN = 'unknown'; // Interactive const INTERACTIVE_YES = true; const INTERACTIVE_NO = false; /** Release the session during backup/restore */ const RELEASESESSION_YES = true; /** Don't release the session during backup/restore */ const RELEASESESSION_NO = false; // Predefined modes (purposes) of the backup const MODE_GENERAL = 10; /** * This is used for importing courses, and for duplicating activities. * * This mode will ensure that files are not included in the backup generation, and * during a restore they are copied from the existing file record. */ const MODE_IMPORT = 20; const MODE_HUB = 30; /** * This mode is intended for duplicating courses and cases where the backup target is * within the same site. * * This mode will ensure that files are not included in the backup generation, and * during a restore they are copied from the existing file record. * * For creating a backup for archival purposes or greater longevity, use MODE_GENERAL. */ const MODE_SAMESITE = 40; const MODE_AUTOMATED = 50; const MODE_CONVERTED = 60; /** * This mode is for asynchronous backups. * These backups will run via adhoc scheduled tasks. */ const MODE_ASYNC = 70; /** * This mode is for course copies. * It is similar to async, but identifies back up and restore tasks * as course copies. * * These copies will run via adhoc scheduled tasks. */ const MODE_COPY = 80; // Target (new/existing/current/adding/deleting) const TARGET_CURRENT_DELETING = 0; const TARGET_CURRENT_ADDING = 1; const TARGET_NEW_COURSE = 2; const TARGET_EXISTING_DELETING= 3; const TARGET_EXISTING_ADDING = 4; // Execution mode const EXECUTION_INMEDIATE = 1; const EXECUTION_DELAYED = 2; // Status of the backup_controller const STATUS_CREATED = 100; const STATUS_REQUIRE_CONV= 200; const STATUS_PLANNED = 300; const STATUS_CONFIGURED = 400; const STATUS_SETTING_UI = 500; const STATUS_NEED_PRECHECK=600; const STATUS_AWAITING = 700; const STATUS_EXECUTING = 800; const STATUS_FINISHED_ERR= 900; const STATUS_FINISHED_OK =1000; // 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 // Type of operation const OPERATION_BACKUP ='backup'; // We are performing one backup const OPERATION_RESTORE ='restore';// We are performing one restore // Options for "Include enrolment methods" restore setting. const ENROL_NEVER = 0; const ENROL_WITHUSERS = 1; const ENROL_ALWAYS = 2; // Version and release (to keep CFG->backup_version (and release) updated automatically). /** * Usually same than major release version, this is used to mark important * point is backup when some behavior/approach channged, in order to allow * conditional coding based on it. */ const VERSION = 2024100700; /** * Usually same than major release zero version, mainly for informative/historic purposes. */ const RELEASE = '4.5'; /** * Cipher to be used in backup and restore operations. */ const CIPHER = 'aes-256-cbc'; /** * Bytes enforced for key, using the cypher above. Restrictive? Yes, but better than unsafe lengths */ const CIPHERKEYLEN = 32; } /* * Exception class used by all the @backup stuff */ abstract class backup_exception extends moodle_exception { public function __construct($errorcode, $a=NULL, $debuginfo=null) { parent::__construct($errorcode, 'error', '', $a, $debuginfo); } } log.php 0000644 00000000475 15215711721 0006046 0 ustar 00 <?php // log.php - old scheduled backups report. Now redirecting // to the new admin one require_once("../config.php"); require_login(); require_capability('moodle/backup:backupcourse', context_system::instance()); redirect("$CFG->wwwroot/report/backups/index.php", '', 'admin', 1); cc/entity11.lti.class.php 0000644 00000011420 15215711721 0011213 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2011 Darko Miletic (dmiletic@moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc11_lti extends entities11 { public function generate_node() { cc2moodle::log_action('Creating BasicLTI mods'); $response = ''; if (!empty(cc2moodle::$instances['instances'][MOODLE_TYPE_LTI])) { foreach (cc2moodle::$instances['instances'][MOODLE_TYPE_LTI] as $instance) { $response .= $this->create_node_course_modules_mod_basiclti($instance); } } return $response; } private function create_node_course_modules_mod_basiclti($instance) { $sheet_mod_basiclti = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_LTI); $topic_data = $this->get_basiclti_data($instance); $result = ''; if (!empty($topic_data)) { $find_tags = array('[#mod_instance#]' , '[#mod_basiclti_name#]' , '[#mod_basiclti_intro#]' , '[#mod_basiclti_timec#]' , '[#mod_basiclti_timem#]' , '[#mod_basiclti_toolurl#]', '[#mod_basiclti_icon#]' ); $replace_values = array($instance['instance'], $topic_data['title'], $topic_data['description'], time(),time(), $topic_data['launchurl'], $topic_data['icon'] ); $result = str_replace($find_tags, $replace_values, $sheet_mod_basiclti); } return $result; } protected function getValue($node, $default = '') { $result = $default; if (is_object($node) && ($node->length > 0) && !empty($node->item(0)->nodeValue)) { $result = htmlspecialchars(trim($node->item(0)->nodeValue), ENT_COMPAT, 'UTF-8', false); } return $result; } public function get_basiclti_data($instance) { $topic_data = array(); $basiclti_file = $this->get_external_xml($instance['resource_indentifier']); if (!empty($basiclti_file)) { $basiclti_file_path = cc2moodle::$path_to_manifest_folder . DIRECTORY_SEPARATOR . $basiclti_file; $basiclti_file_dir = dirname($basiclti_file_path); $basiclti = $this->load_xml_resource($basiclti_file_path); if (!empty($basiclti)) { $xpath = cc2moodle::newx_path($basiclti, cc112moodle::$basicltins); $topic_title = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:title'),'Untitled'); $blti_description = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:description')); $launch_url = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:launch_url')); $launch_icon = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:icon')); $tool_raw = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:code'),null); $tool_url = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:url'),null); $tool_desc = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:description'),null); $topic_data['title' ] = $topic_title; $topic_data['description'] = $blti_description; $topic_data['launchurl' ] = $launch_url; $topic_data['icon' ] = $launch_icon; $topic_data['orgid' ] = $tool_raw; $topic_data['orgurl' ] = $tool_url; $topic_data['orgdesc' ] = $tool_desc; } } return $topic_data; } } cc/entity.quiz.class.php 0000644 00000125322 15215711721 0011260 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2009 Mauro Rondinelli (mauro.rondinelli [AT] uvcms.com) * @copyright 2011 Darko Miletic (dmiletic@moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc_quiz extends entities { public function generate_node_question_categories() { $instances = $this->generate_instances(); $node_course_question_categories = $this->create_node_course_question_categories($instances); $node_course_question_categories = empty($node_course_question_categories) ? '' : $node_course_question_categories; return $node_course_question_categories; } public function generate_node_course_modules_mod() { cc2moodle::log_action('Creating Quiz mods'); $node_course_modules_mod = ''; $instances = $this->generate_instances(); if (!empty($instances)) { foreach ($instances as $instance) { if ($instance['is_question_bank'] == 0) { $node_course_modules_mod .= $this->create_node_course_modules_mod($instance); } } } return $node_course_modules_mod; } private function create_node_course_modules_mod_quiz_feedback() { $sheet_question_mod_feedback = cc2moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_FEEDBACK); return $sheet_question_mod_feedback; } private function generate_instances() { $last_instance_id = 0; $last_question_id = 0; $last_answer_id = 0; $instances = array(); $types = array(MOODLE_TYPE_QUIZ, MOODLE_TYPE_QUESTION_BANK); foreach ($types as $type) { if (!empty(cc2moodle::$instances['instances'][$type])) { foreach (cc2moodle::$instances['instances'][$type] as $instance) { if ($type == MOODLE_TYPE_QUIZ) { $is_question_bank = 0; } else { $is_question_bank = 1; } $assessment_file = $this->get_external_xml($instance['resource_indentifier']); if (!empty($assessment_file)) { $assessment = $this->load_xml_resource( cc2moodle::$path_to_manifest_folder . DIRECTORY_SEPARATOR . $assessment_file ); if (!empty($assessment)) { $replace_values = array('unlimited' => 0); $questions = $this->get_questions( $assessment, $last_question_id, $last_answer_id, dirname($assessment_file), $is_question_bank ); $question_count = count($questions); if (!empty($question_count)) { $last_instance_id++; $instances[$instance['resource_indentifier']]['questions'] = $questions; $instances[$instance['resource_indentifier']]['id'] = $last_instance_id; $instances[$instance['resource_indentifier']]['title'] = $instance['title']; $instances[$instance['resource_indentifier']]['is_question_bank'] = $is_question_bank; $instances[$instance['resource_indentifier']]['options']['timelimit'] = $this->get_global_config($assessment, 'qmd_timelimit', 0); $instances[$instance['resource_indentifier']]['options']['max_attempts'] = $this->get_global_config($assessment, 'cc_maxattempts', 0, $replace_values); } } } } } } return $instances; } private function create_node_course_modules_mod($instance) { $sheet_question_mod = cc2moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ); $node_course_modules_quiz_question_instances = $this->create_node_course_modules_mod_quiz_question_instances($instance); $node_course_modules_quiz_feedback = $this->create_node_course_modules_mod_quiz_feedback($instance); $questions_strings = $this->get_questions_string($instance); $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $find_tags = array('[#mod_id#]', '[#mod_name#]', '[#mod_intro#]', '[#mod_stamp#]', '[#question_string#]', '[#date_now#]', '[#mod_max_attempts#]', '[#mod_timelimit#]', '[#node_question_instance#]', '[#node_questions_feedback#]'); $replace_values = array($instance['id'], self::safexml($instance['title']), self::safexml($instance['title']), self::safexml($quiz_stamp), self::safexml($questions_strings), time(), $instance['options']['max_attempts'], $instance['options']['timelimit'], $node_course_modules_quiz_question_instances, $node_course_modules_quiz_feedback); // This one has tags. $node_question_mod = str_replace($find_tags, $replace_values, $sheet_question_mod); return $node_question_mod; } private function get_global_config($assessment, $option, $default_value, $replace_values = '') { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $metadata = $xpath->query('/xmlns:questestinterop/xmlns:assessment/xmlns:qtimetadata/xmlns:qtimetadatafield'); foreach ($metadata as $field) { $field_label = $xpath->query('xmlns:fieldlabel', $field); $field_label = !empty($field_label->item(0)->nodeValue) ? $field_label->item(0)->nodeValue : ''; if (strtolower($field_label) == strtolower($option)) { $field_entry = $xpath->query('xmlns:fieldentry', $field); $response = !empty($field_entry->item(0)->nodeValue) ? $field_entry->item(0)->nodeValue : ''; } } $response = !empty($response) ? trim($response) : ''; if (!empty($replace_values)) { foreach ($replace_values as $key => $value) { $response = ($key == $response) ? $value : $response; } } $response = empty($response) ? $default_value : $response; return $response; } private function create_node_course_modules_mod_quiz_question_instances($instance) { $node_course_module_mod_quiz_questions_instances = ''; $sheet_question_mod_instance = cc2moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_QUESTION_INSTANCE); $find_tags = array('[#question_id#]' , '[#instance_id#]'); if (!empty($instance['questions'])) { foreach ($instance['questions'] as $question) { $replace_values = array($question['id'] , $question['id']); $node_course_module_mod_quiz_questions_instances .= str_replace( $find_tags, $replace_values, $sheet_question_mod_instance ); } $node_course_module_mod_quiz_questions_instances = str_replace( $find_tags, $replace_values, $node_course_module_mod_quiz_questions_instances ); } return $node_course_module_mod_quiz_questions_instances; } private function get_questions_string($instance) { $questions_string = ''; if (!empty($instance['questions'])) { foreach ($instance['questions'] as $question) { $questions_string .= $question['id'] . ','; } } $questions_string = !empty($questions_string) ? substr($questions_string, 0, strlen($questions_string) - 1) : ''; return $questions_string; } private function create_node_course_question_categories($instances) { $sheet_question_categories = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES); if (!empty($instances)) { $node_course_question_categories_question_category = ''; foreach ($instances as $instance) { $node_course_question_categories_question_category .= $this->create_node_course_question_categories_question_category($instance); } $find_tags = array('[#node_course_question_categories_question_category#]'); $replace_values = array($node_course_question_categories_question_category); $node_course_question_categories = str_replace($find_tags, $replace_values, $sheet_question_categories); } $node_course_question_categories = empty($node_course_question_categories) ? '' : $node_course_question_categories; return $node_course_question_categories; } private function create_node_course_question_categories_question_category($instance) { $sheet_question_categories_quetion_category = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY); $find_tags = array('[#quiz_id#]', '[#quiz_name#]', '[#quiz_stamp#]', '[#node_course_question_categories_question_category_questions#]'); $node_course_question_categories_questions = $this->create_node_course_question_categories_question_category_question($instance); $node_course_question_categories_questions = empty($node_course_question_categories_questions) ? '' : $node_course_question_categories_questions; $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $replace_values = array($instance['id'], self::safexml($instance['title']), $quiz_stamp, $node_course_question_categories_questions); $node_question_categories = str_replace($find_tags, $replace_values, $sheet_question_categories_quetion_category); return $node_question_categories; } private function create_node_course_question_categories_question_category_question($instance) { global $USER; $node_course_question_categories_question = ''; $find_tags = array('[#question_id#]', '[#question_title#]', '[#question_text#]', '[#question_type#]', '[#question_general_feedback#]', '[#question_defaultgrade#]', '[#date_now#]', '[#question_type_nodes#]', '[#question_stamp#]', '[#question_version#]', '[#logged_user#]'); $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION); $questions = $instance['questions']; if (!empty($questions)) { foreach ($questions as $question) { $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $quiz_version = 'localhost+' . time() . '+' . $this->generate_random_string(6); $question_moodle_type = $question['moodle_type']; $question_cc_type = $question['cc_type']; $question_type_node = ''; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_MULTIPLE_CHOICE) ? $this->create_node_course_question_categories_question_category_question_multiple_choice($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_TRUE_FALSE) ? $this->create_node_course_question_categories_question_category_question_true_false($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_ESSAY) ? $this->create_node_course_question_categories_question_category_question_eesay($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_SHORTANSWER) ? $this->create_node_course_question_categories_question_category_question_shortanswer($question) : $question_type_node; $replace_values = array($question['id'], self::safexml($this->truncate_text($question['title'], 255, true)), self::safexml($question['title']), $question_moodle_type, self::safexml($question['feedback']), $question['defaultgrade'], //default grade time(), $question_type_node, $quiz_stamp, $quiz_version, $USER->id); $node_course_question_categories_question .= str_replace($find_tags, $replace_values, $sheet_question_categories_question); } } $node_course_question_categories_question = empty($node_course_question_categories_question) ? '' : $node_course_question_categories_question; return $node_course_question_categories_question; } private function get_questions($assessment, &$last_question_id, &$last_answer_id, $root_path, $is_question_bank) { $questions = array(); $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); if (!$is_question_bank) { $questions_items = $xpath->query('/xmlns:questestinterop/xmlns:assessment/xmlns:section/xmlns:item'); } else { $questions_items = $xpath->query('/xmlns:questestinterop/xmlns:objectbank/xmlns:item'); } foreach ($questions_items as $question_item) { $count_questions = $xpath->evaluate('count(xmlns:presentation/xmlns:flow/xmlns:material/xmlns:mattext)', $question_item); if ($count_questions == 0) { $question_title = $xpath->query('xmlns:presentation/xmlns:material/xmlns:mattext', $question_item); } else { $question_title = $xpath->query('xmlns:presentation/xmlns:flow/xmlns:material/xmlns:mattext', $question_item); } $question_title = !empty($question_title->item(0)->nodeValue) ? $question_title->item(0)->nodeValue : ''; $question_identifier = $xpath->query('@ident', $question_item); $question_identifier = !empty($question_identifier->item(0)->nodeValue) ? $question_identifier->item(0)->nodeValue : ''; if (!empty($question_identifier)) { $question_type = $this->get_question_type($question_identifier, $assessment); if (!empty($question_type['moodle'])) { $last_question_id++; $questions[$question_identifier]['id'] = $last_question_id; $question_title = $this->update_sources($question_title, $root_path); $question_title = !empty($question_title) ? str_replace("%24", "\$", $this->include_titles($question_title)) : ''; $questions[$question_identifier]['title'] = $question_title; $questions[$question_identifier]['identifier'] = $question_identifier; $questions[$question_identifier]['moodle_type'] = $question_type['moodle']; $questions[$question_identifier]['cc_type'] = $question_type['cc']; $questions[$question_identifier]['feedback'] = $this->get_general_feedback($assessment, $question_identifier); $questions[$question_identifier]['defaultgrade'] = $this->get_defaultgrade($assessment, $question_identifier); $questions[$question_identifier]['answers'] = $this->get_answers($question_identifier, $assessment, $last_answer_id); } } } $questions = !empty($questions) ? $questions : ''; return $questions; } private function str_replace_once($search, $replace, $subject) { $first_char = strpos($subject, $search); if ($first_char !== false) { $before_str = substr($subject, 0, $first_char); $after_str = substr($subject, $first_char + strlen($search)); return $before_str . $replace . $after_str; } else { return $subject; } } private function get_defaultgrade($assessment, $question_identifier) { $result = 1; $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $query = '//xmlns:item[@ident="' . $question_identifier . '"]'; $query .= '//xmlns:qtimetadatafield[xmlns:fieldlabel="cc_weighting"]/xmlns:fieldentry'; $defgrade = $xpath->query($query); if (!empty($defgrade) && ($defgrade->length > 0)) { $resp = (int)$defgrade->item(0)->nodeValue; if ($resp >= 0 && $resp <= 99) { $result = $resp; } } return $result; } private function get_general_feedback($assessment, $question_identifier) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $respconditions = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($respconditions)) { foreach ($respconditions as $respcondition) { $continue = $respcondition->getAttributeNode('continue'); $continue = !empty($continue->nodeValue) ? strtolower($continue->nodeValue) : ''; if ($continue == 'yes') { $display_feedback = $xpath->query('xmlns:displayfeedback', $respcondition); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } return $feedback; } private function get_feedback($assessment, $identifier, $item_identifier, $question_type) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $resource_processing = $xpath->query('//xmlns:item[@ident="' . $item_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($resource_processing)) { foreach ($resource_processing as $response) { $varequal = $xpath->query('xmlns:conditionvar/xmlns:varequal', $response); $varequal = !empty($varequal->item(0)->nodeValue) ? $varequal->item(0)->nodeValue : ''; if (strtolower($varequal) == strtolower($identifier) || ($question_type == CC_QUIZ_ESSAY)) { $display_feedback = $xpath->query('xmlns:displayfeedback', $response); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $item_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } return $feedback; } private function get_answers_fib($question_identifier, $identifier, $assessment, &$last_answer_id) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $answers_fib = array(); $response_items = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); foreach ($response_items as $response_item) { $setvar = $xpath->query('xmlns:setvar', $response_item); $setvar = is_object($setvar->item(0)) ? $setvar->item(0)->nodeValue : ''; if ($setvar != '') { $last_answer_id++; $answer_title = $xpath->query('xmlns:conditionvar/xmlns:varequal[@respident="' . $identifier . '"]', $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? $answer_title->item(0)->nodeValue : ''; $case = $xpath->query('xmlns:conditionvar/xmlns:varequal/@case', $response_item); $case = is_object($case->item(0)) ? $case->item(0)->nodeValue : 'no' ; $case = strtolower($case) == 'yes' ? 1 : 0; $display_feedback = $xpath->query('xmlns:displayfeedback', $response_item); unset($feedbacks_identifiers); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } $answers_fib[] = array('id' => $last_answer_id, 'title' => $answer_title, 'score' => $setvar, 'feedback' => $feedback, 'case' => $case); } } $answers_fib = empty($answers_fib) ? '' : $answers_fib; return $answers_fib; } private function get_answers_pattern_match($question_identifier, $identifier, $assessment, &$last_answer_id) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $answers_fib = array(); $response_items = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); foreach ($response_items as $response_item) { $setvar = $xpath->query('xmlns:setvar', $response_item); $setvar = is_object($setvar->item(0)) ? $setvar->item(0)->nodeValue : ''; if ($setvar != '') { $last_answer_id++; $answer_title = $xpath->query('xmlns:conditionvar/xmlns:varequal[@respident="' . $identifier . '"]', $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? $answer_title->item(0)->nodeValue : ''; if (empty($answer_title)) { $answer_title = $xpath->query('xmlns:conditionvar/xmlns:varsubstring[@respident="' . $identifier . '"]', $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? '*' . $answer_title->item(0)->nodeValue . '*' : ''; } if (empty($answer_title)) { $answer_title = '*'; } $case = $xpath->query('xmlns:conditionvar/xmlns:varequal/@case', $response_item); $case = is_object($case->item(0)) ? $case->item(0)->nodeValue : 'no' ; $case = strtolower($case) == 'yes' ? 1 : 0; $display_feedback = $xpath->query('xmlns:displayfeedback', $response_item); unset($feedbacks_identifiers); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } $answers_fib[] = array('id' => $last_answer_id, 'title' => $answer_title, 'score' => $setvar, 'feedback' => $feedback, 'case' => $case); } } $answers_fib = empty($answers_fib) ? '' : $answers_fib; return $answers_fib; } private function get_answers($identifier, $assessment, &$last_answer_id) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $answers = array(); $question_cc_type = $this->get_question_type($identifier, $assessment); $question_cc_type = $question_cc_type['cc']; $is_multiresponse = ($question_cc_type == CC_QUIZ_MULTIPLE_RESPONSE); if ($question_cc_type == CC_QUIZ_MULTIPLE_CHOICE || $is_multiresponse || $question_cc_type == CC_QUIZ_TRUE_FALSE) { $query_answers = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_lid/xmlns:render_choice/xmlns:response_label'; $query_answers_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_lid/xmlns:render_choice/xmlns:response_label'; $query_indentifer = '@ident'; $query_title = 'xmlns:material/xmlns:mattext'; } if ($question_cc_type == CC_QUIZ_ESSAY) { $query_answers = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_str'; $query_answers_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_str'; $query_indentifer = '@ident'; $query_title = 'xmlns:render_fib'; } if ($question_cc_type == CC_QUIZ_FIB || $question_cc_type == CC_QUIZ_PATTERN_MACHT) { $xpath_query = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_str/@ident'; $xpath_query_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_str/@ident'; $count_response = $xpath->evaluate('count(' . $xpath_query_with_flow . ')'); if ($count_response == 0) { $answer_identifier = $xpath->query($xpath_query); } else { $answer_identifier = $xpath->query($xpath_query_with_flow); } $answer_identifier = !empty($answer_identifier->item(0)->nodeValue) ? $answer_identifier->item(0)->nodeValue : ''; if ($question_cc_type == CC_QUIZ_FIB) { $answers = $this->get_answers_fib ($identifier, $answer_identifier, $assessment, $last_answer_id); } else { $answers = $this->get_answers_pattern_match ($identifier, $answer_identifier, $assessment, $last_answer_id); } } else { $count_response = $xpath->evaluate('count(' . $query_answers_with_flow . ')'); if ($count_response == 0) { $response_items = $xpath->query($query_answers); } else { $response_items = $xpath->query($query_answers_with_flow); } if (!empty($response_items)) { if ($is_multiresponse) { $correct_answer_score = 0; //get the correct answers count $canswers_query = "//xmlns:item[@ident='{$identifier}']//xmlns:setvar[@varname='SCORE'][.=100]/../xmlns:conditionvar//xmlns:varequal[@case='Yes'][not(parent::xmlns:not)]"; $canswers = $xpath->query($canswers_query); if ($canswers->length > 0) { $correct_answer_score = round(1.0 / (float)$canswers->length, 7); //weird $correct_answers_ident = array(); foreach ($canswers as $cnode) { $correct_answers_ident[$cnode->nodeValue] = true; } } } foreach ($response_items as $response_item) { $last_answer_id++; $answer_identifier = $xpath->query($query_indentifer, $response_item); $answer_identifier = !empty($answer_identifier->item(0)->nodeValue) ? $answer_identifier->item(0)->nodeValue : ''; $answer_title = $xpath->query($query_title, $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? $answer_title->item(0)->nodeValue : ''; $answer_feedback = $this->get_feedback($assessment, $answer_identifier, $identifier, $question_cc_type); $answer_score = $this->get_score($assessment, $answer_identifier, $identifier); if ($is_multiresponse && isset($correct_answers_ident[$answer_identifier])) { $answer_score = $correct_answer_score; } $answers[] = array('id' => $last_answer_id, 'title' => $answer_title, 'score' => $answer_score, 'identifier' => $answer_identifier, 'feedback' => $answer_feedback); } } } $answers = empty($answers) ? '' : $answers; return $answers; } private function get_score($assessment, $identifier, $question_identifier) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $resource_processing = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($resource_processing)) { foreach ($resource_processing as $response) { $question_cc_type = $this->get_question_type($question_identifier, $assessment); $question_cc_type = $question_cc_type['cc']; $varequal = $xpath->query('xmlns:conditionvar/xmlns:varequal', $response); $varequal = !empty($varequal->item(0)->nodeValue) ? $varequal->item(0)->nodeValue : ''; if (strtolower($varequal) == strtolower($identifier)) { $score = $xpath->query('xmlns:setvar', $response); $score = !empty($score->item(0)->nodeValue) ? $score->item(0)->nodeValue : ''; } } } $score = empty($score) ? 0 : sprintf("%.7F", $score); return $score; } private function create_node_course_question_categories_question_category_question_multiple_choice($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_MULTIPLE_CHOICE); if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $answer_string = $this->get_answers_string($question['answers']); $is_single = ($question['cc_type'] == CC_QUIZ_MULTIPLE_CHOICE) ? 1 : 0; $find_tags = array('[#node_course_question_categories_question_category_question_answer#]', '[#answer_string#]', '[#is_single#]'); $replace_values = array($node_course_question_categories_question_answer, self::safexml($answer_string), $is_single); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_eesay($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_EESAY); if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $find_tags = array('[#node_course_question_categories_question_category_question_answer#]'); $replace_values = array($node_course_question_categories_question_answer); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_shortanswer($question) { //, &$fib_questions) { $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_SHORTANSWER); $node_course_question_categories_question_answer = ''; if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $answers_string = $this->get_answers_string($question['answers']); $use_case = 0; foreach ($question['answers'] as $answer) { if ($answer['case'] == 1) { $use_case = 1; } } $find_tags = array('[#answers_string#]', '[#use_case#]', '[#node_course_question_categories_question_category_question_answer#]'); $replace_values = array(self::safexml($answers_string), self::safexml($use_case), $node_course_question_categories_question_answer); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_true_false($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE); $trueanswer = null; $falseanswer = null; if (!empty($question['answers'])) { // Identify the true and false answers. foreach ($question['answers'] as $answer) { if ($answer['identifier'] == 'true') { $trueanswer = $answer; } else if ($answer['identifier'] == 'false') { $falseanswer = $answer; } else { // Should not happen, but just in case. throw new coding_exception("Unknown answer identifier detected" . " in true/false quiz question with id {$question['id']}."); } $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } // Make sure the true and false answer was found. if (is_null($trueanswer) || is_null($falseanswer)) { throw new coding_exception("Unable to correctly identify the " . "true and false answers in the question with id {$question['id']}."); } } $find_tags = array('[#node_course_question_categories_question_category_question_answer#]', '[#true_answer_id#]', '[#false_answer_id#]'); $replace_values = array($node_course_question_categories_question_answer, $trueanswer['id'], $falseanswer['id']); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function get_answers_string($answers) { $answer_string = ''; if (!empty($answers)) { foreach ($answers as $answer) { $answer_string .= $answer['id'] . ','; } } $answer_string = !empty($answer_string) ? substr($answer_string, 0, strlen($answer_string) - 1) : ''; return $answer_string; } private function create_node_course_question_categories_question_category_question_answer($answer) { $sheet_question_categories_question_answer = cc2moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_ANSWER); $find_tags = array('[#answer_id#]', '[#answer_text#]', '[#answer_score#]', '[#answer_feedback#]'); $replace_values = array($answer['id'], self::safexml($answer['title']), $answer['score'], self::safexml($answer['feedback'])); $node_question_categories_question_answer = str_replace($find_tags, $replace_values, $sheet_question_categories_question_answer); return $node_question_categories_question_answer; } private function get_question_type($identifier, $assessment) { $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $metadata = $xpath->query('//xmlns:item[@ident="' . $identifier . '"]/xmlns:itemmetadata/xmlns:qtimetadata/xmlns:qtimetadatafield'); foreach ($metadata as $field) { $field_label = $xpath->query('xmlns:fieldlabel', $field); $field_label = !empty($field_label->item(0)->nodeValue) ? $field_label->item(0)->nodeValue : ''; if ($field_label == 'cc_profile') { $field_entry = $xpath->query('xmlns:fieldentry', $field); $type = !empty($field_entry->item(0)->nodeValue) ? $field_entry->item(0)->nodeValue : ''; } } $return_type = array(); $return_type['moodle'] = ''; $return_type['cc'] = $type; if ($type == CC_QUIZ_MULTIPLE_CHOICE) { $return_type['moodle'] = MOODLE_QUIZ_MULTIPLE_CHOICE; } if ($type == CC_QUIZ_MULTIPLE_RESPONSE) { $return_type['moodle'] = MOODLE_QUIZ_MULTIPLE_CHOICE; } if ($type == CC_QUIZ_TRUE_FALSE) { $return_type['moodle'] = MOODLE_QUIZ_TRUE_FALSE; } if ($type == CC_QUIZ_ESSAY) { $return_type['moodle'] = MOODLE_QUIZ_ESSAY; } if ($type == CC_QUIZ_FIB) { $return_type['moodle'] = MOODLE_QUIZ_SHORTANSWER; } if ($type == CC_QUIZ_PATTERN_MACHT) { $return_type['moodle'] = MOODLE_QUIZ_SHORTANSWER; } return $return_type; } } cc/schemas11/imslticp_v1p0.xsd 0000644 00000041141 15215711721 0012133 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imslticp_v1p0" targetNamespace="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS LTICP 1.0.0" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Chuck Severance (IMS GLC) and Colin Smythe (IMS GLC) Date: 30th April, 2010 Version: 1.0 Status: Final Release Description: This is the set of Common Profile objects used in LTI. This XSD was created as part of the BasicLTI Final Release. History: V1.0 - the first Final Release. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Basic Learning Tools Interoperability Version 1.0 found at http://www.imsglobal.org/lti and the original IMS GLC schema binding or code base http://www.imsglobal.org/lti. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2010. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <xs:attributeGroup name="extension.Icon.Attr"> <xs:anyAttribute namespace = "##other" processContents = "strict"/> </xs:attributeGroup> <xs:attributeGroup name="extension.LocalizedString.Attr"> <xs:anyAttribute namespace = "##other" processContents = "strict"/> </xs:attributeGroup> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <xs:simpleType name="Name.Type"> <xs:restriction base="xs:Name"/> </xs:simpleType> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Vendor.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Vendor complexType is the container for the information about the vendor of the tool to be launched/used using BasicLTI. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="code" type="Name.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="name" type="LocalizedString.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="description" type="LocalizedString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="url" minOccurs = "0" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value = "4096"/> <xs:minLength value = "1"/> <xs:whiteSpace value = "preserve"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="contact" type="Contact.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="Contact.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Contact class is the container for the vendor contact information. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="email" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value = "4096"/> <xs:minLength value = "1"/> <xs:whiteSpace value = "preserve"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:complexType> <xs:complexType name="ProductInfo.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The productInfo complexType is the container for the information about the tool itself. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="code" type="Name.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="name" type="LocalizedString.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="version" type="xs:normalizedString" minOccurs = "1" maxOccurs = "1"/> <xs:element name="description" type="LocalizedString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="technical_description" type="LocalizedString.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="ToolLocator.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The ToolLocator complexType is the container for information about the electronic location of the tool. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="vendor" type="Vendor.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="tool_info" type="ProductInfo.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="deployment_url" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value = "4096"/> <xs:minLength value = "1"/> <xs:whiteSpace value = "preserve"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="LocalizedString.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Localized complexType is the container for localized string entries. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="key" use="optional" type="Name.Type"/> <xs:attributeGroup ref="extension.LocalizedString.Attr"/> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="Icon.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Icon complexType is the container for information about an icon to be used with the tool. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:anyURI"> <xs:attribute name="key" use="optional" type="Name.Type"/> <xs:attribute name="platform" use="optional" type="Name.Type"/> <xs:attribute name="style" use="optional" type="Name.Type"/> <xs:attributeGroup ref="extension.Icon.Attr"/> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_imsdt_v1p1.xsd 0000644 00000032377 15215711721 0012620 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imsdt_v1p1" targetNamespace="http://www.imsglobal.org/xsd/imsccv1p1/imsdt_v1p1" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS CC DTPC 1.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st August, 2010 Version: 1.1 Status: Final Description: This is the IMS GLC Discussion Topics Data Model for the Common Cartridge. History: Version 1.0 - the first release of this data model; Version 1.1 - updates made to the namespace. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <xs:complexType name="EmptyPrimitiveType.Type"> <xs:complexContent> <xs:restriction base="xs:anyType"/> </xs:complexContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Topic.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Topic complexType for the discussion topic object. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:normalizedString" minOccurs = "1" maxOccurs = "1"/> <xs:element name="text" type="Text.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="attachments" type="Attachments.Type" minOccurs = "0" maxOccurs = "1"/> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:complexType> <xs:complexType name="Attachments.Type"> <xs:sequence> <xs:element name="attachment" type="Attachment.Type" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="Text.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Text for the discussion topic. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="texttype" use="required"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="text/plain"/> <xs:enumeration value="text/html"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="Attachment.Type"> <xs:complexContent> <xs:extension base="EmptyPrimitiveType.Type"> <xs:attribute name="href" use="required" type="xs:normalizedString"/> </xs:extension> </xs:complexContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="topic" type="Topic.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_imswl_v1p1.xsd 0000644 00000031027 15215711721 0012622 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1" targetNamespace="http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS CC WBLNK 1.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st August, 2010 Version: 1.1 Status: Final Description: This is the IMS GLC Web Links Data Model for the Common Cartridge. History: Version 1.0 - the first release of this data model; Version 1.1 - updates made to the namespace. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <xs:complexType name="EmptyPrimitiveType.Type"> <xs:complexContent> <xs:restriction base="xs:anyType"/> </xs:complexContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="WebLink.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The WebLink complexType for the associated object. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:normalizedString" minOccurs = "1" maxOccurs = "1"/> <xs:element name="url" type="URL.Type" minOccurs = "1" maxOccurs = "1"/> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:complexType> <xs:complexType name="URL.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The URL for the web link. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="EmptyPrimitiveType.Type"> <xs:attribute name="href" use="required" type="xs:normalizedString"/> <xs:attribute name="target" use="optional" type="xs:normalizedString"/> <xs:attribute name="windowFeatures" use="optional" type="xs:normalizedString"/> </xs:extension> </xs:complexContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="webLink" type="WebLink.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/imsbasiclti_v1p0p1.xsd 0000644 00000031550 15215711721 0013056 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" targetNamespace="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS BLTI 1.0.0" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:import namespace="http://www.imsglobal.org/xsd/imslticm_v1p0" schemaLocation="imslticm_v1p0.xsd"/> <xs:import namespace="http://www.imsglobal.org/xsd/imslticp_v1p0" schemaLocation="imslticp_v1p0.xsd"/> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Chuck Severance (IMS GLC) and Colin Smythe (IMS GLC) Date: 9th June, 2010 Version: 1.0.1 Status: Final Release Description: This is the description of the basicLTI link description. History: V1.0 - the first final release. V1.0.1 - the multiplicity for the extensions attribute has been changed from 0..1 to 0..*. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Basic Learning Tools Interoperability Version 1.0 found at http://www.imsglobal.org/lti and the original IMS GLC schema binding or code base http://www.imsglobal.org/lti. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2010. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="BasicLTILink.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The BasicLTILink class is the container for information required to use the BasicLTI mechanism. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:normalizedString" minOccurs = "1" maxOccurs = "1"/> <xs:element name="description" type="xs:string" minOccurs = "0" maxOccurs = "1"/> <xs:element name="custom" type="lticm:PropertySet.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="extensions" type="lticm:PlatformPropertySet.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="launch_url" minOccurs = "0" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value = "4096"/> <xs:minLength value = "1"/> <xs:whiteSpace value = "preserve"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="secure_launch_url" minOccurs = "0" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:maxLength value = "4096"/> <xs:minLength value = "1"/> <xs:whiteSpace value = "preserve"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="icon" type="lticp:Icon.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="secure_icon" type="lticp:Icon.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="vendor" type="lticp:Vendor.Type" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="basic_lti_link" type="BasicLTILink.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_lommanifest_v1p0.xsd 0000644 00000131654 15215711721 0014013 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" targetNamespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:sch="http://purl.oclc.org/dsdl/schematron" version="IMS CC MD 1.3 MAN 1.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st January, 2011 Version: 1.1 Status: Final Description: This is the IMS GLC Meta-data v1.3 binding of the IEEE LOMv1.0 for the Common Cartridge v1.1 Manifest Metadata. This is based on the LOM Strict bindings. The core changes are: a) MetaMetadata complexType is removed; b) Annotation complexType is removed; c) In the Technical complexType only the format element is permitted; d) In the Educational complexType only the 'learningResourceType' element and 'intendedEndUserRole' are permitted; e) In the General complexType the 'structure' and 'aggregationLevel' elements are prohibited; f) Only the 'contribute' element in the 'LifeCycle' complexType are permitted; History: Version 1.1: The first usage of this XSD for the CC Manifest Profile. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2011 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation> Schematron Strict Selection Validation Rules -------------------------------------------- This is the set of Schematron rules that have been created to enforce the use of the "Unordered" stereotype. These rules ensure that the number of entries in an unordered complexType of an element obey the multiplicity constraints. This is required so that any-order/any-multiplicity complexTypes can be constructed and validated. </xs:documentation> <xs:appinfo> <sch:ns uri="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" prefix="lom"/> <sch:title>Schematron validation rules for the enforcement of the Unordered stereotype.</sch:title> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="LOM.Type"> <sch:title>[RULESET] For the LOM.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom"> <sch:assert test="count(lom:general) = 0 or count(lom:general) = 1"> [RULE for Root Class Attribute 1] Invalid number of "general" elements: <sch:value-of select="count(lom:general)"/>. </sch:assert> <sch:assert test="count(lom:lifeCycle) = 0 or count(lom:lifeCycle) = 1"> [RULE for Root Class Attribute 2] Invalid number of "lifeCycle" elements: <sch:value-of select="count(lom:lifeCycle)"/>. </sch:assert> <sch:assert test="count(lom:technical) = 0 or count(lom:technical) = 1"> [RULE for Root Class Attribute 3] Invalid number of "technical" elements: <sch:value-of select="count(lom:technical)"/>. </sch:assert> <sch:assert test="count(lom:rights) = 0 or count(lom:rights) = 1"> [RULE for Root Class Attribute 5] Invalid number of "rights" elements: <sch:value-of select="count(lom:rights)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Rights.Type"> <sch:title>[RULESET] For the Rights.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:rights"> <sch:assert test="count(lom:cost) = 0 or count(lom:cost) = 1"> [RULE for Local Attribute 1] Invalid number of "cost" elements: <sch:value-of select="count(lom:cost)"/>. </sch:assert> <sch:assert test="count(lom:copyrightAndOtherRestrictions) = 0 or count(lom:copyrightAndOtherRestrictions) = 1"> [RULE for Local Attribute 2] Invalid number of "copyrightAndOtherRestrictions" elements: <sch:value-of select="count(lom:copyrightAndOtherRestrictions)"/>. </sch:assert> <sch:assert test="count(lom:description) = 0 or count(lom:description) = 1"> [RULE for Local Attribute 3] Invalid number of "description" elements: <sch:value-of select="count(lom:description)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="General.Type"> <sch:title>[RULESET] For the General.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:general"> <sch:assert test="count(lom:title) = 0 or count(lom:title) = 1"> [RULE for Local Attribute 2] Invalid number of "title" elements: <sch:value-of select="count(lom:title)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="ContributeLifeCycle.Type"> <sch:title>[RULESET] For the ContributeLifeCycle.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:lifeCycle/lom:contribute"> <sch:assert test="count(lom:role) = 0 or count(lom:role) = 1"> [RULE for Local Attribute 1] Invalid number of "role" elements: <sch:value-of select="count(lom:role)"/>. </sch:assert> <sch:assert test="count(lom:date) = 0 or count(lom:date) = 1"> [RULE for Local Attribute 3] Invalid number of "date" elements: <sch:value-of select="count(lom:date)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Relation.Type"> <sch:title>[RULESET] For the Relation.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:relation"> <sch:assert test="count(lom:kind) = 0 or count(lom:kind) = 1"> [RULE for Local Attribute 1] Invalid number of "kind" elements: <sch:value-of select="count(lom:kind)"/>. </sch:assert> <sch:assert test="count(lom:resource) = 0 or count(lom:resource) = 1"> [RULE for Local Attribute 2] Invalid number of "resource" elements: <sch:value-of select="count(lom:resource)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Resource.Type"> <sch:title>[RULESET] For the Resource.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:relation/lom:resource"> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Classification.Type"> <sch:title>[RULESET] For the Classification.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:classification"> <sch:assert test="count(lom:purpose) = 0 or count(lom:purpose) = 1"> [RULE for Local Attribute 1] Invalid number of "purpose" elements: <sch:value-of select="count(lom:purpose)"/>. </sch:assert> <sch:assert test="count(lom:description) = 0 or count(lom:description) = 1"> [RULE for Local Attribute 3] Invalid number of "description" elements: <sch:value-of select="count(lom:description)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Taxon.Type"> <sch:title>[RULESET] For the Taxon.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:classification/lom:taxonPath/lom:taxon"> <sch:assert test="count(lom:id) = 0 or count(lom:id) = 1"> [RULE for Local Attribute 1] Invalid number of "id" elements: <sch:value-of select="count(lom:id)"/>. </sch:assert> <sch:assert test="count(lom:entry) = 0 or count(lom:entry) = 1"> [RULE for Local Attribute 2] Invalid number of "entry" elements: <sch:value-of select="count(lom:entry)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="DateTime.Type"> <sch:title>[RULESET] For the DateTime.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:lifeCycle/lom:contribute/lom:date"> <sch:assert test="count(lom:dateTime) = 0 or count(lom:dateTime) = 1"> [RULE for Local Attribute 1] Invalid number of "dateTime" elements: <sch:value-of select="count(lom:dateTime)"/>. </sch:assert> <sch:assert test="count(lom:description) = 0 or count(lom:description) = 1"> [RULE for Local Attribute 2] Invalid number of "description" elements: <sch:value-of select="count(lom:description)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> <!-- RULESET ******************************************************************* --> <sch:pattern abstract="false" id="Identifier.Type"> <sch:title>[RULESET] For the Identifier.Type complexType.</sch:title> <sch:rule abstract="false" context="lom:lom/lom:general/lom:identifier"> <sch:assert test="count(lom:catalog) = 0 or count(lom:catalog) = 1"> [RULE for Local Attribute 1] Invalid number of "catalog" elements: <sch:value-of select="count(lom:catalog)"/>. </sch:assert> <sch:assert test="count(lom:entry) = 0 or count(lom:entry) = 1"> [RULE for Local Attribute 2] Invalid number of "entry" elements: <sch:value-of select="count(lom:entry)"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="lom:lom/lom:relation/lom:resource/lom:identifier"> <sch:assert test="count(lom:catalog) = 0 or count(lom:catalog) = 1"> [RULE for Local Attribute 1] Invalid number of "catalog" elements: <sch:value-of select="count(lom:catalog)"/>. </sch:assert> <sch:assert test="count(lom:entry) = 0 or count(lom:entry) = 1"> [RULE for Local Attribute 2] Invalid number of "entry" elements: <sch:value-of select="count(lom:entry)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- *************************************************************************** --> </xs:appinfo> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Global List Types *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <xs:simpleType name="CharacterString.Type"> <xs:restriction base="xs:string"/> </xs:simpleType> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Classification.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Classification complexType is the container for information which describes where this learning object falls within a particular classification system. To define multiple classifications, there may be multiple instances of this category. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="purpose" type="Purpose.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="taxonPath" type="TaxonPath.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="description" type="LangString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="keyword" type="LangString.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="Context.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Context complexType is the container for the information about the principal environment within which the learning and use of this learning object is intended to take place. Suggested good practice is to use one of the values of the value space and to use an additional instance of this data element for further refinement. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="source" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="LOMv1.0"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="higher education"/> <xs:enumeration value="school"/> <xs:enumeration value="training"/> <xs:enumeration value="other"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="ContributeLifeCycle.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Contribute complexType is the container for the entities (i.e. people, organizations) that have contributed to the state of the learning object. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="role" type="RoleLifeCycle.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="entity" type="CharacterString.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="date" type="DateTime.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="CopyrightAndOtherRestrictions.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The CopyrightAndOtherRestrictions complexType defines whether copyright or other restrictions apply to the use of this learning object. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="yes"/> <xs:enumeration value="no"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Cost.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Cost complexType is the container for whether use of this learning object requires payment. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="yes"/> <xs:enumeration value="no"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="DateTime.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The DateTime abstract complexType is the container for the annotated date/time. An accuracy of at least one second is supported. The ISO 8601 format is used. An associated description is also provided. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="dateTime" type="CharacterString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="description" type="LangString.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="Educational.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Educational complexType is the container for the information that describes the key educational or pedagogic characteristics of this learning object. This is pedagogical informtion essential to those involved in achieving a quality learning experience. The audience for this metadata includes teachers, managers, authors and learners. CC MANIFEST PROFILE Only a single instance of the 'learningResourceType' and multple instances of the 'ntendedEnduserRole' are permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="learningResourceType" type="LearningResourceType.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="context" type="Context.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="intendedEndUserRole" type="IntendedEndUserRole.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="General.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> CC AMANIFEST PROFILE The 'structure' and aggregationLevel' elements are prohibited. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="identifier" type="Identifier.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="title" type="LangString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="language" type="CharacterString.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="description" type="LangString.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="keyword" type="LangString.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="coverage" type="LangString.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="Identifier.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Identifier complexType is the container for the globally unique idenitifer that identifies the associated parent object. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="catalog" type="CharacterString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="entry" type="CharacterString.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="IntendedEndUserRole.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The IntendedEndUserRole complexType is the container for the information about the principal user(s) for which this learning object was designed, most dominant first. For Strict LOM binding this has an enumerated vocabulary. The Classification element can be used to describe the role through the skills the user is intended to master, or the tasks he or she is intended to be able to accomplish. CC Manifest Profile: Only the Iinstructor' 'Mentor' and 'Teacher' vocab values are permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="source" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="IMSGLC_CC_Rolesv1p1"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Instructor"/> <xs:enumeration value="Learner"/> <xs:enumeration value="Mentor"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Kind.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Kind complexType is the container for the nature of the relationship between this learning object and the target learning object, identified by information in the associated Resource complexType. In LOMv1.0 (Strict) this is an enumerated vocabulary. CC MANIFEST PROFILE Only the 'value' element is permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="ispartof"/> <xs:enumeration value="haspart"/> <xs:enumeration value="isversionof"/> <xs:enumeration value="hasversion"/> <xs:enumeration value="isformatof"/> <xs:enumeration value="hasformat"/> <xs:enumeration value="references"/> <xs:enumeration value="isreferencedby"/> <xs:enumeration value="isbasedon"/> <xs:enumeration value="isbasisfor"/> <xs:enumeration value="requires"/> <xs:enumeration value="isrequiredby"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="LangString.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LangString complexType is the container for a group of language specific characterstrings. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="string" type="LanguageString.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="LearningResourceType.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LearningResourceType complexType is the container for the information about the specific kind of learning object. The most dominant kind shall be first. The vocabulary terms are defined as in OED:1989 and as used by any educational communties of practice. CC MANIFEST PROFILE Only 'value' is permited and this is fixed as 'IMS Common Cartridge'. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="IMS Common Cartridge"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="LifeCycle.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LifeCycle complexType is the container for the history and current state of this learning object and those entities that have affected this learning object during its evolution. CC MANIFEST PROFILE Only the 'contribite' element is permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="contribute" type="ContributeLifeCycle.Type" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="LOM.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LOM complexType is the container for the metadata instance. CC MANIFEST PROFILE The MetaMetadata and Annotation complexTypes are prohibited. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="general" type="General.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="lifeCycle" type="LifeCycle.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="technical" type="Technical.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="educational" type="Educational.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="rights" type="Rights.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="relation" type="Relation.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="classification" type="Classification.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="Purpose.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Purpose complexType is the container for the information on the purpose of classifying this learning obect. For the Strict LOM binding this is an enumerated vocabulary. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="discipline"/> <xs:enumeration value="idea"/> <xs:enumeration value="prerequisite"/> <xs:enumeration value="educational objective"/> <xs:enumeration value="accessibility restrictions"/> <xs:enumeration value="educational level"/> <xs:enumeration value="skill level"/> <xs:enumeration value="security level"/> <xs:enumeration value="competency"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Relation.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Relation complexType is the container for the information that defines the relationship between this learning object and other learning objects, if any. To define multiple relationships, there may be multiple instances of this category. If there is more than one target learning object the each object shall have a new relationship instance. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="kind" type="Kind.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="resource" type="Resource.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="Resource.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Resource complexType is the container for the information about the target learning object that this relationship references. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="identifier" type="Identifier.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="description" type="LangString.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="Rights.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Rights complexType describes the intellectual property rights and conditions of use for this learning object. NOTE: The intent is to reuse results of ongoing work in the Intellectual Property Rights and e-commerce communities. This category currently provides the absolute minimum level of detail only. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="cost" type="Cost.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="copyrightAndOtherRestrictions" type="CopyrightAndOtherRestrictions.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="description" type="LangString.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="RoleLifeCycle.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Role complexType is the container for the kind of contribution. CC MANIFEST PROFILE Only the 'value' elemet is permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="author"/> <xs:enumeration value="publisher"/> <xs:enumeration value="unknown"/> <xs:enumeration value="initiator"/> <xs:enumeration value="terminator"/> <xs:enumeration value="validator"/> <xs:enumeration value="editor"/> <xs:enumeration value="graphical designer"/> <xs:enumeration value="technical implementor"/> <xs:enumeration value="content provider"/> <xs:enumeration value="technical validator"/> <xs:enumeration value="educational validator"/> <xs:enumeration value="script writer"/> <xs:enumeration value="instructional designer"/> <xs:enumeration value="subject matter expert"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Taxon.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Taxon complexType is the container for the information about a particular term within a taxonomy. A taxon is a node that has a defined label or term. A taxon may also have an alphanumeric designation or identifier for standardized reference. Either or both the label and the entry may be used to designate a particular taxon. An ordered list of taxons creates a taxonomic path i.e. 'taxononomic stairway': this is a path from a more general to more specific entry in a classification. </xs:documentation> </xs:annotation> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="id" type="CharacterString.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="entry" type="LangString.Type" minOccurs = "0" maxOccurs = "1"/> </xs:choice> </xs:complexType> <xs:complexType name="TaxonPath.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The TaxonPath complexType is the container for the information about the taxonomic path in a specific classification system. Each succeeding level is a refinement in the definition of the preceding level. There may be different paths in the same or different classifications, which describe the same characteristic. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="taxon" type="Taxon.Type" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="Technical.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Technical complexType is the container for the information that describes the technical requirements and characteristics of this learning object. CC MANIFEST PROFILE Only the 'format' element is permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="format" type="CharacterString.Type" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="LanguageString.Type"> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="language" use="optional" type="CharacterString.Type"/> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="lom" type="LOM.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/imslticm_v1p0.xsd 0000644 00000031125 15215711721 0012131 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imslticm_v1p0" targetNamespace="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS LTICM 1.0.0" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Chuck Severance (IMS GLC) and Colin Smythe (IMS GLC) Date: 30th April, 2010 Version: 1.0 Status: Final Release Description: This is the description of the Common Messaging objects in LTI. This version was created for the BasicLTI Final release. History: V1.0 - First final release. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Basic Learning Tools Interoperability Version 1.0 found at http://www.imsglobal.org/lti and the original IMS GLC schema binding or code base http://www.imsglobal.org/lti. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2010. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <xs:attributeGroup name="extension.Property.Attr"> <xs:anyAttribute namespace = "##other" processContents = "strict"/> </xs:attributeGroup> <xs:attributeGroup name="extensions.PlatformPropertySet.Attr"> <xs:anyAttribute namespace = "##other" processContents = "strict"/> </xs:attributeGroup> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <xs:simpleType name="Name.Type"> <xs:restriction base="xs:Name"/> </xs:simpleType> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="PropertySet.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The PropertySet complexType is the container for the set of properties. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="property" type="Property.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="PlatformPropertySet.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Platform complexType is the container for the set of properties for the platform. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="PropertySet.Type"> <xs:sequence> </xs:sequence> <xs:attribute name="platform" use="required" type="Name.Type"/> <xs:attributeGroup ref="extensions.PlatformPropertySet.Attr"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="Property.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The Property complexType is the container for each property. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="name" use="required" type="Name.Type"/> <xs:attributeGroup ref="extension.Property.Attr"/> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_qtiasiv1p2p1_v1p0.xsd 0000644 00000350457 15215711721 0013745 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" targetNamespace="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:sch="http://purl.oclc.org/dsdl/schematron" version="IMS CC 1.1 QTI ASI 1.2.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:import namespace = "http://www.w3.org/XML/1998/namespace" schemaLocation = "xml.xsd"/> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st October, 2010 Version: 1.1 Status: Final Release Description: This model forms part of the IMS Common Cartridge v1.1 specification. This model is the profile of the IMS QTI v1.2.1 specification. The changes made to the original specification to create this profile are: Root (a) Only one Assessment or one ObjectBank can be exchanged i.e. not Sections or Items; (b) The qticomment attribute has been removed. Assessment (a) Only a single Section must be contained in the Assessment; (b) The metadata is mandatory (see the specification for the fields that must be defined) with a multiplicity of 1; (c) All of the other Assessment fields have been removed apart from the optional PresentationMaterial; (d) The title attribute has been made mandatory. Object Bank (a) Only Items can be containd in the object bank; (b) The Qticomment structure has been removed. Section (a) All of the internal structure has been removed except the containment of Items (at least one is required); (b) The language attribute has been removed. Item (a) The deprecated attributes have been removed from the ItemMetadata class; (b) All qticomment attributes have been removed from various classes; (c) The Item pre and post condition attributes have been removed from the Item class; (d) The itemproc_extension structure has been removed from the Item class; (e) The reference structure has been removed from the Item class; (f) The objectives attribute has been removed from the Item class; (g) The itemcontrol attribute has been removed from the Item class; (h) The item rubric attribute and its associated classes have been removed from the Item class; (i) The duration attribute has been removed from the Item class; (j) The itemmetadata attribute has been made mandatory in the Item class; (k) The label, language and maxattempts attributes have been removed from the Item class; (l) The interpretvar attribute has been removed frm the Outcomes class; (m) The response_xy, response_num, response_grp and response_extension attributes have been removed from the Presentation class; (n) All extension attributes have been removed from various classes; (o) The ItemType, LevelofDifficulty, RenderingType, ResponseMultiplicity, Status and TypeofSolution have been deleted; (p) The ItemFeedback Class has been restructured as only flow_mat is used; (q) A single decvar is required i.e. for the default variable SCORE. Item-Response (a) The Rrange and Rarea attributes have been removed from the ResponseLabel Class. Item-Render (a) The render_hotspot attributes and the RenderHotspot class have been removed; (b) The render_slider attributes and the RenderSlider class have been removed; (c) The render_extension attribute has been removed from the ResponseType class; (d) The response_na attribute has been removed; (e) The qticomment attribute has been removed from the ResponseLabel class; Item-General (a) The Interpretvar class has been removed; (b) The DurationBase class has been removed and all derived attributes from the Conditionvar and CompositionLogic classes; (c) The var_extension attribute has been removed from the Conditionvar class; (d) The qticomment attributes have been removed from the Hint and Solution classes; (e) Setvar is restricted to the Set action; (f) Variable types are restricted to 'Integer' and 'Decimal'; (g) The defaultval and cutvalue, attributes for decvar have been removed; (h) Outcomes is limtied to a single decvar which must have only the variable "SCORE" declared; (i) The classes or, varlt, varlte, vargt, vargte, varinside, varsubset and unanswered have been removed; (j) The classes 'and', 'not' and 'conditionvar' have been significantly simplied i.e. see clause above for detail; (k) The feedbacktype attribute for the displayfeedback element is now mandatory with no default value. Selection and Ordering (a) The entire Selection and Ordering (Package) has been removed. Outcomes (a) The entire Outcomes Processing (Package) has been removed. Material (a) The Reference Class has been removed as this is not required; (b) All entityref attributes have been removed; (c) Extensions have been removed; (d) Only 'mattext', 'matref' and 'matbreak' are permitted as types of material. Common (a) The Rubric class has been removed Items and Sections and it is only permitted to contain a single instance of material; (b) The ContextControl class has been removed i.e. no Control switches required; (c) The QtiComment Class has been removed; (d) The MetadataRule Class has been removed; (e) the Parameterization Class has been removed; (f) The 'view' attribute has been removed; (g) The Vocabulary Class has been removed and its attribute in the QtiMetadata Class also removed. History: Version 1.0: This profile is taken from the UML description of the QTIv1.2.1 specification. Version 1.1: Material has been returned as an alternative to flow_mat in ItemFeedback, SolutionMaterial and HintMaterial. Also, Assessment Rubric has been permitted but it must only contain a single instance of material. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation> Schematron Validation Rules Information --------------------------------------- Author: Colin Smythe Date: 5th January, 2011 Version: 1.1 Status: Final Release Description: This set of schematron rules have been created to increase the validation capability of the QTIv1.2.1 XSD. A total of 12 rule sets have been created to ensure that: [1] Only valid Assessment metadata is defined; [2] Only valid Question types are defined in the Itemmetatadata fields; [3] The structure of a True/False question is correct; [4] The structure of a Multiple-Choice (Single Response) question is correct; [5] The structure of a Multiple-Choice (Multiple Response) question is correct; [6] The structure of a Fill In Blank question is correct; [7] The structure of a Pattern Match question is correct; [8] The structure of an Essay question is correct; [9] The structures of the response processing/response-label are correct; [10] The structure of a Hint in an Item is correct; [11] The structure of a Solution in an Item is correct; [12] The structure of the response processing feedback structure in an Item is correct; [13] Generic features of an Item are correctly implemented. Rule Set: [1] The set of rules to ensure that only valid Assessment metadata is defined. The rules are: (a) Only valid entries are defined for the Assessment metadata fieldlabel values; (b) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_assessmenttype'; (c) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_scoretype'; (d) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_feedbackpermitted'; (e) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_hintspermitted'; (f) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_solutionspermitted'; (g) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'qmd_timelimit'; (h) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'cc_allow_late_submission'; (i) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'cc_maxattempts'; (j) Only valid entries are defined for the Assessment metadata fieldentry values for fieldlabel = 'cc_profile'; (k) There must be no more than one Assessment metadata field with fieldlabel='cc_profile'; (l) There must be no more than one Assessment metadata field with fieldlabel='qmd_assessmenttype'; (m) There must be no more than one Assessment metadata field with fieldlabel='qmd_scoretype'; (n) There must be no more than one Assessment metadata field with fieldlabel='qmd_feedbackpermitted'; (o) There must be no more than one Assessment metadata field with fieldlabel='qmd_hintspermitted'; (p) There must be no more than one Assessment metadata field with fieldlabel='qmd_solutionspermitted'; (q) There must be no more than one Assessment metadata field with fieldlabel='qmd_timelimit'; (r) There must be no more than one Assessment metadata field with fieldlabel='cc_allow_late_submission'; (s) There must be no more than one Assessment metadata field with fieldlabel='cc_maxattempts'. [2] The set of rules to ensure that only valid Question types are defined in the Itemmetatadata fields. The rules are: (a) Only valid entries are defined for the Itemmetadata fieldlabel values; (b) Only valid entries are defined for the Itemmetadata fieldentry values for fieldlabel = 'cc_profile'; (c) Only valid entries are defined for the Itemmetadata fieldentry values for fieldlabel = 'cc_weighting'; (d) Only valid entries are defined for the Itemmetadata fieldentry values for fieldlabel = 'qmd_scoringpermitted'; (e) Only valid entries are defined for the Itemmetadata fieldentry values for fieldlabel = 'qmd_computerscored'; (f) The Item metadata field with fieldlabel='cc_profile' must be present; (g) There must only be one Item metadata fields with fieldlabel='cc_profile'for each Item; (h) There must be no more than one Item metadata field with fieldlabel='cc_weighting'for each Item; (i) There must be no more than one Item metadata field with fieldlabel='qmd_scoringpermitted' for each Item; (j) There must be no more than one Item metadata field with fieldlabel='qmd_computerscored'for each Item. [3] The set of rules to ensure that the structure of a True/False question is correct. The rules are: (a) That @rcardinality='Single'; (b) The element 'response_str' is not used; (c) The element 'render_fib' is not used; (d) That there are zero or two response labels; (e) That the response processing uses the correct identifiers i.e. varequal/@respident value is equal to the response_lid/@ident; (f) The varsubstring element must not be used. [4] The set of rules to ensure that the structure of a Multiple-Choice (Single Response) question is correct. The rules are: (a) That @rcardinality='Single'; (b) The element 'response_str' is not used; (c) The element 'render_fib' is not used; (d) That there are at least three response labels; (e) That the response processing uses the correct identifiers i.e. varequal/@respident value is equal to the response_lid/@ident; (f) The varsubstring element must not be used. [5] The set of rules to ensure that the structure of a Multiple-Choice (Multiple Response) question is correct. The rules are: (a) That @rcardinality='Multiple'; (b) The element 'response_str' is not used; (c) The element 'render_fib' is not used; (d) That there are at least two response labels; (e) That the response processing uses the correct identifiers i.e. varequal/@respident value is equal to the response_lid/@ident; (f) The varsubstring element must not be used.. [6] The set of rules to ensure that the structure of a Fill-in-Blank question is correct. The rules are: (a) The element 'response_lid' is not used; (b) The element 'render_choice' is not used; (c) That the response processing uses the correct identifiers i.e. varequal/@respident value is equal to the response_str/@ident; (d) There is no use of the 'varsubstring' element. [7] The set of rules to ensure that the structure of a Pattern Match question is correct. The rules are: (a) The element 'response_lid' is not used; (b) The element 'render_choice' is not used; (c) That the response processing uses the correct identifiers i.e. varequal/@respident value is equal to the response_str/@ident. [8] The set of rules to ensure that the structure of an Essay question is correct. The rules are: (a) The element 'response_lid' is not used; (b) The element 'render_choice' is not used; (c) There is no use of the 'varequal' element; (d) There is no use of the 'varsubstring' element; (e) No more than a single Solution feedback can be supplied; (f) The fieldentry Item metadata for an Essay question with the fieldlabel='qmd_computerscored' must be set as 'No'. [9] The set of rules to ensure that the Response Processing/Response-Label structures are valid. The rules are: (a) There are no invalid response labels used in the 'varequal' element for T/F and MC questions. [10] The set of rules to ensure that the structure of a Hint in an Item is correct. The rules are: (a) The element 'displayfeedback@feedbacktype=Hint' is accompanied by the @linkrefid='hint' attribute; (b) The element 'itemfeedback@ident=hint' is not followed by the 'solution' element; (c) The element 'itemfeedback@ident=hint' is not followed by the 'flow_mat' element; (d) If the element 'displayfeedback@feedbacktype=Hint' is used it must be accompanied by hint feedback. [11] The set of rules to ensure that the structure of a Solution in an Item is correct. The rules are: (a) The element 'displayfeedback@feedbacktype=Solution' is accompanied by the @linkrefid='solution' attribute; (b) The element 'itemfeedback@ident=solution' is not followed by the 'hint' element; (c) The element 'itemfeedback@ident=solution' is not followed by the 'flow_mat' element; (d) If the element 'displayfeedback@feedbacktype=Solution' is used it must be accompanied by solution feedback. [12] The set of rules to ensure that the general structure of the Response Processing in an Item is correct. The rules are: (a) The element 'displayfeedback@feedbacktype=Response' is not accompanied by the @linkrefid='solution' attribute; (b) The element 'displayfeedback@feedbacktype=Response' is not accompanied by the @linkrefid='hint' attribute; (c) To check that for each element 'displayfeedback@feedbacktype=Response' the corresponding 'itemfeedback' element exists; (d) To check that a given response processing itemfeedback identifier there is a corresponding displayfeedback trigger for that identifier. [13] The set of rules to ensure that the general features in an Item are correct. The rules are: (a) To ensure that each Item has a unique @ident attribute. </xs:documentation> <xs:appinfo> <sch:ns uri="http://www.imsglobal.org/xsd/ims_qtiasiv1p2" prefix="asi"/> <sch:title>Schematron validation rules for the Common Cartridge v1p1 profile of QTI v1.2.1</sch:title> <!-- RULE 1: Assessment Metadata Fields Validation --> <sch:pattern abstract="false" name="RULE SET 1"> <sch:title>RULE SET 1: Ensure that only valid metadata is defined in the Assessment metadata fields</sch:title> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield"> <sch:assert test="asi:fieldlabel='cc_profile' or asi:fieldlabel='qmd_assessmenttype' or asi:fieldlabel='qmd_scoretype' or asi:fieldlabel='qmd_feedbackpermitted' or asi:fieldlabel='qmd_hintspermitted' or asi:fieldlabel='qmd_solutionspermitted' or asi:fieldlabel='qmd_timelimit' or asi:fieldlabel='cc_allow_late_submission' or asi:fieldlabel='cc_maxattempts'"> [RULE 1a] Invalid Assessment metadata fieldlabel value: <sch:value-of select="asi:fieldlabel"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_assessmenttype']"> <sch:assert test="../asi:fieldentry='Examination'"> [RULE 1b] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_assessmenttype' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoretype']"> <sch:assert test="../asi:fieldentry='Percentage'"> [RULE 1c] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_scoretype' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_feedbackpermitted']"> <sch:assert test="../asi:fieldentry='No' or ../asi:fieldentry='Yes'"> [RULE 1d] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_feedbackpermitted' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_hintspermitted']"> <sch:assert test="../asi:fieldentry='No' or ../asi:fieldentry='Yes'"> [RULE 1e] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_hintspermitted' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_solutionspermitted']"> <sch:assert test="../asi:fieldentry='No' or ../asi:fieldentry='Yes'"> [RULE 1f] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_solutionspermitted' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_timelimit']"> <sch:assert test="not(contains(../asi:fieldentry, '.')) and ../asi:fieldentry > 0 and ../asi:fieldentry < 527041"> [RULE 1g] Invalid fieldentry Assessment metadata for the fieldlabel='qmd_timelimit' i.e. not a valid integer with value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='cc_allow_late_submission']"> <sch:assert test="../asi:fieldentry='No' or ../asi:fieldentry='Yes'"> [RULE 1h] Invalid fieldentry Assessment metadata for the fieldlabel='cc_allow_late_submission' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='cc_maxattempts']"> <sch:assert test="../asi:fieldentry='Examination' or ../asi:fieldentry='1' or ../asi:fieldentry='2' or ../asi:fieldentry='3' or ../asi:fieldentry='4' or ../asi:fieldentry='5' or ../asi:fieldentry='unlimited'"> [RULE 1i] Invalid fieldentry Assessment metadata for the fieldlabel='cc_maxattempts' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='cc_profile']"> <sch:assert test="../asi:fieldentry='cc.exam.v0p1'"> [RULE 1j] Invalid fieldentry Assessment metadata for the fieldlabel='cc_profile' value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:assessment/asi:qtimetadata"> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile']) < 2"> [RULE 1k] There are repeated assessment metadata fields of fieldlabel='cc_profile' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_assessmenttype']) < 2"> [RULE 1l] There are repeated assessment metadata fields of fieldlabel='qmd_assessmenttype' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_assessmenttype'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoretype']) < 2"> [RULE 1m] There are repeated assessment metadata fields of fieldlabel='qmd_scoretype' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoretype'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_feedbackpermitted']) < 2"> [RULE 1n] There are repeated assessment metadata fields of fieldlabel='qmd_feedbackpermitted' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_feedbackpermitted'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_hintspermitted']) < 2"> [RULE 1o] There are repeated assessment metadata fields of fieldlabel='qmd_hintspermitted' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_hintspermitted'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_solutionspermitted']) < 2"> [RULE 1p] There are repeated assessment metadata fields of fieldlabel='qmd_solutionspermitted' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_solutionspermitted'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_timelimit']) < 2"> [RULE 1q] There are repeated assessment metadata fields of fieldlabel='qmd_timelimit' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_timelimit'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_allow_late_submission']) < 2"> [RULE 1r] There are repeated assessment metadata fields of fieldlabel='cc_allow_late_submission' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_allow_late_submission'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_maxattempts']) < 2"> [RULE 1s] There are repeated assessment metadata fields of fieldlabel='cc_maxattempts' with count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_maxattempts'])"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 2: Item Metadata Fields Validation --> <sch:pattern abstract="false" name="RULE SET 2"> <sch:title>RULE SET 2: Ensure that only valid metadata is defined in the Itemmetatadata fields</sch:title> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata/asi:qtimetadatafield"> <sch:assert test="asi:fieldlabel='cc_profile' or asi:fieldlabel='cc_question_category' or asi:fieldlabel='cc_weighting' or asi:fieldlabel='qmd_scoringpermitted' or asi:fieldlabel='qmd_computerscored'"> [RULE 2a] Invalid Itemmetadata fieldlabel value: <sch:value-of select="../asi:fieldlabel"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='cc_profile']"> <sch:assert test="../asi:fieldentry='cc.multiple_choice.v0p1' or ../asi:fieldentry='cc.multiple_response.v0p1' or ../asi:fieldentry='cc.true_false.v0p1' or ../asi:fieldentry='cc.fib.v0p1' or ../asi:fieldentry='cc.pattern_match.v0p1' or ../asi:fieldentry='cc.essay.v0p1'"> [RULE 2b] Invalid fieldentry Question Type (fieldlabel='cc_profile') value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='cc_weighting']"> <sch:assert test="not(contains(../asi:fieldentry, '.')) and ../asi:fieldentry > 0 and ../asi:fieldentry < 100"> [RULE 2c] Invalid fieldentry Item metadata weighting for the fieldlabel='cc_weighting' i.e. not an integer in the range 1..99, with value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoringpermitted']"> <sch:assert test="../asi:fieldentry = 'Yes'"> [RULE 2d] Invalid fieldentry Item metadata for the fieldlabel='qmd_scoringpermitted' with value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_computerscored']"> <sch:assert test="../asi:fieldentry = 'No' or ../asi:fieldentry = 'Yes'"> [RULE 2e] Invalid fieldentry Item metadata for the fieldlabel='qmd_computerscored' with value: <sch:value-of select="../asi:fieldentry"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:itemmetadata/asi:qtimetadata"> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile']) > 0"> [RULE 2f] The Item metadata field with fieldlabel='cc_profile' is missing. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile'])=0 or count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile'])=1"> [RULE 2g] There are too many Item metadata fields with fieldlabel='cc_profile' i.e. count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_profile'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_weighting']) < 2"> [RULE 2h] There are too many Item metadata fields with fieldlabel='cc_weighting' i.e. count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='cc_weighting'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoringpermitted']) < 2"> [RULE 2i] There are too many Item metadata fields with fieldlabel='qmd_scoringpermitted' i.e. count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_scoringpermitted'])"/>. </sch:assert> <sch:assert test="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_computerscored']) < 2"> [RULE 2j] There are too many Item metadata fields with fieldlabel='qmd_computerscored' i.e. count=<sch:value-of select="count(asi:qtimetadatafield/asi:fieldlabel[.='qmd_computerscored'])"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 3: True/False Question Structure Fields Validation --> <sch:pattern name="RULE SET 3"> <sch:title>RULE SET 3: Ensure that the structure of a True/False question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.true_false.v0p1']"> <sch:assert test="../../../../asi:presentation/asi:response_lid[@rcardinality='Single']"> [RULE 3a] Incorrect cardinality attribute value for the T/F question: <sch:value-of select="../../../../asi:presentation/asi:response_lid/@rcardinality"/> </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str) = 0"> [RULE 3b] The invalid 'response_str' element has been used for a T/F question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_fib) = 0"> [RULE 3c] The invalid 'render_fib' element has been used for a T/F question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_choice/asi:response_label) = 2"> [RULE 3d] There are too many or too few response labels for the T/F question. </sch:assert> <sch:assert test="../../../../asi:presentation/asi:response_lid/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"> [RULE 3e] The T/F response processing Varequal respident attribute (<sch:value-of select="../../../../asi:presentation/asi:response_lid/@ident"/>) value is not equal to the corresonding Response_lid ident (<sch:value-of select="../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/>) attribute for the True/False question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varsubstring) = 0"> [RULE 3f] There is an invalid use of the varsubstring element in a T/F question. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 4: Multiple Choice-Single Response Question Structure Fields Validation --> <sch:pattern name="RULE SET 4"> <sch:title>RULE SET 4: Ensure that the structure of a MC-SR question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.multiple_choice.v0p1']"> <sch:assert test="../../../../asi:presentation/asi:response_lid[@rcardinality='Single']"> [RULE 4a] Incorrect cardinality attribute value for the MC-SR question: <sch:value-of select="../../../../asi:presentation/asi:response_lid/@rcardinality"/>. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str) = 0"> [RULE 4b] The invalid 'response_str' element has been used for a MC-SR question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_fib) = 0"> [RULE 4c] The invalid 'render_fib' element has been used for a MC-SR question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_choice/asi:response_label) > 2"> [RULE 4d] There are too too few response labels for the MC-SR question. </sch:assert> <sch:assert test="../../../../asi:presentation/asi:response_lid/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"> [RULE 4e] The MC-SR response processing Varequal respident attribute (<sch:value-of select="../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/>) value is not equal to the corresonding Response_lid ident (<sch:value-of select="../../../../asi:presentation/asi:response_lid/@ident"/>) attribute for the Multiple Choice single response question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/descendant::asi:varsubstring) = 0"> [RULE 4f] There is an invalid use of the varsubstring element in a MC-SR question. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 5: Multiple Choice-Multiple Response Question Structure Fields Validation --> <sch:pattern name="RULE SET 5"> <sch:title>RULE SET 5: Ensure that the structure of a MC-MR question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.multiple_response.v0p1']"> <sch:assert test="../../../../asi:presentation/asi:response_lid[@rcardinality='Multiple']"> [RULE 5a] Incorrect cardinality attribute value for the MC-MR question: <sch:value-of select="../../../../asi:presentation/asi:response_lid/@rcardinality"/>. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str) = 0"> [RULE 5b] The invalid 'response_str' element has been used for a MC-MR question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_fib) = 0"> [RULE 5c] The invalid 'render_fib' element has been used for a MC-MR question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_lid/asi:render_choice/asi:response_label) > 1"> [RULE 5d] There are too too few response labels for the MC-MR question. </sch:assert> <sch:assert test="../../../../asi:presentation/asi:response_lid/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident or ../../../../asi:presentation/asi:response_lid/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:and/asi:varequal/@respident"> [RULE 5e] The MC-MR response processing Varequal respident attribute (<sch:value-of select="../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/>) value is not equal to the corresonding Response_lid ident (<sch:value-of select="../../../../asi:presentation/asi:response_lid/@ident"/>) attribute for the Multiple Choice multiple response question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varsubstring) = 0"> [RULE 5f] There is an invalid use of the varsubstring element in a MC-MR question. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 6: Fill-in-Blank Question Structure Fields Validation --> <sch:pattern name="RULE SET 6"> <sch:title>RULE SET 6: Ensure that the structure of a FIB question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.fib.v0p1']"> <sch:assert test="count(../../../../asi:presentation/asi:response_lid) = 0"> [RULE 6a] The invalid 'response_lid' element has been used for a FIB question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str/asi:render_choice) = 0"> [RULE 6b] The invalid 'render_choice' element has been used for a FIB question. </sch:assert> <sch:assert test="../../../../asi:presentation/asi:response_str/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"> [RULE 6c] The FIB response processing Varequal respident attribute (<sch:value-of select="../../../../asi:presentation/asi:response_str/@ident"/>) value is not equal to the corresonding Response_str ident (<sch:value-of select="../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/>) attribute for the FIB question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varsubstring) = 0"> [RULE 6d] There is an invalid use of the 'varsubstring' element in the FIB question. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 7: Pattern Match Question Structure Fields Validation --> <sch:pattern name="RULE SET 7"> <sch:title>RULE SET 7: Ensure that the structure of a Pattern Match question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.pattern_match.v0p1']"> <sch:assert test="count(../../../../asi:presentation/asi:response_lid) = 0"> [RULE 7a] The invalid 'response_lid' element has been used for a Pattern Match question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str/asi:render_choice) = 0"> [RULE 7b] The invalid 'render_choice' element has been used for a Pattern Match question. </sch:assert> <sch:assert test="../../../../asi:presentation/asi:response_str/@ident = ../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"> [RULE 7c] The FIB response processing Varequal respident attribute (<sch:value-of select="../../../../asi:presentation/asi:response_str/@ident"/>) value is not equal to the corresonding Response_str ident (<sch:value-of select="../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/>) attribute for the Pattern Match question. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 8: Essay Question Structure Fields Validation --> <sch:pattern name="RULE SET 8"> <sch:title>RULE SET 8: Ensure that the structure of a Essay question is correct</sch:title> <sch:rule abstract="false" context="asi:fieldentry[.='cc.essay.v0p1']"> <sch:assert test="count(../../../../asi:presentation/asi:response_lid) = 0"> [RULE 8a] The invalid 'response_lid' element has been used for an Essay question. </sch:assert> <sch:assert test="count(../../../../asi:presentation/asi:response_str/asi:render_choice) = 0"> [RULE 8b] The invalid 'render_choice' element has been used for an Essay question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/descendant::asi:varequal) = 0"> [RULE 8c] There is an invalid use of the 'varequal' element in an Essay question. </sch:assert> <sch:assert test="count(../../../../asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varsubstring) = 0"> [RULE 8d] There is an invalid use of the 'varsubstring' element in an Essay question. </sch:assert> <sch:assert test="count(../../../../asi:itemfeedback/asi:solution) < 2"> [RULE 8e] Too many Solution feedback structures are supplied. </sch:assert> <sch:assert test="../../../asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel='qmd_computerscored' and ../../../asi:qtimetadata/asi:qtimetadatafield/asi:fieldentry = 'No'"> [RULE 8f] Invalid fieldentry Item metadata for an Essay question with the fieldlabel='qmd_computerscored' with value: <sch:value-of select="../../../asi:qtimetadata/asi:qtimetadatafield/asi:fieldlabel[.='qmd_computerscored']/../asi:fieldentry"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 9: General Response Processing/Response-Label structure validation --> <!-- <sch:pattern name="RULE SET 9"> <sch:title>RULE SET 9: Ensure that the response processing operates on valid response labels</sch:title> <sch:rule abstract="false" context="asi:item"> <sch:let name="ResponseLidIdent" value="asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal/@respident"/> <sch:let name="VarEqualValue" value="asi:resprocessing/asi:respcondition/asi:conditionvar/asi:varequal[@respident=$ResponseLidIdent]"/> <sch:assert test="count(asi:presentation/asi:response_lid[@ident = $ResponseLidIdent]/asi:render_choice/asi:response_label[@ident=$VarEqualValue]) = count(asi:presentation/asi:response_lid[@ident = $ResponseLidIdent]/asi:render_choice/asi:response_label)"> [RULE 9a] At least <sch:value-of select="count(asi:presentation/asi:response_lid[@ident = $ResponseLidIdent]/asi:render_choice/asi:response_label)-count(asi:presentation/asi:response_lid[@ident = $ResponseLidIdent]/asi:render_choice/asi:response_label[@ident=$VarEqualValue])"/> of the varequal response label values (<sch:value-of select="$VarEqualValue"/>) is/are invalid in a T/F or MC question. </sch:assert> </sch:rule> </sch:pattern> --> <!-- **************************************************************************** --> <!-- RULE 10: Hint structure validation --> <sch:pattern name="RULE SET 10"> <sch:title>RULE SET 10: Ensure that the structure of a Hint in an Item is correct</sch:title> <sch:rule abstract="false" context="asi:item"> <sch:assert test="count(asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Solution' and @linkrefid='hint']) = 0"> [RULE 10a] The displayfeedback@feedbacktype='Solution' has been used to identify a Hint. </sch:assert> <sch:assert test="count(asi:itemfeedback[@ident='hint']/asi:solution)=0"> [RULE 10b] The itemfeedback@ident='hint' has been used to identify a Solution. </sch:assert> <sch:assert test="count(asi:itemfeedback[@ident='hint']/asi:flow_mat)=0"> [RULE 10c] The itemfeedback@ident='hint' has been used to identify Response feedback. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:item/asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Hint']"> <sch:assert test="count(../../../asi:itemfeedback[@ident='hint']) = 1"> [RULE 10d] A displayfeedback[@feedbacktype='Hint'] trigger has no accompanying hint feedback. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 11: Solution structure validation (in this Rule any type of Item is permitted support for solutions) --> <sch:pattern name="RULE SET 11"> <sch:title>RULE SET 11: Ensure that the structure of a Solution in an Item is correct</sch:title> <sch:rule abstract="false" context="asi:item"> <sch:assert test="count(asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Hint' and @linkrefid='solution']) = 0"> [RULE 11a] The displayfeedback@feedbacktype='Hint' has been used to identify a Solution. </sch:assert> <sch:assert test="count(asi:itemfeedback[@ident='solution']/asi:hint) = 0"> [RULE 11b] The itemfeedback@ident='solution' has been used to identify a Hint. </sch:assert> <sch:assert test="count(asi:itemfeedback[@ident='solution']/asi:flow_mat) = 0"> [RULE 11c] The itemfeedback@ident='solution' has been used to identify Response feedback. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:item/asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Solution']"> <sch:assert test="count(../../../asi:itemfeedback[@ident='solution']) = 1"> [RULE 11d] A displayfeedback[@feedbacktype='Solution'] trigger has no accompanying solution feedback. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 12: General Response Processing Feedback structure validation --> <sch:pattern name="RULE SET 12"> <sch:title>RULE SET 12: Ensure that the structure of the response processing feedback structure in an Item is correct</sch:title> <sch:rule abstract="false" context="asi:item"> <sch:assert test="count(asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Response' and @linkrefid='solution']) = 0"> [RULE 12a] The displayfeedback@feedbacktype='Response' has been used to identify a Solution. </sch:assert> <sch:assert test="count(asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Response' and @linkrefid='hint']) = 0"> [RULE 12b] The displayfeedback@feedbacktype='Response' has been used to identify a Hint. </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:item/asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Response']"> <sch:assert test="../../../asi:itemfeedback/@ident = @linkrefid"> [RULE 12c] For the given displayfeedback identifier (<sch:value-of select="@linkrefid"/>) there is no itemfeedback with the correct identifier (<sch:value-of select="../../../asi:itemfeedback/@ident"/>). </sch:assert> </sch:rule> <sch:rule abstract="false" context="asi:item/asi:itemfeedback/asi:flow_mat"> <sch:assert test="../@ident = ../../asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Response']/@linkrefid"> [RULE 12d] For the given response processing itemfeedback identifier (<sch:value-of select="../@ident"/>) there is no displayfeedback trigger with the correct identifier (<sch:value-of select="../../asi:resprocessing/asi:respcondition/asi:displayfeedback[@feedbacktype='Response']/@linkrefid"/>). </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 13: The generic features of an Item must be correct --> <sch:pattern name="RULE SET 13"> <sch:title>RULE SET 13: Ensure that the generic features of an Item are correct</sch:title> <sch:rule abstract="false" context="asi:item"> <sch:assert test="not(@ident=preceding-sibling::asi:item/@ident)"> [RULE 13a] There are Items that have common identifiers: <sch:value-of select="@ident"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> </xs:appinfo> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <xs:complexType name="EmptyPrimitiveTypeType"> <xs:complexContent> <xs:restriction base="xs:anyType"/> </xs:complexContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <xs:element name="matbreak" type="EmptyPrimitiveTypeType"/> <xs:element name="other" type="EmptyPrimitiveTypeType"/> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="questestinteropType"> <xs:annotation> <xs:documentation source="umlcommentbox"> The root structure can consist of either: * A single object bank; * An assessment; * A set of item(s); * A set of section(s); * Any combination of items and sections. As the root this is mandatory and can occur once. Common Cartridge Profile A Commn Cartridge is allowed to contain only an objectbank or an assessement. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "1"> <xs:element name="objectbank" type="objectbankType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="assessment" type="assessmentType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="assessmentType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General An Assessment object is the logical construction of the test. An Assessment must contain at least one Section which in turn will contain Items and/or Sections. An Assessment must eventually be constructed from a set of Items i.e. questions that are to be offered in the test. An Assessment object contains all of the information to make the use of individual Items meaningful i.e. apart from the Sections the object includes the relationships between the Sections, the group evaluation processing and the corresponding feedback. Common Cartridge Profile The Assessment must consist of only one Section. The only other permitted attributes are the mandatory identifier and title. Optional language, metadata, rubric and presentation_material is also supported. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="qtimetadata" type="qtimetadataType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="rubric" type="rubricType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="presentation_material" type="presentation_materialType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="section" type="sectionType" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> <xs:attribute name="title" use="required" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="objectbankType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General An object bank is an unordered set of items and sections. It is recommended that there is a metadata description for the object bank as a whole. The object bank should have a globally unique identifier. Common Cartridge Profile In the Common Cartridge the object-bank can only contain Items. Meta-data support is available but the profile has no mandated entries. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="qtimetadata" type="qtimetadataType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="item" type="itemType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> </xs:complexType> <xs:complexType name="sectionType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General A Section is the core grouping concept within QTI. A Section consists of one or more Items or Sections (or references to Items and Sections). In general, the objects contained within a Secton will have some relationship either in terms of content or content delivery. The Section data structure is used to define arbitrarily complex hierarchical section and item data structures. It may contain meta-data, objectives, rubric control switches, assessment-level processing, feedback and selection and sequencing information for sections and items. Common Cartridge Profile In Common Cartridge the Section can only contain a list of Items. No metadata is permitted. The identifier is mandatory but the title and language are optional. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="item" type="itemType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> <xs:attribute name="title" use="optional" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="itemType"> <xs:annotation> <xs:documentation source="umlcommentbox"> An Item the smallest exchangeable object within QTI-XML. An Item is more than a ‘Question’ in that it contains the ‘Question’, the presentation/rendering instructions, the response processing to be applied to the participant’s response(s), the feedback that may be presented (including hints and solutions) and the meta-data describing the Item. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="itemmetadata" type="itemmetadataType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="presentation" type="presentationType" minOccurs = "0" maxOccurs = "1"/> <xs:element name="resprocessing" type="resprocessingType" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="itemfeedback" type="itemfeedbackType" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> <xs:attribute name="title" use="optional" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="itemmetadataType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains all of the QTI-specific metada-data to be applied to the Item. This meta-data can consist of either entries defined using an external vocabulary or the individually named entries. Usage of the contained attributes are deprecated apart from the 'qtimetadata' attribute. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="qtimetadata" type="qtimetadataType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="presentationType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains all of the instructions for the presentation of the question during an evaluation. This information includes the actual material to be presented. The labels for the possible responses are also identified and these are used by the response processing element defined elsewhere in the Item. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "1"> <xs:element name="flow" type="flowType" minOccurs = "1" maxOccurs = "1"/> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_lid" type="response_lidType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_str" type="response_strType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:choice> </xs:sequence> <xs:attribute name="label" use="optional" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> <xs:attribute name="x0" use="optional" type="xs:string"/> <xs:attribute name="y0" use="optional" type="xs:string"/> <xs:attribute name="width" use="optional" type="xs:string"/> <xs:attribute name="height" use="optional" type="xs:string"/> </xs:complexType> <xs:complexType name="resprocessingType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This contains all of the instructions for the response processing. This includes the scoring variables to contain the associated scores and the set of response condition tests that are to be applied to the received user response. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="outcomes" type="outcomesType" minOccurs = "1" maxOccurs = "1"/> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="respcondition" type="respconditionType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="outcomesType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains all of the variable declarations that are to be made available to the scoring algorithm. Each variable is declared using the 'decvar' structure apart from the default variable called ‘SCORE’ that is an integer and has a default value of zero (0). Common Cartridge Profile A single variable is permitted. This is "SCORE". </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="decvar" type="decvarType" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="itemfeedbackType"> <xs:annotation> <xs:documentation source="umlcommentbox"> The container for the feedback that is to be presented as a result of the user’s responses. The feedback can include hints and solutions and both of these can be revealed in a variety of different ways. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:choice minOccurs = "1" maxOccurs = "1"> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:element name="solution" type="solutionType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="hint" type="hintType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> <xs:attribute name="title" use="optional" type="xs:string"/> </xs:complexType> <xs:complexType name="respconditionType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains the actual test to be applied to the user responses to determine their correctness or otherwise. Each 'respcondition' contains an actual test, the assignment of a value to the associate scoring variables and the identification of the feedback to be associated with the test. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="conditionvar" type="conditionvarType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="setvar" type="setvarType" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="displayfeedback" type="displayfeedbackType" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="title" use="optional" type="xs:string"/> <xs:attribute name="continue" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Yes"/> <xs:enumeration value="No"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> <xs:complexType name="presentation_materialType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the information that is to be presented to set the context for the contained objects. This can be used to contain question-based material that is common to all of the child objects. This is used in Assessment and Section objects. </xs:documentation> <xs:documentation source="umlcommentbox"> This is the information that is used to describe the solution. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="flow_matType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This allows the materials to be displayed to the users to be grouped together using flows. The manner in which these flows are handled is dependent upon the display-engine. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="class" use="optional" default="Block" type="xs:string"/> </xs:complexType> <xs:complexType name="materialType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the container for any content that is to be displayed by the question-engine. The supported content types are text (emphasised or not), images, audio, video, application and applet. The content can be internally referenced to avoid the need for duplicate copies. Alternative information can be defined – this is used if the primary content cannot be displayed. Common Cartridge Profile Only the 'mattext', 'matref' and 'matbreak' types are permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="mattext" type="mattextType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="matref" type="matrefType" minOccurs = "1" maxOccurs = "1"/> <xs:element ref="matbreak" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:element name="altmaterial" type="altmaterialType" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="label" use="optional" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="altmaterialType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the container for alternative content. This content is to be displayed if, for whatever reason, the primary content cannot be rendered. Alternative language implementations of the host 'material' element are also supported using this structure. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="mattext" type="mattextType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="matref" type="matrefType" minOccurs = "1" maxOccurs = "1"/> <xs:element ref="matbreak" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="flowType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the container for all of the instructions for the presentation with flow blocking of the question during a test. This information includes the actual material to be presented. The labels for the possible responses are also identified and these are used by the response processing element defined elsewhere in the Item. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="flow" type="flowType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_lid" type="response_lidType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_str" type="response_strType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="class" use="optional" default="Block" type="xs:string"/> </xs:complexType> <xs:complexType name="render_choiceType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains the instructions for the question-engine to render the question using a classical multiple choice format. The number of possible responses is determined by the 'response_label' structures contained. Both flowed and non-flowed formats are supported </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_label" type="response_labelType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="flow_label" type="flow_labelType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="shuffle" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Yes"/> <xs:enumeration value="No"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="minnumber" use="optional" type="xs:string"/> <xs:attribute name="maxnumber" use="optional" type="xs:string"/> </xs:complexType> <xs:complexType name="render_fibType"> <xs:annotation> <xs:documentation source="umlcommentbox"> Contains the instructions for the question-engine to render the question using a classical fill-in blank format. The number of possible responses is determined by the 'response_label' structures contained. Both flowed and non-flowed formats are supported. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_label" type="response_labelType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="flow_label" type="flow_labelType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="encoding" use="optional" default="utf-8" type="xs:string"/> <xs:attribute name="charset" use="optional" default="ascii-us" type="xs:string"/> <xs:attribute name="rows" use="optional" type="xs:string"/> <xs:attribute name="columns" use="optional" type="xs:string"/> <xs:attribute name="maxchars" use="optional" type="xs:string"/> <xs:attribute name="minnumber" use="optional" type="xs:string"/> <xs:attribute name="maxnumber" use="optional" type="xs:string"/> <xs:attribute name="prompt" use="optional" default="Box"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Asterisk"/> <xs:enumeration value="Box"/> <xs:enumeration value="Dashline"/> <xs:enumeration value="Underline"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="fibtype" use="optional" default="String"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Decimal"/> <xs:enumeration value="Integer"/> <xs:enumeration value="Scientific"/> <xs:enumeration value="String"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> <xs:complexType name="qtimetadataType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General The QTI-specific meta-data assigned to the parent object i.e. Assessment, Section, Item or Object-bank. This approach requires that the approriate external vocabulary is defined and referenced in label/field tuple. Common Cartridge Profile The Vocabulary structure has been removed. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="qtimetadatafield" type="qtimetadatafieldType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="qtimetadatafieldType"> <xs:annotation> <xs:documentation source="umlcommentbox"> The structure responsible for containing each of the QTI-specific meta-data fields. The label should appear in the vocabulary. If no vocabulary is used then external validation cannot take place. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="fieldlabel" type="xs:string" minOccurs = "1" maxOccurs = "1"/> <xs:element name="fieldentry" type="xs:string" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> <xs:attribute ref="xml:lang" use="optional"/> </xs:complexType> <xs:complexType name="rubricType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is used to contain contextual information i.e. rubric that is important to the object e.g. it could contain standard data values that might or might not be useful for answering the question. Common Cartridge Profile This is used in assessments to provide instructions. Only a single mattext is permitted in the material. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="response_labelType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This contains the possible response choices that are presented to the user. This information includes the material to be shown to the user and the logical label that is associated with that response. The label is used in the response processing. Flow and non-flow approaches are supported. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "0" maxOccurs = "unbounded"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="ident" use="required" type="xs:string"/> <xs:attribute name="labelrefid" use="optional" type="xs:string"/> <xs:attribute name="rshuffle" use="optional" default="Yes"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Yes"/> <xs:enumeration value="No"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="match_group" use="optional" type="xs:string"/> <xs:attribute name="match_max" use="optional" type="xs:string"/> </xs:complexType> <xs:complexType name="flow_labelType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the blocking/paragraph equivalent to the 'response_label' structure. This structure shold always be used to encapsulate the 'response_label construct. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="flow_label" type="flow_labelType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="response_label" type="response_labelType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="class" use="optional" default="Block" type="xs:string"/> </xs:complexType> <xs:complexType name="response_lidType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This contains the instructions for the presentation of questions whose response will be either a 'response_lid' (logical identifier), 'response_xy' (xy-co-ordinate), 'response_str' (a string), 'response_num' (a number) and 'response_grp' ( a group of logical identifiers). The question can be rendered in a variety of ways depending on the way in which the material is to be presented to the participant. Common Cartridge Profile Only the response_lid and response_str response types are supported with render_choice and render_fib rendering. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "0" maxOccurs = "1"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:choice minOccurs = "1" maxOccurs = "1"> <xs:element name="render_choice" type="render_choiceType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="render_fib" type="render_fibType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:choice minOccurs = "0" maxOccurs = "1"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="rcardinality" use="optional" default="Single"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Single"/> <xs:enumeration value="Multiple"/> <xs:enumeration value="Ordered"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="rtiming" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Yes"/> <xs:enumeration value="No"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="ident" use="required" type="xs:string"/> </xs:complexType> <xs:complexType name="response_strType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This contains the instructions for the presentation of questions whose response will be either a 'response_lid' (logical identifier), 'response_xy' (xy-co-ordinate), 'response_str' (a string), 'response_num' (a number) and 'response_grp' ( a group of logical identifiers). The question can be rendered in a variety of ways depending on the way in which the material is to be presented to the participant. Common Cartridge Profile Only the response_lid and response_str response types are supported with render_choice and render_fib rendering. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "0" maxOccurs = "1"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:choice minOccurs = "1" maxOccurs = "1"> <xs:element name="render_choice" type="render_choiceType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="render_fib" type="render_fibType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> <xs:choice minOccurs = "0" maxOccurs = "1"> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="material_ref" type="material_refType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> <xs:attribute name="rcardinality" use="optional" default="Single"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Single"/> <xs:enumeration value="Multiple"/> <xs:enumeration value="Ordered"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="rtiming" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Yes"/> <xs:enumeration value="No"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="ident" use="required" type="xs:string"/> </xs:complexType> <xs:complexType name="solutionType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General Contains the solution(s) that are to be revealed to the participant. When these solutions are revealed is outside the scope of the specification. The information can be revealed in several manners. The default mode is to show the 'Complete' solution. Common Cartridge Profile The material is presented in the form of flow_mat only. The only form of feedback is 'Complete'. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="solutionmaterial" type="solutionmaterialType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="feedbackstyle" use="optional" default="Complete"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Complete"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> <xs:complexType name="hintType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General Contains the hints(s) that are to be revealed to the participant. When these hints are revealed is outside the scope of the specification. The information can be revealed in several manners. The default mode is to show the 'Complete' hint. Common Cartridge Profile The material is presented in the form of flow_mat only. The only form of feedback is 'Complete'. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="hintmaterial" type="hintmaterialType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="feedbackstyle" use="optional" default="Complete"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Complete"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:complexType> <xs:complexType name="conditionvarType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General The conditional test that is to be applied to the user’s response. A wide range of separate and combinatorial tests can be applied. Common Cartridge Profile The extension has been removed. All of the conditions operating on duration have been removed. The only permited conditions are 'varequal', 'varsubstring', 'and', 'other'. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="and" type="andType" minOccurs = "1" maxOccurs = "1"/> <xs:element ref="other" minOccurs = "1" maxOccurs = "1"/> <xs:element name="varequal" type="varequalType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="varsubstring" type="varsubstringType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="solutionmaterialType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the information that is used to describe the solution. </xs:documentation> </xs:annotation> <xs:choice> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "unbounded"/> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="hintmaterialType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the information that is presented as a hint. </xs:documentation> </xs:annotation> <xs:choice> <xs:element name="flow_mat" type="flow_matType" minOccurs = "1" maxOccurs = "unbounded"/> <xs:element name="material" type="materialType" minOccurs = "1" maxOccurs = "unbounded"/> </xs:choice> </xs:complexType> <xs:complexType name="andType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This defines the set of clauses that can be used within the logic statements of 'and', 'or' and 'not'. These logic statements enable the construction of complex conditional tests. Common Cartridge Profile The extension has been removed. All of the conditions operating on duration have been removed. The 'or' conditional has been removed and 'and' and 'not' have been considerably simplified. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="not" type="notType" minOccurs = "1" maxOccurs = "1"/> <xs:element name="varequal" type="varequalType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="notType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This defines the set of clauses that can be used within the logic statements of 'and', 'or' and 'not'. These logic statements enable the construction of complex conditional tests. Common Cartridge Profile The extension has been removed. All of the conditions operating on duration have been removed. The 'or' conditional has been removed and 'and' and 'not' have been considerably simplified. </xs:documentation> </xs:annotation> <xs:sequence> <xs:choice minOccurs = "1" maxOccurs = "unbounded"> <xs:element name="varequal" type="varequalType" minOccurs = "1" maxOccurs = "1"/> </xs:choice> </xs:sequence> </xs:complexType> <xs:complexType name="mattextType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the container of any text that is to be displayed to the users. It is the base type for normal and emphasised text. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="texttype" use="optional" default="text/plain" type="xs:string"/> <xs:attribute name="charset" use="optional" default="ascii-us" type="xs:string"/> <xs:attribute name="label" use="optional" type="xs:string"/> <xs:attribute name="uri" use="optional" type="xs:string"/> <xs:attribute name="width" use="optional" type="xs:string"/> <xs:attribute name="height" use="optional" type="xs:string"/> <xs:attribute name="x0" use="optional" type="xs:string"/> <xs:attribute name="y0" use="optional" type="xs:string"/> <xs:attribute ref="xml:lang" use="optional"/> <xs:attribute ref="xml:space" use="optional" default="default"/> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="material_refType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is used to contain a reference to the required full material block. This material will have had an identifier assigned to enable such a reference to be reconciled when the instance is parsed into the system. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="EmptyPrimitiveTypeType"> <xs:attribute name="linkrefid" use="required" type="xs:string"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="matrefType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is used to contain a reference to the required full material block. This material will have had an identifier assigned to enable such a reference to be reconciled when the instance is parsed into the system. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="EmptyPrimitiveTypeType"> <xs:attribute name="linkrefid" use="required" type="xs:string"/> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="decvarType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This is used for the declaration of the scoring variables. The name of the variable that is to be declared. The default name is ‘SCORE’. All numeric variables have the associated ‘x.min’, ‘x.max’ and ‘x.normalised’ variables declared. Several of the optional attributes depend upon the type of variable that is declared. Common Cartridge Profile Only the variable types of decimal and integer are permitted. Only a single "SCORE" variable is permitted. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="varname" use="required" fixed="SCORE" type="xs:string"/> <xs:attribute name="vartype" use="optional" default="Integer"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Decimal"/> <xs:enumeration value="Integer"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="minvalue" use="optional" type="xs:string"/> <xs:attribute name="maxvalue" use="optional" type="xs:string"/> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="setvarType"> <xs:annotation> <xs:documentation source="umlcommentbox"> QTI General This is responsible for changing the value of the scoring variable as a result of the associated response processing test. The default action is to 'Set' the 'SCORE' variable to the value given in the string. Common Cartridge Profile Only the actons Set, Add and Subtract are premitted. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="varname" use="optional" default="SCORE" type="xs:string"/> <xs:attribute name="action" use="optional" default="Set"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Set"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="varequalType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the test of equivalence. The data for the test is contained as a string. The accompanying attributes are the identifier for the associated 'response_label', the ordinal 'index' of the response and whether or not the value is case-sensitive when processing strings. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="respident" use="required" type="xs:string"/> <xs:attribute name="case" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="No"/> <xs:enumeration value="Yes"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="displayfeedbackType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is used for assigning an associated feedback to the aggregated scoring if the ‘True’ state results. Feedback for hints, solutions and response processing are supported. The feedback is identified using the pointer 'linkrefid'. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="feedbacktype" use="required"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Response"/> <xs:enumeration value="Solution"/> <xs:enumeration value="Hint"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="linkrefid" use="required" type="xs:string"/> </xs:extension> </xs:simpleContent> </xs:complexType> <xs:complexType name="varsubstringType"> <xs:annotation> <xs:documentation source="umlcommentbox"> This is the test for a contained substring. The data for comparison is presented as a string. The associated attributes are the response label identifier, the ordinal index of the response and whether or not the value is case-sensitive when processing the strings. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="respident" use="required" type="xs:string"/> <xs:attribute name="case" use="optional" default="No"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="No"/> <xs:enumeration value="Yes"/> </xs:restriction> </xs:simpleType> </xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="questestinterop" type="questestinteropType"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/xml.xsd 0000644 00000013140 15215711721 0010237 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.w3.org/XML/1998/namespace" xml:lang="en"> <xs:annotation> <xs:documentation> See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace. This schema document describes the XML namespace, in a form suitable for import by other schema documents. Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The following names are currently defined in this namespace and should not be used with conflicting semantics by any Working Group, specification, or document instance: base (as an attribute name): denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification. id (as an attribute name): denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification. lang (as an attribute name): denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification. space (as an attribute name): denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification. Father (in any context at all): denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups: In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000 reserves for Jon Bosak in perpetuity the XML name xml:Father </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation>This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define. To enable this, such a schema must import this schema for the XML namespace, e.g. as follows: <schema . . .> . . . <import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/> Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g. <type . . .> . . . <attributeGroup ref="xml:specialAttrs"/> will define a type which will schema-validate an instance element with any of those attributes</xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation>In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2007/08/xml.xsd. At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd. The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2007/08/xml.xsd will not change. </xs:documentation> </xs:annotation> <xs:attribute name="lang"> <xs:annotation> <xs:documentation>Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility. See RFC 3066 at http://www.ietf.org/rfc/rfc3066.txt and the IANA registry at http://www.iana.org/assignments/lang-tag-apps.htm for further information. The union allows for the 'un-declaration' of xml:lang with the empty string.</xs:documentation> </xs:annotation> <xs:simpleType> <xs:union memberTypes="xs:token"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="" /> </xs:restriction> </xs:simpleType> </xs:union> </xs:simpleType> </xs:attribute> <xs:attribute name="space"> <xs:simpleType> <xs:restriction base="xs:NCName"> <xs:enumeration value="default" /> <xs:enumeration value="preserve" /> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="base" type="xs:anyURI"> <xs:annotation> <xs:documentation>See http://www.w3.org/TR/xmlbase/ for information about this attribute.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attribute name="id" type="xs:ID"> <xs:annotation> <xs:documentation>See http://www.w3.org/TR/xml-id/ for information about this attribute.</xs:documentation> </xs:annotation> </xs:attribute> <xs:attributeGroup name="specialAttrs"> <xs:attribute ref="xml:base" /> <xs:attribute ref="xml:lang" /> <xs:attribute ref="xml:space" /> <xs:attribute ref="xml:id" /> </xs:attributeGroup> </xs:schema> cc/schemas11/imslticc_v1p0p1.xsd 0000644 00000034304 15215711721 0012362 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" targetNamespace="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS LTICC 1.0.0" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:import namespace="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" schemaLocation="imsbasiclti_v1p0p1.xsd"/> <xs:import namespace="http://www.imsglobal.org/xsd/imslticp_v1p0" schemaLocation="imslticp_v1p0.xsd" /> <xs:import namespace="http://www.imsglobal.org/xsd/imslticm_v1p0" schemaLocation="imslticm_v1p0.xsd" /> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Chuck Severance (IMS GLC) and Colin Smythe (IMS GLC) Date: 9th June, 2010 Version: 1.0.1 Status: Final Release Description: This is the description of the resource linkfile that is to be placed inside a Common Cartridge. History: V1.0 - the first Final Release. V1.0.1 - changed to use the imsbasiclti_v1p0p1.xsd. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <xs:attributeGroup name="extension.ResourceRef.Attr"> <xs:anyAttribute namespace = "##other" processContents = "strict"/> </xs:attributeGroup> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <xs:simpleType name="Name.Type"> <xs:restriction base="xs:Name"/> </xs:simpleType> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="CartridgeBasicLTILink.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The CartridgeBasicLTILink class is the container for the information about the use of BasicLTI with a Common Cartridge. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="blti:BasicLTILink.Type"> <xs:sequence> <xs:element name="cartridge_bundle" type="ResourceRef.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="cartridge_icon" type="ResourceRef.Type" minOccurs = "0" maxOccurs = "1"/> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="CartridgeToolLocator.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The ToolLocator complexType is the container for the tool locator information for the cartridge BasicLTI resource. </xs:documentation> </xs:annotation> <xs:complexContent> <xs:extension base="lticp:ToolLocator.Type"> <xs:sequence> <xs:element name="tool_settings" type="lticm:PropertySet.Type" minOccurs = "0" maxOccurs = "1"/> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:extension> </xs:complexContent> </xs:complexType> <xs:complexType name="ResourceRef.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The ResourceRef complexType is the container for the resource reference. </xs:documentation> </xs:annotation> <xs:simpleContent> <xs:extension base="xs:normalizedString"> <xs:attribute name="identifierref" use="required" type="Name.Type"/> <xs:attributeGroup ref="extension.ResourceRef.Attr"/> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="cartridge_basiclti_link" type="CartridgeBasicLTILink.Type"/> <xs:element name="lti_tool_locator" type="CartridgeToolLocator.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_lomresource_v1p0.xsd 0000644 00000041757 15215711721 0014040 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" targetNamespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS CC MD RES 1.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st January, 2011 Version: 1.1 Status: Final Description: This is the IMS GLC Meta-data v1.3 binding of the IEEE LOMv1.0 for the Common Cartridge v1.1 Resource Metadata. This is based on the LOM strict binding. The core changes are: a) General complexType removed; b) LifeCycle complexType removed; c) Metametadata complexType removed; d) Technical complexType removed; e) Rights complexType removed; f) Annotation complexType removed; g) Classification complexType removed; h) The educational element is required at least once in the metadata instance; i) For Educational the context is required once and intendedEndUserRole at least once; j) All order is now imposed using sequence; k) All vocabs are constrained as required for the context and intendedEndUserRole elements. History: Version 1.0 - the first release of this profile for the CC Resource metadata; Version 1.1 - the 'mentor' enumeration value is added and the 'school' enumeration value are added License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2011 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Global List Types *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <xs:simpleType name="CharacterString.Type"> <xs:restriction base="xs:string"/> </xs:simpleType> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Context.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Context complexType is the container for the information about the principal environment within which the learning and use of this learning object is intended to take place. Suggested good practice is to use one of the values of the value space and to use an additional instance of this data element for further refinement. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="source" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="LOMv1.0"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="higher education"/> <xs:enumeration value="school"/> <xs:enumeration value="training"/> <xs:enumeration value="other"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Educational.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The Educational complexType is the container for the information that describes the key educational or pedagogic characteristics of this learning object. This is pedagogical informtion essential to those involved in achieving a quality learning experience. The audience for this metadata includes teachers, managers, authors and learners. CC Resource Profile: Only single context and intendedUserRole elements are permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="context" type="Context.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="intendedEndUserRole" type="IntendedEndUserRole.Type" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="IntendedEndUserRole.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The IntendedEndUserRole complexType is the container for the information about the principal user(s) for which this learning object was designed, most dominant first. For Strict LOM binding this has an enumerated vocabulary. The Classification element can be used to describe the role through the skills the user is intended to master, or the tasks he or she is intended to be able to accomplish. CC Resource Profile: Only the 'Instructor', 'Mentor' and 'Learner' vocab values are permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="source" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="IMSGLC_CC_Rolesv1p1"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="value" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="Instructor"/> <xs:enumeration value="Learner"/> <xs:enumeration value="Mentor"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="LangString.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LangString complexType is the container for a group of language specific characterstrings. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="string" type="LanguageString.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="LOM.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> The LOM complexType is the container for the metadata instance. CC Resource Profile - only a single Educational element is permitted. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="educational" type="Educational.Type" minOccurs = "1" maxOccurs = "unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="LanguageString.Type"> <xs:simpleContent> <xs:extension base="xs:string"> <xs:attribute name="language" use="optional" type="CharacterString.Type"/> </xs:extension> </xs:simpleContent> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="lom" type="LOM.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/ccv1p1_imscp_v1p2_v1p0.xsd 0000644 00000174025 15215711721 0013457 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" targetNamespace="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" xmlns:autz="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" xmlns:lomm="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" xmlns:lomr="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:sch="http://purl.oclc.org/dsdl/schematron" version="IMS CC 1.1 CP 1.2" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd" /> <xs:import namespace="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" schemaLocation="ccv1p1_imsccauth_v1p1.xsd" /> <xs:import namespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" schemaLocation="ccv1p1_lommanifest_v1p0.xsd"/> <xs:import namespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" schemaLocation="ccv1p1_lomresource_v1p0.xsd"/> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st January, 2011 Version: 1.1 Status: Final Release Description: This model forms a part of the IMS GLC Common Catrtridge specification. This model is the profile of the IMS GLC Content Package Core specification (v1.2). The changes made to create the profile are: (a) The 'default' characteristic for the 'Organizations' class has been removed; (b) The 'isvisible' characteristic for the Item class has been removed; (c) The 'parameters' characteristic for the Item class has been removed; (d) The 'structure' characteristic for the Organization class has been fixed to 'rooted-hierarchy'; (e) The 'extension' and 'version' characteristics for the Manifest class have been removed; (f) The 'extension' attribute and 'extension' characteristic have been removed the Organizations class; (g) The 'extension' attribute and 'extension' characteristic have been removed the Organization class; (h) The 'extension' attribute and 'extension' characteristic have been removed from the Resources class; (i) The 'extension' attribute and the 'extension' characteristic have been removed from the Resource class; (j) The 'extension' attribute and 'extension' characteristic have been removed from the Item class; (k) The 'extension' attribute and the 'extension' characteristic have been removed from the File class; (l) The 'extension' attribute and 'extension' characteristic have been removed from the Dependency class; (m) The 'type' characteristic of the Resource class has a new enumerated list of 'PredfinedContentTypes'; (n) The 'title' attribute for the Item class has its multiplicity changed to '1' except for the top-level item attribute in the Orgaization class where it must not occur; (o) Manifests are NO longer allowed to contain sub-manifests; (p) The multiplicity of the Organization class in the Organizations class has been made '0..1'; (q) Only the metadata in the Manifest class is permitted to have the 'schema' and 'schemaversion' attributes; (r) The 'schema', 'schemaversion' and 'title' attributes are redefined directly as 'String' primitiveTypes; (s) The multiplicity of the 'item' attribute in the Organization class has been made '1'; (t) The value for the 'schema' attribute has been set as 'IMS Common Cartridge'; (u) The value for the 'schemaversion' attribute has been set to '1.1.0'; (v) The authorizations attribute has been added as an imported extension to the manifest; (w) The protect attribute has been added as an imported extension to the resource; (x) The IEEE LOM attribute has been added as an imported extension to the manifest metadata; (y) The 'intendeduse' attribute has been added to the ResourceType complexType. History: This profile is taken from the formal representation in UML of the IMS CP v1.2 core specification. This is the second version of the profile for the CCv1.1. The two changes are the addition of the ResourceMetadata class and the vocabulary for the permitted resource types. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2011 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <xs:annotation> <xs:documentation> Schematron Validation Rules Information --------------------------------------- Author: Colin Smythe Date: 29th November, 2010 Version: 1.1 Status: Final Release Description: This set of schematron rules have been created to increase the validation capability of the CPv1.2 XSD for CCv1.1. A total of 10 rule sets have been created to ensure that: [1] The set of rules to ensure that resources for Discussion Topics are correctly provided; [2] The set of rules to ensure that resources for Web Links are correctly provided; [3] The set of rules to ensure that resources for Assessments are correctly provided; [4] The set of rules to ensure that resources for Question Banks are correctly provided; [5] The set of rules to ensure that resources for Web Content are correctly provided; [6] The set of rules to ensure that resources for Associated Content are correctly provided; [7] The set of general rules to ensure that Dependencies are used correctly; [8] The set of general rules to ensure that Items are defined correctly; [9] The set of general rules to ensure that Files are used correctly; [10] The set of general rules to ensure that Resources are used correctly; [11] The set of rules to ensure that resources for BasicLTI are correctly provided. Rule Set: [1] The set of rules to ensure that resources for Discussion Topics are correctly provided. The rules are: (a) Detect for the Discussion Topic Resource when the required 'file' element is missing; (b) Detect for the Discussion Topic Resource when too many 'file' elements are declared; (c) Ensure for the Discussion Topic Resource that the 'href' attribute is not used on the 'resource' element; (d) Detect for the Discussion Topic Resource when it has a prohibited dependency on a Discussion Topic; (e) Detect for the Discussion Topic Resource when it has a prohibited dependency on a Web Link; (f) Detect for the Discussion Topic Resource when it has a prohibited dependency on an Assessment; (g) Detect for the Discussion Topic Resource when it has a prohibited dependency on a Question Bank; (h) Detect for the Discussion Topic Resource when it has a prohibited dependency on a BasicLTI link; [2] The set of rules to ensure that resources for Web Links are correctly provided. The rules are: (a) Detect for the Web Links Resource when the required 'file' element is missing; (b) Detect for the Web Links Resource when too many 'file' elements are declared; (c) Ensure for the Web Links Resource that the 'href' attribute is not used on the 'resource' element; (d) Ensure for the Web Links Resource that the 'dependency' element is not used; [3] The set of rules to ensure that resources for Assessments are correctly provided. The rules are: (a) Detect for the Assessment Resource when the required 'file' element is missing; (b) Detect for the Assessment Resource when too many 'file' elements are declared; (c) Ensure for the Assessment Resource that the 'href' attribute is not used on the 'resource' element; (d) Detect for the Assessment Resource when it has a prohibited dependency on a Discussion Topic; (e) Detect for the Assessment Resource when it has a prohibited dependency on a Web Link; (f) Detect for the Assessment Resource when it has a prohibited dependency on an Assessment; (g) Detect for the Assessment Resource when it has a prohibited dependency on a Question Bank; (h) Detect for the Assessment Resource when it has a prohibited dependency on a BasicLTI link; [4] The set of rules to ensure that resources for Question Banks are correctly provided. The rules are: (a) Ensure that no more than one Qestion Bank is defiend as a resource; (b) Detect for the Question Bank Resource when the required 'file' element is missing; (c) Detect for the Question Bank Resource when too many 'file' elements are declared; (d) Ensure for the Question Bank Resource that the 'href' attribute is not used on the 'resource' element; (e) Detect for the Question Bank Resource when it has a prohibited dependency on a Discussion Topic; (f) Detect for the Question Bank Resource when it has a prohibited dependency on a Web Link; (g) Detect for the Question Bank Resource when it has a prohibited dependency on an Assessment; (h) Detect for the Question Bank Resource when it has a prohibited dependency on a Question Bank; (i) Detect for the Question Bank Resource when it has a prohibited dependency on a BasicLTI link; [5] The set of rules to ensure that resources for Web Content are correctly provided. The rules are: (a) Detect for the Web Content Resource when it has a prohibited dependency on a Discussion Topic; (b) Detect for the Web Content Resource when it has a prohibited dependency on a Web Link; (c) Detect for the Web Content Resource when it has a prohibited dependency on an Assessment; (d) Detect for the Web Content Resource when it has a prohibited dependency on a Question Bank; (e) Detect for the Web Content Resource when it has a prohibited dependency on a Associated Content; (f) Detect for the Web Content Resource when it has a prohibited dependency on a BasicLTI link; (g) A Web Content Resource has a missing 'href' attribute (required because the resource is directly referenced by an Item). [TO BE VERIFIED] [6] The set of rules to ensure that resources for Associated Content are correctly provided. The rules are: (a) Detect for the Associated Content Resource when it has a prohibited dependency on a Discussion Topic; (b) Detect for the Associated Content Resource when it has a prohibited dependency on a Web Link; (c) Detect for the Associated Content Resource when it has a prohibited dependency on an Assessment; (d) Detect for the Associated Content Resource when it has a prohibited dependency on a Question Bank; (e) Detect for the Associated Content Resource when it has a prohibited dependency on a Associated Content; (f) Detect for the Associated Content Resource when it has a prohibited dependency on a BasicLTI link; (g) An Associated Content Resource has a missing 'href' attribute (required because the resource is directly referenced by an Item). [TO BE VERIFIED] [7] The set of general rules to ensure that Dependencies are used correctly. The rules are: (a) A 'dependency' element has a circular reference to its host 'resource' element with identifier; (b) In a 'resource' element at least two 'dependency' elements reference the same 'resource' element. [8] The set of general rules to ensure that Items are defined correctly. The rules are: (a) An Item has a prohibited reference(s) to a Question Bank Resource; (b) A Learning Object Item contains prohibited child Item(s). [9] The set of general rules to ensure that Files are used correctly. The rules are: (a) In a 'resource' element at least two 'file' elements have the same 'href' attribute; [10] The set of general rules to ensure that Resources are used correctly. The rules are: (a) For a 'resource' element with the 'href' attribute the 'file' element is missing; (b) For a 'resource' element with the 'href' attribute there is no 'file' element with the correct 'href' attribute. [11] The set of rules to ensure that resources for BasicLTI are correctly provided. The rules are: (a) Detect for the BasicLTI Resource when it has a prohibited dependency on a Discussion Topic; (b) Detect for the BasicLTI Resource when it has a prohibited dependency on a Web Link; (c) Detect for the BasicLTI Resource when it has a prohibited dependency on an Assessment; (d) Detect for the BasicLTI Resource when it has a prohibited dependency on a Question Bank; (e) Detect for the BasicLTI Resource when it has a prohibited dependency on a Web Content; (f) Detect for the BasicLTI Resource when it has a prohibited dependency on a BasicLTI link. </xs:documentation> <xs:appinfo> <sch:ns uri="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" prefix="cp"/> <sch:title>Schematron validation rules for the Common Cartridge v1p1 profile of CP v1.1.4/1.2</sch:title> <!-- RULE 1: Discussion Topic Resource Validation --> <sch:pattern abstract="false" name="RULE SET 1"> <sch:title>RULE SET 1: The set of rules to ensure that resources for Discussion Topics are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='imsdt_xmlv1p0']"> <sch:assert test="count(cp:file) > 0"> [RULE 1a] For the Discussion Topic Resource the required 'file' element is missing. </sch:assert> <sch:assert test="count(cp:file) < 2"> [RULE 1b] For the Discussion Topic Resource there are too many 'file' element references: <sch:value-of select="count(cp:file)"/>. </sch:assert> <sch:assert test="count(@href) = 0"> [RULE 1c] For the Discussion Topic Resource the 'href' attribute must not be used on the 'resource' element. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 1d] The Discussion Topic Resource has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 1e] The Discussion Topic Resource has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 1f] The Discussion Topic Resource has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 1g] The Discussion Topic Resource has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 1h] The Discussion Topic Resource has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 2: Web Links Resource Validation --> <sch:pattern abstract="false" name="RULE SET 2"> <sch:title>RULE SET 2: The set of rules to ensure that resources for Web Links are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='imswl_xmlv1p0']"> <sch:assert test="count(cp:file) > 0"> [RULE 2a] For the Web Link Resource the required 'file' element is missing. </sch:assert> <sch:assert test="count(cp:file) < 2"> [RULE 2b] For the Web Link Resource there are too many 'file' element references: <sch:value-of select="count(cp:file)"/>. </sch:assert> <sch:assert test="count(@href) = 0"> [RULE 2c] For the Web Link Resource the 'href' attribute must not be used on the 'resource' element. </sch:assert> <sch:assert test="count(cp:dependency) = 0"> [RULE 2d] For the Web Link Resource the prohibited 'dependency' element is used: <sch:value-of select="count(cp:dependency)"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 3: Assessment Resource Validation --> <sch:pattern abstract="false" name="RULE SET 3"> <sch:title>RULE SET 3: The set of rules to ensure that resources for Assessments are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']"> <sch:assert test="count(cp:file) > 0"> [RULE 3a] For the Assessments Resource the required 'file' element is missing. </sch:assert> <sch:assert test="count(cp:file) < 2"> [RULE 3b] For the Assessments Resource there are too many 'file' element references: <sch:value-of select="count(cp:file)"/>. </sch:assert> <sch:assert test="count(@href) = 0"> [RULE 3c] For the Assessments Resource the 'href' attribute must not be used on the 'resource' element. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 3d] The Assessment Resource has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 3e] The Assessment Resource has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 3f] The Assessment Resource has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 3g] The Assessment Resource has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 3h] The Assessment Resource has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 4: Question Bank Resource Validation --> <sch:pattern abstract="false" name="RULE SET 4"> <sch:title>RULE SET 4: The set of rules to ensure that resources for Question Banks are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resources"> <sch:assert test="count(cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']) < 2"> [RULE 4a] The are too many Question Bank Resources (a maximum of one is permitted). Number of Question Bank Resources is: <sch:value-of select="count(cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'])"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']"> <sch:assert test="count(cp:file) > 0"> [RULE 4b] For the Question Bank Resource the required 'file' element is missing. </sch:assert> <sch:assert test="count(cp:file) < 2"> [RULE 4c] For the Question Bank Resource there are too many 'file' element references: <sch:value-of select="count(cp:file)"/>. </sch:assert> <sch:assert test="count(@href) = 0"> [RULE 4d] For the Question Bank Resource the 'href' attribute must not be used on the 'resource' element. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 4e] The Question Bank Resource has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 4f] The Question Bank Resource has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 4g] The Question Bank Resource has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 4h] The Question Bank Resource has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 4i] The Question Bank Resource has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/> </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 5: Web Content Resource Validation --> <sch:pattern abstract="false" name="RULE SET 5"> <sch:title>RULE SET 5: The set of rules to ensure that resources for Web Content are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='webcontent']"> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 5a] The Web Content has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 5b] The Web Content has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 5c] The Web Content has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 5d] The Web Content has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='associatedcontent/imscc_xmlv1p0/learning-application-resource']/@identifier]) = 0"> [RULE 5e] The Web Content has a prohibited dependency on an Associated Content: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 5f] The Web Content has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> </sch:rule> <!-- UNDER REVIEW <sch:rule abstract="false" context="cp:resources"> <sch:assert test="cp:resource[@type='webcontent'][@identifier=//cp:item/@identifierref][@href]"> [RULE 5g] An Web Content resource has a missing 'href' attribute (required because the resource is directly referenced by an Item). Check the following resources: <sch:value-of select="cp:resource[@type='webcontent']/@identifier"/>. </sch:assert> </sch:rule> --> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 6: Associated Content Resource Validation --> <sch:pattern abstract="false" name="RULE SET 6"> <sch:title>RULE SET 6: The set of rules to ensure that resources for Associated Content are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='associatedcontent/imscc_xmlv1p0/learning-application-resource']"> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 6a] The Associated Content Resource has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 6b] The Associated Content Resource has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 6c] The Associated Content Resource has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 6d] The Associated Content Resource has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='associatedcontent/imscc_xmlv1p0/learning-application-resource']/@identifier]) = 0"> [RULE 6e] The Associated Content Resource has a prohibited dependency on an Associated Content: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 6f] The Associated Content Resource has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> </sch:rule> <!-- UNDER REVIEW <sch:rule abstract="false" context="cp:resources"> <sch:assert test="cp:resource[@type='associatedcontent/imscc_xmlv1p0/learning-application-resource'][@identifier=//cp:item/@identifierref][@href]"> [RULE 6g] An Associated Content resource has a missing 'href' attribute (required because the resource is directly referenced by an Item). Check the following resources: <sch:value-of select="cp:resource[@type='associatedcontent/imscc_xmlv1p0/learning-application-resource']/@identifier"/>. </sch:assert> </sch:rule> --> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 7: General Dependency Validation --> <sch:pattern abstract="false" name="RULE SET 7"> <sch:title>RULE SET 7: The set of rules to ensure that Dependencies are used correctly.</sch:title> <sch:rule abstract="false" context="cp:dependency"> <sch:assert test="@identifierref != parent::cp:resource/@identifier"> [RULE 7a] A 'dependency' element has a circular reference to its host 'resource' element with identifier: <sch:value-of select="@identifierref"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="cp:resource"> <sch:assert test="not(cp:dependency[@identifierref = preceding-sibling::cp:dependency/@identifierref])"> [RULE 7b] In a 'resource' element at least two 'dependency' elements reference the same 'resource' element. <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 8: General Item Validation --> <sch:pattern abstract="false" name="RULE SET 8"> <sch:title>RULE SET 8: The set of rules to ensure that Items are defined correctly.</sch:title> <sch:rule abstract="false" context="cp:resources"> <sch:assert test="count(cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'][@identifier=//cp:item/@identifierref]) = 0"> [RULE 8a] An Item has a prohibited reference(s) to a Question Bank Resource: <sch:value-of select="cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier"/>. </sch:assert> </sch:rule> <sch:rule abstract="false" context="cp:item/cp:item[@identifierref]"> <sch:assert test="count(cp:item) = 0"> [RULE 8b] A Learning Object Item contains prohibited child Item(s): <sch:value-of select="cp:item/@identifier"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 9: General File Validation --> <sch:pattern abstract="false" name="RULE SET 9"> <sch:title>RULE SET 9: The set of rules to ensure that Files are used correctly.</sch:title> <sch:rule abstract="false" context="cp:resource"> <sch:assert test="not(cp:file[@href = preceding-sibling::cp:file/@href])"> [RULE 9a] In a 'resource' element at least two 'file' elements have the same 'href' attribute: <sch:value-of select="cp:file/@href"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 10: General Resource Validation --> <sch:pattern abstract="false" name="RULE SET 10"> <sch:title>RULE SET 10: The set of general rules to ensure that Resources are used correctly.</sch:title> <sch:rule abstract="false" context="cp:resource[@href]"> <sch:assert test="count(cp:file) > 0"> [RULE 10a] For a 'resource' element with the 'href' attribute the 'file' element is missing: <sch:value-of select="@identifier"/>. </sch:assert> <sch:assert test="@href=cp:file/@href"> [RULE 10b] For a 'resource' element with the 'href' attribute there is no 'file' element with the correct 'href' attribute: <sch:value-of select="@identifier"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> <!-- RULE 11: BasicLTI Resource Validation --> <sch:pattern abstract="false" name="RULE SET 11"> <sch:title>RULE SET 11: The set of rules to ensure that resources for BasicLTI are correctly provided</sch:title> <sch:rule abstract="false" context="cp:resource[@type='imsbasiclti_xmlv1p0']"> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsdt_xmlv1p0']/@identifier]) = 0"> [RULE 11a] The BasicLTI has a prohibited dependency on a Discussion Topic: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imswl_xmlv1p0']/@identifier]) = 0"> [RULE 11b] The BasicLTI has a prohibited dependency on a Web Link: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/assessment']/@identifier]) = 0"> [RULE 11c] The Web Content has a prohibited dependency on an Assessment: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsqti_xmlv1p2/imscc_xmlv1p0/question-bank']/@identifier]) = 0"> [RULE 11d] The Web Content has a prohibited dependency on a Question Bank: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='webcontent']/@identifier]) = 0"> [RULE 11e] The Web Content has a prohibited dependency on an Associated Content: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> <sch:assert test="count(cp:dependency[@identifierref=//cp:resource[@type='imsbasiclti_xmlv1p0']/@identifier]) = 0"> [RULE 11f] The Web Content has a prohibited dependency on a BasicLTI: <sch:value-of select="cp:dependency/@identifierref"/>. </sch:assert> </sch:rule> </sch:pattern> <!-- **************************************************************************** --> </xs:appinfo> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <xs:attributeGroup name="protected.Resource.Attr"> <xs:attribute ref="autz:protected" use="optional" default="false"/> </xs:attributeGroup> <!-- ================================================================================================== --> <!-- Generate Global List Types *********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Dependency.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This identifies a resource whose files this resource depends upon. The reference to an identifier in the resources section is contained in the Identifierref attribute.</p> </xs:documentation> </xs:annotation> <xs:sequence> </xs:sequence> <xs:attribute name="identifierref" use="required" type="xs:IDREF"/> </xs:complexType> <xs:complexType name="File.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>A listing of file that this resource is dependent on. The href attribute identifies the location of the file. This may have some meta-data.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="metadata" type="Metadata.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="href" use="required" type="xs:anyURI"/> </xs:complexType> <xs:complexType name="Item.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the structure that describes the shape of the organization. It is used in a hierarchical organizational scheme by ordering and nesting. Each Item has an identifier that is unique within the Manifest file. Identifierref acts as a reference to an identifier in the resources section. An Item has a title and may have meta-data. The parameters and isvisible attributes have been removed for this profile.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:string" minOccurs = "1" maxOccurs = "1"/> <xs:element name="item" type="Item.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="metadata" type="Metadata.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="identifier" use="required" type="xs:ID"/> <xs:attribute name="identifierref" use="optional" type="xs:IDREF"/> </xs:complexType> <xs:complexType name="ItemOrg.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the structure that describes the top-level shape of the organization and so only a single Item is permitted. The top-level has an identifier that is unique within the Manifest file. It may have meta-data but it has not title. It can contain any number of Items. The parameters and isvisible attributes have been removed for this profile</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="item" type="Item.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="metadata" type="Metadata.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="identifier" use="required" type="xs:ID"/> </xs:complexType> <xs:complexType name="Manifest.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>A reusable unit of instruction. It encapsulates meta-data, organizations, resource references and authorization.<br/> It must have an identifier that is unique within the Manifest. The version and reference base is optional.<br/> The base provides the relative path offset for the content file(s).</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="metadata" type="ManifestMetadata.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="organizations" type="Organizations.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="resources" type="Resources.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element ref="autz:authorizations" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="identifier" use="required" type="xs:ID"/> <xs:attribute ref="xml:base" use="optional"/> </xs:complexType> <xs:complexType name="ManifestMetadata.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the meta-data describing the Manifest. The schema describes the schema that defines and controls the Manifest.<br/> Schemaversion describes the version of the above schema (e.g., 1,0, 1.1). In Common Cartridge all of the meta-data is described<br/> using the iEEE LOM standard format. CC manifest metadata is required.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="schema" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the meta-data describing the associated class (but not the manifest which has its own meta-data structure). <br/> In Common Cartridge all of the meta-data is described using the IEEE LOM standard format.</p> </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> <xs:enumeration value="IMS Common Cartridge"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="schemaversion" minOccurs = "1" maxOccurs = "1"> <xs:simpleType> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the meta-data describing the associated class (but not the manifest which has its own meta-data structure). <br/> In Common Cartridge all of the meta-data is described using the IEEE LOM standard format.</p> </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> <xs:enumeration value="1.1.0"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element ref="lomm:lom" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="Metadata.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the meta-data describing the associated class (but not the manifest which has its own meta-data structure). <br/> In Common Cartridge all of the meta-data is described using the IEEE LOM standard format.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:group ref="grpStrict.any"/> </xs:sequence> </xs:complexType> <xs:complexType name="Organization.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>Describes a particular hierarchical organization in this profile; this is the only type of oranization that is permitted. The identifier, for the organization, that is unique within the Manifest file. The organization may have meta-data.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:string" minOccurs = "0" maxOccurs = "1"/> <xs:element name="item" type="ItemOrg.Type" minOccurs = "1" maxOccurs = "1"/> <xs:element name="metadata" type="Metadata.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> <xs:attribute name="identifier" use="required" type="xs:ID"/> <xs:attribute name="structure" use="required" fixed="rooted-hierarchy" type="xs:string"/> </xs:complexType> <xs:complexType name="Organizations.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>Describes zero or one structures or organizations for the Cartridge. Only one organization is permitted so the default attribute has been removed for this Profile.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="organization" type="Organization.Type" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="Resource.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>A reference to a resource which consists of one or more physical files. The Identifer of the resource is unique within the scope of its containing Manifest file. The Type attribute indicates the type of resource - for the Cartridge this is an enumerated set. The resource may have meta-data, zero or more files references, and zero or more dependencies.</p> <p>CCV1.1 PROFILE: The authorizations 'protected' attribute and the 'intendedUse' attribute are added.</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="metadata" type="ResourceMetadata.Type" minOccurs = "0" maxOccurs = "1"/> <xs:element name="file" type="File.Type" minOccurs = "0" maxOccurs = "unbounded"/> <xs:element name="dependency" type="Dependency.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute name="identifier" use="required" type="xs:ID"/> <xs:attribute name="type" use="required"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="webcontent"/> <xs:enumeration value="imsqti_xmlv1p2/imscc_xmlv1p1/assessment"/> <xs:enumeration value="imsqti_xmlv1p2/imscc_xmlv1p1/question-bank"/> <xs:enumeration value="associatedcontent/imscc_xmlv1p1/learning-application-resource"/> <xs:enumeration value="imsdt_xmlv1p1"/> <xs:enumeration value="imswl_xmlv1p1"/> <xs:enumeration value="imsbasiclti_xmlv1p0"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="href" use="optional" type="xs:anyURI"/> <xs:attribute ref="xml:base" use="optional"/> <xs:attribute name="intendeduse" use="optional"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="lessonplan"/> <xs:enumeration value="syllabus"/> <xs:enumeration value="unspecified"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attributeGroup ref="protected.Resource.Attr"/> </xs:complexType> <xs:complexType name="ResourceMetadata.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>This is the container for resource specific metadata..</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element ref="lomr:lom" minOccurs = "1" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <xs:complexType name="Resources.Type" mixed="false"> <xs:annotation> <xs:documentation source="umldocumentation"> <p>A collection of references to resources. There is no assumption of order or hierarchy. The base attribute provides the relative path offset for the content file(s).</p> </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="resource" type="Resource.Type" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> <xs:attribute ref="xml:base" use="optional"/> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="manifest" type="Manifest.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/schemas11/cc11libxml2validator.xsd 0000644 00000002256 15215711721 0013374 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns ="http://dummy.libxml2.validator" targetNamespace ="http://dummy.libxml2.validator" xmlns:xs ="http://www.w3.org/2001/XMLSchema" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:imscp ="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" xmlns:imslom ="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" xmlns:lom ="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" xmlns:cc ="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" version="1.1" elementFormDefault ="qualified" attributeFormDefault="unqualified" > <xs:import namespace="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1" schemaLocation="ccv1p1_imscp_v1p2_v1p0.xsd" /> <xs:import namespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest" schemaLocation="ccv1p1_lommanifest_v1p0.xsd"/> <xs:import namespace="http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource" schemaLocation="ccv1p1_lomresource_v1p0.xsd"/> <xs:import namespace="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" schemaLocation="ccv1p1_imsccauth_v1p1.xsd" /> </xs:schema> cc/schemas11/ccv1p1_imsccauth_v1p1.xsd 0000644 00000031246 15215711721 0013452 0 ustar 00 <?xml version = "1.0" encoding = "UTF-8"?> <xs:schema xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" targetNamespace="http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="IMS CC AUTHZ 1.1" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xs:annotation> <xs:documentation> XSD Data File Information ------------------------- Author: Colin Smythe Date: 31st August, 2010 Version: 1.1 Status: Final Description: This is the IMS GLC Authorization Data Model for the Common Cartridge. History: Version 1.0 - the first release of this data model; Version 1.1 - updates made to the namespace. License: IPR, License and Distribution Notices This machine readable file is derived from IMS Global Learning Consortium (GLC) specification IMS Common Cartridge Version 1.1 found at http://www.imsglobal.org/cc and the original IMS GLC schema binding or code base http://www.imsglobal.org/cc. Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by the schema binding contained in this document. IMS GLC takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain to the implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on IMS GLCs procedures with respect to rights in IMS GLC specifications can be found at the IMS GLC Intellectual Property Rights web page: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf. Copyright (c) IMS Global Learning Consortium 1999-2011. All Rights Reserved. License Notice for Users Users of products or services that include this document are hereby granted a worldwide, royalty-free, non-exclusive license to use this document. Distribution Notice for Developers Developers of products or services that are not original incorporators of this document and have not changed this document, that is, are distributing a software product that incorporates this document as is from a third-party source other than IMS, are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof. Developers of products or services that are original incorporators of this document and wish to provide distribution of this document as is or with modifications and developers of products and services that are not original incorporators of this document and have changed this document, are required to register with the IMS GLC community on the IMS GLC website as described in the following two paragraphs:- * If you wish to distribute this document as is, with no modifications, you are hereby granted permission to copy, display and distribute the contents of this document in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid license registration with IMS and receive an email from IMS granting the license. To register, follow the instructions on the IMS website: http://www.imsglobal.org/specificationdownload.cfm. Once registered you are granted permission to transfer unlimited distribution rights of this document for the purposes of third-party or other distribution of your product or service that incorporates this document as long as this IPR, License and Distribution notice remains in place in its entirety; * If you wish to create and distribute a derived work from this document, you are hereby granted permission to copy, display and distribute the contents of the derived work in any medium for any purpose without fee or royalty provided that you include this IPR, License and Distribution notice in its entirety on ALL copies, or portions thereof, that you make and you complete a valid profile registration with IMS GLC and receive an email from IMS GLC granting the license. To register, follow the instructions on the IMS GLC website: http://www.imsglobal.org/profile/. Once registered you are granted permission to transfer unlimited distribution rights of the derived work for the purposes of third-party or other distribution of your product or service that incorporates the derived work as long as this IPR, License and Distribution notice remains in place in its entirety. The limited permissions granted above are perpetual and will not be revoked by IMS GLC or its successors or assigns. THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTERS OWN RISK, AND NEITHER THE CONSORTIUM NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION. Source UML File Information --------------------------- The source file information must be supplied as an XMI file (without diagram layout information). The supported UML authoring tools are: (a) Poseidon – v6 (and later) Source XSLT File Information ---------------------------- XSL Generator: UMLtoXSDTransformv0p9.xsl XSLT Processor: Xalan Release: 1.0 Beta 3 Date: 31st May, 2009 IMS GLC Auto-generation Binding Tool-kit (I-BAT) ------------------------------------------------ This file was auto-generated using the IMS GLC Binding Auto-generation Tool-kit (I-BAT). While every attempt has been made to ensure that this tool auto-generates the files correctly, users should be aware that this is an experimental tool. Permission is given to make use of this tool. IMS GLC makes no claim on the materials created by third party users of this tool. Details on how to use this tool are contained in the IMS GLC "I-BAT" Documentation available at the IMS GLC web-site. Tool Copyright: 2005-2010 (c) IMS Global Learning Consortium Inc. All Rights Reserved. </xs:documentation> </xs:annotation> <!-- Generate Global Attributes *********************************************************************** --> <xs:attribute name="protected" type="xs:boolean"/> <!-- ================================================================================================== --> <!-- Generate Namespaced extension Group ************************************************************* --> <xs:group name="grpStrict.any"> <xs:annotation> <xs:documentation> Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. The extension has a definition of "strict" i.e. they must have their own namespace. </xs:documentation> </xs:annotation> <xs:sequence> <xs:any namespace = "##other" processContents = "strict" minOccurs = "0" maxOccurs = "unbounded"/> </xs:sequence> </xs:group> <!-- ================================================================================================== --> <!-- Generate Special DataTypes ********************************************************************** --> <!-- ================================================================================================== --> <!-- Generate the enumerated simpleType declarations ************************************************** --> <!-- ================================================================================================== --> <!-- Generate the simpleType elements based IMS data-types ******************************************* --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon simpleType ************************************ --> <!-- ================================================================================================== --> <!-- Generate the derived data-type elements based upon derived simpleType **************************** --> <!-- ================================================================================================== --> <!-- Generate the data-type ComplexTypes ************************************************************** --> <xs:complexType name="Authorizations.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The set of authorizations for the associated object. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="authorization" type="Authorization.Type" minOccurs = "1" maxOccurs = "1"/> <xs:group ref="grpStrict.any"/> </xs:sequence> <xs:attribute name="access" use="required"> <xs:simpleType> <xs:restriction base="xs:string"> <xs:enumeration value="cartridge"/> <xs:enumeration value="resource"/> </xs:restriction> </xs:simpleType> </xs:attribute> <xs:attribute name="import" use="optional" default="false" type="xs:boolean"/> </xs:complexType> <xs:complexType name="Authorization.Type"> <xs:annotation> <xs:documentation source="umldocumentation"> The authorization detail for the cartridge in terms of the web service. </xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="cartridgeId" type="xs:normalizedString" minOccurs = "1" maxOccurs = "1"/> <xs:element name="webservice" type="xs:normalizedString" minOccurs = "0" maxOccurs = "1"/> </xs:sequence> </xs:complexType> <!-- ================================================================================================== --> <!-- Declaration of the elements ********************************************************************** --> <!-- ================================================================================================== --> <!-- Declaration of the root element(s) *************************************************************** --> <xs:element name="authorizations" type="Authorizations.Type"/> <!-- ================================================================================================== --> </xs:schema> cc/cc2moodle.php 0000644 00000072142 15215711721 0007521 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2009 Mauro Rondinelli (mauro.rondinelli [AT] uvcms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once($CFG->dirroot . '/backup/cc/entities.class.php'); require_once($CFG->dirroot . '/backup/cc/entity.label.class.php'); require_once($CFG->dirroot . '/backup/cc/entity.resource.class.php'); require_once($CFG->dirroot . '/backup/cc/entity.forum.class.php'); require_once($CFG->dirroot . '/backup/cc/entity.quiz.class.php'); class cc2moodle { const CC_TYPE_FORUM = 'imsdt_xmlv1p0'; const CC_TYPE_QUIZ = 'imsqti_xmlv1p2/imscc_xmlv1p0/assessment'; const CC_TYPE_QUESTION_BANK = 'imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'; const CC_TYPE_WEBLINK = 'imswl_xmlv1p0'; const CC_TYPE_WEBCONTENT = 'webcontent'; const CC_TYPE_ASSOCIATED_CONTENT = 'associatedcontent/imscc_xmlv1p0/learning-application-resource'; const CC_TYPE_EMPTY = ''; public static $restypes = array('associatedcontent/imscc_xmlv1p0/learning-application-resource', 'webcontent'); public static $forumns = array('dt' => 'http://www.imsglobal.org/xsd/imsdt_v1p0'); public static $quizns = array('xmlns' => 'http://www.imsglobal.org/xsd/ims_qtiasiv1p2'); public static $resourcens = array('wl' => 'http://www.imsglobal.org/xsd/imswl_v1p0'); /** * * @return array */ public static function getquizns() { return static::$quizns; } /** * * @return array */ public static function getforumns() { return static::$forumns; } /** * * @return array */ public static function getresourcens() { return static::$resourcens; } public static function get_manifest($folder) { if (!is_dir($folder)) { return false; } // Before iterate over directories, try to find one manifest at top level if (file_exists($folder . '/imsmanifest.xml')) { return $folder . '/imsmanifest.xml'; } $result = false; try { $dirIter = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::KEY_AS_PATHNAME); $recIter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::CHILD_FIRST); foreach ($recIter as $info) { if ($info->isFile() && ($info->getFilename() == 'imsmanifest.xml')) { $result = $info->getPathname(); break; } } } catch (Exception $e) {} return $result; } public static $instances = array(); public static $manifest; public static $path_to_manifest_folder; public static $namespaces = array('imscc' => 'http://www.imsglobal.org/xsd/imscc/imscp_v1p1', 'lomimscc' => 'http://ltsc.ieee.org/xsd/imscc/LOM', 'lom' => 'http://ltsc.ieee.org/xsd/LOM', 'voc' => 'http://ltsc.ieee.org/xsd/LOM/vocab', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'cc' => 'http://www.imsglobal.org/xsd/imsccauth_v1p0'); function __construct($path_to_manifest) { static::$manifest = new DOMDocument(); static::$manifest->validateOnParse = false; static::$path_to_manifest_folder = dirname($path_to_manifest); static::log_action('Proccess start'); static::log_action('Load the manifest file: ' . $path_to_manifest); if (!static::$manifest->load($path_to_manifest, LIBXML_NONET)) { static::log_action('Cannot load the manifest file: ' . $path_to_manifest, true); } } public function is_auth() { $xpath = static::newx_path(static::$manifest, static::$namespaces); $count_auth = $xpath->evaluate('count(/imscc:manifest/cc:authorizations)'); if ($count_auth > 0) { $response = true; } else { $response = false; } return $response; } protected function get_metadata($section, $key) { $xpath = static::newx_path(static::$manifest, static::$namespaces); $metadata = $xpath->query('/imscc:manifest/imscc:metadata/lomimscc:lom/lomimscc:' . $section . '/lomimscc:' . $key . '/lomimscc:string'); $value = !empty($metadata->item(0)->nodeValue) ? $metadata->item(0)->nodeValue : ''; return $value; } public function generate_moodle_xml() { global $CFG, $OUTPUT; $cdir = static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'course_files'; if (!file_exists($cdir)) { mkdir($cdir, $CFG->directorypermissions, true); } $sheet_base = static::loadsheet(SHEET_BASE); // MOODLE_BACKUP / INFO / DETAILS / MOD $node_info_details_mod = $this->create_code_info_details_mod(); // MOODLE_BACKUP / BLOCKS / BLOCK $node_course_blocks_block = $this->create_node_course_blocks_block(); // MOODLE_BACKUP / COURSES / SECTIONS / SECTION $node_course_sections_section = $this->create_node_course_sections_section(); // MOODLE_BACKUP / COURSES / QUESTION_CATEGORIES $node_course_question_categories = $this->create_node_question_categories(); // MOODLE_BACKUP / COURSES / MODULES / MOD $node_course_modules_mod = $this->create_node_course_modules_mod(); // MOODLE_BACKUP / COURSE / HEADER $node_course_header = $this->create_node_course_header(); // GENERAL INFO $filename = optional_param('file', 'not_available.zip', PARAM_RAW); $filename = basename($filename); $www_root = $CFG->wwwroot; $find_tags = array('[#zip_filename#]', '[#www_root#]', '[#node_course_header#]', '[#node_info_details_mod#]', '[#node_course_blocks_block#]', '[#node_course_sections_section#]', '[#node_course_question_categories#]', '[#node_course_modules#]'); $replace_values = array($filename, $www_root, $node_course_header, $node_info_details_mod, $node_course_blocks_block, $node_course_sections_section, $node_course_question_categories, $node_course_modules_mod); $result_xml = str_replace($find_tags, $replace_values, $sheet_base); // COPY RESOURSE FILES $entities = new entities(); $entities->move_all_files(); if (array_key_exists("index", self::$instances)) { if (!file_put_contents(static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'moodle.xml', $result_xml)) { static::log_action('Cannot save the moodle manifest file: ' . static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'moodle.xml', true); } else { $status = true; } } else { $status = false; echo $OUTPUT->notification('The course is empty'); static::log_action('The course is empty', false); } return $status; } protected function get_sections_numbers($instances) { $count = 0; if (array_key_exists("index", $instances)) { foreach ($instances["index"] as $instance) { if ($instance["deep"] == ROOT_DEEP) { $count++; } } } return $count; } protected function create_node_course_header() { $node_course_header = ''; $sheet_course_header = static::loadsheet(SHEET_COURSE_HEADER); $course_title = trim($this->get_metadata('general', 'title')); $course_title = empty($course_title) ? 'Untitled Course' : $course_title; $course_description = $this->get_metadata('general', 'description'); $section_count = $this->get_sections_numbers(static::$instances) - 1; if ($section_count == -1) { $section_count = 0; } if (empty($course_title)) { $this->log_action('The course title not found', true); } $course_short_name = $this->create_course_code($course_title); $find_tags = array('[#course_name#]', '[#course_short_name#]', '[#course_description#]', '[#date_now#]', '[#section_count#]'); $replace_values = array(entities::safexml($course_title), entities::safexml($course_short_name), entities::safexml($course_description), time(), $section_count); $node_course_header = str_replace($find_tags, $replace_values, $sheet_course_header); return $node_course_header; } protected function create_node_question_categories() { $quiz = new cc_quiz(); static::log_action('Creating node: QUESTION_CATEGORIES'); $node_course_question_categories = $quiz->generate_node_question_categories(); return $node_course_question_categories; } protected function create_node_course_modules_mod() { $labels = new cc_label(); $resources = new cc_resource(); $forums = new cc_forum(); $quiz = new cc_quiz(); static::log_action('Creating node: COURSE/MODULES/MOD'); // LABELS $node_course_modules_mod_label = $labels->generate_node(); // RESOURCES (WEB CONTENT AND WEB LINK) $node_course_modules_mod_resource = $resources->generate_node(); // FORUMS $node_course_modules_mod_forum = $forums->generate_node(); // QUIZ $node_course_modules_mod_quiz = $quiz->generate_node_course_modules_mod(); //TODO: label $node_course_modules = $node_course_modules_mod_label . $node_course_modules_mod_resource . $node_course_modules_mod_forum . $node_course_modules_mod_quiz; return $node_course_modules; } protected function create_node_course_sections_section() { static::log_action('Creating node: COURSE/SECTIONS/SECTION'); $node_course_sections_section = ''; $sheet_course_sections_section = static::loadsheet(SHEET_COURSE_SECTIONS_SECTION); $topics = $this->get_nodes_by_criteria('deep', ROOT_DEEP); $i = 0; if (!empty($topics)) { foreach ($topics as $topic) { $i++; $node_node_course_sections_section_mods_mod = $this->create_node_course_sections_section_mods_mod($topic['index']); if ($topic['moodle_type'] == MOODLE_TYPE_LABEL) { $find_tags = array('[#section_id#]', '[#section_number#]', '[#section_summary#]', '[#node_course_sections_section_mods_mod#]'); $replace_values = array($i, $i - 1, entities::safexml($topic['title']), $node_node_course_sections_section_mods_mod); } else { $find_tags = array('[#section_id#]', '[#section_number#]', '[#section_summary#]', '[#node_course_sections_section_mods_mod#]'); $replace_values = array($i, $i - 1, '', $node_node_course_sections_section_mods_mod); } $node_course_sections_section .= str_replace($find_tags, $replace_values, $sheet_course_sections_section); } } return $node_course_sections_section; } protected function create_node_course_blocks_block() { global $CFG; static::log_action('Creating node: COURSE/BLOCKS/BLOCK'); $sheet_course_blocks_block = static::loadsheet(SHEET_COURSE_BLOCKS_BLOCK); $node_course_blocks_block = ''; $format_config = $CFG->dirroot . '/course/format/weeks/config.php'; if (@is_file($format_config) && is_readable($format_config)) { require ($format_config); } if (!empty($format['defaultblocks'])) { $blocknames = $format['defaultblocks']; } else { if (isset($CFG->defaultblocks)) { $blocknames = $CFG->defaultblocks; } else { $blocknames = 'activity_modules,search_forums,course_list:news_items,calendar_upcoming,recent_activity'; } } $blocknames = explode(':', $blocknames); $blocks_left = explode(',', $blocknames[0]); $blocks_right = explode(',', $blocknames[1]); $find_tags = array('[#block_id#]', '[#block_name#]', '[#block_position#]', '[#block_weight#]'); $i = 0; $weight = 0; foreach ($blocks_left as $block) { $i++; $weight++; $replace_values = array($i, $block, 'l', $weight); $node_course_blocks_block .= str_replace($find_tags, $replace_values, $sheet_course_blocks_block); } $weight = 0; foreach ($blocks_right as $block) { $i++; $weight ++; $replace_values = array($i, $block, 'r', $weight); $node_course_blocks_block .= str_replace($find_tags, $replace_values, $sheet_course_blocks_block); } return $node_course_blocks_block; } /** * * Is activity visible or not * @param string $identifier * @return number */ protected function get_module_visible($identifier) { //Should item be hidden or not $mod_visible = 1; if (!empty($identifier)) { $xpath = static::newx_path(static::$manifest, static::$namespaces); $query = '/imscc:manifest/imscc:resources/imscc:resource[@identifier="' . $identifier . '"]'; $query .= '//lom:intendedEndUserRole/voc:vocabulary/lom:value'; $intendeduserrole = $xpath->query($query); if (!empty($intendeduserrole) && ($intendeduserrole->length > 0)) { $role = trim($intendeduserrole->item(0)->nodeValue); if (strcasecmp('Instructor', $role) == 0) { $mod_visible = 0; } } } return $mod_visible; } protected function create_node_course_sections_section_mods_mod($root_parent) { $sheet_course_sections_section_mods_mod = static::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD); $childs = $this->get_nodes_by_criteria('root_parent', $root_parent); if ($childs) { $node_course_sections_section_mods_mod = ''; foreach ($childs as $child) { if ($child['moodle_type'] == MOODLE_TYPE_LABEL) { if ($child['index'] == $child['root_parent']) { $is_summary = true; } else { $is_summary = false; } } else { $is_summary = false; } if (!$is_summary) { $indent = $child['deep'] - ROOT_DEEP; if ($indent > 0) { $indent = $indent - 1; } $find_tags = array('[#mod_id#]', '[#mod_instance_id#]', '[#mod_type#]', '[#date_now#]', '[#mod_indent#]', '[#mod_visible#]'); $replace_values = array($child['index'], $child['instance'], $child['moodle_type'], time(), $indent, $this->get_module_visible($child['resource_indentifier'])); $node_course_sections_section_mods_mod .= str_replace($find_tags, $replace_values, $sheet_course_sections_section_mods_mod); } } $response = $node_course_sections_section_mods_mod; } else { $response = ''; } return $response; } public function get_nodes_by_criteria($key, $value) { $response = array(); if (array_key_exists('index', static::$instances)) { foreach (static::$instances['index'] as $item) { if ($item[$key] == $value) { $response[] = $item; } } } return $response; } //Modified here protected function create_code_info_details_mod() { static::log_action('Creating node: INFO/DETAILS/MOD'); $xpath = static::newx_path(static::$manifest, static::$namespaces); $items = $xpath->query('/imscc:manifest/imscc:organizations/imscc:organization/imscc:item | /imscc:manifest/imscc:resources/imscc:resource[@type="' . static::CC_TYPE_QUESTION_BANK . '"]'); $this->create_instances($items); $count_quiz = $this->count_instances(MOODLE_TYPE_QUIZ); $count_forum = $this->count_instances(MOODLE_TYPE_FORUM); $count_resource = $this->count_instances(MOODLE_TYPE_RESOURCE); $count_label = $this->count_instances(MOODLE_TYPE_LABEL); $sheet_info_details_mod_instances_instance = static::loadsheet(SHEET_INFO_DETAILS_MOD_INSTANCE); if ($count_resource > 0) { $resource_instance = $this->create_mod_info_details_mod_instances_instance($sheet_info_details_mod_instances_instance, $count_resource, static::$instances['instances'][MOODLE_TYPE_RESOURCE]); } if ($count_quiz > 0) { $quiz_instance = $this->create_mod_info_details_mod_instances_instance($sheet_info_details_mod_instances_instance, $count_quiz, static::$instances['instances'][MOODLE_TYPE_QUIZ]); } if ($count_forum > 0) { $forum_instance = $this->create_mod_info_details_mod_instances_instance($sheet_info_details_mod_instances_instance, $count_forum, static::$instances['instances'][MOODLE_TYPE_FORUM]); } if ($count_label > 0) { $label_instance = $this->create_mod_info_details_mod_instances_instance($sheet_info_details_mod_instances_instance, $count_label, static::$instances['instances'][MOODLE_TYPE_LABEL]); } $resource_mod = $count_resource ? $this->create_mod_info_details_mod(MOODLE_TYPE_RESOURCE, $resource_instance) : ''; $quiz_mod = $count_quiz ? $this->create_mod_info_details_mod(MOODLE_TYPE_QUIZ, $quiz_instance) : ''; $forum_mod = $count_forum ? $this->create_mod_info_details_mod(MOODLE_TYPE_FORUM, $forum_instance) : ''; $label_mod = $count_label ? $this->create_mod_info_details_mod(MOODLE_TYPE_LABEL, $label_instance) : ''; //TODO: label return $label_mod . $resource_mod . $quiz_mod . $forum_mod; } protected function create_mod_info_details_mod($mod_type, $node_info_details_mod_instances_instance) { $sheet_info_details_mod = static::loadsheet(SHEET_INFO_DETAILS_MOD); $find_tags = array('[#mod_type#]' ,'[#node_info_details_mod_instances_instance#]'); $replace_values = array($mod_type , $node_info_details_mod_instances_instance); return str_replace($find_tags, $replace_values, $sheet_info_details_mod); } protected function create_mod_info_details_mod_instances_instance($sheet, $instances_quantity, $instances) { $instance = ''; $find_tags = array('[#mod_instance_id#]', '[#mod_name#]', '[#mod_user_info#]'); for ($i = 1; $i <= $instances_quantity; $i++) { $user_info = ($instances[$i - 1]['common_cartriedge_type'] == static::CC_TYPE_FORUM) ? 'true' : 'false'; if ($instances[$i - 1]['common_cartriedge_type'] == static::CC_TYPE_EMPTY) { if ($instances[$i - 1]['deep'] <= ROOT_DEEP ) { continue; } } $replace_values = array($instances[$i - 1]['instance'], entities::safexml($instances[$i - 1]['title']), $user_info); $instance .= str_replace($find_tags, $replace_values, $sheet); } return $instance; } protected function create_instances($items, $level = 0, &$array_index = 0, $index_root = 0) { $level++; $i = 1; if ($items) { $xpath = self::newx_path(static::$manifest, static::$namespaces); foreach ($items as $item) { $array_index++; if ($item->nodeName == "item") { $identifierref = ''; if ($item->hasAttribute('identifierref')) { $identifierref = $item->getAttribute('identifierref'); } $title = ''; $titles = $xpath->query('imscc:title', $item); if ($titles->length > 0) { $title = $titles->item(0)->nodeValue; } $cc_type = $this->get_item_cc_type($identifierref); $moodle_type = $this->convert_to_moodle_type($cc_type); //Fix the label issue - MDL-33523 if (empty($identifierref) && empty($title)) { $moodle_type = TYPE_UNKNOWN; } } elseif ($item->nodeName == "resource") { $identifierref = $xpath->query('@identifier', $item); $identifierref = !empty($identifierref->item(0)->nodeValue) ? $identifierref->item(0)->nodeValue : ''; $cc_type = $this->get_item_cc_type($identifierref); $moodle_type = $this->convert_to_moodle_type($cc_type); $title = 'Quiz Bank ' . ($this->count_instances($moodle_type) + 1); } if ($level == ROOT_DEEP) { $index_root = $array_index; } static::$instances['index'][$array_index]['common_cartriedge_type'] = $cc_type; static::$instances['index'][$array_index]['moodle_type'] = $moodle_type; static::$instances['index'][$array_index]['title'] = $title ? $title : ''; static::$instances['index'][$array_index]['root_parent'] = $index_root; static::$instances['index'][$array_index]['index'] = $array_index; static::$instances['index'][$array_index]['deep'] = $level; static::$instances['index'][$array_index]['instance'] = $this->count_instances($moodle_type); static::$instances['index'][$array_index]['resource_indentifier'] = $identifierref; static::$instances['instances'][$moodle_type][] = array('title' => $title, 'instance' => static::$instances['index'][$array_index]['instance'], 'common_cartriedge_type' => $cc_type, 'resource_indentifier' => $identifierref, 'deep' => $level); $more_items = $xpath->query('imscc:item', $item); if ($more_items->length > 0) { $this->create_instances($more_items, $level, $array_index, $index_root); } $i++; } } } public function count_instances($type) { $quantity = 0; if (array_key_exists('index', static::$instances)) { if (static::$instances['index'] && $type) { foreach (static::$instances['index'] as $instance) { if (!empty($instance['moodle_type'])) { $types[] = $instance['moodle_type']; } } $quantity_instances = array_count_values($types); $quantity = array_key_exists($type, $quantity_instances) ? $quantity_instances[$type] : 0; } } return $quantity; } public function convert_to_moodle_type($cc_type) { $type = TYPE_UNKNOWN; if ($cc_type == static::CC_TYPE_FORUM) { $type = MOODLE_TYPE_FORUM; } if ($cc_type == static::CC_TYPE_QUIZ) { $type = MOODLE_TYPE_QUIZ; } if ($cc_type == static::CC_TYPE_WEBLINK) { $type = MOODLE_TYPE_RESOURCE; } if ($cc_type == static::CC_TYPE_WEBCONTENT) { $type = MOODLE_TYPE_RESOURCE; } if ($cc_type == static::CC_TYPE_ASSOCIATED_CONTENT) { $type = MOODLE_TYPE_RESOURCE; } if ($cc_type == static::CC_TYPE_QUESTION_BANK) { $type = MOODLE_TYPE_QUESTION_BANK; } //TODO: label if ($cc_type == static::CC_TYPE_EMPTY) { $type = MOODLE_TYPE_LABEL; } return $type; } public function get_item_cc_type($identifier) { $xpath = static::newx_path(static::$manifest, static::$namespaces); $nodes = $xpath->query('/imscc:manifest/imscc:resources/imscc:resource[@identifier="' . $identifier . '"]/@type'); if ($nodes && !empty($nodes->item(0)->nodeValue)) { return $nodes->item(0)->nodeValue; } else { return ''; } } public static function newx_path(DOMDocument $manifest, $namespaces = '') { $xpath = new DOMXPath($manifest); if (!empty($namespaces)) { foreach ($namespaces as $prefix => $ns) { if (!$xpath->registerNamespace($prefix, $ns)) { static::log_action('Cannot register the namespace: ' . $prefix . ':' . $ns, true); } } } return $xpath; } public static function loadsheet($file) { $content = (is_readable($file) && ($content = file_get_contents($file))) ? $content : false; static::log_action('Loading sheet: ' . $file); if (!$content) { static::log_action('Cannot load the xml sheet: ' . $file, true); } static::log_action('Load OK!'); return $content; } public static function log_file() { return static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'cc2moodle.log'; } public static function log_action($text, $critical_error = false) { $full_message = strtoupper(date("j/n/Y g:i:s a")) . " - " . $text . "\r"; file_put_contents(static::log_file(), $full_message, FILE_APPEND); if ($critical_error) { static::critical_error($text); } } protected static function critical_error($text) { $path_to_log = static::log_file(); echo ' <p> <hr />A critical error has been found! <p>' . $text . '</p> <p> The process has been stopped. Please see the <a href="' . $path_to_log . '">log file</a> for more information.</p> <p>Log: ' . $path_to_log . '</p> <hr /> </p> '; die(); } protected function create_course_code($title) { //Making sure that text of the short name does not go over the DB limit. //and leaving the space to add additional characters by the platform $code = substr(strtoupper(str_replace(' ', '', trim($title))),0,94); return $code; } } cc/cc_lib/cc_utils.php 0000644 00000035761 15215711721 0010700 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('xmlbase.php'); /** * * Various helper utils * @author Darko Miletic dmiletic@moodlerooms.com * */ abstract class cc_helpers { /** * Checks extension of the supplied filename * * @param string $filename */ public static function is_html($filename) { $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($extension, array('htm', 'html')); } /** * Generates unique identifier * @param string $prefix * @param string $suffix * @return string */ public static function uuidgen($prefix = '', $suffix = '', $uppercase = true) { $uuid = trim(sprintf('%s%04x%04x%s', $prefix, mt_rand(0, 65535), mt_rand(0, 65535), $suffix)); $result = $uppercase ? strtoupper($uuid) : strtolower($uuid); return $result; } /** * Creates new folder with random name * @param string $where * @param string $prefix * @param string $suffix * @return mixed - directory short name or false in case of failure */ public static function randomdir($where, $prefix = '', $suffix = '') { global $CFG; $dirname = false; $randomname = self::uuidgen($prefix, $suffix, false); $newdirname = $where.DIRECTORY_SEPARATOR.$randomname; if (mkdir($newdirname)) { chmod($newdirname, $CFG->directorypermissions); $dirname = $randomname; } return $dirname; } public static function build_query($attributes, $search) { $result = ''; foreach ($attributes as $attribute) { if ($result != '') { $result .= ' | '; } $result .= "//*[starts-with(@{$attribute},'{$search}')]/@{$attribute}"; } return $result; } public static function process_embedded_files(&$doc, $attributes, $search, $customslash = null) { $result = array(); $query = self::build_query($attributes, $search); $list = $doc->nodeList($query); foreach ($list as $filelink) { $rvalue = str_replace($search, '', $filelink->nodeValue); if (!empty($customslash)) { $rvalue = str_replace($customslash, '/', $rvalue); } $result[] = rawurldecode($rvalue); } return $result; } /** * * Get list of embedded files * @param string $html * @return multitype:mixed */ public static function embedded_files($html) { $result = array(); $doc = new XMLGenericDocument(); $doc->doc->validateOnParse = false; $doc->doc->strictErrorChecking = false; if (!empty($html) && $doc->loadHTML($html)) { $attributes = array('src', 'href'); $result1 = self::process_embedded_files($doc, $attributes, '@@PLUGINFILE@@'); $result2 = self::process_embedded_files($doc, $attributes, '$@FILEPHP@$', '$@SLASH@$'); $result = array_merge($result1, $result2); } return $result; } public static function embedded_mapping($packageroot, $contextid = null) { $main_file = $packageroot . DIRECTORY_SEPARATOR . 'files.xml'; $mfile = new XMLGenericDocument(); if (!$mfile->load($main_file)) { return false; } $query = "/files/file[filename!='.']"; if (!empty($contextid)) { $query .= "[contextid='{$contextid}']"; } $files = $mfile->nodeList($query); $depfiles = array(); foreach ($files as $node) { $mainfile = intval($mfile->nodeValue('sortorder', $node)); $filename = $mfile->nodeValue('filename', $node); $filepath = $mfile->nodeValue('filepath', $node); $source = $mfile->nodeValue('source', $node); $author = $mfile->nodeValue('author', $node); $license = $mfile->nodeValue('license', $node); $hashedname = $mfile->nodeValue('contenthash', $node); $hashpart = substr($hashedname, 0, 2); $location = 'files'.DIRECTORY_SEPARATOR.$hashpart.DIRECTORY_SEPARATOR.$hashedname; $type = $mfile->nodeValue('mimetype', $node); $depfiles[$filepath.$filename] = array( $location, ($mainfile == 1), strtolower(str_replace(' ', '_', $filename)), $type, $source, $author, $license, strtolower(str_replace(' ', '_', $filepath))); } return $depfiles; } public static function add_files(cc_i_manifest &$manifest, $packageroot, $outdir, $allinone = true) { global $CFG; if (pkg_static_resources::instance()->finished) { return; } $files = cc_helpers::embedded_mapping($packageroot); $rdir = $allinone ? new cc_resource_location($outdir) : null; foreach ($files as $virtual => $values) { $clean_filename = $values[2]; if (!$allinone) { $rdir = new cc_resource_location($outdir); } $rtp = $rdir->fullpath().$values[7].$clean_filename; //Are there any relative virtual directories? //let us try to recreate them $justdir = $rdir->fullpath(false).$values[7]; if (!file_exists($justdir)) { if (!mkdir($justdir, $CFG->directorypermissions, true)) { throw new RuntimeException('Unable to create directories!'); } } $source = $packageroot.DIRECTORY_SEPARATOR.$values[0]; if (!copy($source, $rtp)) { throw new RuntimeException('Unable to copy files!'); } $resource = new cc_resource($rdir->rootdir(), $values[7].$clean_filename, $rdir->dirname(false)); $res = $manifest->add_resource($resource, null, cc_version11::webcontent); pkg_static_resources::instance()->add($virtual, $res[0], $rdir->dirname(false).$values[7].$clean_filename, $values[1], $resource); } pkg_static_resources::instance()->finished = true; } /** * * Excerpt from IMS CC 1.1 overview : * No spaces in filenames, directory and file references should * employ all lowercase or all uppercase - no mixed case * * @param cc_i_manifest $manifest * @param string $packageroot * @param integer $contextid * @param string $outdir * @param boolean $allinone * @throws RuntimeException */ public static function handle_static_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) { self::add_files($manifest, $packageroot, $outdir, $allinone); return pkg_static_resources::instance()->get_values(); } public static function handle_resource_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) { $result = array(); self::add_files($manifest, $packageroot, $outdir, $allinone); $files = self::embedded_mapping($packageroot, $contextid); $rootnode = null; $rootvals = null; $depfiles = array(); $depres = array(); $flocation = null; foreach ($files as $virtual => $values) { $vals = pkg_static_resources::instance()->get_identifier($virtual); $resource = $vals[3]; $identifier = $resource->identifier; $flocation = $vals[1]; if ($values[1]) { $rootnode = $resource; $rootvals = $flocation; continue; } $depres[] = $identifier; $depfiles[] = $vals[1]; $result[$virtual] = array($identifier, $flocation, false); } if (!empty($rootnode)) { $rootnode->files = array_merge($rootnode->files, $depfiles); $result[$virtual] = array($rootnode->identifier, $rootvals, true); } return $result; } public static function process_linked_files($content, cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $webcontent = false) { // Detect all embedded files // locate their physical counterparts in moodle 2 backup // copy all files in the cc package stripping any spaces and using only lowercase letters // add those files as resources of the type webcontent to the manifest // replace the links to the resource using $IMS-CC-FILEBASE$ and their new locations // cc_resource has array of files and array of dependencies // most likely we would need to add all files as independent resources and than // attach them all as dependencies to the forum tag. $lfiles = self::embedded_files($content); $text = $content; $deps = array(); if (!empty($lfiles)) { $files = self::handle_static_content($manifest, $packageroot, $contextid, $outdir); $replaceprefix = $webcontent ? '' : '$IMS-CC-FILEBASE$'; foreach ($lfiles as $lfile) { if (isset($files[$lfile])) { $filename = str_replace('%2F', '/', rawurlencode($lfile)); $content = str_replace('@@PLUGINFILE@@'.$filename, $replaceprefix.'../'.$files[$lfile][1], $content); // For the legacy stuff. $content = str_replace('$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $filename), $replaceprefix.'../'.$files[$lfile][1], $content); $deps[] = $files[$lfile][0]; } } $text = $content; } return array($text, $deps); } public static function relative_location($originpath, $linkingpath) { return false; } } final class cc_resource_location { /** * * Root directory * @var string */ private $rootdir = null; /** * * new directory * @var string */ private $dir = null; /** * * Full precalculated path * @var string */ private $fullpath = null; /** * * ctor * @param string $rootdir - path to the containing directory * @throws InvalidArgumentException * @throws RuntimeException */ public function __construct($rootdir) { $rdir = realpath($rootdir); if (empty($rdir)) { throw new InvalidArgumentException('Invalid path!'); } $dir = cc_helpers::randomdir($rdir, 'i_'); if ($dir === false) { throw new RuntimeException('Unable to create directory!'); } $this->rootdir = $rdir; $this->dir = $dir; $this->fullpath = $rdir.DIRECTORY_SEPARATOR.$dir; } /** * * Newly created directory * @return string */ public function dirname($endseparator=false) { return $this->dir.($endseparator ? '/' : ''); } /** * * Full path to the new directory * @return string */ public function fullpath($endseparator=false) { return $this->fullpath.($endseparator ? DIRECTORY_SEPARATOR : ''); } /** * Returns containing dir * @return string */ public function rootdir($endseparator=false) { return $this->rootdir.($endseparator ? DIRECTORY_SEPARATOR : ''); } } class pkg_static_resources { /** * @var array */ private $values = array(); /** * @var boolean */ public $finished = false; /** * @var pkg_static_resources */ private static $instance = null; private function __clone() { } private function __construct() { } /** * @return pkg_static_resources */ public static function instance() { if (empty(self::$instance)) { $c = __CLASS__; self::$instance = new $c(); } return self::$instance; } /** * * add new element * @param string $identifier * @param string $file * @param boolean $main */ public function add($key, $identifier, $file, $main, $node = null) { $this->values[$key] = array($identifier, $file, $main, $node); } /** * @return array */ public function get_values() { return $this->values; } public function get_identifier($location) { return isset($this->values[$location]) ? $this->values[$location] : false; } public function reset() { $this->values = array(); $this->finished = false; } } class pkg_resource_dependencies { /** * @var array */ private $values = array(); /** * @var pkg_resource_dependencies */ private static $instance = null; private function __clone() { } private function __construct() { } /** * @return pkg_resource_dependencies */ public static function instance() { if (empty(self::$instance)) { $c = __CLASS__; self::$instance = new $c(); } return self::$instance; } /** * @param array $deps */ public function add(array $deps) { $this->values = array_merge($this->values, $deps); } public function reset() { $this->values = array(); } /** * @return array */ public function get_deps() { return $this->values; } } cc/cc_lib/cc_general.php 0000644 00000004703 15215711721 0011145 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'gral_lib/cssparser.php'; require_once 'xmlbase.php'; class general_cc_file extends XMLGenericDocument { /** * * Root element * @var DOMElement */ protected $root = null; protected $rootns = null; protected $rootname = null; protected $ccnamespaces = array(); protected $ccnsnames = array(); public function __construct() { parent::__construct(); foreach ($this->ccnamespaces as $key => $value){ $this->registerNS($key,$value); } } protected function on_create() { $rootel = $this->append_new_element_ns($this->doc, $this->ccnamespaces[$this->rootns], $this->rootname); //add all namespaces foreach ($this->ccnamespaces as $key => $value) { $dummy_attr = "{$key}:dummy"; $this->doc->createAttributeNS($value,$dummy_attr); } // add location of schemas $schemaLocation=''; foreach ($this->ccnsnames as $key => $value) { $vt = empty($schemaLocation) ? '' : ' '; $schemaLocation .= $vt.$this->ccnamespaces[$key].' '.$value; } if (!empty($schemaLocation) && isset($this->ccnamespaces['xsi'])) { $this->append_new_attribute_ns($rootel, $this->ccnamespaces['xsi'], 'xsi:schemaLocation', $schemaLocation); } $this->root = $rootel; } } cc/cc_lib/cc_metadata_file.php 0000644 00000002757 15215711721 0012316 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Metadata management * * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Metadata File Education Type * */ class cc_metadata_file_educational{ public $value = array(); public function set_value($value){ $arr = array($value); $this->value[] = $arr; } } /** * Metadata File * */ class cc_metadata_file implements cc_i_metadata_file { public $arrayeducational = array(); public function add_metadata_file_educational($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->value)? $this->arrayeducational['value']=$obj->value:null; } } cc/cc_lib/cc_assesment_sfib.php 0000644 00000021163 15215711721 0012534 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once('cc_asssesment.php'); class cc_assesment_question_sfib extends cc_assesment_question_proc_base { public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) { parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $this->qtype = cc_qti_profiletype::field_entry; $this->correct_answer_node_id = $this->questions->nodeValue( 'plugin_qtype_truefalse_question/truefalse/trueanswer', $this->question_node ); $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade'); $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000'; } public function on_generate_metadata() { parent::on_generate_metadata(); $category = $this->questions->nodeValue('../../name', $this->question_node); if (!empty($category)) { $this->qmetadata->set_category($category); } } public function on_generate_presentation() { parent::on_generate_presentation(); $response_str = new cc_assesment_response_strtype(); $response_fib = new cc_assesment_render_fibtype(); // The standard requires that only rows attribute must be set, // the rest may or may not be configured. For the sake of brevity we leave it empty. $response_fib->set_rows(1); $response_str->set_render_fib($response_fib); $this->qpresentation->set_response_str($response_str); } public function on_generate_feedbacks() { parent::on_generate_feedbacks(); // Question combined feedback. $responsenodes = $this->questions->nodeList('plugin_qtype_shortanswer_question//answer', $this->question_node); $count = 0; foreach ($responsenodes as $respnode) { $content = $this->questions->nodeValue('feedback', $respnode); if (empty($content)) { continue; } $correct = (int)$this->questions->nodeValue('fraction', $respnode) == 1; $answerid = (int)$this->questions->nodeValue('@id', $respnode); $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); $ident = $correct ? 'correct' : 'incorrect'; $ident .= '_'.$count.'_fb'; cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $ident); pkg_resource_dependencies::instance()->add($result[1]); if ($correct) { $this->correct_feedbacks[$answerid] = $ident; } else { $this->incorrect_feedbacks[$answerid] = $ident; } ++$count; } } public function on_generate_response_processing() { parent::on_generate_response_processing(); // General unconditional feedback must be added as a first respcondition // without any condition and just displayfeedback (if exists). if (!empty($this->general_feedback)) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('General feedback'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(); // Define the condition for success. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid('general_fb'); } // Answer separate conditions. $correct_responses = $this->questions->nodeList( 'plugin_qtype_shortanswer_question//answer[fraction=1]', $this->question_node); $incorrect_responses = $this->questions->nodeList( 'plugin_qtype_shortanswer_question//answer[fraction<1]', $this->question_node); $items = array( array($correct_responses, $this->correct_feedbacks), array($incorrect_responses, $this->incorrect_feedbacks) ); foreach ($items as $respfeed) { foreach ($respfeed[0] as $coresponse) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->enable_continue(); $this->qresprocessing->add_respcondition($qrespcondition); $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $respc = $this->questions->nodeValue('answertext', $coresponse); $resid = $this->questions->nodeValue('@id', $coresponse); $qvarequal = new cc_assignment_conditionvar_varequaltype($respc); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident('response'); $qvarequal->enable_case(false); if (!empty($respfeed[1][$resid])) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($respfeed[1][$resid]); } } } // Success condition. // For all question types outside of the Essay question, scoring is done in a // single <respcondition> with a continue flag set to No. The outcome is always // a variable named SCORE which value must be set to 100 in case of correct answer. // Partial scores (not 0 or 100) are not supported. $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('Correct'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(false); $qsetvar = new cc_assignment_setvartype(100); $qrespcondition->add_setvar($qsetvar); // Define the condition for success. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); foreach ($correct_responses as $coresponse) { $respc = $this->questions->nodeValue('answertext', $coresponse); $qvarequal = new cc_assignment_conditionvar_varequaltype($respc); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident('response'); $qvarequal->enable_case(false); } // Add incorrect handling. $qrespcondition = new cc_assesment_respconditiontype(); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(false); // Define the condition for failure. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qsetvar = new cc_assignment_setvartype(0); $qrespcondition->add_setvar($qsetvar); } } cc/cc_lib/cc_basiclti.php 0000644 00000017652 15215711721 0011331 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_general.php'; class basicltil1_resurce_file extends general_cc_file { const deafultname = 'basiclti.xml'; protected $rootns = 'xmlns'; protected $rootname = 'cartridge_basiclti_link'; protected $ccnamespaces = array('xmlns' => 'http://www.imsglobal.org/xsd/imslticc_v1p0', 'blti' => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0', 'lticm' => 'http://www.imsglobal.org/xsd/imslticm_v1p0', 'lticp' => 'http://www.imsglobal.org/xsd/imslticp_v1p0', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('xmlns' => 'http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd', 'blti' => 'http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd', 'lticm' => 'http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd', 'lticp' => 'http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd'); protected $title = 'Untitled'; protected $description = 'description'; protected $custom_properties = array(); protected $extension_properties = array(); protected $extension_platform = null; protected $launch_url = null; protected $secure_launch_url = null; protected $icon = null; protected $secure_icon = null; protected $vendor = false; protected $vendor_code = 'I'; protected $vendor_name = null; protected $vendor_description = null; protected $vendor_url = null; protected $vendor_contact = null; protected $cartridge_bundle = null; protected $cartridge_icon = null; public function set_title($title) { $this->title = self::safexml($title); } public function set_description($description) { $this->description = self::safexml($description); } public function set_launch_url($url) { $this->launch_url = $url; } public function set_secure_launch_url($url) { $this->secure_launch_url = $url; } public function set_launch_icon($icon) { $this->icon = $icon; } public function set_secure_launch_icon($icon) { $this->secure_icon = $icon; } public function set_vendor_code($code) { $this->vendor_code = $code; $this->vendor = true; } public function set_vendor_name($name) { $this->vendor_name = self::safexml($name); $this->vendor = true; } public function set_vendor_description($desc) { $this->vendor_description = self::safexml($desc); $this->vendor = true; } public function set_vendor_url($url) { $this->vendor_url = $url; $this->vendor = true; } public function set_vendor_contact($email) { $this->vendor_contact = array('email' => $email); $this->vendor = true; } public function add_custom_property($property, $value) { $this->custom_properties[$property] = $value; } public function add_extension($extension, $value) { $this->extension_properties[$extension] = $value; } public function set_extension_platform($value) { $this->extension_platform = $value; } public function set_cartridge_bundle($value) { $this->cartridge_bundle = $value; } public function set_cartridge_icon($value) { $this->cartridge_icon = $value; } protected function on_save() { //this has to be done like this since order of apearance of the tags is also mandatory //and specified in basiclti schema files //main items $rns = $this->ccnamespaces['blti']; $this->append_new_element_ns($this->root, $rns, 'title' , $this->title ); $this->append_new_element_ns($this->root, $rns, 'description', $this->description); //custom properties if (!empty($this->custom_properties)) { $custom = $this->append_new_element_ns($this->root, $rns, 'custom'); foreach ($this->custom_properties as $property => $value) { $node = $this->append_new_element_ns($custom, $this->ccnamespaces['lticm'], 'property' , $value); $this->append_new_attribute_ns($node, $this->ccnamespaces['xmlns'],'name', $property); } } //extension properties if (!empty($this->extension_properties)) { $extension = $this->append_new_element_ns($this->root, $rns, 'extensions'); if (!empty($this->extension_platform)) { $this->append_new_attribute_ns($extension, $this->ccnamespaces['xmlns'], 'platform', $this->extension_platform); } foreach ($this->extension_properties as $property => $value) { $node = $this->append_new_element_ns($extension, $this->ccnamespaces['lticm'], 'property' , $value); $this->append_new_attribute_ns($node, $this->ccnamespaces['xmlns'], 'name', $property); } } $this->append_new_element_ns($this->root, $rns, 'launch_url' , $this->launch_url ); if (!empty($this->secure_launch_url)) { $this->append_new_element_ns($this->root, $rns, 'secure_launch_url' , $this->secure_launch_url); } $this->append_new_element_ns($this->root, $rns, 'icon' , $this->icon ); if (!empty($this->secure_icon)) { $this->append_new_element_ns($this->root, $rns, 'secure_icon' , $this->secure_icon); } //vendor info $vendor = $this->append_new_element_ns($this->root, $rns, 'vendor'); $vcode = empty($this->vendor_code) ? 'I' : $this->vendor_code; $this->append_new_element_ns($vendor, $this->ccnamespaces['lticp'], 'code', $vcode); $this->append_new_element_ns($vendor, $this->ccnamespaces['lticp'], 'name', $this->vendor_name); if (!empty($this->vendor_description)) { $this->append_new_element_ns($vendor, $this->ccnamespaces['lticp'], 'description', $this->vendor_description); } if (!empty($this->vendor_url)) { $this->append_new_element_ns($vendor, $this->ccnamespaces['lticp'], 'url', $this->vendor_url); } if (!empty($this->vendor_contact)) { $vcontact = $this->append_new_element_ns($vendor, $this->ccnamespaces['lticp'], 'contact'); $this->append_new_element_ns($vcontact, $this->ccnamespaces['lticp'], 'email', $this->vendor_contact['email']); } //cartridge bundle and icon if (!empty($this->cartridge_bundle)) { $cbundle = $this->append_new_element_ns($this->root, $this->ccnamespaces['xmlns'], 'cartridge_bundle'); $this->append_new_attribute_ns($cbundle, $this->ccnamespaces['xmlns'], 'identifierref', $this->cartridge_bundle); } if (!empty($this->cartridge_icon)) { $cicon = $this->append_new_element_ns($this->root, $this->ccnamespaces['xmlns'], 'cartridge_icon'); $this->append_new_attribute_ns($cicon, $this->ccnamespaces['xmlns'], 'identifierref', $this->cartridge_icon); } return true; } } cc/cc_lib/cc_asssesment.php 0000644 00000270024 15215711721 0011716 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once('cc_utils.php'); require_once('cc_general.php'); abstract class cc_xml_namespace { const xml = 'http://www.w3.org/XML/1998/namespace'; } abstract class cc_qti_metadata { // Assessment. const qmd_assessmenttype = 'qmd_assessmenttype'; const qmd_scoretype = 'qmd_scoretype'; const qmd_feedbackpermitted = 'qmd_feedbackpermitted'; const qmd_hintspermitted = 'qmd_hintspermitted'; const qmd_solutionspermitted = 'qmd_solutionspermitted'; const qmd_timelimit = 'qmd_timelimit'; const cc_allow_late_submission = 'cc_allow_late_submission'; const cc_maxattempts = 'cc_maxattempts'; const cc_profile = 'cc_profile'; // Items. const cc_weighting = 'cc_weighting'; const qmd_scoringpermitted = 'qmd_scoringpermitted'; const qmd_computerscored = 'qmd_computerscored'; const cc_question_category = 'cc_question_category'; } abstract class cc_qti_profiletype { const multiple_choice = 'cc.multiple_choice.v0p1'; const multiple_response = 'cc.multiple_response.v0p1'; const true_false = 'cc.true_false.v0p1'; const field_entry = 'cc.fib.v0p1'; const pattern_match = 'cc.pattern_match.v0p1'; const essay = 'cc.essay.v0p1'; /** * * validates a profile value * @param string $value * @return boolean */ public static function valid($value) { static $verification_values = array( self::essay, self::field_entry, self::multiple_choice, self::multiple_response, self::pattern_match, self::true_false ); return in_array($value, $verification_values); } } abstract class cc_qti_values { const exam_profile = 'cc.exam.v0p1'; const Yes = 'Yes'; const No = 'No'; const Response = 'Response'; const Solution = 'Solution'; const Hint = 'Hint'; const Examination = 'Examination'; const Percentage = 'Percentage'; const unlimited = 'unlimited'; const Single = 'Single'; const Multiple = 'Multiple'; const Ordered = 'Ordered'; const Asterisk = 'Asterisk'; const Box = 'Box'; const Dashline = 'Dashline'; const Underline = 'Underline'; const Decimal = 'Decimal'; const Integer = 'Integer'; const Scientific = 'Scientific'; const String = 'String'; const SCORE = 'SCORE'; const Set = 'Set'; const Complete = 'Complete'; const texttype = 'text/plain'; const htmltype = 'text/html'; } abstract class cc_qti_tags { const questestinterop = 'questestinterop'; const assessment = 'assessment'; const qtimetadata = 'qtimetadata'; const qtimetadatafield = 'qtimetadatafield'; const fieldlabel = 'fieldlabel'; const fieldentry = 'fieldentry'; const section = 'section'; const ident = 'ident'; const item = 'item'; const title = 'title'; const itemmetadata = 'itemmetadata'; const presentation = 'presentation'; const material = 'material'; const mattext = 'mattext'; const matref = 'matref'; const matbreak = 'matbreak'; const texttype = 'texttype'; const response_lid = 'response_lid'; const render_choice = 'render_choice'; const response_label = 'response_label'; const resprocessing = 'resprocessing'; const outcomes = 'outcomes'; const decvar = 'decvar'; const respcondition = 'respcondition'; const conditionvar = 'conditionvar'; const other = 'other'; const displayfeedback = 'displayfeedback'; const maxvalue = 'maxvalue'; const minvalue = 'minvalue'; const varname = 'varname'; const vartype = 'vartype'; const continue_ = 'continue'; const feedbacktype = 'feedbacktype'; const linkrefid = 'linkrefid'; const varequal = 'varequal'; const respident = 'respident'; const itemfeedback = 'itemfeedback'; const flow_mat = 'flow_mat'; const rcardinality = 'rcardinality'; const charset = 'charset'; const label = 'label'; const uri = 'uri'; const width = 'width'; const height = 'height'; const x0 = 'x0'; const y0 = 'y0'; const xml_lang = 'lang'; const xml_space = 'space'; const rubric = 'rubric'; const altmaterial = 'altmaterial'; const presentation_material = 'presentation_material'; const t_class = 'class'; const material_ref = 'material_ref'; const rtiming = 'rtiming'; const render_fib = 'render_fib'; const shuffle = 'shuffle'; const minnumber = 'minnumber'; const maxnumber = 'maxnumber'; const encoding = 'encoding'; const maxchars = 'maxchars'; const prompt = 'prompt'; const fibtype = 'fibtype'; const rows = 'rows'; const columns = 'columns'; const labelrefid = 'labelrefid'; const rshuffle = 'rshuffle'; const match_group = 'match_group'; const match_max = 'match_max'; const flow = 'flow'; const response_str = 'response_str'; const flow_label = 'flow_label'; const setvar = 'setvar'; const action = 'action'; const and_ = 'and'; const not_ = 'not'; const case_ = 'case'; const varsubstring = 'varsubstring'; const hint = 'hint'; const solution = 'solution'; const feedbackstyle = 'feedbackstyle'; const solutionmaterial = 'solutionmaterial'; const hintmaterial = 'hintmaterial'; } class cc_question_metadata_base { /** * @var array */ protected $metadata = array(); /** * @param string $setting * @param mixed $value */ protected function set_setting($setting, $value = null) { $this->metadata[$setting] = $value; } /** * @param string $setting * @return mixed */ protected function get_setting($setting) { $result = null; if (array_key_exists($setting, $this->metadata)) { $result = $this->metadata[$setting]; } return $result; } /** * @param string $setting * @param string $namespace * @param string $value */ protected function set_setting_wns($setting, $namespace, $value = null) { $this->metadata[$setting] = array($namespace => $value); } /** * @param string $setting * @param boolean $value */ protected function enable_setting_yesno($setting, $value = true) { $svalue = $value ? cc_qti_values::Yes : cc_qti_values::No; $this->set_setting($setting, $svalue); } /** * @param XMLGenericDocument $doc * @param DOMNode $item * @param string $namespace */ public function generate_attributes(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { foreach ($this->metadata as $attribute => $value) { if (!is_null($value)) { if (!is_array($value)) { $doc->append_new_attribute_ns($item, $namespace, $attribute, $value); } else { $ns = key($value); $nval = current($value); if (!is_null($nval)) { $doc->append_new_attribute_ns($item, $ns, $attribute, $nval); } } } } } /** * @param XMLGenericDocument $doc * @param DOMNode $item * @param string $namespace */ public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $qtimetadata = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::qtimetadata); foreach ($this->metadata as $label => $entry) { if (!is_null($entry)) { $qtimetadatafield = $doc->append_new_element_ns($qtimetadata, $namespace, cc_qti_tags::qtimetadatafield); $doc->append_new_element_ns($qtimetadatafield, $namespace, cc_qti_tags::fieldlabel, $label); $doc->append_new_element_ns($qtimetadatafield, $namespace, cc_qti_tags::fieldentry, $entry); } } } } class cc_question_metadata extends cc_question_metadata_base { public function set_category($value) { $this->set_setting(cc_qti_metadata::cc_question_category, $value); } public function set_weighting($value) { $this->set_setting(cc_qti_metadata::cc_weighting, $value); } public function enable_scoringpermitted($value = true) { $this->enable_setting_yesno(cc_qti_metadata::qmd_scoringpermitted, $value); } public function enable_computerscored($value = true) { $this->enable_setting_yesno(cc_qti_metadata::qmd_computerscored, $value); } /** * * Constructs metadata * @param string $profile * @throws InvalidArgumentException */ public function __construct($profile) { if (!cc_qti_profiletype::valid($profile)) { throw new InvalidArgumentException('Invalid profile type!'); } $this->set_setting(cc_qti_metadata::cc_profile, $profile); $this->set_setting(cc_qti_metadata::cc_question_category); $this->set_setting(cc_qti_metadata::cc_weighting ); $this->set_setting(cc_qti_metadata::qmd_scoringpermitted); $this->set_setting(cc_qti_metadata::qmd_computerscored ); } } class cc_assesment_metadata extends cc_question_metadata_base { public function enable_hints($value = true) { $this->enable_setting_yesno(cc_qti_metadata::qmd_hintspermitted, $value); } public function enable_solutions($value = true) { $this->enable_setting_yesno(cc_qti_metadata::qmd_solutionspermitted, $value); } public function enable_latesubmissions($value = true) { $this->enable_setting_yesno(cc_qti_metadata::cc_allow_late_submission, $value); } public function enable_feedback($value = true) { $this->enable_setting_yesno(cc_qti_metadata::qmd_feedbackpermitted, $value); } public function set_timelimit($value) { $ivalue = (int)$value; if (($ivalue < 0) || ($ivalue > 527401)) { throw new OutOfRangeException('Time limit value out of permitted range!'); } $this->set_setting(cc_qti_metadata::qmd_timelimit, $value); } public function set_maxattempts($value) { $valid_values = array(cc_qti_values::Examination, cc_qti_values::unlimited, 1, 2, 3, 4, 5); if (!in_array($value, $valid_values)) { throw new OutOfRangeException('Max attempts has invalid value'); } $this->set_setting(cc_qti_metadata::cc_maxattempts, $value); } public function __construct() { //prepared default values $this->set_setting(cc_qti_metadata::cc_profile , cc_qti_values::exam_profile); $this->set_setting(cc_qti_metadata::qmd_assessmenttype, cc_qti_values::Examination ); $this->set_setting(cc_qti_metadata::qmd_scoretype , cc_qti_values::Percentage ); //optional empty values $this->set_setting(cc_qti_metadata::qmd_feedbackpermitted ); $this->set_setting(cc_qti_metadata::qmd_hintspermitted ); $this->set_setting(cc_qti_metadata::qmd_solutionspermitted ); $this->set_setting(cc_qti_metadata::qmd_timelimit ); $this->set_setting(cc_qti_metadata::cc_allow_late_submission); $this->set_setting(cc_qti_metadata::cc_maxattempts ); } } class cc_assesment_mattext extends cc_question_metadata_base { protected $value = null; public function __construct($value = null) { $this->set_setting(cc_qti_tags::texttype, cc_qti_values::texttype); $this->set_setting(cc_qti_tags::charset);//, 'ascii-us'); $this->set_setting(cc_qti_tags::label); $this->set_setting(cc_qti_tags::uri); $this->set_setting(cc_qti_tags::width); $this->set_setting(cc_qti_tags::height); $this->set_setting(cc_qti_tags::x0); $this->set_setting(cc_qti_tags::y0); $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml); $this->set_setting_wns(cc_qti_tags::xml_space, cc_xml_namespace::xml);//, 'default'); $this->value = $value; } public function set_label($value) { $this->set_setting(cc_qti_tags::label, $value); } public function set_uri($value) { $this->set_setting(cc_qti_tags::uri, $value); } public function set_width_height($width = null, $height = null) { $this->set_setting(cc_qti_tags::width, $width); $this->set_setting(cc_qti_tags::height, $height); } public function set_coor($x = null, $y = null) { $this->set_setting(cc_qti_tags::x0, $x); $this->set_setting(cc_qti_tags::y0, $y); } public function set_lang($lang = null) { $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml, $lang); } public function set_content($content, $type = cc_qti_values::texttype, $charset = null) { $this->value = $content; $this->set_setting(cc_qti_tags::texttype, $type); $this->set_setting(cc_qti_tags::charset, $charset); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $mattext = $doc->append_new_element_ns_cdata($item, $namespace, cc_qti_tags::mattext, $this->value); $this->generate_attributes($doc, $mattext, $namespace); } } class cc_assesment_matref { protected $linkref = null; public function __construct($linkref) { $this->linkref = $linkref; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $doc->append_new_element_ns($item, $namespace, cc_qti_tags::matref, $this->linkref); $doc->append_new_attribute_ns($node, $namespace, cc_qti_tags::linkrefid, $this->linkref); } } class cc_assesment_response_matref extends cc_assesment_matref { public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::material_ref); $doc->append_new_attribute_ns($node, $namespace, cc_qti_tags::linkrefid, $this->linkref); } } class cc_assesment_matbreak { public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $doc->append_new_element_ns($item, $namespace, cc_qti_tags::matbreak); } } abstract class cc_assesment_material_base extends cc_question_metadata_base { /** * @var mixed */ protected $mattag = null; protected $tagname = null; protected function set_tag_value($object) { $this->mattag = $object; } public function set_mattext(cc_assesment_mattext $object) { $this->set_tag_value($object); } public function set_matref(cc_assesment_matref $object) { $this->set_tag_value($object); } public function set_matbreak(cc_assesment_matbreak $object) { $this->set_tag_value($object); } public function set_lang($value) { $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml, $value); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $material = $doc->append_new_element_ns($item, $namespace, $this->tagname); $this->generate_attributes($doc, $material, $namespace); if (!empty($this->mattag)) { $this->mattag->generate($doc, $material, $namespace); } return $material; } } class cc_assesment_altmaterial extends cc_assesment_material_base { public function __construct($value = null) { $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml); $this->tagname = cc_qti_tags::altmaterial; } } class cc_assesment_material extends cc_assesment_material_base { protected $altmaterial = null; public function __construct($value = null) { $this->set_setting(cc_qti_tags::label); $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml); $this->tagname = cc_qti_tags::material; } public function set_label($value) { $this->set_setting(cc_qti_tags::label, $value); } public function set_altmaterial(cc_assesment_altmaterial $object) { $this->altmaterial = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $material = parent::generate($doc, $item, $namespace); if (!empty($this->altmaterial)) { $this->altmaterial->generate($doc, $material, $namespace); } } } class cc_assesment_rubric_base extends cc_question_metadata_base { protected $material = null; public function set_material($object) { $this->material = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $rubric = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::rubric); if (!empty($this->material)) { $this->material->generate($doc, $rubric, $namespace); } } } class cc_assesment_presentation_material_base extends cc_question_metadata_base { protected $flowmats = array(); public function add_flow_mat($object) { $this->flowmats[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::presentation_material); if (!empty($this->flowmats)) { foreach ($this->flowmats as $flow_mat) { $flow_mat->generate($doc, $node, $namespace); } } } } class cc_assesment_flow_mat_base extends cc_question_metadata_base { protected $mattag = null; protected function set_tag_value($object) { $this->mattag = $object; } public function set_flow_mat(cc_assesment_flow_mat_base $object) { $this->set_tag_value($object); } public function set_material(cc_assesment_material $object) { $this->set_tag_value($object); } public function set_material_ref(cc_assesment_matref $object) { $this->set_tag_value($object); } public function __construct($value = null) { $this->set_setting(cc_qti_tags::t_class); } public function set_class($value) { $this->set_setting(cc_qti_tags::t_class, $value); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::flow_mat); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->mattag)) { $this->mattag->generate($doc, $node, $namespace); } } } class cc_assesment_section extends cc_question_metadata_base { /** * @var array */ protected $items = array(); public function __construct() { $this->set_setting(cc_qti_tags::ident, cc_helpers::uuidgen('I_')); $this->set_setting(cc_qti_tags::title); $this->set_setting_wns(cc_qti_tags::xml_lang, cc_xml_namespace::xml); } public function set_ident($value) { $this->set_setting(cc_qti_tags::ident, $value); } public function set_title($value) { $this->set_setting(cc_qti_tags::title, $value); } public function set_lang($value) { $this->set_setting_wns(cc_qti_tags::xml_lang, cc_xml_namespace::xml, $value); } public function add_item(cc_assesment_section_item $object) { $this->items[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::section); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->items)) { foreach ($this->items as $item) { $item->generate($doc, $node, $namespace); } } } } class cc_assesment_itemmetadata extends cc_question_metadata_base { public function add_metadata($object) { $this->metadata[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::itemmetadata); if (!empty($this->metadata)) { foreach ($this->metadata as $metaitem) { $metaitem->generate($doc, $node, $namespace); } } } } class cc_assesment_decvartype extends cc_question_metadata_base { public function __construct() { $this->set_setting(cc_qti_tags::varname, cc_qti_values::SCORE); $this->set_setting(cc_qti_tags::vartype, cc_qti_values::Integer); $this->set_setting(cc_qti_tags::minvalue); $this->set_setting(cc_qti_tags::maxvalue); } public function set_vartype($value) { $this->set_setting(cc_qti_tags::vartype, $value); } public function set_limits($min = null, $max = null) { $this->set_setting(cc_qti_tags::minvalue, $min); $this->set_setting(cc_qti_tags::maxvalue, $max); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::decvar); $this->generate_attributes($doc, $node, $namespace); } } class cc_assignment_conditionvar_othertype extends cc_question_metadata_base { public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $doc->append_new_element_ns($item, $namespace, cc_qti_tags::other); } } class cc_assignment_conditionvar_varequaltype extends cc_question_metadata_base { protected $tagname = null; protected $answerid = null; public function __construct($value = null) { if (is_null($value)) { throw new InvalidArgumentException('Must not pass null!'); } $this->answerid = $value; $this->set_setting(cc_qti_tags::respident); $this->set_setting(cc_qti_tags::case_);//, cc_qti_values::No ); $this->tagname = cc_qti_tags::varequal; } public function set_respident($value) { $this->set_setting(cc_qti_tags::respident, $value); } public function enable_case($value = true) { $this->enable_setting_yesno(cc_qti_tags::case_, $value); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, $this->tagname, $this->answerid); $this->generate_attributes($doc, $node, $namespace); } } class cc_assignment_conditionvar_varsubstringtype extends cc_assignment_conditionvar_varequaltype { public function __construct($value) { parent::__construct($value); $this->tagname = cc_qti_tags::varsubstring; } } class cc_assignment_conditionvar_andtype extends cc_question_metadata_base { protected $nots = array(); protected $varequals = array(); public function set_not(cc_assignment_conditionvar_varequaltype $object) { $this->nots[] = $object; } public function set_varequal(cc_assignment_conditionvar_varequaltype $object) { $this->varequals[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::and_); if (!empty($this->nots)) { foreach ($this->nots as $notv) { $not = $doc->append_new_element_ns($node, $namespace, cc_qti_tags::not_); $notv->generate($doc, $not, $namespace); } } if (!empty($this->varequals)) { foreach ($this->varequals as $varequal) { $varequal->generate($doc, $node, $namespace); } } } } class cc_assignment_conditionvar extends cc_question_metadata_base { /** * @var cc_assignment_conditionvar_andtype */ protected $and = null; /** * @var cc_assignment_conditionvar_othertype */ protected $other = null; /** * @var array */ protected $varequal = array(); /** * @var cc_assignment_conditionvar_varsubstringtype */ protected $varsubstring = null; public function set_and(cc_assignment_conditionvar_andtype $object) { $this->and = $object; } public function set_other(cc_assignment_conditionvar_othertype $object) { $this->other = $object; } public function set_varequal(cc_assignment_conditionvar_varequaltype $object) { $this->varequal[] = $object; } public function set_varsubstring(cc_assignment_conditionvar_varsubstringtype $object) { $this->varsubstring = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::conditionvar); if (!empty($this->and)) { $this->and->generate($doc, $node, $namespace); } if (!empty($this->other)) { $this->other->generate($doc, $node, $namespace); } if (!empty($this->varequal)) { foreach ($this->varequal as $varequal) { $varequal->generate($doc, $node, $namespace); } } if (!empty($this->varsubstring)) { $this->varsubstring->generate($doc, $node, $namespace); } } } class cc_assignment_displayfeedbacktype extends cc_question_metadata_base { public function __construct() { $this->set_setting(cc_qti_tags::feedbacktype); $this->set_setting(cc_qti_tags::linkrefid); } public function set_feedbacktype($value) { $this->set_setting(cc_qti_tags::feedbacktype, $value); } public function set_linkrefid($value) { $this->set_setting(cc_qti_tags::linkrefid, $value); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::displayfeedback); $this->generate_attributes($doc, $node, $namespace); } } class cc_assignment_setvartype extends cc_question_metadata_base { /** * @var integer */ protected $tagvalue = null; public function __construct($tagvalue = 100) { $this->set_setting(cc_qti_tags::varname, cc_qti_values::SCORE); $this->set_setting(cc_qti_tags::action , cc_qti_values::Set ); $this->tagvalue = $tagvalue; } public function set_varname($value) { $this->set_setting(cc_qti_tags::varname, $value); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::setvar, $this->tagvalue); $this->generate_attributes($doc, $node, $namespace); } } class cc_assesment_respconditiontype extends cc_question_metadata_base { /** * @var cc_assignment_conditionvar */ protected $conditionvar = null; protected $setvar = array(); protected $displayfeedback = array(); public function __construct() { $this->set_setting(cc_qti_tags::title); $this->set_setting(cc_qti_tags::continue_, cc_qti_values::No); } public function set_title($value) { $this->set_setting(cc_qti_tags::title, $value); } public function enable_continue($value = true) { $this->enable_setting_yesno(cc_qti_tags::continue_, $value); } public function set_conditionvar(cc_assignment_conditionvar $object) { $this->conditionvar = $object; } public function add_setvar(cc_assignment_setvartype $object) { $this->setvar[] = $object; } public function add_displayfeedback(cc_assignment_displayfeedbacktype $object) { $this->displayfeedback[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::respcondition); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->conditionvar)) { $this->conditionvar->generate($doc, $node, $namespace); } if (!empty($this->setvar)) { foreach ($this->setvar as $setvar) { $setvar->generate($doc, $node, $namespace); } } if (!empty($this->displayfeedback)) { foreach ($this->displayfeedback as $displayfeedback) { $displayfeedback->generate($doc, $node, $namespace); } } } } class cc_assesment_resprocessingtype extends cc_question_metadata_base { /** * @var cc_assesment_decvartype */ protected $decvar = null; /** * @var array */ protected $respconditions = array(); public function set_decvar(cc_assesment_decvartype $object) { $this->decvar = $object; } public function add_respcondition(cc_assesment_respconditiontype $object) { $this->respconditions[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::resprocessing); $outcomes = $doc->append_new_element_ns($node, $namespace, cc_qti_tags::outcomes); if (!empty($this->decvar)) { $this->decvar->generate($doc, $outcomes, $namespace); } if (!empty($this->respconditions)) { foreach ($this->respconditions as $rcond) { $rcond->generate($doc, $node, $namespace); } } } } class cc_assesment_itemfeedback_shintmaterial_base extends cc_question_metadata_base { /** * @var string */ protected $tagname = null; /** * @var array */ protected $flow_mats = array(); /** * @var array */ protected $materials = array(); /** * @param cc_assesment_flow_mattype $object */ public function add_flow_mat(cc_assesment_flow_mattype $object) { $this->flow_mats[] = $object; } /** * @param cc_assesment_material $object */ public function add_material(cc_assesment_material $object) { $this->materials[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, $this->tagname); if (!empty($this->flow_mats)) { foreach ($this->flow_mats as $flow_mat) { $flow_mat->generate($doc, $node, $namespace); } } if (!empty($this->materials)) { foreach ($this->materials as $material) { $material->generate($doc, $node, $namespace); } } } } class cc_assesment_itemfeedback_hintmaterial extends cc_assesment_itemfeedback_shintmaterial_base { public function __construct() { $this->tagname = cc_qti_tags::hint; } } class cc_assesment_itemfeedback_solutionmaterial extends cc_assesment_itemfeedback_shintmaterial_base { public function __construct() { $this->tagname = cc_qti_tags::solutionmaterial; } } class cc_assesment_itemfeedback_shintype_base extends cc_question_metadata_base { /** * @var string */ protected $tagname = null; /** * @var array */ protected $items = array(); public function __construct() { $this->set_setting(cc_qti_tags::feedbackstyle, cc_qti_values::Complete); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, $this->tagname); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->items)) { foreach ($this->items as $telement) { $telement->generate($doc, $node, $namespace); } } } } class cc_assesment_itemfeedback_solutiontype extends cc_assesment_itemfeedback_shintype_base { public function __construct() { parent::__construct(); $this->tagname = cc_qti_tags::solution; } /** * @param cc_assesment_itemfeedback_solutionmaterial $object */ public function add_solutionmaterial(cc_assesment_itemfeedback_solutionmaterial $object) { $this->items[] = $object; } } class cc_assesment_itemfeedbac_hinttype extends cc_assesment_itemfeedback_shintype_base { public function __construct() { parent::__construct(); $this->tagname = cc_qti_tags::hint; } /** * @param cc_assesment_itemfeedback_hintmaterial $object */ public function add_hintmaterial(cc_assesment_itemfeedback_hintmaterial $object) { $this->items[] = $object; } } class cc_assesment_itemfeedbacktype extends cc_question_metadata_base { /** * @var cc_assesment_flow_mattype */ protected $flow_mat = null; /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_itemfeedback_solutiontype */ protected $solution = null; protected $hint = null; /** @var cc_assignment_displayfeedbacktype item feedback. */ protected $itemfeedback; public function __construct() { $this->set_setting(cc_qti_tags::ident, cc_helpers::uuidgen('I_')); $this->set_setting(cc_qti_tags::title); } /** * @param string $value */ public function set_ident($value) { $this->set_setting(cc_qti_tags::ident, $value); } /** * @param string $value */ public function set_title($value) { $this->set_setting(cc_qti_tags::title, $value); } /** * @param cc_assesment_flow_mattype $object */ public function set_flow_mat(cc_assesment_flow_mattype $object) { $this->flow_mat = $object; } /** * @param cc_assesment_material $object */ public function set_material(cc_assesment_material $object) { $this->material = $object; } /** * @param cc_assesment_itemfeedback_solutiontype $object */ public function set_solution(cc_assesment_itemfeedback_solutiontype $object) { $this->solution = $object; } public function set_hint($object) { $this->hint = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::itemfeedback); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->flow_mat) && empty($this->material)) { $this->flow_mat->generate($doc, $node, $namespace); } if (!empty($this->material) && empty($this->flow_mat)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->solution)) { $this->solution->generate($doc, $node, $namespace); } if (!empty($this->itemfeedback)) { $this->itemfeedback->generate($doc, $node, $namespace); } } } class cc_assesment_section_item extends cc_assesment_section { /** * @var cc_assesment_itemmetadata */ protected $itemmetadata = null; /** * @var cc_assesment_presentation */ protected $presentation = null; protected $resprocessing = array(); protected $itemfeedback = array(); public function set_itemmetadata(cc_assesment_itemmetadata $object) { $this->itemmetadata = $object; } public function set_presentation(cc_assesment_presentation $object) { $this->presentation = $object; } public function add_resprocessing(cc_assesment_resprocessingtype $object) { $this->resprocessing[] = $object; } public function add_itemfeedback(cc_assesment_itemfeedbacktype $object) { $this->itemfeedback[] = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::item); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->itemmetadata)) { $this->itemmetadata->generate($doc, $node, $namespace); } if (!empty($this->presentation)) { $this->presentation->generate($doc, $node, $namespace); } if (!empty($this->resprocessing)) { foreach ($this->resprocessing as $resprocessing) { $resprocessing->generate($doc, $node, $namespace); } } if (!empty($this->itemfeedback)) { foreach ($this->itemfeedback as $itemfeedback) { $itemfeedback->generate($doc, $node, $namespace); } } } } class cc_assesment_render_choicetype extends cc_question_metadata_base { /** * @var array */ protected $materials = array(); /** * @var array */ protected $material_refs = array(); /** * @var array */ protected $response_labels = array(); /** * @var array */ protected $flow_labels = array(); public function __construct() { $this->set_setting(cc_qti_tags::shuffle, cc_qti_values::No); $this->set_setting(cc_qti_tags::minnumber); $this->set_setting(cc_qti_tags::maxnumber); } public function add_material(cc_assesment_material $object) { $this->materials[] = $object; } public function add_material_ref(cc_assesment_response_matref $object) { $this->material_refs[] = $object; } public function add_response_label(cc_assesment_response_labeltype $object) { $this->response_labels[] = $object; } public function add_flow_label($object) { $this->flow_labels[] = $object; } public function enable_shuffle($value = true) { $this->enable_setting_yesno(cc_qti_tags::shuffle, $value); } public function set_limits($min = null, $max = null) { $this->set_setting(cc_qti_tags::minnumber, $min); $this->set_setting(cc_qti_tags::maxnumber, $max); } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::render_choice); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->materials)) { foreach ($this->materials as $mattag) { $mattag->generate($doc, $node, $namespace); } } if (!empty($this->material_refs)) { foreach ($this->material_refs as $matreftag) { $matreftag->generate($doc, $node, $namespace); } } if (!empty($this->response_labels)) { foreach ($this->response_labels as $resplabtag) { $resplabtag->generate($doc, $node, $namespace); } } if (!empty($this->flow_labels)) { foreach ($this->flow_labels as $flowlabtag) { $flowlabtag->generate($doc, $node, $namespace); } } } } class cc_assesment_flow_mattype extends cc_question_metadata_base { /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_response_matref */ protected $material_ref = null; /** * @var cc_assesment_flow_mattype */ protected $flow_mat = null; public function __construct() { $this->set_setting(cc_qti_tags::t_class); } public function set_class($value) { $this->set_setting(cc_qti_tags::t_class, $value); } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_material_ref(cc_assesment_response_matref $object) { $this->material_ref = $object; } public function set_flow_mat(cc_assesment_flow_mattype $object) { $this->flow_mat = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::flow_mat); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->flow_mat)) { $this->flow_mat->generate($doc, $node, $namespace); } if (!empty($this->material)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->material_ref)) { $this->material_ref->generate($doc, $node, $namespace); } } } class cc_assesment_response_labeltype extends cc_question_metadata_base { /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_response_matref */ protected $material_ref = null; /** * @var cc_assesment_flow_mattype */ protected $flow_mat = null; public function __construct() { $this->set_setting(cc_qti_tags::ident, cc_helpers::uuidgen('I_')); $this->set_setting(cc_qti_tags::labelrefid); $this->set_setting(cc_qti_tags::rshuffle); $this->set_setting(cc_qti_tags::match_group); $this->set_setting(cc_qti_tags::match_max); } public function set_ident($value) { $this->set_setting(cc_qti_tags::ident, $value); } public function get_ident() { return $this->get_setting(cc_qti_tags::ident); } public function set_labelrefid($value) { $this->set_setting(cc_qti_tags::labelrefid, $value); } public function enable_rshuffle($value = true) { $this->enable_setting_yesno(cc_qti_tags::rshuffle, $value); } public function set_match_group($value) { $this->set_setting(cc_qti_tags::match_group, $value); } public function set_match_max($value) { $this->set_setting(cc_qti_tags::match_max, $value); } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_material_ref(cc_assesment_response_matref $object) { $this->material_ref = $object; } public function set_flow_mat(cc_assesment_flow_mattype $object) { $this->flow_mat = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::response_label); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->material)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->material_ref)) { $this->material_ref->generate($doc, $node, $namespace); } if (!empty($this->flow_mat)) { $this->flow_mat->generate($doc, $node, $namespace); } } } class cc_assesment_flow_labeltype extends cc_question_metadata_base { /** * @var cc_assesment_flow_labeltype */ protected $flow_label = null; /** * @var cc_assesment_response_labeltype */ protected $response_label = null; /** @var cc_assesment_material assesment material. */ protected $material; /** @var cc_assesment_response_matref assesment response material ref. */ protected $material_ref; public function __construct() { $this->set_setting(cc_qti_tags::t_class); } public function set_class($value) { $this->set_setting(cc_qti_tags::t_class, $value); } public function set_flow_label(cc_assesment_flow_labeltype $object) { $this->flow_label = $object; } public function set_response_label(cc_assesment_response_labeltype $object) { $this->response_label = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::flow_label); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->material)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->material_ref)) { $this->material_ref->generate($doc, $node, $namespace); } if (!empty($this->response_label)) { $this->response_label->generate($doc, $node, $namespace); } if (!empty($this->flow_label)) { $this->flow_label->generate($doc, $node, $namespace); } } } class cc_assesment_render_fibtype extends cc_question_metadata_base { /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_response_matref */ protected $material_ref = null; /** * @var cc_assesment_response_labeltype */ protected $response_label = null; /** * * Enter description here ... * @var unknown_type */ protected $flow_label = null; public function __construct() { $this->set_setting(cc_qti_tags::encoding ); $this->set_setting(cc_qti_tags::charset ); $this->set_setting(cc_qti_tags::rows ); $this->set_setting(cc_qti_tags::columns ); $this->set_setting(cc_qti_tags::maxchars ); $this->set_setting(cc_qti_tags::minnumber); $this->set_setting(cc_qti_tags::maxnumber); $this->set_setting(cc_qti_tags::prompt, cc_qti_values::Box); $this->set_setting(cc_qti_tags::fibtype, cc_qti_values::String); } public function set_encoding($value) { $this->set_setting(cc_qti_tags::encoding, $value); } public function set_charset($value) { $this->set_setting(cc_qti_tags::charset, $value); } public function set_rows($value) { $this->set_setting(cc_qti_tags::rows, $value); } public function set_columns($value) { $this->set_setting(cc_qti_tags::columns, $value); } public function set_maxchars($value) { $this->set_setting(cc_qti_tags::columns, $value); } public function set_limits($min = null, $max = null) { $this->set_setting(cc_qti_tags::minnumber, $min); $this->set_setting(cc_qti_tags::maxnumber, $max); } public function set_prompt($value) { $this->set_setting(cc_qti_tags::prompt, $value); } public function set_fibtype($value) { $this->set_setting(cc_qti_tags::fibtype, $value); } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_material_ref(cc_assesment_response_matref $object) { $this->material_ref = $object; } public function set_response_label(cc_assesment_response_labeltype $object) { $this->response_label = $object; } public function set_flow_label($object) { $this->flow_label = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::render_fib); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->material) && empty($this->material_ref)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->material_ref) && empty($this->material)) { $this->material_ref->generate($doc, $node, $namespace); } if (!empty($this->response_label)) { $this->response_label->generate($doc, $node, $namespace); } if (!empty($this->flow_label)) { $this->flow_label->generate($doc, $node, $namespace); } } } class cc_response_lidtype extends cc_question_metadata_base { /** * @var string */ protected $tagname = null; /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_response_matref */ protected $material_ref = null; /** * @var cc_assesment_render_choicetype */ protected $render_choice = null; /** * @var cc_assesment_render_fibtype */ protected $render_fib = null; public function __construct() { $this->set_setting(cc_qti_tags::rcardinality, cc_qti_values::Single); $this->set_setting(cc_qti_tags::rtiming); $this->set_setting(cc_qti_tags::ident, cc_helpers::uuidgen('I_')); $this->tagname = cc_qti_tags::response_lid; } public function set_rcardinality($value) { $this->set_setting(cc_qti_tags::rcardinality, $value); } public function enable_rtiming($value = true) { $this->enable_setting_yesno(cc_qti_tags::rtiming, $value); } public function set_ident($value) { $this->set_setting(cc_qti_tags::ident, $value); } public function get_ident() { return $this->get_setting(cc_qti_tags::ident); } public function set_material_ref(cc_assesment_response_matref $object) { $this->material_ref = $object; } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_render_choice(cc_assesment_render_choicetype $object) { $this->render_choice = $object; } public function set_render_fib(cc_assesment_render_fibtype $object) { $this->render_fib = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, $this->tagname); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->material) && empty($this->material_ref)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->material_ref) && empty($this->material)) { $this->material_ref->generate($doc, $node, $namespace); } if (!empty($this->render_choice) && empty($this->render_fib)) { $this->render_choice->generate($doc, $node, $namespace); } if (!empty($this->render_fib) && empty($this->render_choice)) { $this->render_fib->generate($doc, $node, $namespace); } } } class cc_assesment_response_strtype extends cc_response_lidtype { public function __construct() { $rtt = parent::__construct(); $this->tagname = cc_qti_tags::response_str; } } class cc_assesment_flowtype extends cc_question_metadata_base { /** * @var cc_assesment_flowtype */ protected $flow = null; /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_assesment_response_matref */ protected $material_ref = null; /** * @var cc_response_lidtype */ protected $response_lid = null; /** * @var cc_assesment_response_strtype */ protected $response_str = null; public function __construct() { $this->set_setting(cc_qti_tags::t_class); } public function set_class($value) { $this->set_setting(cc_qti_tags::t_class, $value); } public function set_flow(cc_assesment_flowtype $object) { $this->flow = $object; } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_material_ref(cc_assesment_response_matref $object) { $this->material_ref = $object; } public function set_response_lid(cc_response_lidtype $object) { $this->response_lid = $object; } public function set_response_str(cc_assesment_response_strtype $object) { $this->response_str = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::flow); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->flow)) { $this->flow->generate($doc, $node, $namespace); } if (!empty($this->material)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->response_lid)) { $this->response_lid->generate($doc, $node, $namespace); } if (!empty($this->response_str)) { $this->response_str->generate($doc, $node, $namespace); } } } class cc_assesment_presentation extends cc_question_metadata_base { /** * @var cc_assesment_flowtype */ protected $flow = null; /** * @var cc_assesment_material */ protected $material = null; /** * @var cc_response_lidtype */ protected $response_lid = null; /** * @var cc_assesment_response_strtype */ protected $response_str = null; public function __construct() { $this->set_setting(cc_qti_tags::label); $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml); $this->set_setting(cc_qti_tags::x0); $this->set_setting(cc_qti_tags::y0); $this->set_setting(cc_qti_tags::width); $this->set_setting(cc_qti_tags::height); } public function set_label($value) { $this->set_setting(cc_qti_tags::label, $value); } public function set_lang($value) { $this->set_setting_wns(cc_qti_tags::xml_lang , cc_xml_namespace::xml, $value); } public function set_coor($x = null, $y = null) { $this->set_setting(cc_qti_tags::x0, $x); $this->set_setting(cc_qti_tags::y0, $y); } public function set_size($width = null, $height = null) { $this->set_setting(cc_qti_tags::width, $width); $this->set_setting(cc_qti_tags::height, $height); } public function set_flow(cc_assesment_flowtype $object) { $this->flow = $object; } public function set_material(cc_assesment_material $object) { $this->material = $object; } public function set_response_lid(cc_response_lidtype $object) { $this->response_lid = $object; } public function set_response_str(cc_assesment_response_strtype $object) { $this->response_str = $object; } public function generate(XMLGenericDocument &$doc, DOMNode &$item, $namespace) { $node = $doc->append_new_element_ns($item, $namespace, cc_qti_tags::presentation); $this->generate_attributes($doc, $node, $namespace); if (!empty($this->flow)) { $this->flow->generate($doc, $node, $namespace); } if (!empty($this->material) && empty($this->flow)) { $this->material->generate($doc, $node, $namespace); } if (!empty($this->response_lid) && empty($this->flow)) { $this->response_lid->generate($doc, $node, $namespace); } if (!empty($this->response_str) && empty($this->flow)) { $this->response_str->generate($doc, $node, $namespace); } } } class assesment1_resurce_file extends general_cc_file { const deafultname = 'assesment.xml'; protected $rootns = 'xmlns'; protected $rootname = cc_qti_tags::questestinterop; protected $ccnamespaces = array('xmlns' => 'http://www.imsglobal.org/xsd/ims_qtiasiv1p2', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('xmlns' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_4/ims_qtiasiv1p2_localised.xsd'); /** * @var string */ protected $assessment_title = 'Untitled'; /** * @var cc_assesment_metadata */ protected $metadata = null; /** * @var cc_assesment_rubric_base */ protected $rubric = null; /** * @var cc_assesment_presentation_material_base */ protected $presentation_material = null; /** * @var cc_assesment_section */ protected $section = null; public function set_metadata(cc_assesment_metadata $object) { $this->metadata = $object; } public function set_rubric(cc_assesment_rubric_base $object) { $this->rubric = $object; } public function set_presentation_material(cc_assesment_presentation_material_base $object) { $this->presentation_material = $object; } public function set_section(cc_assesment_section $object) { $this->section = $object; } public function set_title($value) { $this->assessment_title = self::safexml($value); } protected function on_save() { $rns = $this->ccnamespaces[$this->rootns]; //root assesment element - required $assessment = $this->append_new_element_ns($this->root, $rns, cc_qti_tags::assessment); $this->append_new_attribute_ns($assessment, $rns, cc_qti_tags::ident, cc_helpers::uuidgen('QDB_')); $this->append_new_attribute_ns($assessment, $rns, cc_qti_tags::title, $this->assessment_title); //metadata - optional if (!empty($this->metadata)) { $this->metadata->generate($this, $assessment, $rns); } //rubric - optional if (!empty($this->rubric)) { $this->rubric->generate($this, $assessment, $rns); } //presentation_material - optional if (!empty($this->presentation_material)) { $this->presentation_material->generate($this, $assessment, $rns); } //section - required if (!empty($this->section)) { $this->section->generate($this, $assessment, $rns); } return true; } } class assesment11_resurce_file extends assesment1_resurce_file { protected $ccnsnames = array('xmlns' => 'http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_qtiasiv1p2p1_v1p0.xsd'); } abstract class cc_assesment_helper { public static $correct_fb = null; public static $incorrect_fb = null; public static function add_feedback($qitem, $content, $content_type, $ident) { if (empty($content)) { return false; } $qitemfeedback = new cc_assesment_itemfeedbacktype(); $qitem->add_itemfeedback($qitemfeedback); if (!empty($ident)) { $qitemfeedback->set_ident($ident); } $qflowmat = new cc_assesment_flow_mattype(); $qitemfeedback->set_flow_mat($qflowmat); $qmaterialfb = new cc_assesment_material(); $qflowmat->set_material($qmaterialfb); $qmattext = new cc_assesment_mattext(); $qmaterialfb->set_mattext($qmattext); $qmattext->set_content($content, $content_type); return true; } public static function add_answer($qresponse_choice, $content, $content_type) { $qresponse_label = new cc_assesment_response_labeltype(); $qresponse_choice->add_response_label($qresponse_label); $qrespmaterial = new cc_assesment_material(); $qresponse_label->set_material($qrespmaterial); $qrespmattext = new cc_assesment_mattext(); $qrespmaterial->set_mattext($qrespmattext); $qrespmattext->set_content($content, $content_type); return $qresponse_label; } public static function add_response_condition($node, $title, $ident, $feedback_refid, $respident) { $qrespcondition = new cc_assesment_respconditiontype(); $node->add_respcondition($qrespcondition); //define rest of the conditions $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qvarequal = new cc_assignment_conditionvar_varequaltype($ident); $qvarequal->enable_case(); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident($respident); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($feedback_refid); } public static function add_assesment_description($rt, $content, $contenttype) { if (empty($rt) || empty($content)) { return; } $activity_rubric = new cc_assesment_rubric_base(); $rubric_material = new cc_assesment_material(); $activity_rubric->set_material($rubric_material); $rubric_mattext = new cc_assesment_mattext(); $rubric_material->set_label('Summary'); $rubric_material->set_mattext($rubric_mattext); $rubric_mattext->set_content($content, $contenttype); $rt->set_rubric($activity_rubric); } public static function add_respcondition($node, $title, $feedback_refid, $grade_value = null, $continue = false ) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title($title); $node->add_respcondition($qrespcondition); $qrespcondition->enable_continue($continue); //Add setvar if grade present if ($grade_value !== null) { $qsetvar = new cc_assignment_setvartype($grade_value); $qrespcondition->add_setvar($qsetvar); } //define the condition for success $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($feedback_refid); } /** * * Enter description here ... * @param XMLGenericDocument $qdoc * @param unknown_type $manifest * @param cc_assesment_section $section * @param unknown_type $rootpath * @param unknown_type $contextid * @param unknown_type $outdir */ public static function process_questions(&$qdoc, &$manifest, cc_assesment_section &$section, $rootpath, $contextid, $outdir) { $question_file = $rootpath . DIRECTORY_SEPARATOR . 'questions.xml'; //load questions file $questions = new XMLGenericDocument(); if (!$questions->load($question_file)) { return false; } pkg_resource_dependencies::instance()->reset(); $questioncount = 0; $questionforexport = 0; $qids = $qdoc->nodeList('//question_instances//questionid'); foreach ($qids as $qid) { /** @var DOMNode $qid */ $value = $qid->nodeValue; if (intval($value) == 0) { continue; } $question_node = $questions->node("//question_category/questions/question[@id='{$value}']"); if (empty($question_node)) { continue; } ++$questionforexport; //process question //question type $qtype = $questions->nodeValue('qtype', $question_node); $question_processor = null; switch ($qtype) { case 'multichoice': $single_correct_answer = (int)$questions->nodeValue('plugin_qtype_multichoice_question/multichoice/single', $question_node) > 0; //TODO: Add checking for the nunmber of valid responses //If question is marked as multi response but contains only one valid answer it //should be handle as single response - classic multichoice if ($single_correct_answer) { $question_processor = new cc_assesment_question_multichoice($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); } else { $question_processor = new cc_assesment_question_multichoice_multiresponse($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); } $question_processor->generate(); ++$questioncount; break; case 'truefalse': $question_processor = new cc_assesment_question_truefalse($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $question_processor->generate(); ++$questioncount; break; case 'essay': $question_processor = new cc_assesment_question_essay($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $question_processor->generate(); ++$questioncount; break; case 'shortanswer': //This is rather ambiguos since shortanswer supports partial pattern match //In order to detect pattern match we need to scan for all the responses //if at least one of the responses uses wildcards it should be treated as //pattern match, otherwise it should be simple fill in the blank if (self::has_matching_element($questions, $question_node)) { //$question_processor = new cc_assesment_question_patternmatch($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $questionforexport--; } else { $question_processor = new cc_assesment_question_sfib($qdoc, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); } if (!empty($question_processor)) { $question_processor->generate(); ++$questioncount; } break; default: ; break; } } //return dependencies return ($questioncount == 0) || ($questioncount != $questionforexport)? false: pkg_resource_dependencies::instance()->get_deps(); } /** * * Checks if question has matching element * @param XMLGenericDocument $questions * @param object $question_node * @return bool */ public static function has_matching_element(XMLGenericDocument $questions, $question_node) { $answers = $questions->nodeList('plugin_qtype_shortanswer_question//answertext', $question_node); $result = false; foreach ($answers as $answer) { $prepare = str_replace('\*', '\#', $answer->nodeValue); $result = (strpos($prepare, '*') !== false); if ($result) { break; } } return $result; } } class cc_assesment_question_proc_base { /** * @var XMLGenericDocument */ protected $quiz = null; /** * @var XMLGenericDocument */ protected $questions = null; /** * @var cc_manifest */ protected $manifest = null; /** * @var cc_assesment_section */ protected $section = null; /** * @var DOMElement */ protected $question_node = null; /** * @var string */ protected $rootpath = null; /** * @var string */ protected $contextid = null; /** * @var string */ protected $outdir = null; /** * @var string */ protected $qtype = null; /** * @var cc_question_metadata */ protected $qmetadata = null; /** * @var cc_assesment_section_item */ protected $qitem = null; /** * @var cc_assesment_presentation */ protected $qpresentation = null; /** * @var cc_response_lidtype */ protected $qresponse_lid = null; protected $qresprocessing = null; protected $correct_grade_value = null; protected $correct_answer_node_id = null; protected $correct_answer_ident = null; protected $total_grade_value = null; protected $answerlist = null; protected $general_feedback = null; protected $correct_feedbacks = array(); protected $incorrect_feedbacks = array(); /** * @param XMLGenericDocument $questions * @param cc_manifest $manifest * @param cc_assesment_section $section * @param DOMElement $question_node * @param string $rootpath * @param string $contextid * @param string $outdir */ public function __construct(XMLGenericDocument &$quiz, XMLGenericDocument &$questions, cc_manifest &$manifest, cc_assesment_section &$section, &$question_node, $rootpath, $contextid, $outdir) { $this->quiz = $quiz; $this->questions = $questions; $this->manifest = $manifest; $this->section = $section; $this->question_node = $question_node; $this->rootpath = $rootpath; $this->contextid = $contextid; $this->outdir = $outdir; // $qitem = new cc_assesment_section_item(); $this->section->add_item($qitem); $qitem->set_title($this->questions->nodeValue('name', $this->question_node)); $this->qitem = $qitem; } public function on_generate_metadata() { if (empty($this->qmetadata)) { $this->qmetadata = new cc_question_metadata($this->qtype); //Get weighting value $weighting_value = (int)$this->questions->nodeValue('defaultmark', $this->question_node); if ($weighting_value > 1) { $this->qmetadata->set_weighting($weighting_value); } //Get category $question_category = $this->questions->nodeValue('../../name', $this->question_node); if (!empty($question_category)) { $this->qmetadata->set_category($question_category); } $rts = new cc_assesment_itemmetadata(); $rts->add_metadata($this->qmetadata); $this->qitem->set_itemmetadata($rts); } } public function on_generate_presentation() { if (empty($this->qpresentation)) { $qpresentation = new cc_assesment_presentation(); $this->qitem->set_presentation($qpresentation); //add question text $qmaterial = new cc_assesment_material(); $qmattext = new cc_assesment_mattext(); $question_text = $this->questions->nodeValue('questiontext', $this->question_node); $result = cc_helpers::process_linked_files( $question_text, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); $qmattext->set_content($result[0], cc_qti_values::htmltype); $qmaterial->set_mattext($qmattext); $qpresentation->set_material($qmaterial); $this->qpresentation = $qpresentation; pkg_resource_dependencies::instance()->add($result[1]); } } public function on_generate_answers() {} public function on_generate_feedbacks() { $general_question_feedback = $this->questions->nodeValue('generalfeedback', $this->question_node); if (empty($general_question_feedback)) { return; } $name = 'general_fb'; //Add question general feedback - the one that should be always displayed $result = cc_helpers::process_linked_files( $general_question_feedback, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback($this->qitem, $result[0], cc_qti_values::htmltype, $name); pkg_resource_dependencies::instance()->add($result[1]); $this->general_feedback = $name; } public function on_generate_response_processing() { $qresprocessing = new cc_assesment_resprocessingtype(); $this->qitem->add_resprocessing($qresprocessing); $qdecvar = new cc_assesment_decvartype(); $qresprocessing->set_decvar($qdecvar); //according to the Common Cartridge 1.1 Profile: Implementation document //this should always be set to 0, 100 in case of question type that is not essay $qdecvar->set_limits(0,100); $qdecvar->set_vartype(cc_qti_values::Decimal); $this->qresprocessing = $qresprocessing; } public function generate() { $this->on_generate_metadata(); $this->on_generate_presentation(); $this->on_generate_answers(); $this->on_generate_feedbacks(); $this->on_generate_response_processing(); } } class cc_assesment_question_multichoice extends cc_assesment_question_proc_base { public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) { parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $this->qtype = cc_qti_profiletype::multiple_choice; /** * * What is needed is a maximum grade value taken from the answer fraction * It is supposed to always be between 1 and 0 in decimal representation, * however that is not always the case so a change in test was needed * but since we support here one correct answer type * correct answer would always have to be 1 */ $correct_answer_node = $this->questions->node("plugin_qtype_multichoice_question/answers/answer[fraction > 0]", $this->question_node); if (empty($correct_answer_node)) { throw new RuntimeException('No correct answer!'); } $this->correct_answer_node_id = $this->questions->nodeValue('@id', $correct_answer_node); $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade'); $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000'; } public function on_generate_answers() { //add responses holder $qresponse_lid = new cc_response_lidtype(); $this->qresponse_lid = $qresponse_lid; $this->qpresentation->set_response_lid($qresponse_lid); $qresponse_choice = new cc_assesment_render_choicetype(); $qresponse_lid->set_render_choice($qresponse_choice); //Mark that question has only one correct answer - //which applies for multiple choice and yes/no questions $qresponse_lid->set_rcardinality(cc_qti_values::Single); //are we to shuffle the responses? $shuffle_answers = (int)$this->quiz->nodeValue('/activity/quiz/shuffleanswers') > 0; $qresponse_choice->enable_shuffle($shuffle_answers); $answerlist = array(); $qa_responses = $this->questions->nodeList('plugin_qtype_multichoice_question/answers/answer', $this->question_node); foreach ($qa_responses as $node) { $answer_content = $this->questions->nodeValue('answertext', $node); $id = ((int)$this->questions->nodeValue('@id', $node) == $this->correct_answer_node_id); $result = cc_helpers::process_linked_files( $answer_content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); $qresponse_label = cc_assesment_helper::add_answer( $qresponse_choice, $result[0], cc_qti_values::htmltype); pkg_resource_dependencies::instance()->add($result[1]); $answer_ident = $qresponse_label->get_ident(); $feedback_ident = $answer_ident.'_fb'; if (empty($this->correct_answer_ident) && $id) { $this->correct_answer_ident = $answer_ident; } //add answer specific feedbacks if not empty $content = $this->questions->nodeValue('feedback', $node); if (!empty($content)) { $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $feedback_ident); pkg_resource_dependencies::instance()->add($result[1]); $answerlist[$answer_ident] = $feedback_ident; } } $this->answerlist = $answerlist; } public function on_generate_feedbacks() { parent::on_generate_feedbacks(); //Question combined feedbacks $correct_question_fb = $this->questions->nodeValue('plugin_qtype_multichoice_question/multichoice/correctfeedback', $this->question_node); $incorrect_question_fb = $this->questions->nodeValue('plugin_qtype_multichoice_question/multichoice/incorrectfeedback', $this->question_node); $proc = array('correct_fb' => $correct_question_fb, 'general_incorrect_fb' => $incorrect_question_fb); foreach ($proc as $ident => $content) { if (empty($content)) { continue; } $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $ident); pkg_resource_dependencies::instance()->add($result[1]); if ($ident == 'correct_fb') { $this->correct_feedbacks[] = $ident; } else { $this->incorrect_feedbacks[] = $ident; } } } public function on_generate_response_processing() { parent::on_generate_response_processing(); //respconditions /** * General unconditional feedback must be added as a first respcondition * without any condition and just displayfeedback (if exists) */ if (!empty($this->general_feedback)) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('General feedback'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(); //define the condition for success $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid('general_fb'); } //success condition /** * For all question types outside of the Essay question, scoring is done in a * single <respcondition> with a continue flag set to No. The outcome is always * a variable named SCORE which value must be set to 100 in case of correct answer. * Partial scores (not 0 or 100) are not supported. */ $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('Correct'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(false); $qsetvar = new cc_assignment_setvartype(100); $qrespcondition->add_setvar($qsetvar); //define the condition for success $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qvarequal = new cc_assignment_conditionvar_varequaltype($this->correct_answer_ident); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident($this->qresponse_lid->get_ident()); if (array_key_exists($this->correct_answer_ident, $this->answerlist)) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($this->answerlist[$this->correct_answer_ident]); } foreach ($this->correct_feedbacks as $ident) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($ident); } //rest of the conditions foreach ($this->answerlist as $ident => $refid) { if ($ident == $this->correct_answer_ident) { continue; } $qrespcondition = new cc_assesment_respconditiontype(); $this->qresprocessing->add_respcondition($qrespcondition); $qsetvar = new cc_assignment_setvartype(0); $qrespcondition->add_setvar($qsetvar); //define the condition for fail $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qvarequal = new cc_assignment_conditionvar_varequaltype($ident); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident($this->qresponse_lid->get_ident()); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($refid); foreach ($this->incorrect_feedbacks as $ident) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($ident); } } } } class cc_assesment_question_multichoice_multiresponse extends cc_assesment_question_proc_base { /** * @var DOMNodeList */ protected $correct_answers = null; public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) { parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $this->qtype = cc_qti_profiletype::multiple_response; $correct_answer_nodes = $this->questions->nodeList("plugin_qtype_multichoice_question/answers/answer[fraction > 0]", $this->question_node); if ($correct_answer_nodes->length == 0) { throw new RuntimeException('No correct answer!'); } $this->correct_answers = $correct_answer_nodes; //$this->correct_answer_node_id = $this->questions->nodeValue('@id', $correct_answer_node); $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade'); $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000'; } public function on_generate_answers() { //add responses holder $qresponse_lid = new cc_response_lidtype(); $this->qresponse_lid = $qresponse_lid; $this->qpresentation->set_response_lid($qresponse_lid); $qresponse_choice = new cc_assesment_render_choicetype(); $qresponse_lid->set_render_choice($qresponse_choice); //Mark that question has more than one correct answer $qresponse_lid->set_rcardinality(cc_qti_values::Multiple); //are we to shuffle the responses? $shuffle_answers = (int)$this->quiz->nodeValue('/activity/quiz/shuffleanswers') > 0; $qresponse_choice->enable_shuffle($shuffle_answers); $answerlist = array(); $qa_responses = $this->questions->nodeList('plugin_qtype_multichoice_question/answers/answer', $this->question_node); foreach ($qa_responses as $node) { $answer_content = $this->questions->nodeValue('answertext', $node); $answer_grade_fraction = (float)$this->questions->nodeValue('fraction', $node); $result = cc_helpers::process_linked_files( $answer_content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); $qresponse_label = cc_assesment_helper::add_answer( $qresponse_choice, $result[0], cc_qti_values::htmltype); pkg_resource_dependencies::instance()->add($result[1]); $answer_ident = $qresponse_label->get_ident(); $feedback_ident = $answer_ident.'_fb'; //add answer specific feedbacks if not empty $content = $this->questions->nodeValue('feedback', $node); if (!empty($content)) { $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $feedback_ident); pkg_resource_dependencies::instance()->add($result[1]); } $answerlist[$answer_ident] = array($feedback_ident, ($answer_grade_fraction > 0)); } $this->answerlist = $answerlist; } public function on_generate_feedbacks() { parent::on_generate_feedbacks(); //Question combined feedbacks $correct_question_fb = $this->questions->nodeValue('plugin_qtype_multichoice_question/multichoice/correctfeedback', $this->question_node); $incorrect_question_fb = $this->questions->nodeValue('plugin_qtype_multichoice_question/multichoice/incorrectfeedback', $this->question_node); if (empty($correct_question_fb)) { //Hardcode some text for now $correct_question_fb = 'Well done!'; } if (empty($incorrect_question_fb)) { //Hardcode some text for now $incorrect_question_fb = 'Better luck next time!'; } $proc = array('correct_fb' => $correct_question_fb, 'incorrect_fb' => $incorrect_question_fb); foreach ($proc as $ident => $content) { if (empty($content)) { continue; } $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $ident); pkg_resource_dependencies::instance()->add($result[1]); if ($ident == 'correct_fb') { $this->correct_feedbacks[$ident] = $ident; } else { $this->incorrect_feedbacks[$ident] = $ident; } } } public function on_generate_response_processing() { parent::on_generate_response_processing(); //respconditions /** * General unconditional feedback must be added as a first respcondition * without any condition and just displayfeedback (if exists) */ cc_assesment_helper::add_respcondition( $this->qresprocessing, 'General feedback', $this->general_feedback, null, true ); //success condition /** * For all question types outside of the Essay question, scoring is done in a * single <respcondition> with a continue flag set to No. The outcome is always * a variable named SCORE which value must be set to 100 in case of correct answer. * Partial scores (not 0 or 100) are not supported. */ $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('Correct'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(false); $qsetvar = new cc_assignment_setvartype(100); $qrespcondition->add_setvar($qsetvar); //define the condition for success $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); //create root and condition $qandcondition = new cc_assignment_conditionvar_andtype(); $qconditionvar->set_and($qandcondition); foreach ($this->answerlist as $ident => $refid) { $qvarequal = new cc_assignment_conditionvar_varequaltype($ident); $qvarequal->enable_case(); if ($refid[1]) { $qandcondition->set_varequal($qvarequal); } else { $qandcondition->set_not($qvarequal); } $qvarequal->set_respident($this->qresponse_lid->get_ident()); } $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); //TODO: this needs to be fixed reset($this->correct_feedbacks); $ident = key($this->correct_feedbacks); $qdisplayfeedback->set_linkrefid($ident); //rest of the conditions foreach ($this->answerlist as $ident => $refid) { cc_assesment_helper::add_response_condition( $this->qresprocessing, 'Incorrect feedback', $refid[0], $this->general_feedback, $this->qresponse_lid->get_ident() ); } //Final element for incorrect feedback reset($this->incorrect_feedbacks); $ident = key($this->incorrect_feedbacks); cc_assesment_helper::add_respcondition( $this->qresprocessing, 'Incorrect feedback', $ident, 0 ); } } cc/cc_lib/cc_converter_lti.php 0000644 00000004252 15215711721 0012406 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; require_once 'cc_basiclti.php'; class cc_converter_lti extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->cc_type = cc_version11::basiclti; $this->defaultfile = 'lti.xml'; $this->defaultname = basicltil1_resurce_file::deafultname; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new basicltil1_resurce_file(); $contextid = $this->doc->nodeValue('/activity/@contextid'); $title = $this->doc->nodeValue('/activity/lti/name'); $text = $this->doc->nodeValue('/activity/lti/intro'); $rt->set_title($title); $result = cc_helpers::process_linked_files($text, $this->manifest, $this->rootpath, $contextid, $outdir); $rt->set_description($result[0]); $rt->set_launch_url($this->doc->nodeValue('/activity/lti/toolurl')); $rt->set_launch_icon(''); $this->store($rt, $outdir, $title, $result[1]); return true; } } cc/cc_lib/xmlbase.php 0000644 00000026633 15215711721 0010524 0 ustar 00 <?php /** * Implementation of Common Cartridge library based on * {@link http://www.imsglobal.org/cc/ IMS Common Cartridge Standard v1.2} * * @author Darko Miletic * @author Daniel Muhlrad (daniel.muhlrad@uvcms.com) * @version 1.0 * @copyright 2009 {@link http://www.uvcms.com UVCMS e-learning} * @package cc_library * */ require_once('gral_lib/cssparser.php'); /** * Base XML class * */ class XMLGenericDocument { private $charset; /** * Document * @var DOMDocument */ public $doc = null; /** * * Xpath * @var DOMXPath */ protected $dxpath = null; protected $filename; private $filepath; private $isloaded = false; private $arrayPrefixNS = array(); private $is_html = false; /** * @param string $value * @return string */ public static function safexml($value) { $result = htmlspecialchars(html_entity_decode($value, ENT_QUOTES, 'UTF-8'), ENT_NOQUOTES, 'UTF-8', false); return $result; } function __construct($ch = 'UTF-8', $validatenow = true) { $this->charset = $ch; $this->documentInit(); $this->doc->validateOnParse = $validatenow; } function __destruct() { $this->dxpath = null; $this->doc = null; } private function documentInit($withonCreate = true) { $hg = false; if ($this->isloaded) { $guardstate = $this->doc->validateOnParse; $hg = true; unset($this->dxpath); unset($this->doc); $this->isloaded = false; } $this->doc = new DOMDocument("1.0", $this->charset); $this->doc->strictErrorChecking = true; if ($hg) { $this->doc->validateOnParse = $guardstate; } $this->doc->formatOutput = true; $this->doc->preserveWhiteSpace = true; if ($withonCreate) { $this->on_create(); } } public function viewXML() { return $this->doc->saveXML(); } public function registerNS($prefix, $nsuri) { $this->arrayPrefixNS[$prefix] = $nsuri; } public function load($fname) { // Sine xml will remain loaded should the repeated load fail we should recreate document to be empty. $this->documentInit(false); $this->isloaded = $this->doc->load($fname); if ($this->isloaded) { $this->filename = $fname; $this->processPath(); $this->is_html = false; } return $this->on_load(); } public function loadUrl($url) { $this->documentInit(); $this->isloaded = true; $this->doc->loadXML( file_get_contents($url) ); $this->is_html = false; return $this->on_load(); } public function loadHTML($content) { $this->documentInit(); $this->doc->validateOnParse = false; $this->isloaded = true; $this->doc->loadHTML($content); $this->is_html = true; return $this->on_load(); } public function loadXML($content) { $this->documentInit(); $this->doc->validateOnParse = false; $this->isloaded = true; $this->doc->load($content); $this->is_html = true; return $this->on_load(); } public function loadHTMLFile($fname) { // Sine xml will remain loaded should the repeated load fail // we should recreate document to be empty. $this->documentInit(); $this->doc->validateOnParse = false; $this->isloaded = $this->doc->loadHTMLFile($fname); if ($this->isloaded) { $this->filename = $fname; $this->processPath(); $this->is_html=true; } return $this->on_load(); } public function loadXMLFile($fname) { // Sine xml will remain loaded should the repeated load fail // we should recreate document to be empty. $this->documentInit(); $this->doc->validateOnParse = false; $this->isloaded = $this->doc->load($fname); if ($this->isloaded) { $this->filename = $fname; $this->processPath(); $this->is_html = true; } return $this->on_load(); } public function loadString($content) { $this->doc = new DOMDocument("1.0", $this->charset); $content = '<virtualtag>'.$content.'</virtualtag>'; $this->doc->loadXML($content); return true; } public function save() { $this->saveTo($this->filename); } public function saveTo($fname) { $status = false; if ($this->on_save()) { if ($this->is_html) { $this->doc->saveHTMLFile($fname); } else { $this->doc->save($fname); } $this->filename = $fname; $this->processPath(); $status = true; } return $status; } public function validate() { return $this->doc->validate(); } public function attributeValue($path, $attrname, $node = null) { $this->chkxpath(); $result = null; $resultlist = null; if (is_null($node)) { $resultlist = $this->dxpath->query($path); } else { $resultlist = $this->dxpath->query($path, $node); } if (is_object($resultlist) && ($resultlist->length > 0) && $resultlist->item(0)->hasAttribute($attrname)) { $result = $resultlist->item(0)->getAttribute($attrname); } return $result; } /** * * Get's text value of the node based on xpath query * @param string $path * @param DOMNode $node * @param int $count * @return string */ public function nodeValue($path, $node = null, $count = 1) { $nd = $this->node($path, $node, $count); return $this->nodeTextValue($nd); } /** * * Get's text value of the node * @param DOMNode $node * @return string */ public function nodeTextValue($node) { $result = ''; if (is_object($node)) { if ($node->hasChildNodes()) { $chnodesList = $node->childNodes; $types = array(XML_TEXT_NODE, XML_CDATA_SECTION_NODE); foreach ($chnodesList as $chnode) { if (in_array($chnode->nodeType, $types)) { $result .= $chnode->wholeText; } } } } return $result; } /** * * Enter description here ... * @param string $path * @param DOMNode $nd * @param int $count * @return DOMNode */ public function node($path, $nd = null, $count = 1) { $result = null; $resultlist = $this->nodeList($path,$nd); if (is_object($resultlist) && ($resultlist->length > 0)) { $result = $resultlist->item($count - 1); } return $result; } /** * * Enter description here ... * @param string $path * @param DOMNode $node * @return DOMNodeList */ public function nodeList($path, $node = null) { $this->chkxpath(); $resultlist = null; if (is_null($node)) { $resultlist = $this->dxpath->query($path); } else { $resultlist = $this->dxpath->query($path, $node); } return $resultlist; } /** * * Create new attribute * @param string $namespace * @param string $name * @param string $value * @return DOMAttr */ public function create_attribute_ns($namespace, $name, $value = null) { $result = $this->doc->createAttributeNS($namespace, $name); if (!is_null($value)) { $result->nodeValue = $value; } return $result; } /** * * Create new attribute * @param string $name * @param string $value * @return DOMAttr */ public function create_attribute($name, $value = null) { $result = $this->doc->createAttribute($name); if (!is_null($value)) { $result->nodeValue = $value; } return $result; } /** * * Adds new node * @param DOMNode $parentnode * @param string $namespace * @param string $name * @param string $value * @return DOMNode */ public function append_new_element_ns(DOMNode &$parentnode, $namespace, $name, $value = null) { $newnode = null; if (is_null($value)) { $newnode = $this->doc->createElementNS($namespace, $name); } else { $newnode = $this->doc->createElementNS($namespace, $name, $value); } return $parentnode->appendChild($newnode); } /** * * New node with CDATA content * @param DOMNode $parentnode * @param string $namespace * @param string $name * @param string $value */ public function append_new_element_ns_cdata(DOMNode &$parentnode, $namespace, $name, $value = null) { $newnode = $this->doc->createElementNS($namespace, $name); if (!is_null($value)) { $cdata = $this->doc->createCDATASection($value); $newnode->appendChild($cdata); } return $parentnode->appendChild($newnode); } /** * * Adds new node * @param DOMNode $parentnode * @param string $name * @param string $value * @return DOMNode */ public function append_new_element(DOMNode &$parentnode, $name, $value = null) { $newnode = null; if (is_null($value)) { $newnode = $this->doc->createElement($name); } else { $newnode = $this->doc->createElement($name, $value); } return $parentnode->appendChild($newnode); } /** * * Adds new attribute * @param DOMNode $node * @param string $name * @param string $value * @return DOMNode */ public function append_new_attribute(DOMNode &$node, $name, $value = null) { return $node->appendChild($this->create_attribute($name, $value)); } /** * * Adds new attribute * @param DOMNode $node * @param string $namespace * @param string $name * @param string $value * @return DOMNode */ public function append_new_attribute_ns(DOMNode &$node, $namespace, $name, $value = null) { return $node->appendChild($this->create_attribute_ns($namespace, $name, $value)); } public function fileName() { return $this->filename; } public function filePath() { return $this->filepath; } protected function on_load() { return $this->isloaded; } protected function on_save() { return true; } protected function on_create() { return true; } public function resetXpath() { $this->dxpath = null; $this->chkxpath(); } private function chkxpath() { if (!isset($this->dxpath) || is_null($this->dxpath)) { $this->dxpath = new DOMXPath($this->doc); foreach ($this->arrayPrefixNS as $nskey => $nsuri) { $this->dxpath->registerNamespace($nskey, $nsuri); } } } protected function processPath() { $path_parts = pathinfo($this->filename); $this->filepath = array_key_exists('dirname', $path_parts) ? $path_parts['dirname']."/" : ''; } } cc/cc_lib/cc_converter_basiclti.php 0000644 00000003741 15215711721 0013412 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; require_once 'cc_basiclti.php'; class cc_converter_basiclti extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->cc_type = cc_version11::basiclti; $this->defaultfile = 'basiclti.xml'; $this->defaultname = basicltil1_resurce_file::deafultname; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new basicltil1_resurce_file(); $title = $this->doc->nodeValue('/activity/basiclti/name'); $rt->set_title($title); $rt->set_launch_url($this->doc->nodeValue('/activity/basiclti/toolurl')); $rt->set_launch_icon(''); $rt->set_vendor_code($this->doc->nodeValue('/activity/basiclti/organizationid')); $rt->set_vendor_description($this->doc->nodeValue('/activity/basiclti/organizationdescr')); $rt->set_vendor_url($this->doc->nodeValue('/activity/basiclti/organizationurl')); $this->store($rt, $outdir, $title); return true; } } cc/cc_lib/cc_convert_moodle2.php 0000644 00000020476 15215711721 0012636 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!extension_loaded('fileinfo')) { die('You must install fileinfo extension!'); } abstract class cc_convert_moodle2 { /** * * Enter description here ... * @param unknown_type $packagedir * @param unknown_type $outdir * @throws DOMException * @throws InvalidArgumentException */ public static function convert($packagedir, $outdir) { $dir = realpath($packagedir); if (empty($dir)) { throw new InvalidArgumentException('Directory does not exist!'); } $odir = realpath($outdir); if (empty($odir)) { throw new InvalidArgumentException('Directory does not exist!'); } $coursefile = $dir.DIRECTORY_SEPARATOR.'course'.DIRECTORY_SEPARATOR.'course.xml'; $doc = new XMLGenericDocument(); if ($doc->load($coursefile)) { $course_name = $doc->nodeValue('/course/fullname'); $course_desc = $doc->nodeValue('/course/summary'); $course_language = $doc->nodeValue('/course/lang'); $course_language = empty($course_language) ? 'en' : $course_language; $course_category = $doc->nodeValue('/course/category/name'); //Initialize the manifest metadata class $meta = new cc_metadata_manifest(); //Package metadata $metageneral = new cc_metadata_general(); $metageneral->set_language($course_language); $metageneral->set_title($course_name, $course_language); $metageneral->set_description($course_desc, $course_language); $metageneral->set_catalog('category'); $metageneral->set_entry($course_category); $meta->add_metadata_general($metageneral); // Create the manifest $manifest = new cc_manifest(cc_version::v11); $manifest->add_metadata_manifest($meta); $organization = null; //Package structure - default organization and resources //Get the course structure - this will be transformed into organization //Step 1 - Get the list and order of sections/topics $moodle_backup = $dir . DIRECTORY_SEPARATOR . 'moodle_backup.xml'; $secp = new XMLGenericDocument(); $docp = new XMLGenericDocument(); if ($docp->load($moodle_backup)) { //sections $sections = array(); $coursef = new XMLGenericDocument(); $course_file = $dir . DIRECTORY_SEPARATOR .'course' . DIRECTORY_SEPARATOR . 'course.xml'; $coursef->load($course_file); //$numsections = (int)$coursef->nodeValue('/course/numsections'); // TODO MDL-35781, this is commented because numsections is now optional attribute $section_list = $docp->nodeList('/moodle_backup/information/contents/sections/section'); if (!empty($section_list)) { $count = 0; foreach ($section_list as $node) { //if ($count > $numsections) { // break; //} $sectionid = $docp->nodeValue('sectionid', $node); $sectiontitle = $docp->nodeValue('title' , $node); $sectionpath = $docp->nodeValue('directory', $node); $sequence = array(); //Get section stuff $section_file = $dir . DIRECTORY_SEPARATOR . $sectionpath . DIRECTORY_SEPARATOR . 'section.xml'; if ($secp->load($section_file)) { $rawvalue = $secp->nodeValue('/section/sequence'); if ($rawvalue != '$@NULL@$') { $sequence = explode(',', $rawvalue); } } $sections[$sectionid] = array($sectiontitle, $sequence); $count++; } } //organization title $organization = new cc_organization(); //Add section/topic items foreach ($sections as $sectionid => $values) { $item = new cc_item(); $item->title = $values[0]; self::process_sequence($item, $manifest, $values[1], $dir, $odir); $organization->add_item($item); } $manifest->put_nodes(); } if (!empty($organization)) { $manifest->add_new_organization($organization); } $manifestpath = $outdir.DIRECTORY_SEPARATOR.'imsmanifest.xml'; $manifest->saveTo($manifestpath); } } /** * * Process the activites and create item structure * @param cc_i_item $item * @param array $sequence * @param string $packageroot - directory path * @throws DOMException */ protected static function process_sequence(cc_i_item &$item, cc_i_manifest &$manifest, array $sequence, $packageroot, $outdir) { $moodle_backup = $packageroot . DIRECTORY_SEPARATOR . 'moodle_backup.xml'; $doc = new XMLGenericDocument(); if(!$doc->load($moodle_backup)) { return; } $activities = $doc->nodeList('/moodle_backup/information/contents/activities/activity'); if (!empty($activities)) { $dpp = new XMLGenericDocument(); foreach ($activities as $activity) { $moduleid = $doc->nodeValue('moduleid', $activity); if (in_array($moduleid, $sequence)) { //detect activity type $directory = $doc->nodeValue('directory', $activity); $path = $packageroot . DIRECTORY_SEPARATOR . $directory; $module_file = $path . DIRECTORY_SEPARATOR . 'module.xml'; if ($dpp->load($module_file)) { $activity_type = $dpp->nodeValue('/module/modulename'); $activity_indentation = $dpp->nodeValue('/module/indent'); $aitem = self::item_indenter($item, $activity_indentation); $caller = "cc_converter_{$activity_type}"; if (class_exists($caller)) { $obj = new $caller($aitem, $manifest, $packageroot, $path); if (!$obj->convert($outdir)) { throw new RuntimeException("failed to convert {$activity_type}"); } } } } } } } protected static function item_indenter(cc_i_item &$item, $level = 0) { $indent = (int)$level; $indent = ($indent) <= 0 ? 0 : $indent; $nprev = null; $nfirst = null; for ($pos = 0, $size = $indent; $pos < $size; $pos++) { $nitem = new cc_item(); $nitem->title = ''; if (empty($nfirst)) { $nfirst = $nitem; } if (!empty($nprev)) { $nprev->add_child_item($nitem); } $nprev = $nitem; } $result = $item; if (!empty($nfirst)) { $item->add_child_item($nfirst); $result = $nprev; } return $result; } } cc/cc_lib/cc_manifest.php 0000644 00000027412 15215711721 0011340 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Manifest management * * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_utils.php'); require_once('xmlbase.php'); require_once('cc_resources.php'); require_once('cc_version_base.php'); require_once('gral_lib/pathutils.php'); /** * Manifest Class * */ class cc_manifest extends XMLGenericDocument implements cc_i_manifest { private $ccversion = null; private $ccobj = null; private $rootmanifest = null; private $activemanifest = null; private $parentmanifest = null; private $parentparentmanifest = null; private $ares = array(); private $mainidentifier = null; public function __construct($ccver = cc_version::v1, $activemanifest=null, $parentmanifest=null, $parentparentmanifest=null) { if (is_int($ccver)) { $this->ccversion=$ccver; $classname = "cc_version{$ccver}"; $this->ccobj = new $classname; parent::__construct('UTF-8', true); } else if (is_object($ccver) && (get_class($ccver)=='cc_manifest')) { $this->doc = $ccver->doc; $this->rootmanifest = $ccver->rootmanifest; $this->activemanifest = $activemanifest; $this->parentmanifest = $parentmanifest; $this->parentparentmanifest = $parentparentmanifest; $this->ccversion = $ccver->ccversion; $this->ccobj = $ccver; $this->register_namespaces_for_xpath(); } } /** * Register Namespace for use XPATH * */ public function register_namespaces_for_xpath() { $scnam = $this->activemanifest->get_cc_namespaces(); foreach ($scnam as $key => $value) { $this->registerNS($key, $value); } } /** * TODO - implement this method - critical * Enter description here ... */ private function fill_manifest() { } /** * Add Metadata For Manifest * * @param cc_i_metadata_manifest $met */ public function add_metadata_manifest(cc_i_metadata_manifest $met) { $metanode = $this->node("//imscc:manifest[@identifier='". $this->activemanifest->manifestID(). "']/imscc:metadata"); $nmeta = $this->activemanifest->create_metadata_node($met, $this->doc, $metanode); $metanode->appendChild($nmeta); } /** * Add Metadata For Resource * * @param cc_i_metadata_resource $met * @param string $identifier */ public function add_metadata_resource(cc_i_metadata_resource $met, $identifier) { $metanode = $this->node("//imscc:resource". "[@identifier='". $identifier. "']"); $metanode2 = $this->node("//imscc:resource". "[@identifier='". $identifier. "']/imscc:file"); $nspaces = $this->activemanifest->get_cc_namespaces(); $dnode = $this->append_new_element_ns($metanode2, $nspaces['imscc'], 'metadata'); $this->activemanifest->create_metadata_resource_node($met, $this->doc, $dnode); } /** * Add Metadata For File * * @param cc_i_metadata_file $met * @param string $identifier * @param string $filename */ public function add_metadata_file(cc_i_metadata_file $met, $identifier, $filename) { if (empty($met) || empty($identifier) || empty($filename)) { throw new Exception('Try to add a metadata file with nulls values given!'); } $metanode = $this->node("//imscc:resource". "[@identifier='". $identifier. "']/imscc:file". "[@href='". $filename. "']"); $nspaces = $this->activemanifest->get_cc_namespaces(); $dnode = $this->doc->createElementNS($nspaces['imscc'], "metadata"); $metanode->appendChild($dnode); $this->activemanifest->create_metadata_file_node($met, $this->doc, $dnode); } public function on_create() { $this->activemanifest = cc_builder_creator::factory($this->ccversion); $this->rootmanifest = $this->activemanifest; $result = $this->activemanifest->create_manifest($this->doc); $this->register_namespaces_for_xpath(); return $result; } public function get_relative_base_path() { return $this->activemanifest->base(); } public function parent_manifest() { return new cc_manifest($this, $this->parentmanifest, $this->parentparentmanifest); } public function root_manifest() { return new cc_manifest($this, $this->rootmanifest); } public function manifestID() { return $this->activemanifest->manifestID(); } public function get_manifest_namespaces() { return $this->rootmanifest->get_cc_namespaces(); } /** * Add a new organization * * @param cc_i_organization $org */ public function add_new_organization(cc_i_organization &$org) { $norg = $this->activemanifest->create_organization_node($org, $this->doc); $orgnode = $this->node("//imscc:manifest[@identifier='". $this->activemanifest->manifestID(). "']/imscc:organizations"); $orgnode->appendChild($norg); } public function get_resources($searchspecific='') { $reslist = $this->get_resource_list($searchspecific); $resourcelist = array(); foreach ($reslist as $resourceitem) { $resourcelist[] = new cc_resource($this, $resourceitem); } return $resourcelist; } public function get_cc_namespace_path($nsname) { if (is_string($nsname) && (!empty($nsname))) { $scnam = $this->activemanifest->get_cc_namespaces(); return $scnam[$nsname]; } return null; } public function get_resource_list($searchspecific = '') { return $this->nodeList("//imscc:manifest[@identifier='". $this->activemanifest->manifestID(). "']/imscc:resources/imscc:resource".$searchspecific); } public function on_load() { $this->register_namespaces_for_xpath(); $this->fill_manifest(); return true; } public function on_save() { return true; } /** * Add a resource to the manifest * * @param cc_i_resource $res * @param string $identifier * @param string $type * @return array */ public function add_resource(cc_i_resource $res, $identifier = null, $type = 'webcontent') { if (!$this->ccobj->valid($type)) { throw new Exception("Type invalid..."); } if ($res == null) { throw new Exception('Invalid Resource or dont give it'); } $rst = $res; // TODO: This has to be reviewed since it does not handle multiple files properly. // Dependencies. if (is_object($identifier)) { $this->activemanifest->create_resource_node($rst, $this->doc, $identifier); } else { $nresnode = null; $rst->type = $type; if (!cc_helpers::is_html($rst->filename)) { $rst->href = null; } $this->activemanifest->create_resource_node($rst, $this->doc, $nresnode); foreach ($rst->files as $file) { $ident = $this->get_identifier_by_filename($file); if ($ident == null) { $newres = new cc_resource($rst->manifestroot, $file); if (!cc_helpers::is_html($file)) { $newres->href = null; } $newres->type = 'webcontent'; $this->activemanifest->create_resource_node($newres, $this->doc, $nresnode); } } } $tmparray = array($rst->identifier, $rst->files[0]); return $tmparray; } private function check_if_exist_in_other($name, $identifier) { $status = array(); foreach ($this->activemanifest->resources as $value) { if (($value->identifier != $identifier) && isset($value->files[$name])) { $status[] = $value->identifier; } } return $status; } private function replace_file_x_dependency($depen, $name) { foreach ($depen as $key => $value) { ($key); $ident = $this->get_identifier_by_filename($name); $this->activemanifest->resources[$value]->files = $this->array_remove_by_value($this->activemanifest->resources[$value]->files, $name); if (!in_array($ident, $this->activemanifest->resources[$value]->dependency)) { array_push($this->activemanifest->resources[$value]->dependency, $ident); } } return true; } private function get_identifier_by_filename($name) { $result = null; if (isset($this->activemanifest->resources_ind[$name])) { $result = $this->activemanifest->resources_ind[$name]; } return $result; } private function array_remove_by_value($arr, $value) { return array_values(array_diff($arr, array($value))); } private function array_remove_by_key($arr, $key) { return array_values(array_diff_key($arr, array($key))); } public function update_instructoronly($identifier, $value = false) { if (isset($this->activemanifest->resources[$identifier])) { $resource = $this->activemanifest->resources[$identifier]; $resource->instructoronly = $value; } } /** * Append the resources nodes in the Manifest * * @return DOMNode */ public function put_nodes() { $resnodestr = "//imscc:manifest[@identifier='".$this->activemanifest->manifestID(). "']/imscc:resources"; $resnode = $this->node($resnodestr); foreach ($this->activemanifest->resources as $k => $v) { ($k); $depen = $this->check_if_exist_in_other($v->files[0], $v->identifier); if (!empty($depen)) { $this->replace_file_x_dependency($depen, $v->files[0]); $v->type = 'webcontent'; } } foreach ($this->activemanifest->resources as $node) { $rnode = $this->activemanifest->create_resource_node($node, $this->doc, null); $resnode->appendChild($rnode); if ($node->instructoronly) { $metafileceduc = new cc_metadata_resouce_educational(); $metafileceduc->set_value(intended_user_role::INSTRUCTOR); $metafile = new cc_metadata_resouce(); $metafile->add_metadata_resource_educational($metafileceduc); $this->activemanifest->create_metadata_educational($metafile, $this->doc, $rnode); } } return $resnode; } } cc/cc_lib/cc_converter_url.php 0000644 00000004430 15215711721 0012416 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; require_once 'cc_weblink.php'; class cc_converter_url extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->cc_type = cc_version11::weblink; $this->defaultfile = 'url.xml'; $this->defaultname = 'weblink.xml'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new url11_resurce_file(); $title = $this->doc->nodeValue('/activity/url/name'); $rt->set_title($title); $url = $this->doc->nodeValue('/activity/url/externalurl'); if (!empty($url)) { /** * * Display value choices * 0 - automatic (system chooses what to do) (usualy defaults to the open) * 1 - embed - display within a frame * 5 - open - just open it full in the same frame * 6 - in popup - popup - new frame */ $display = intval($this->doc->nodeValue('/activity/forum/display')); $target = ($display == 6) ? '_blank' : '_self'; //TODO: Moodle also supports custom parameters //this should be transformed somehow into url where possible $rt->set_url($url, $target); } $this->store($rt, $outdir, $title); return true; } } cc/cc_lib/cc_metadata_resource.php 0000644 00000003007 15215711721 0013213 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Metadata managing * * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Metadata Resource Educational Type * */ class cc_metadata_resouce_educational{ public $value = array(); public function set_value($value){ $arr = array($value); $this->value[] = $arr; } } /** * Metadata Resource * */ class cc_metadata_resouce implements cc_i_metadata_resource { public $arrayeducational = array(); public function add_metadata_resource_educational($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->value)? $this->arrayeducational['value']=$obj->value:null; } } cc/cc_lib/cc_interfaces.php 0000644 00000005726 15215711721 0011661 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * CC Manifest Interface */ interface cc_i_manifest { public function on_create(); public function on_load(); public function on_save(); public function add_new_organization(cc_i_organization &$org); public function get_resources(); public function get_resource_list(); public function add_resource(cc_i_resource $res, $identifier=null, $type='webcontent'); public function add_metadata_manifest(cc_i_metadata_manifest $met); public function add_metadata_resource(cc_i_metadata_resource $met,$identifier); public function add_metadata_file(cc_i_metadata_file $met,$identifier,$filename); public function put_nodes(); } /** * CC Organization Interface */ interface cc_i_organization { public function add_item(cc_i_item &$item); public function has_items(); public function attr_value(&$nod, $name, $ns=null); public function process_organization(&$node,&$doc); } /** * CC Item Interface */ interface cc_i_item { public function add_child_item(cc_i_item &$item); public function attach_resource($res); // can be object or value public function has_child_items(); public function attr_value(&$nod, $name, $ns=null); public function process_item(&$node,&$doc); } /** * CC Resource Interface */ interface cc_i_resource { public function get_attr_value(&$nod, $name, $ns=null); public function add_resource($fname, $location=''); public function import_resource(DOMElement &$node, cc_i_manifest &$doc); public function process_resource($manifestroot, &$fname,$folder); } /** * CC Metadata Manifest Interface */ interface cc_i_metadata_manifest { public function add_metadata_general($obj); public function add_metadata_technical($obj); public function add_metadata_rights($obj); public function add_metadata_lifecycle($obj); } /** * CC Metadata Resource Interface */ interface cc_i_metadata_resource { public function add_metadata_resource_educational($obj); } /** * CC Metadata File Interface */ interface cc_i_metadata_file { public function add_metadata_file_educational($obj); } cc/cc_lib/cc_forum.php 0000644 00000007137 15215711721 0010664 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_general.php'; class forum1_resurce_file extends general_cc_file { const deafultname = 'discussion.xml'; protected $rootns = 'dt'; protected $rootname = 'dt:topic'; protected $ccnamespaces = array('dt' => 'http://www.imsglobal.org/xsd/imsdt_v1p0', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('dt' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_6/imsdt_v1p0_localised.xsd'); protected $title = null; protected $text_type = 'text/plain'; protected $text = null; protected $attachments = array(); public function set_title($title) { $this->title = self::safexml($title); } public function set_text($text, $type='text/plain') { $this->text = self::safexml($text); $this->text_type = $type; } public function set_attachments(array $attachments) { $this->attachments = $attachments; } protected function on_save() { $this->append_new_element($this->root, 'title', $this->title); $text = $this->append_new_element($this->root, 'text', $this->text); $this->append_new_attribute($text, 'texttype', $this->text_type); if (!empty($this->attachments)) { $attachments = $this->append_new_element($this->root, 'attachments'); foreach ($this->attachments as $value) { $att = $this->append_new_element($attachments, 'attachment'); $this->append_new_attribute($att, 'href', $value); } } return true; } } class forum11_resurce_file extends forum1_resurce_file { protected $rootns = 'dt'; protected $rootname = 'topic'; protected $ccnamespaces = array('dt' => 'http://www.imsglobal.org/xsd/imsccv1p1/imsdt_v1p1', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('dt' => 'http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imsdt_v1p1.xsd'); protected function on_save() { $rns = $this->ccnamespaces[$this->rootns]; $this->append_new_element_ns($this->root, $rns, 'title', $this->title); $text = $this->append_new_element_ns($this->root, $rns, 'text', $this->text); $this->append_new_attribute_ns($text, $rns, 'texttype', $this->text_type); if (!empty($this->attachments)) { $attachments = $this->append_new_element_ns($this->root, $rns, 'attachments'); foreach ($this->attachments as $value) { $att = $this->append_new_element_ns($attachments, $rns, 'attachment'); $this->append_new_attribute_ns($att, $rns, 'href', $value); } } return true; } } cc/cc_lib/cc_version11.php 0000644 00000012227 15215711721 0011357 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_version1.php'); /** * Version 1.1 class of Common Cartridge * */ class cc_version11 extends cc_version1 { const webcontent = 'webcontent'; const questionbank = 'imsqti_xmlv1p2/imscc_xmlv1p1/question-bank'; const assessment = 'imsqti_xmlv1p2/imscc_xmlv1p1/assessment'; const associatedcontent = 'associatedcontent/imscc_xmlv1p1/learning-application-resource'; const discussiontopic = 'imsdt_xmlv1p1'; const weblink = 'imswl_xmlv1p1'; const basiclti = 'imsbasiclti_xmlv1p0'; public static $checker = array(self::webcontent, self::assessment, self::associatedcontent, self::discussiontopic, self::questionbank, self::weblink, self::basiclti); /** * Validate if the type are valid or not * * @param string $type * @return bool */ public function valid($type) { return in_array($type, self::$checker); } public function __construct() { $this->ccnamespaces = array('imscc' => 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', 'lomimscc' => 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest' , 'lom' => 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource' , 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance' ); $this->ccnsnames = array('imscc' => 'http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imscp_v1p2_v1p0.xsd' , 'lomimscc' => 'http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lommanifest_v1p0.xsd', 'lom' => 'http://www.imsglobal.org/profile/cc/ccv1p1/LOM/ccv1p1_lomresource_v1p0.xsd' ); $this->ccversion = '1.1.0'; $this->camversion = '1.1.0'; $this->_generator = 'Moodle 2 Common Cartridge generator'; } protected function update_items($items, DOMDocument &$doc, DOMElement &$xmlnode) { foreach ($items as $key => $item) { $itemnode = $doc->createElementNS($this->ccnamespaces['imscc'], 'item'); $this->update_attribute($doc, 'identifier' , $key , $itemnode); $this->update_attribute($doc, 'identifierref', $item->identifierref, $itemnode); if (!is_null($item->title)) { $titlenode = $doc->createElementNS($this->ccnamespaces['imscc'], 'title'); $titlenode->appendChild(new DOMText($item->title)); $itemnode->appendChild($titlenode); } if ($item->has_child_items()) { $this->update_items($item->childitems, $doc, $itemnode); } $xmlnode->appendChild($itemnode); } } /** * Create Education Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ public function create_metadata_educational($met, DOMDocument &$doc, $xmlnode) { $metadata = $doc->createElementNS($this->ccnamespaces['imscc'], 'metadata'); $xmlnode->insertBefore($metadata, $xmlnode->firstChild); $lom = $doc->createElementNS($this->ccnamespaces['lom'], 'lom'); $metadata->appendChild($lom); $educational = $doc->createElementNS($this->ccnamespaces['lom'], 'educational'); $lom->appendChild($educational); foreach ($met->arrayeducational as $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { $userrole = $doc->createElementNS($this->ccnamespaces['lom'], 'intendedEndUserRole'); $educational->appendChild($userrole); $nd4 = $doc->createElementNS($this->ccnamespaces['lom'], 'source', 'IMSGLC_CC_Rolesv1p1'); $nd5 = $doc->createElementNS($this->ccnamespaces['lom'], 'value', $v[0]); $userrole->appendChild($nd4); $userrole->appendChild($nd5); } } return $metadata; } } cc/cc_lib/cc_page.php 0000644 00000011262 15215711721 0010442 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_general.php'); class page11_resurce_file extends general_cc_file { protected $rootns = 'xmlns'; protected $rootname = 'html'; protected $ccnamespaces = array('xmlns' => 'http://www.w3.org/1999/xhtml'); protected $content = null; protected $title = null; protected $intro = null; public function set_content($value) { // We are not cleaning up this one on purpose. $this->content = $value; } public function set_title($value) { $this->title = self::safexml($value); } public function set_intro($value) { $this->intro = self::safexml(strip_tags($value)); } protected function on_create() { $impl = new DOMImplementation(); $dtd = $impl->createDocumentType( 'html', '-//W3C//DTD XHTML 1.0 Strict//EN', 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'); $doc = $impl->createDocument($this->ccnamespaces[$this->rootns], null, $dtd); $doc->formatOutput = true; $doc->preserveWhiteSpace = true; $this->doc = $doc; parent::on_create(); } public function on_save() { $rns = $this->ccnamespaces[$this->rootns]; // Add the basic tags. $head = $this->append_new_element_ns($this->root, $rns, 'head'); $this->append_new_attribute_ns($head, $rns, 'profile', 'http://dublincore.org/documents/dc-html/'); // Linking Dublin Core Metadata 1.1. $link_dc = $this->append_new_element_ns($head, $rns, 'link'); $this->append_new_attribute_ns($link_dc, $rns, 'rel', 'schema.DC'); $this->append_new_attribute_ns($link_dc, $rns, 'href', 'http://purl.org/dc/elements/1.1/'); $link_dcterms = $this->append_new_element_ns($head, $rns, 'link'); $this->append_new_attribute_ns($link_dcterms, $rns, 'rel', 'schema.DCTERMS'); $this->append_new_attribute_ns($link_dcterms, $rns, 'href', 'http://purl.org/dc/terms/'); // Content type. $meta_type = $this->append_new_element_ns($head, $rns, 'meta'); $this->append_new_attribute_ns($meta_type, $rns, 'name', 'DC.type'); $this->append_new_attribute_ns($meta_type, $rns, 'scheme', 'DCTERMS.DCMIType'); $this->append_new_attribute_ns($meta_type, $rns, 'content', 'Text'); // Content description. if (!empty($this->intro)) { $meta_description = $this->append_new_element_ns($head, $rns, 'meta'); $this->append_new_attribute_ns($meta_description, $rns, 'name', 'DC.description'); $this->append_new_attribute_ns($meta_description, $rns, 'content', $this->intro); } $meta = $this->append_new_element_ns($head, $rns, 'meta'); $this->append_new_attribute_ns($meta, $rns, 'http-equiv', 'Content-type'); $this->append_new_attribute_ns($meta, $rns, 'content', 'text/html; charset=UTF-8'); // Set the title. $title = $this->append_new_element_ns($head, $rns, 'title', $this->title); $body = $this->append_new_element_ns($this->root, $rns, 'body'); // We are unable to use DOM for embedding HTML due to numerous content errors. // Therefore we place a dummy tag that will be later replaced with the real content. $this->append_new_element_ns($body, $rns, 'div', '##REPLACE##'); return true; } public function saveTo($fname) { $result = $this->on_save(); if ($result) { $dret = str_replace('<?xml version="1.0"?>'."\n", '', $this->viewXML()); $dret = str_replace('<div>##REPLACE##</div>', $this->content, $dret); $result = (file_put_contents($fname, $dret) !== false); if ($result) { $this->filename = $fname; $this->processPath(); } } return $result; } } cc/cc_lib/cc_converter_quiz.php 0000644 00000007722 15215711721 0012613 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_converters.php'); require_once('cc_general.php'); require_once('cc_asssesment.php'); require_once('cc_assesment_truefalse.php'); require_once('cc_assesment_essay.php'); require_once('cc_assesment_sfib.php'); class cc_converter_quiz extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path) { $this->cc_type = cc_version11::assessment; $this->defaultfile = 'quiz.xml'; $this->defaultname = assesment11_resurce_file::deafultname; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new assesment11_resurce_file(); $title = $this->doc->nodeValue('/activity/quiz/name'); $rt->set_title($title); // Metadata. $metadata = new cc_assesment_metadata(); $rt->set_metadata($metadata); $metadata->enable_feedback(); $metadata->enable_hints(); $metadata->enable_solutions(); // Attempts. $max_attempts = (int)$this->doc->nodeValue('/activity/quiz/attempts_number'); if ($max_attempts > 0) { // Qti does not support number of specific attempts bigger than 5 (??) if ($max_attempts > 5) { $max_attempts = cc_qti_values::unlimited; } $metadata->set_maxattempts($max_attempts); } // Time limit must be converted into minutes. $timelimit = (int)floor((int)$this->doc->nodeValue('/activity/quiz/timelimit') / 60); if ($timelimit > 0) { $metadata->set_timelimit($timelimit); $metadata->enable_latesubmissions(false); } $contextid = $this->doc->nodeValue('/activity/@contextid'); $result = cc_helpers::process_linked_files( $this->doc->nodeValue('/activity/quiz/intro'), $this->manifest, $this->rootpath, $contextid, $outdir); cc_assesment_helper::add_assesment_description($rt, $result[0], cc_qti_values::htmltype); // Section. $section = new cc_assesment_section(); $rt->set_section($section); // Process the actual questions. $ndeps = cc_assesment_helper::process_questions($this->doc, $this->manifest, $section, $this->rootpath, $contextid, $outdir); if ($ndeps === false) { // No exportable questions in quiz or quiz has no questions // so just skip it. return true; } // Store any additional dependencies. $deps = array_merge($result[1], $ndeps); // Store everything. $this->store($rt, $outdir, $title, $deps); return true; } } cc/cc_lib/cc_weblink.php 0000644 00000007024 15215711721 0011162 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_general.php'; class url1_resurce_file extends general_cc_file { const deafultname = 'weblink.xml'; protected $rootns = 'wl'; protected $rootname = 'wl:webLink'; protected $ccnamespaces = array('wl' => 'http://www.imsglobal.org/xsd/imswl_v1p0', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('wl' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_5/imswl_v1p0_localised.xsd'); protected $url = null; protected $title = null; protected $href = null; protected $target = '_self'; protected $window_features = null; /** * * Set the url title * @param string $title */ public function set_title($title) { $this->title = self::safexml($title); } /** * * Set the url specifics * @param string $url * @param string $target * @param string $window_features */ public function set_url($url, $target='_self', $window_features=null) { $this->url = $url; $this->target = $target; $this->window_features = $window_features; } protected function on_save() { $this->append_new_element($this->root, 'title', $this->title); $url = $this->append_new_element($this->root, 'url'); $this->append_new_attribute($url, 'href', $this->url); if (!empty($this->target)) { $this->append_new_attribute($url, 'target', $this->target); } if (!empty($this->window_features)) { $this->append_new_attribute($url, 'windowFeatures', $this->window_features); } return true; } } class url11_resurce_file extends url1_resurce_file { protected $rootname = 'webLink'; protected $ccnamespaces = array('wl' => 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance'); protected $ccnsnames = array('wl' => 'http://www.imsglobal.org/profile/cc/ccv1p1/ccv1p1_imswl_v1p1.xsd'); protected function on_save() { $rns = $this->ccnamespaces[$this->rootns]; $this->append_new_element_ns($this->root, $rns, 'title', $this->title); $url = $this->append_new_element_ns($this->root, $rns, 'url'); $this->append_new_attribute_ns($url, $rns, 'href', $this->url); if (!empty($this->target)) { $this->append_new_attribute_ns($url, $rns, 'target', $this->target); } if (!empty($this->window_features)) { $this->append_new_attribute_ns($url, $rns, 'windowFeatures', $this->window_features); } return true; } } cc/cc_lib/cc_converter_forum.php 0000644 00000004521 15215711721 0012745 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; require_once 'cc_forum.php'; class cc_converter_forum extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->cc_type = cc_version11::discussiontopic; $this->defaultfile = 'forum.xml'; $this->defaultname = 'discussion.xml'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new forum11_resurce_file(); $title = $this->doc->nodeValue('/activity/forum/name'); $rt->set_title($title); $text = $this->doc->nodeValue('/activity/forum/intro'); $deps = null; if (!empty($text)) { $textformat = intval($this->doc->nodeValue('/activity/forum/introformat')); $contextid = $this->doc->nodeValue('/activity/@contextid'); $result = cc_helpers::process_linked_files($text, $this->manifest, $this->rootpath, $contextid, $outdir); $textformat = ($textformat == 1) ? 'text/html' : 'text/plain'; $rt->set_text($result[0], $textformat); $deps = $result[1]; } $this->store($rt, $outdir, $title, $deps); return true; } } cc/cc_lib/cc_converters.php 0000644 00000010316 15215711721 0011717 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_interfaces.php'); abstract class cc_converter { /** * * Enter description here ... * @var cc_item */ protected $item = null; /** * * Enter description here ... * @var cc_manifest */ protected $manifest = null; /** * * Enter description here ... * @var string */ protected $rootpath = null; /** * * Enter description here ... * @var string */ protected $path = null; /** * * Enter description here ... * @var string */ protected $defaultfile = null; /** * * Enter description here ... * @var string */ protected $defaultname = null; /** * * Enter description here ... * @var string */ protected $cc_type = null; /** * * Document * @var XMLGenericDocument */ protected $doc = null; /** * * ctor * @param cc_i_item $item * @param cc_i_manifest $manifest * @param string $rootpath * @param string $path * @throws InvalidArgumentException */ public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path) { $rpath = realpath($rootpath); if (empty($rpath)) { throw new InvalidArgumentException('Invalid path!'); } $rpath2 = realpath($path); if (empty($rpath)) { throw new InvalidArgumentException('Invalid path!'); } $doc = new XMLGenericDocument(); if (!$doc->load($path . DIRECTORY_SEPARATOR . $this->defaultfile)) { throw new RuntimeException('File does not exist!'); } $this->doc = $doc; $this->item = $item; $this->manifest = $manifest; $this->rootpath = $rpath; $this->path = $rpath2; } /** * * performs conversion * @param string $outdir - root directory of common cartridge * @return boolean */ abstract public function convert($outdir); /** * * Is the element visible in the course? * @throws RuntimeException * @return bool */ protected function is_visible() { $tdoc = new XMLGenericDocument(); if (!$tdoc->load($this->path . DIRECTORY_SEPARATOR . 'module.xml')) { throw new RuntimeException('File does not exist!'); } $visible = (int)$tdoc->nodeValue('/module/visible'); return ($visible > 0); } /** * * Stores any files that need to be stored */ protected function store(general_cc_file $doc, $outdir, $title, $deps = null) { $rdir = new cc_resource_location($outdir); $rtp = $rdir->fullpath(true).$this->defaultname; if ( $doc->saveTo($rtp) ) { $resource = new cc_resource($rdir->rootdir(), $this->defaultname, $rdir->dirname(true)); $resource->dependency = empty($deps) ? array() : $deps; $resource->instructoronly = !$this->is_visible(); $res = $this->manifest->add_resource($resource, null, $this->cc_type); $resitem = new cc_item(); $resitem->attach_resource($res[0]); $resitem->title = $title; $this->item->add_child_item($resitem); } else { throw new RuntimeException("Unable to save file {$rtp}!"); } } } cc/cc_lib/cc_converter_folder.php 0000644 00000003307 15215711721 0013071 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_converters.php'); require_once('cc_general.php'); class cc_converter_folder extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path) { $this->defaultfile = 'folder.xml'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $resitem = new cc_item(); $resitem->title = $this->doc->nodeValue('/activity/folder/name'); $this->item->add_child_item($resitem); $contextid = $this->doc->nodeValue('/activity/@contextid'); cc_helpers::handle_static_content($this->manifest, $this->rootpath, $contextid, $outdir); return true; } } cc/cc_lib/cc_assesment_essay.php 0000644 00000006172 15215711721 0012740 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc_assesment_question_essay extends cc_assesment_question_proc_base { public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) { parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $this->qtype = cc_qti_profiletype::essay; $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade'); $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000'; } public function on_generate_metadata() { parent::on_generate_metadata(); // Mark essay for manual grading. $this->qmetadata->enable_scoringpermitted(); $this->qmetadata->enable_computerscored(false); } public function on_generate_presentation() { parent::on_generate_presentation(); $response_str = new cc_assesment_response_strtype(); $response_fib = new cc_assesment_render_fibtype(); $row_value = (int)$this->questions->nodeValue('plugin_qtype_essay_question//responsefieldlines', $this->question_node); $response_fib->set_rows($row_value); $response_str->set_render_fib($response_fib); $this->qpresentation->set_response_str($response_str); } public function on_generate_response_processing() { parent::on_generate_response_processing(); // Response conditions. if (!empty($this->general_feedback)) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('General feedback'); $this->qresprocessing->add_respcondition($qrespcondition); // Define the condition for success. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid('general_fb'); } } } cc/cc_lib/cc_converter_resource.php 0000644 00000004261 15215711721 0013445 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_converters.php'); require_once('cc_general.php'); class cc_converter_resource extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path) { $this->cc_type = cc_version11::webcontent; $this->defaultfile = 'resource.xml'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $title = $this->doc->nodeValue('/activity/resource/name'); $contextid = $this->doc->nodeValue('/activity/@contextid'); $files = cc_helpers::handle_resource_content($this->manifest, $this->rootpath, $contextid, $outdir); $deps = null; $resvalue = null; foreach ($files as $values) { if ($values[2]) { $resvalue = $values[0]; break; } } $resitem = new cc_item(); $resitem->identifierref = $resvalue; $resitem->title = $title; $this->item->add_child_item($resitem); // Checking the visibility. $this->manifest->update_instructoronly($resvalue, !$this->is_visible()); return true; } } cc/cc_lib/cc_resources.php 0000644 00000020060 15215711721 0011534 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_interfaces.php'); require_once('xmlbase.php'); require_once('gral_lib/pathutils.php'); require_once('gral_lib/ccdependencyparser.php'); require_once('cc_version_base.php'); require_once('cc_version1.php'); require_once('cc_manifest.php'); /** * Common Cartridge Version * */ class cc_version{ const v1 = 1; const v11 = 11; } class cc1_resource_type { const webcontent = 'webcontent'; const questionbank = 'imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'; const assessment = 'imsqti_xmlv1p2/imscc_xmlv1p0/assessment'; const associatedcontent = 'associatedcontent/imscc_xmlv1p0/learning-application-resource'; const discussiontopic = 'imsdt_xmlv1p0'; const weblink = 'imswl_xmlv1p0'; public static $checker = array(self::webcontent, self::assessment, self::associatedcontent, self::discussiontopic, self::questionbank, self::weblink); } class cc11_resource_type { const webcontent = 'webcontent'; const questionbank = 'imsqti_xmlv1p2/imscc_xmlv1p1/question-bank'; const assessment = 'imsqti_xmlv1p2/imscc_xmlv1p1/assessment'; const associatedcontent = 'associatedcontent/imscc_xmlv1p1/learning-application-resource'; const discussiontopic = 'imsdt_xmlv1p1'; const weblink = 'imswl_xmlv1p1'; const basiclti = 'imsbasiclti_xmlv1p0'; public static $checker = array(self::webcontent, self::assessment, self::associatedcontent, self::discussiontopic, self::questionbank, self::weblink, self::basiclti); } /** * Resource Class * */ class cc_resource implements cc_i_resource { public $identifier = null; public $type = null; public $dependency = array(); public $identifierref = null; public $href = null; public $base = null; public $persiststate = null; public $metadata = array(); public $filename = null; public $files = array(); public $isempty = null; public $manifestroot = null; public $folder = null; public $instructoronly = false; private $throwonerror = true; public function __construct($manifest, $file, $folder='', $throwonerror = true) { $this->throwonerror = $throwonerror; if (is_string($manifest)) { $this->folder = $folder; $this->process_resource($manifest, $file, $folder); $this->manifestroot = $manifest; } else if (is_object($manifest)) { $this->import_resource($file, $manifest); } } /** * Add resource * * @param string $fname * @param string $location */ public function add_resource($fname, $location ='') { $this->process_resource($fname, $location, null); } /** * Import a resource * * @param DOMElement $node * @param cc_i_manifest $doc */ public function import_resource(DOMElement &$node, cc_i_manifest &$doc) { $searchstr = "//imscc:manifest[@identifier='".$doc->manifestID(). "']/imscc:resources/imscc:resource"; $this->identifier = $this->get_attr_value($node, "identifier"); $this->type = $this->get_attr_value($node, "type"); $this->href = $this->get_attr_value($node, "href"); $this->base = $this->get_attr_value($node, "base"); $this->persiststate = null; $nodo = $doc->nodeList($searchstr."[@identifier='". $this->identifier."']/metadata/@href"); $this->metadata = $nodo->nodeValue; $this->filename = $this->href; $nlist = $doc->nodeList($searchstr."[@identifier='". $this->identifier."']/imscc:file/@href"); $this->files = array(); foreach ($nlist as $file) { $this->files[] = $file->nodeValue; } $nlist = $doc->nodeList($searchstr."[@identifier='". $this->identifier."']/imscc:dependency/@identifierref"); $this->dependency = array(); foreach ($nlist as $dependency) { $this->dependency[] = $dependency->nodeValue; } $this->isempty = false; } /** * Get a attribute value * * @param DOMElement $nod * @param string $name * @param string $ns * @return string */ public function get_attr_value(&$nod, $name, $ns=null) { if (is_null($ns)) { return ($nod->hasAttribute($name) ? $nod->getAttribute($name) : null); } return ($nod->hasAttributeNS($ns, $name) ? $nod->getAttributeNS($ns, $name) : null); } /** * Process a resource * * @param string $manifestroot * @param string $fname * @param string $folder */ public function process_resource($manifestroot, &$fname, $folder) { $file = empty($folder) ? $manifestroot.'/'.$fname : $manifestroot.'/'.$folder.'/'.$fname; if (!file_exists($file) && $this->throwonerror) { throw new Exception('The file doesnt exist!'); } GetDepFiles($manifestroot, $fname, $this->folder, $this->files); array_unshift($this->files, $folder.$fname); $this->init_empty_new(); $this->href = $folder.$fname; $this->identifierref = $folder.$fname; $this->filename = $fname; $this->isempty = false; $this->folder = $folder; } public function adjust_path($mroot, $fname) { $result = null; if (file_exists($fname->filename)) { $result = pathDiff($fname->filename, $mroot); } else if (file_exists($mroot.$fname->filename) || file_exists($mroot.DIRECTORY_SEPARATOR.$fname->filename)) { $result = $fname->filename; toUrlPath($result); $result = trim($result, "/"); } return $result; } public function init_clean() { $this->identifier = null; $this->type = null; $this->href = null; $this->base = null; $this->metadata = array(); $this->dependency = array(); $this->identifierref = null; $this->persiststate = null; $this->filename = ''; $this->files = array(); $this->isempty = true; } public function init_empty_new() { $this->identifier = cc_helpers::uuidgen('I_', '_R'); $this->type = null; $this->href = null; $this->persiststate = null; $this->filename = null; $this->isempty = false; $this->identifierref = null; } public function get_manifestroot() { return $this->manifestroot; } } cc/cc_lib/cc_version_base.php 0000644 00000010666 15215711721 0012214 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_organization.php'; /** * Abstract Version Base class * */ abstract class cc_version_base { protected $_generator = null; protected $ccnamespaces = array(); protected $isrootmanifest = false; protected $manifestID = null; protected $organizationid = null; public $resources = null; public $resources_ind = null; protected $metadata = null; public $organizations = null; protected $base = null; public $ccversion = null; public $camversion = null; abstract protected function on_create(DOMDocument &$doc, $rootmanifestnode = null, $nmanifestID = null); abstract protected function create_metadata_manifest(cc_i_metadata_manifest $met, DOMDocument &$doc, $xmlnode = null); abstract protected function create_metadata_resource(cc_i_metadata_resource $met, DOMDocument &$doc, $xmlnode = null); abstract protected function create_metadata_file(cc_i_metadata_file $met, DOMDocument &$doc, $xmlnode = null); abstract protected function create_resource(cc_i_resource &$res, DOMDocument &$doc, $xmlnode=null); abstract protected function create_organization(cc_i_organization &$org, DOMDocument &$doc, $xmlnode=null); public function get_cc_namespaces() { return $this->ccnamespaces; } public function create_manifest(DOMDocument &$doc, $rootmanifestnode = null) { return $this->on_create($doc, $rootmanifestnode); } public function create_resource_node(cc_i_resource &$res, DOMDocument &$doc, $xmlnode = null) { return $this->create_resource($res, $doc, $xmlnode); } public function create_metadata_node(&$met, DOMDocument &$doc, $xmlnode = null) { return $this->create_metadata_manifest($met, $doc, $xmlnode); } public function create_metadata_resource_node(&$met, DOMDocument &$doc, $xmlnode = null) { return $this->create_metadata_resource($met, $doc, $xmlnode); } public function create_metadata_file_node(&$met, DOMDocument &$doc, $xmlnode = null) { return $this->create_metadata_file($met, $doc, $xmlnode); } public function create_organization_node(cc_i_organization &$org, DOMDocument &$doc, $xmlnode = null) { return $this->create_organization($org, $doc, $xmlnode); } public function manifestID() { return $this->manifestID; } public function set_manifestID($id) { $this->manifestID = $id; } public function get_base() { return $this->base; } public function set_base($baseval) { $this->base = $baseval; } public function import_resources(DOMElement &$node, cc_i_manifest &$doc) { if (is_null($this->resources)) { $this->resources = array(); } $nlist = $node->getElementsByTagNameNS($this->ccnamespaces['imscc'], 'resource'); if (is_object($nlist)) { foreach ($nlist as $nd) { $sc = new cc_resource($doc, $nd); $this->resources[$sc->identifier] = $sc; } } } public function import_organization_items(DOMElement &$node, cc_i_manifest &$doc) { if (is_null($this->organizations)) { $this->organizations = array(); } $nlist = $node->getElementsByTagNameNS($this->ccnamespaces['imscc'], 'organization'); if (is_object($nlist)) { foreach ($nlist as $nd) { $sc = new cc_organization($nd, $doc); $this->organizations[$sc->identifier] = $sc; } } } public function set_generator($value) { $this->_generator = $value; } } cc/cc_lib/cc_organization.php 0000644 00000020674 15215711721 0012241 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_utils.php'; require_once 'cc_version_base.php'; require_once 'cc_resources.php'; require_once 'cc_manifest.php'; /** * Organization Class * */ class cc_organization implements cc_i_organization { public $title = null; public $identifier = null; public $structure = null; public $itemlist = null; private $metadata = null; private $sequencing = null; /** @var bool true if empty, otherwise false. */ protected $isempty; public function __construct($node=null, $doc=null) { if (is_object($node) && is_object($doc)) { $this->process_organization($node,$doc); } else { $this->init_new(); } } /** * Add one Item into the Organization * * @param cc_i_item $item */ public function add_item(cc_i_item &$item) { if (is_null($this->itemlist)) { $this->itemlist = array(); } $this->itemlist[$item->identifier] = $item; } /** * Add new Item into the Organization * * @param string $title * @return cc_i_item */ public function add_new_item($title='') { $nitem = new cc_item(); $nitem->title = $title; $this->add_item($nitem); return $nitem; } public function has_items() { return is_array($this->itemlist) && (count($this->itemlist) > 0); } public function attr_value(&$nod, $name, $ns=null) { return is_null($ns) ? ($nod->hasAttribute($name) ? $nod->getAttribute($name) : null) : ($nod->hasAttributeNS($ns, $name) ? $nod->getAttributeNS($ns, $name) : null); } public function process_organization(&$node,&$doc) { $this->identifier = $this->attr_value($node,"identifier"); $this->structure = $this->attr_value($node,"structure"); $this->title = ''; $nlist = $node->getElementsByTagName('title'); if (is_object($nlist) && ($nlist->length > 0) ) { $this->title = $nlist->item(0)->nodeValue; } $nlist = $doc->nodeList("//imscc:organization[@identifier='".$this->identifier."']/imscc:item"); $this->itemlist=array(); foreach ($nlist as $item) { $this->itemlist[$item->getAttribute("identifier")] = new cc_item($item,$doc); } $this->isempty=false; } public function init_new() { $this->title = null; $this->identifier = cc_helpers::uuidgen('O_'); $this->structure = 'rooted-hierarchy'; $this->itemlist = null; $this->metadata = null; $this->sequencing = null; } public function uuidgen() { $uuid = sprintf('%04x%04x', mt_rand(0, 65535), mt_rand(0, 65535)); return strtoupper(trim($uuid)); } } /** * Item Class * */ class cc_item implements cc_i_item { public $identifier = null; public $identifierref = null; public $isvisible = null; public $title = null; public $parameters = null; public $childitems = null; private $parentItem = null; private $isempty = true; /** @var mixed node structure. */ public $structure; public function __construct($node=null,$doc=null) { if (is_object($node)) { $clname = get_class($node); if ($clname =='cc_resource') { $this->init_new_item(); $this->identifierref = $node->identifier; $this->title = is_string($doc) && (!empty($doc)) ? $doc : 'item'; } else if ($clname =='cc_manifest') { $this->init_new_item(); $this->identifierref = $node->manifestID(); $this->title = is_string($doc) && (!empty($doc)) ? $doc : 'item'; } else if ( is_object($doc)){ $this->process_item($node,$doc); } else { $this->init_new_item(); } } else { $this->init_new_item(); } } public function attr_value(&$nod, $name, $ns=null) { return is_null($ns) ? ($nod->hasAttribute($name) ? $nod->getAttribute($name) : null) : ($nod->hasAttributeNS($ns, $name) ? $nod->getAttributeNS($ns, $name) : null); } public function process_item(&$node,&$doc) { $this->identifier = $this->attr_value($node,"identifier"); $this->structure = $this->attr_value($node,"structure"); $this->identifierref = $this->attr_value($node,"identifierref"); $atr = $this->attr_value($node,"isvisible"); $this->isvisible = is_null($atr) ? true : $atr; $nlist = $node->getElementsByTagName('title'); if (is_object($nlist) && ($nlist->length > 0) ) { $this->title = $nlist->item(0)->nodeValue; } $nlist = $doc->nodeList("//imscc:item[@identifier='".$this->identifier."']/imscc:item"); if ($nlist->length > 0) { $this->childitems=array(); foreach ($nlist as $item) { $key=$this->attr_value($item,"identifier"); $this->childitems[$key] = new cc_item($item,$doc); } } $this->isempty = false; } /** * Add one Child Item * * @param cc_i_item $item */ public function add_child_item(cc_i_item &$item) { if (is_null($this->childitems)) { $this->childitems = array(); } $this->childitems[$item->identifier] = $item; } /** * Add new child Item * * @param string $title * @return cc_i_item */ public function add_new_child_item($title='') { $sc = new cc_item(); $sc->title = $title; $this->add_child_item($sc); return $sc; } public function attach_resource($resource) { if ($this->has_child_items()) { throw new Exception("Can not attach resource to item that contains other items!"); } $resident = null; if (is_string($resource)) { $resident = $resource; } else if (is_object($resource)) { $clname = get_class($resource); if ($clname == 'cc_resource') { $resident = $resource->identifier; } else if ($clname == 'cc_manifest') { $resident = $resource->manifestID(); } else { throw new Exception("Unable to attach resource. Invalid object."); } } if (is_null($resident) || (empty($resident))) { throw new Exception("Resource must have valid identifier!"); } $this->identifierref = $resident; } public function has_child_items() { return is_array($this->childitems) && (count($this->childitems) > 0); } public function child_item($identifier) { return $this->has_child_items() ? $this->childitems[$identifier] : null; } public function init_clean() { $this->identifier = null; $this->isvisible = null; $this->title = null; $this->parameters = null; $this->childitems = null; $this->parentItem = null; $this->isempty = true; } public function init_new_item() { $this->identifier = cc_helpers::uuidgen('I_'); $this->isvisible = true; //default is true $this->title = null; $this->parameters = null; $this->childitems = null; $this->parentItem = null; $this->isempty = false; } } cc/cc_lib/cc_metadata.php 0000644 00000015070 15215711721 0011307 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** Defined as specified in CC 1.1 */ class intended_user_role { const LEARNER = 'Learner'; const INSTRUCTOR = 'Instructor'; const MENTOR = 'Mentor'; } class technical_role { const AUTHOR = 'author'; const PUBLISHER = 'publisher'; const UNKNOWN = 'unknown'; const INITIATOR = 'initiator'; const TERMINATOR = 'terminator'; const VALIDATOR = 'validator'; const EDITOR = 'editor'; const GRAPHICAL_DESIGNER = 'graphical designer'; const TECHNICAL_IMPLEMENTER = 'technical implementer'; const CONTENT_PROVIDER = 'content provider'; const TECHNICAL_VALIDATOR = 'technical validator'; const EDUCATION_VALIDATOR = 'educational validator'; const SCRIPT_WRITER = 'script writer'; const INSTRUCTIONAL_DESIGNER= 'instructional designer'; const SUBJET_MATTER_EXPERT = 'subject matter expert'; } class rights_copyright { const YES = 'yes'; const NO = 'no'; } class rights_cost { const YES = 'yes'; const NO = 'no'; } // Language identifier (as defined in ISO 639-1, ISO 639-2, and ISO 3166-1) class language_lom { const US_ENGLISH = 'en-US'; const GB_ENGLISH = 'en-GB'; const AR_SPANISH = 'es-AR'; const GR_GREEK = 'el-GR'; } /** * Metadata Manifest * */ class cc_metadata_manifest implements cc_i_metadata_manifest { public $arraygeneral = array(); public $arraytech = array(); public $arrayrights = array(); public $arraylifecycle = array(); public function add_metadata_general($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->title)? $this->arraygeneral['title']=$obj->title:null; !is_null($obj->language)? $this->arraygeneral['language']=$obj->language:null; !is_null($obj->description)? $this->arraygeneral['description']=$obj->description:null; !is_null($obj->keyword)? $this->arraygeneral['keyword']=$obj->keyword:null; !is_null($obj->coverage)? $this->arraygeneral['coverage']=$obj->coverage:null; !is_null($obj->catalog)? $this->arraygeneral['catalog']=$obj->catalog:null; !is_null($obj->entry)? $this->arraygeneral['entry']=$obj->entry:null; } public function add_metadata_technical($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->format)? $this->arraytech['format']=$obj->format:null; } public function add_metadata_rights($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->copyright)? $this->arrayrights['copyrightAndOtherRestrictions']=$obj->copyright:null; !is_null($obj->description)? $this->arrayrights['description']=$obj->description:null; !is_null($obj->cost)? $this->arrayrights['cost']=$obj->cost:null; } public function add_metadata_lifecycle($obj){ if (empty($obj)){ throw new Exception('Medatada Object given is invalid or null!'); } !is_null($obj->role)? $this->arraylifecycle['role']=$obj->role:null; !is_null($obj->entity)? $this->arraylifecycle['entity']=$obj->entity:null; !is_null($obj->date)? $this->arraylifecycle['date']=$obj->date:null; } } /** * Metadata Lifecycle Type * */ class cc_metadata_lifecycle{ public $role = array(); public $entity = array(); public $date = array(); public function set_role($role){ $this->role[] = array($role); } public function set_entity($entity){ $this->entity[] = array($entity); } public function set_date($date){ $this->date[] = array($date); } } /** * Metadata Rights Type * */ class cc_metadata_rights { public $copyright = array(); public $description = array(); public $cost = array(); public function set_copyright($copy){ $this->copyright[] = array($copy); } public function set_description($des,$language){ $this->description[] = array($language,$des); } public function set_cost($cost){ $this->cost[] = array($cost); } } /** * Metadata Technical Type * */ class cc_metadata_technical { public $format = array(); public function set_format($format){ $this->format[] = array($format); } } /** * Metadata General Type * */ class cc_metadata_general { public $title = array(); public $language = array(); public $description = array(); public $keyword = array(); public $coverage = array(); public $catalog = array(); public $entry = array(); public function set_coverage($coverage,$language){ $this->coverage[] = array($language,$coverage); } public function set_description($description,$language){ $this->description[] = array($language,$description); } public function set_keyword($keyword,$language){ $this->keyword[] = array($language,$keyword); } public function set_language($language){ $this->language[] = array($language); } public function set_title($title,$language){ $this->title[] = array($language,$title); } public function set_catalog($cat){ $this->catalog[] = array($cat); } public function set_entry($entry){ $this->entry[] = array($entry); } } cc/cc_lib/cc_builder_creator.php 0000644 00000002515 15215711721 0012674 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Factory pattern class * Create the version class to use * */ class cc_builder_creator { public static function factory($version){ if (is_null($version)) { throw new Exception("Version is null!"); } if (include_once 'cc_version' . $version . '.php') { $classname = 'cc_version' . $version; return new $classname; } else { throw new Exception ("Dont find cc version class!"); } } } cc/cc_lib/cc_converter_page.php 0000644 00000004652 15215711721 0012536 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; require_once 'cc_page.php'; class cc_converter_page extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->cc_type = cc_version11::webcontent; $this->defaultfile = 'page.xml'; $this->defaultname = uniqid().'.html'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $rt = new page11_resurce_file(); $title = $this->doc->nodeValue('/activity/page/name'); $intro = $this->doc->nodeValue('/activity/page/intro'); $contextid = $this->doc->nodeValue('/activity/@contextid'); $pagecontent = $this->doc->nodeValue('/activity/page/content'); $rt->set_title($title); $rawname = str_replace(' ', '_', strtolower(trim(clean_param($title, PARAM_FILE)))); if (!empty($rawname)) { $this->defaultname = $rawname.".html"; } $result = cc_helpers::process_linked_files( $pagecontent, $this->manifest, $this->rootpath, $contextid, $outdir, true); $rt->set_content($result[0]); $rt->set_intro($intro); //store everything $this->store($rt, $outdir, $title, $result[1]); return true; } } cc/cc_lib/cc_version1.php 0000644 00000051247 15215711721 0011303 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('cc_utils.php'); require_once('cc_version_base.php'); require_once('cc_organization.php'); /** * Version 1 class of Common Cartridge * */ class cc_version1 extends cc_version_base { const webcontent = 'webcontent'; const questionbank = 'imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'; const assessment = 'imsqti_xmlv1p2/imscc_xmlv1p0/assessment'; const associatedcontent = 'associatedcontent/imscc_xmlv1p0/learning-application-resource'; const discussiontopic = 'imsdt_xmlv1p0'; const weblink = 'imswl_xmlv1p0'; /** @var array CC URL profiles. */ protected $ccnsnames = []; public static $checker = array(self::webcontent, self::assessment, self::associatedcontent, self::discussiontopic, self::questionbank, self::weblink); /** * Validate if the type are valid or not * * @param string $type * @return bool */ public function valid($type) { return in_array($type, self::$checker); } public function __construct() { $this->ccnamespaces = array('imscc' => 'http://www.imsglobal.org/xsd/imscc/imscp_v1p1', 'lomimscc' => 'http://ltsc.ieee.org/xsd/imscc/LOM', 'lom' => 'http://ltsc.ieee.org/xsd/LOM', 'voc' => 'http://ltsc.ieee.org/xsd/LOM/vocab', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance' ); $this->ccnsnames = array( 'imscc' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/imscp_v1p2_localised.xsd', 'lom' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_2/lomLoose_localised.xsd', 'lomimscc' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_1/lomLoose_localised.xsd', 'voc' => 'http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/domainProfile_2/vocab/loose.xsd' ); $this->ccversion = '1.0.0'; $this->camversion = '1.0.0'; $this->_generator = 'Moodle 2 Common Cartridge generator'; } protected function on_create(DOMDocument &$doc, $rootmanifestnode = null, $nmanifestID = null) { $doc->formatOutput = true; $doc->preserveWhiteSpace = true; $this->manifestID = is_null($nmanifestID) ? cc_helpers::uuidgen('M_') : $nmanifestID; $mUUID = $doc->createAttribute('identifier'); $mUUID->nodeValue = $this->manifestID; if (is_null($rootmanifestnode)) { if (!empty($this->_generator)) { $comment = $doc->createComment($this->_generator); $doc->appendChild($comment); } $rootel = $doc->createElementNS($this->ccnamespaces['imscc'], 'manifest'); $rootel->appendChild($mUUID); $doc->appendChild($rootel); // Add all namespaces. foreach ($this->ccnamespaces as $key => $value) { $dummy_attr = $key.":dummy"; $doc->createAttributeNS($value, $dummy_attr); } // Add location of schemas. $schemaLocation = ''; foreach ($this->ccnsnames as $key => $value) { $vt = empty($schemaLocation) ? '' : ' '; $schemaLocation .= $vt.$this->ccnamespaces[$key].' '.$value; } $aSchemaLoc = $doc->createAttributeNS($this->ccnamespaces['xsi'], 'xsi:schemaLocation'); $aSchemaLoc->nodeValue = $schemaLocation; $rootel->appendChild($aSchemaLoc); } else { $rootel = $doc->createElementNS($this->ccnamespaces['imscc'], 'imscc:manifest'); $rootel->appendChild($mUUID); } $metadata = $doc->createElementNS($this->ccnamespaces['imscc'], 'metadata'); $schema = $doc->createElementNS($this->ccnamespaces['imscc'], 'schema', 'IMS Common Cartridge'); $schemaversion = $doc->createElementNS($this->ccnamespaces['imscc'], 'schemaversion', $this->ccversion); $metadata->appendChild($schema); $metadata->appendChild($schemaversion); $rootel->appendChild($metadata); if (!is_null($rootmanifestnode)) { $rootmanifestnode->appendChild($rootel); } $organizations = $doc->createElementNS($this->ccnamespaces['imscc'], 'organizations'); $rootel->appendChild($organizations); $resources = $doc->createElementNS($this->ccnamespaces['imscc'], 'resources'); $rootel->appendChild($resources); return true; } protected function update_attribute(DOMDocument &$doc, $attrname, $attrvalue, DOMElement &$node) { $busenew = (is_object($node) && $node->hasAttribute($attrname)); $nResult = null; if (!$busenew && is_null($attrvalue)) { $node->removeAttribute($attrname); } else { $nResult = $busenew ? $node->getAttributeNode($attrname) : $doc->createAttribute($attrname); $nResult->nodeValue = $attrvalue; if (!$busenew) { $node->appendChild($nResult); } } return $nResult; } protected function update_attribute_ns(DOMDocument &$doc, $attrname, $attrnamespace,$attrvalue, DOMElement &$node) { $busenew = (is_object($node) && $node->hasAttributeNS($attrnamespace, $attrname)); $nResult = null; if (!$busenew && is_null($attrvalue)) { $node->removeAttributeNS($attrnamespace, $attrname); } else { $nResult = $busenew ? $node->getAttributeNodeNS($attrnamespace, $attrname) : $doc->createAttributeNS($attrnamespace, $attrname); $nResult->nodeValue = $attrvalue; if (!$busenew) { $node->appendChild($nResult); } } return $nResult; } protected function get_child_node(DOMDocument &$doc, $itemname, DOMElement &$node) { $nlist = $node->getElementsByTagName($itemname); $item = is_object($nlist) && ($nlist->length > 0) ? $nlist->item(0) : null; return $item; } protected function update_child_item(DOMDocument &$doc, $itemname, $itemvalue, DOMElement &$node, $attrtostore=null) { $tnode = $this->get_child_node($doc, 'title', $node); $usenew = is_null($tnode); $tnode = $usenew ? $doc->createElementNS($this->ccnamespaces['imscc'], $itemname) : $tnode; if (!is_null($attrtostore)) { foreach ($attrtostore as $key => $value) { $this->update_attribute($doc, $key, $value, $tnode); } } $tnode->nodeValue = $itemvalue; if ($usenew) { $node->appendChild($tnode); } } protected function update_items($items, DOMDocument &$doc, DOMElement &$xmlnode) { foreach ($items as $key => $item) { $itemnode = $doc->createElementNS($this->ccnamespaces['imscc'], 'item'); $this->update_attribute($doc, 'identifier' , $key , $itemnode); $this->update_attribute($doc, 'identifierref', $item->identifierref, $itemnode); $this->update_attribute($doc, 'parameters' , $item->parameters , $itemnode); if (!empty($item->title)) { $titlenode = $doc->createElementNS($this->ccnamespaces['imscc'], 'title', $item->title); $itemnode->appendChild($titlenode); } if ($item->has_child_items()) { $this->update_items($item->childitems, $doc, $itemnode); } $xmlnode->appendChild($itemnode); } } /** * Create a Resource (How to) * * @param cc_i_resource $res * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_resource(cc_i_resource &$res, DOMDocument &$doc, $xmlnode=null) { $usenew = is_object($xmlnode); $dnode = $usenew ? $xmlnode : $doc->createElementNS($this->ccnamespaces['imscc'], "resource"); $this->update_attribute($doc, 'identifier', $res->identifier, $dnode); $this->update_attribute($doc, 'type', $res->type, $dnode); !is_null($res->href) ? $this->update_attribute($doc, 'href', $res->href, $dnode) : null; $this->update_attribute($doc, 'base', $res->base, $dnode); foreach ($res->files as $file) { $nd = $doc->createElementNS($this->ccnamespaces['imscc'], 'file'); $ndatt = $doc->createAttribute('href'); $ndatt->nodeValue = $file; $nd->appendChild($ndatt); $dnode->appendChild($nd); } $this->resources[$res->identifier] = $res; $this->resources_ind[$res->files[0]] = $res->identifier; foreach ($res->dependency as $dependency) { $nd = $doc->createElementNS($this->ccnamespaces['imscc'], 'dependency'); $ndatt = $doc->createAttribute('identifierref'); $ndatt->nodeValue = $dependency; $nd->appendChild($ndatt); $dnode->appendChild($nd); } return $dnode; } /** * Create an Item Folder (How To) * * @param cc_i_organization $org * @param DOMDocument $doc * @param DOMElement $xmlnode */ protected function create_item_folder(cc_i_organization &$org, DOMDocument &$doc, ?DOMElement &$xmlnode = null) { $itemfoldernode = $doc->createElementNS($this->ccnamespaces['imscc'], 'item'); $this->update_attribute($doc, 'identifier', "root", $itemfoldernode); if ($org->has_items()) { $this->update_items($org->itemlist, $doc, $itemfoldernode); } if (is_null($this->organizations)) { $this->organizations = array(); } $this->organizations[$org->identifier] = $org; $xmlnode->appendChild($itemfoldernode); } /** * Create an Organization (How To) * * @param cc_i_organization $org * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_organization(cc_i_organization &$org, DOMDocument &$doc, $xmlnode = null) { $usenew = is_object($xmlnode); $dnode = $usenew ? $xmlnode : $doc->createElementNS($this->ccnamespaces['imscc'], "organization"); $this->update_attribute($doc, 'identifier', $org->identifier, $dnode); $this->update_attribute($doc, 'structure', $org->structure, $dnode); $this->create_item_folder($org, $doc, $dnode); return $dnode; } /** * Create Metadata For Manifest (How To) * * @param cc_i_metadata_manifest $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_manifest(cc_i_metadata_manifest $met, DOMDocument &$doc, $xmlnode = null) { $dnode = $doc->createElementNS($this->ccnamespaces['lomimscc'], "lom"); if (!empty($xmlnode)) { $xmlnode->appendChild($dnode); } $dnodegeneral = empty($met->arraygeneral) ? null : $this->create_metadata_general($met, $doc, $xmlnode); $dnodetechnical = empty($met->arraytech) ? null : $this->create_metadata_technical($met, $doc, $xmlnode); $dnoderights = empty($met->arrayrights) ? null : $this->create_metadata_rights($met, $doc, $xmlnode); $dnodelifecycle = empty($met->arraylifecycle) ? null : $this->create_metadata_lifecycle($met, $doc, $xmlnode); !is_null($dnodegeneral) ? $dnode->appendChild($dnodegeneral) : null; !is_null($dnodetechnical) ? $dnode->appendChild($dnodetechnical) : null; !is_null($dnoderights) ? $dnode->appendChild($dnoderights) : null; !is_null($dnodelifecycle) ? $dnode->appendChild($dnodelifecycle) : null; return $dnode; } /** * Create Metadata For Resource (How To) * * @param cc_i_metadata_resource $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_resource(cc_i_metadata_resource $met, DOMDocument &$doc, $xmlnode = null) { $dnode = $doc->createElementNS($this->ccnamespaces['lom'], "lom"); !empty($xmlnode) ? $xmlnode->appendChild($dnode) : null; !empty($met->arrayeducational) ? $this->create_metadata_educational($met, $doc, $dnode) : null; return $dnode; } /** * Create Metadata For File (How To) * * @param cc_i_metadata_file $met * @param DOMDocument $doc * @param Object $xmlnode * @return DOMNode */ protected function create_metadata_file(cc_i_metadata_file $met, DOMDocument &$doc, $xmlnode = null) { $dnode = $doc->createElementNS($this->ccnamespaces['lom'], "lom"); !empty($xmlnode) ? $xmlnode->appendChild($dnode) : null; !empty($met->arrayeducational) ? $this->create_metadata_educational($met, $doc, $dnode) : null; return $dnode; } /** * Create General Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_general($met, DOMDocument &$doc, $xmlnode) { $nd = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'general'); foreach ($met->arraygeneral as $name => $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { if ($name != 'language' && $name != 'catalog' && $name != 'entry') { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name); $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'string', $v[1]); $ndatt = $doc->createAttribute('language'); $ndatt->nodeValue = $v[0]; $nd3->appendChild($ndatt); $nd2->appendChild($nd3); $nd->appendChild($nd2); } else { if ($name == 'language') { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name, $v[0]); $nd->appendChild($nd2); } } } } if (!empty($met->arraygeneral['catalog']) || !empty($met->arraygeneral['entry'])) { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'identifier'); $nd->appendChild($nd2); if (!empty($met->arraygeneral['catalog'])) { $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'catalog', $met->arraygeneral['catalog'][0][0]); $nd2->appendChild($nd3); } if (!empty($met->arraygeneral['entry'])) { $nd4 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'entry', $met->arraygeneral['entry'][0][0]); $nd2->appendChild($nd4); } } return $nd; } /** * Create Technical Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_technical($met, DOMDocument &$doc, $xmlnode) { $nd = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'technical'); $xmlnode->appendChild($nd); foreach ($met->arraytech as $name => $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name, $v[0]); $nd->appendChild($nd2); } } return $nd; } /** * Create Rights Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_rights($met, DOMDocument &$doc, $xmlnode) { $nd = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'rights'); foreach ($met->arrayrights as $name => $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { if ($name == 'description') { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name); $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'string', $v[1]); $ndatt = $doc->createAttribute('language'); $ndatt->nodeValue = $v[0]; $nd3->appendChild($ndatt); $nd2->appendChild($nd3); $nd->appendChild($nd2); } else if ($name == 'copyrightAndOtherRestrictions' || $name == 'cost') { $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name); $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'value', $v[0]); $nd2->appendChild($nd3); $nd->appendChild($nd2); } } } return $nd; } /** * Create Lifecycle Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ protected function create_metadata_lifecycle($met, DOMDocument &$doc, $xmlnode) { $nd = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'lifeCycle'); $nd2 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'contribute'); $nd->appendChild($nd2); $xmlnode->appendChild($nd); foreach ($met->arraylifecycle as $name => $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { if ($name == 'role') { $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name); $nd2->appendChild($nd3); $nd4 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'value', $v[0]); $nd3->appendChild($nd4); } else { if ($name == 'date') { $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name); $nd2->appendChild($nd3); $nd4 = $doc->createElementNS($this->ccnamespaces['lomimscc'], 'dateTime', $v[0]); $nd3->appendChild($nd4); } else { $nd3 = $doc->createElementNS($this->ccnamespaces['lomimscc'], $name, $v[0]); $nd2->appendChild($nd3); } } } } return $nd; } /** * Create Education Metadata (How To) * * @param object $met * @param DOMDocument $doc * @param object $xmlnode * @return DOMNode */ public function create_metadata_educational($met, DOMDocument &$doc, $xmlnode) { $nd = $doc->createElementNS($this->ccnamespaces['lom'], 'educational'); $nd2 = $doc->createElementNS($this->ccnamespaces['lom'], 'intendedEndUserRole'); $nd3 = $doc->createElementNS($this->ccnamespaces['voc'], 'vocabulary'); $xmlnode->appendChild($nd); $nd->appendChild($nd2); $nd2->appendChild($nd3); foreach ($met->arrayeducational as $name => $value) { !is_array($value) ? $value = array($value) : null; foreach ($value as $v) { $nd4 = $doc->createElementNS($this->ccnamespaces['voc'], $name, $v[0]); $nd3->appendChild($nd4); } } return $nd; } } cc/cc_lib/cc_converter_label.php 0000644 00000002630 15215711721 0012673 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @subpackage cc-library * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once 'cc_converters.php'; require_once 'cc_general.php'; class cc_converter_label extends cc_converter { public function __construct(cc_i_item &$item, cc_i_manifest &$manifest, $rootpath, $path){ $this->defaultfile = 'label.xml'; parent::__construct($item, $manifest, $rootpath, $path); } public function convert($outdir) { $resitem = new cc_item(); $resitem->title = $this->doc->nodeValue('/activity/label/name'); $this->item->add_child_item($resitem); return true; } } cc/cc_lib/cc_assesment_truefalse.php 0000644 00000022132 15215711721 0013600 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package backup-convert * @copyright 2012 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once('cc_asssesment.php'); class cc_assesment_question_truefalse extends cc_assesment_question_proc_base { public function __construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir) { parent::__construct($quiz, $questions, $manifest, $section, $question_node, $rootpath, $contextid, $outdir); $this->qtype = cc_qti_profiletype::true_false; // Determine the correct answer by finding out which answer has the non zero fraction... // This is because a true / false question type can have 'false' as the correct answer. $answers = $this->questions->nodeList('plugin_qtype_truefalse_question/answers/answer', $this->question_node); foreach ($answers as $answer) { $fraction = $this->questions->nodeValue('fraction', $answer); if ($fraction != 0) { $this->correct_answer_node_id = (int)$this->questions->nodeValue('@id', $answer); } } $maximum_quiz_grade = (int)$this->quiz->nodeValue('/activity/quiz/grade'); $this->total_grade_value = ($maximum_quiz_grade + 1).'.0000000'; } public function on_generate_answers() { // Add responses holder. $qresponse_lid = new cc_response_lidtype(); $this->qresponse_lid = $qresponse_lid; $this->qpresentation->set_response_lid($qresponse_lid); $qresponse_choice = new cc_assesment_render_choicetype(); $qresponse_lid->set_render_choice($qresponse_choice); // Mark that question has only one correct answer - // which applies for multiple choice and yes/no questions. $qresponse_lid->set_rcardinality(cc_qti_values::Single); // Are we to shuffle the responses? $shuffle_answers = (int)$this->quiz->nodeValue('/activity/quiz/shuffleanswers') > 0; $qresponse_choice->enable_shuffle($shuffle_answers); $answerlist = array(); $qa_responses = $this->questions->nodeList('plugin_qtype_truefalse_question/answers/answer', $this->question_node); foreach ($qa_responses as $node) { $answer_content = $this->questions->nodeValue('answertext', $node); $id = ((int)$this->questions->nodeValue('@id', $node) == $this->correct_answer_node_id); $qresponse_label = cc_assesment_helper::add_answer( $qresponse_choice, $answer_content, cc_qti_values::htmltype); $answer_ident = strtolower(trim($answer_content)); $qresponse_label->set_ident($answer_ident); $feedback_ident = ($id) ? 'correct_fb' : 'incorrect_fb'; if (empty($this->correct_answer_ident) && $id) { $this->correct_answer_ident = $answer_ident; } // Add answer specific feedback if not empty. $content = $this->questions->nodeValue('feedback', $node); if (!empty($content)) { $result = cc_helpers::process_linked_files( $content, $this->manifest, $this->rootpath, $this->contextid, $this->outdir); cc_assesment_helper::add_feedback( $this->qitem, $result[0], cc_qti_values::htmltype, $feedback_ident); pkg_resource_dependencies::instance()->add($result[1]); $answerlist[$answer_ident] = $feedback_ident; } } $this->answerlist = $answerlist; } public function on_generate_response_processing() { parent::on_generate_response_processing(); // Response conditions. // General unconditional feedback must be added as a first respcondition // without any condition and just displayfeedback (if exists). if (!empty($this->general_feedback)) { $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('General feedback'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(); // Define the condition for success. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qother = new cc_assignment_conditionvar_othertype(); $qconditionvar->set_other($qother); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid('general_fb'); } // Success condition. // For all question types outside of the Essay question, scoring is done in a // single <respcondition> with a continue flag set to No. The outcome is always // a variable named SCORE which value must be set to 100 in case of correct answer. // Partial scores (not 0 or 100) are not supported. $qrespcondition = new cc_assesment_respconditiontype(); $qrespcondition->set_title('Correct'); $this->qresprocessing->add_respcondition($qrespcondition); $qrespcondition->enable_continue(false); $qsetvar = new cc_assignment_setvartype(100); $qrespcondition->add_setvar($qsetvar); // Define the condition for success. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); // TODO: recheck this. $qvarequal = new cc_assignment_conditionvar_varequaltype($this->correct_answer_ident); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident($this->qresponse_lid->get_ident()); if (array_key_exists($this->correct_answer_ident, $this->answerlist)) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($this->answerlist[$this->correct_answer_ident]); } foreach ($this->correct_feedbacks as $ident) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($ident); } // Rest of the conditions. foreach ($this->answerlist as $ident => $refid) { if ($ident == $this->correct_answer_ident) { continue; } $qrespcondition = new cc_assesment_respconditiontype(); $this->qresprocessing->add_respcondition($qrespcondition); $qsetvar = new cc_assignment_setvartype(0); $qrespcondition->add_setvar($qsetvar); // Define the condition for fail. $qconditionvar = new cc_assignment_conditionvar(); $qrespcondition->set_conditionvar($qconditionvar); $qvarequal = new cc_assignment_conditionvar_varequaltype($ident); $qconditionvar->set_varequal($qvarequal); $qvarequal->set_respident($this->qresponse_lid->get_ident()); $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($refid); foreach ($this->incorrect_feedbacks as $ident) { $qdisplayfeedback = new cc_assignment_displayfeedbacktype(); $qrespcondition->add_displayfeedback($qdisplayfeedback); $qdisplayfeedback->set_feedbacktype(cc_qti_values::Response); $qdisplayfeedback->set_linkrefid($ident); } } } } cc/cc_lib/gral_lib/functions.php 0000644 00000006071 15215711721 0012646 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Librería de funciones básicas V1.0 (June, 16th 2009) * * * @author Daniel Mühlrad * @link daniel.muhlrad@uvcms.com * @version 1.0 * @copyright 2009 * */ /** * Make a Handler error with an exception msg error * * @param integer $errno * @param string $errstr * @param string $errfile * @param string $errline */ function errorHandler($errno, $errstr, $errfile, $errline) { // si deseas podes guardarlos en un archivo ($errfile);($errline); throw new Exception($errstr, $errno); } /** * Return de mime-type of a file * * @param string $file * @param string $default_type * */ function file_mime_type ($file, $default_type = 'application/octet-stream'){ $ftype = $default_type; $magic_path = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'magic' . DIRECTORY_SEPARATOR . 'magic'; $finfo = @finfo_open(FILEINFO_MIME , $magic_path); if ($finfo !== false) { $fres = @finfo_file($finfo, $file); if ( is_string($fres) && !empty($fres) ) { $ftype = $fres; } @finfo_close($finfo); } return $ftype; } function array_remove_by_value($arr,$value) { return array_values(array_diff($arr,array($value))); } function array_remove_by_key($arr,$key) { return array_values(array_diff_key($arr,array($key))); } function cc_print_object($object) { echo '<pre>' . htmlspecialchars(print_r($object,true), ENT_COMPAT) . '</pre>'; } /** * IndexOf - first version of find an element in the Array given * returns the index of the *first* occurance * @param mixed $needle * @param array $haystack * @return mixed The element or false if the function didnt find it */ function indexOf($needle, $haystack) { for ($i = 0; $i < count($haystack) ; $i++) { if ($haystack[$i] == $needle) { return $i; } } return false; } /** * IndexOf2 - second version of find an element in the Array given * * @param mixed $needle * @param array $haystack * @return mixed The index of the element or false if the function didnt find it */ function indexOf2($needle, $haystack) { for($i = 0,$z = count($haystack); $i < $z; $i++){ if ($haystack[$i] == $needle) { //finds the needle return $i; } } return false; } cc/cc_lib/gral_lib/ccdependencyparser.php 0000644 00000011552 15215711721 0014477 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. require_once(__DIR__ .'/../xmlbase.php'); require_once('cssparser.php'); require_once('pathutils.php'); /** * * Older version better suited for PHP < 5.2 * @deprecated * @param mixed $url * @return boolean */ function is_url_deprecated($url) { if ( !preg_match('#^http\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) && !preg_match('#^https\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) && !preg_match('#^ftp\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) ) { $status = false; } else { $status = true; } return $status; } /** * * validates URL * @param string $url * @return boolean */ function is_url($url) { $result = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) !== false; return $result; } function GetDepFiles($manifestroot, $fname, $folder, &$filenames) { static $types = array('xhtml' => true, 'html' => true, 'htm' => true); $extension = strtolower(trim(pathinfo($fname, PATHINFO_EXTENSION))); $filenames = array(); if (isset($types[$extension])) { $dcx = new XMLGenericDocument(); $filename = $manifestroot.$folder.$fname; if (!file_exists($filename)) { $filename = $manifestroot.DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR.$fname; } if (file_exists($filename)) { $res = $dcx->loadHTMLFile($filename); if ($res) { GetDepFilesHTML($manifestroot, $fname, $filenames, $dcx, $folder); } } } } function GetDepFilesHTML($manifestroot, $fname, &$filenames, &$dcx, $folder) { $dcx->resetXpath(); $nlist = $dcx->nodeList("//img/@src | //link/@href | //script/@src | //a[not(starts-with(@href,'#'))]/@href"); $css_obj_array = array(); foreach ($nlist as $nl) { $item = $folder.$nl->nodeValue; $path_parts = pathinfo($item); $fname = $path_parts['basename']; $ext = array_key_exists('extension', $path_parts) ? $path_parts['extension'] : ''; if (!is_url($folder.$nl->nodeValue) && !is_url($nl->nodeValue)) { $path = $folder.$nl->nodeValue; $file = fullPath($path, "/"); toNativePath($file); if (file_exists($manifestroot.DIRECTORY_SEPARATOR.$file)) { $filenames[$file] = $file; } } if ($ext == 'css') { $css = new cssparser(); $css->Parse($dcx->filePath().$nl->nodeValue); $css_obj_array[$item] = $css; } } $nlist = $dcx->nodeList("//*/@class"); foreach ($nlist as $nl) { $item = $folder.$nl->nodeValue; foreach ($css_obj_array as $csskey => $cssobj) { $bimg = $cssobj->Get($item, "background-image"); $limg = $cssobj->Get($item, "list-style-image"); $npath = pathinfo($csskey); if ((!empty($bimg)) && ($bimg != 'none')) { $value = stripUrl($bimg, $npath['dirname'].'/'); $filenames[$value] = $value; } else if ((!empty($limg)) && ($limg != 'none')) { $value = stripUrl($limg, $npath['dirname'].'/'); $filenames[$value] = $value; } } } $elems_to_check = array("body", "p", "ul", "h4", "a", "th"); $do_we_have_it = array(); foreach ($elems_to_check as $elem) { $do_we_have_it[$elem] = ($dcx->nodeList("//".$elem)->length > 0); } foreach ($elems_to_check as $elem) { if ($do_we_have_it[$elem]) { foreach ($css_obj_array as $csskey => $cssobj) { $sb = $cssobj->Get($elem, "background-image"); $sbl = $cssobj->Get($elem, "list-style-image"); $npath = pathinfo($csskey); if ((!empty($sb)) && ($sb != 'none')) { $value = stripUrl($sb, $npath['dirname'].'/'); $filenames[$value] = $value; } else if ((!empty($sbl)) && ($sbl != 'none')) { $value = stripUrl($sbl, $npath['dirname'].'/'); $filenames[$value] = $value; } } } } } cc/cc_lib/gral_lib/pathutils.php 0000644 00000030143 15215711721 0012650 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Function expands all relative parts of supplied path string thus * removing things like ../../ or ./../. * * @param string $path * @param string $dirsep Character that represents directory separator should be * specified here. Default is DIRECTORY_SEPARATOR. * @return string */ function fullPath($path,$dirsep=DIRECTORY_SEPARATOR) { $token = '$IMS-CC-FILEBASE$'; $path = str_replace($token,'',$path); if ( is_string($path) && ($path != '') ) { $sep = $dirsep; $dotDir= '.'; $upDir = '..'; $length= strlen($path); $rtemp= trim($path); $start = strrpos($path, $sep); $canContinue = ($start !== false); $result= $canContinue ? '': $path; $rcount=0; while ($canContinue) { $dirPart = ($start !== false) ? substr($rtemp,$start+1,$length-$start) : $rtemp; $canContinue = ($dirPart !== false); if ($canContinue) { if ($dirPart != $dotDir) { if ($dirPart == $upDir) { $rcount++; } else { if ($rcount > 0) { $rcount--; } else { $result = ($result == '') ? $dirPart : $dirPart.$sep.$result; } } } $rtemp = substr($path,0,$start); $start = strrpos($rtemp, $sep); $canContinue = (($start !== false) || (strlen($rtemp) > 0)); } } //end while } return $result; } /** * Function strips url part from css link * * @param string $path * @param string $rootDir * @return string */ function stripUrl($path, $rootDir='') { $result = $path; if ( is_string($path) && ($path != '') ) { $start=strpos($path,'(')+1; $length=strpos($path,')')-$start; $rut = $rootDir.substr($path,$start,$length); $result=fullPath($rut,'/'); } return $result; } /** * Converts direcotry separator in given path to / to validate in CC * Value is passed byref hence variable itself is changed * * @param string $path */ function toNativePath(&$path) { for ($count = 0 ; $count < strlen($path); ++$count) { $chr = $path[$count]; if (($chr == '\\') || ($chr == '/')) { $path[$count] = '/'; } } } /** * Converts direcotry separator in given path to the one on the server platform * Value is passed byref hence variable itself is changed * * @param string $path */ function toNativePath2(&$path) { for ($count = 0 ; $count < strlen($path); ++$count) { $chr = $path[$count]; if (($chr == '\\') || ($chr == '/')) { $path[$count] = DIRECTORY_SEPARATOR; } } } /** * Converts \ Directory separator to the / more suitable for URL * * @param string $path */ function toUrlPath(&$path) { for ($count = 0 ; $count < strlen($path); ++$count) { $chr = $path[$count]; if (($chr == '\\')) { $path[$count] = '/'; } } } /** * Returns relative path from two directories with full path * * @param string $path1 * @param string $path2 * @return string */ function pathDiff($path1, $path2) { toUrlPath($path1); toUrlPath($path2); $result = ""; $bl2 = strlen($path2); $a = strpos($path1,$path2); if ($a !== false) { $result = trim(substr($path1,$bl2+$a),'/'); } return $result; } /** * Copy a file, or recursively copy a folder and its contents * * @author Aidan Lister <aidan@php.net> * @version 1.0.1 * @link http://aidanlister.com/repos/v/function.copyr.php * @param string $source Source path * @param string $dest Destination path * @return bool Returns TRUE on success, FALSE on failure */ function copyr($source, $dest) { global $CFG; // Simple copy for a file if (is_file($source)) { return copy($source, $dest); } // Make destination directory if (!is_dir($dest)) { mkdir($dest, $CFG->directorypermissions, true); } // Loop through the folder $dir = dir($source); while (false !== $entry = $dir->read()) { // Skip pointers if ($entry == '.' || $entry == '..') { continue; } // Deep copy directories if ($dest !== "$source/$entry") { copyr("$source/$entry", "$dest/$entry"); } } // Clean up $dir->close(); return true; } /** * Function returns array with directories contained in folder (only first level) * * @param string $rootDir directory to look into * @param string $contains which string to look for * @param array $excludeitems array of names to be excluded * @param bool $startswith should the $contains value be searched only from * beginning * @return array Returns array of sub-directories. In case $rootDir path is * invalid it returns FALSE. */ function getDirectories($rootDir, $contains, $excludeitems = null, $startswith = true) { $result = is_dir($rootDir); if ($result) { $dirlist = dir($rootDir); $entry = null; $result = array(); while(false !== ($entry = $dirlist->read())) { $currdir = $rootDir.$entry; if (is_dir($currdir)) { $bret = strpos($entry,$contains); if (($bret !== false)) { if (($startswith && ($bret == 0)) || !$startswith) { if (!( is_array($excludeitems) && in_array($entry,$excludeitems) )) { $result[] = $entry; } } } } } } return $result; } function getFilesOnly($rootDir, $contains, $excludeitems = null, $startswith = true,$extension=null) { $result = is_dir($rootDir); if ($result) { $filelist = dir($rootDir); $entry = null; $result = array(); while(false !== ($entry = $filelist->read())) { $curritem = $rootDir.$entry; $pinfo = pathinfo($entry); $ext = array_key_exists('extension',$pinfo) ? $pinfo['extension'] : null; if (is_file($curritem) && (is_null($extension) || ($ext == $extension) )) { $bret = strpos($entry,$contains); if (($bret !== false)) { if (($startswith && ($bret == 0)) || !$startswith) { if (!( is_array($excludeitems) && in_array($entry,$excludeitems) )) { $result[] = $entry; } } } } } } natcasesort($result); return $result; } /** * Search an identifier in array * * @param array $array * @param string $name * */ function search_ident_by_name($array,$name){ if (empty($array)){ throw new Exception('The array given is null'); } $ident = null; foreach ($array as $k => $v){ ($k); if ($v[1] == $name){ $ident = $v[0]; break; } } return $ident; } /** * Function returns files recursivly with appeneded relative path * * @param string $startDir * @param string $rootDir * @param array $excludedirs * @param array $excludefileext * @return array */ function getRawFiles($startDir, &$fhandle, $rootDir='', $excludedirs = null, $excludefileext = null) { $result = is_dir($startDir); if ($result) { $dirlist = dir($startDir); $entry = null; while(false !== ($entry = $dirlist->read())) { $curritem = $startDir.$entry; if (($entry=='.') || ($entry =='..')) { continue; } if (is_dir($curritem)) { if (!( is_array($excludedirs) && in_array($entry,$excludedirs) )) { getRawFiles($startDir.$entry."/",$fhandle,$rootDir.$entry."/",$excludedirs,$excludefileext); } continue; } if (is_file($curritem)){ $pinfo = pathinfo($entry); $ext = array_key_exists('extension',$pinfo) ? $pinfo['extension'] : ''; if (!is_array($excludefileext) || (is_array($excludefileext) && !in_array($ext,$excludefileext))) { fwrite($fhandle,$rootDir.$entry."\n"); } } } } return $result; } function getRawFiles2($startDir,&$arr, $rootDir='', $excludedirs = null, $excludefileext = null) { $result = is_dir($startDir); if ($result) { $dirlist = dir($startDir); $entry = null; while(false !== ($entry = $dirlist->read())) { $curritem = $startDir.$entry; if (($entry=='.') || ($entry =='..')) { continue; } if (is_dir($curritem)) { if (!( is_array($excludedirs) && in_array($entry,$excludedirs) )) { getRawFiles2($startDir.$entry."/",$arr,$rootDir.$entry."/",$excludedirs,$excludefileext); } continue; } if (is_file($curritem)){ $pinfo = pathinfo($entry); $ext = array_key_exists('extension',$pinfo) ? $pinfo['extension'] : ''; if (!is_array($excludefileext) || (is_array($excludefileext) && !in_array($ext,$excludefileext))) { array_push($arr,$rootDir.$entry); // fwrite($fhandle,$rootDir.$entry."\n"); } } } } return $result; } function GetFiles($startDir, $outfile, $rootDir='', $excludedirs = null, $excludefileext = null) { $fh = @fopen($outfile,"w+"); if ($fh !== FALSE) { getRawFiles($startDir,$fh,$rootDir,$excludedirs,$excludefileext); @fclose($fh); @chmod($outfile,0777); } } /** * Function to get an array with all files in a directory and subdirectories * * @param string $startDir * @param string $rootDir * @param string $excludedirs * @param string $excludefileext * @return array */ function GetFilesArray($startDir, $rootDir='', $excludedirs = null, $excludefileext = null) { $arr = array(); getRawFiles2($startDir,$arr,$rootDir,$excludedirs,$excludefileext); return $arr; } /** * Function returns array with directories contained in folder (only first level) * simmilar to getDirectories but returned items are naturally sorted. * * @param string $rootDir * @param string $contains * @param array $excludeitems * @param bool $startswith * @return array */ function getCourseDirs ($rootDir, $contains, $excludeitems=null, $startswith=true) { $result = getDirectories($rootDir,$contains,$excludeitems,$startswith); if ($result !== false) { natcasesort($result); $result = array_values($result); } return $result; } /** * Delete a directory recursive with files inside * * @param string $dirname * @return bool */ function rmdirr($dirname) { if (!file_exists($dirname)) { return false; } if (is_file($dirname) || is_link($dirname)) { return unlink($dirname); } $dir = dir($dirname); while (false !== $entry = $dir->read()) { if ($entry == '.' || $entry == '..') { continue; } rmdirr($dirname . DIRECTORY_SEPARATOR . $entry); } $dir->close(); return rmdir($dirname); } cc/cc_lib/gral_lib/parser.php 0000644 00000013537 15215711721 0012137 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. require_once $CFG->dirroot .'/backup/cc/cc_lib/xmlbase.php'; require_once 'cssparser.php'; require_once 'pathutils.php'; function is_url($url) { if ( !preg_match('#^http\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) && !preg_match('#^https\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) && !preg_match('#^ftp\\:\\/\\/[a-z0-9\-]+\.([a-z0-9\-]+\.)?[a-z]+#i', $url) ) { $status = false; } else { $status = true; } return $status; } function GetDepFiles($manifestroot, $fname,$folder,&$filenames) { $extension = end(explode('.',$fname)); $filenames = array(); $dcx = new XMLGenericDocument(); $result = true; switch ($extension){ case 'xml': $result = @$dcx->loadXMLFile($manifestroot.$folder.$fname); if (!$result) { $result = @$dcx->loadXMLFile($manifestroot.DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR.$fname); } GetDepFilesXML($manifestroot, $fname,$filenames,$dcx, $folder); break; case 'html': case 'htm': $result = @$dcx->loadHTMLFile($manifestroot.$folder.$fname); if (!$result) { $result = @$dcx->loadHTMLFile($manifestroot.DIRECTORY_SEPARATOR.$folder.DIRECTORY_SEPARATOR.$fname); } GetDepFilesHTML($manifestroot, $fname,$filenames,$dcx, $folder); break; } return $result; } function GetDepFilesXML ($manifestroot, $fname,&$filenames,&$dcx, $folder){ $nlist = $dcx->nodeList("//img/@src | //attachments/attachment/@href | //link/@href | //script/@src"); $css_obj_array = array(); foreach ($nlist as $nl) { $item = $nl->nodeValue; $path_parts = pathinfo($item); $fname = $path_parts['basename']; $ext = array_key_exists('extension',$path_parts) ? $path_parts['extension'] : ''; if (!is_url($nl->nodeValue)) { //$file = $folder.$nl->nodeValue; // DEPENDERA SI SE QUIERE Q SEA RELATIVO O ABSOLUTO $file = $nl->nodeValue; toNativePath($file); $filenames[]=$file; } } $dcx->registerNS('qti','http://www.imsglobal.org/xsd/imscc/ims_qtiasiv1p2.xsd'); $dcx->resetXpath(); $nlist = $dcx->nodeList("//qti:mattext | //text"); $dcx2 = new XMLGenericDocument(); foreach ($nlist as $nl) { if ($dcx2->loadString($nl->nodeValue)){ GetDepFilesHTML($manifestroot,$fname,$filenames,$dcx2,$folder); } } } function GetDepFilesHTML ($manifestroot, $fname,&$filenames,&$dcx, $folder){ $dcx->resetXpath(); $nlist = $dcx->nodeList("//img/@src | //link/@href | //script/@src | //a[not(starts-with(@href,'#'))]/@href"); $css_obj_array=array(); foreach ($nlist as $nl) { $item = $nl->nodeValue; $path_parts = pathinfo($item); $fname = $path_parts['basename']; $ext = array_key_exists('extension',$path_parts) ? $path_parts['extension'] : ''; if (!is_url($folder.$nl->nodeValue) && !is_url($nl->nodeValue)) { $path = $nl->nodeValue; //$file = fullPath($path,"/"); toNativePath($path); $filenames[]= $path; } if ($ext == 'css') { $css = new cssparser(); $css->Parse($dcx->filePath().$nl->nodeValue); $css_obj_array[$item]=$css; } } $nlist = $dcx->nodeList("//*/@class"); foreach ($nlist as $nl) { $item = $nl->nodeValue; foreach ($css_obj_array as $csskey => $cssobj) { $bimg = $cssobj->Get($item,"background-image"); $limg = $cssobj->Get($item,"list-style-image"); $npath = pathinfo($csskey); if ((!empty($bimg))&& ($bimg != 'none')) { $filenames[] = stripUrl($bimg,$npath['dirname'].'/'); } else if ((!empty($limg))&& ($limg != 'none')) { $filenames[] = stripUrl($limg,$npath['dirname'].'/'); } } } $elems_to_check = array("body","p","ul","h4","a","th"); $do_we_have_it = array(); foreach ($elems_to_check as $elem) { $do_we_have_it[$elem]=($dcx->nodeList("//".$elem)->length > 0); } foreach ($elems_to_check as $elem) { if ($do_we_have_it[$elem]) { foreach ($css_obj_array as $csskey => $cssobj) { $sb = $cssobj->Get($elem, "background-image"); $sbl = $cssobj->Get($elem,"list-style-image"); $npath = pathinfo($csskey); if ((!empty($sb)) && ($sb != 'none')) { $filenames[] = stripUrl($sb,$npath['dirname'].'/'); } else if ((!empty($sbl)) && ($sbl != 'none')) { $filenames[] = stripUrl($sbl,$npath['dirname'].'/'); } } } } } cc/cc_lib/gral_lib/cssparser.php 0000644 00000020217 15215711721 0012641 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. class cssparser { private $css; private $html; public function __construct($html = true) { // Register "destructor" core_shutdown_manager::register_function(array(&$this, "finalize")); $this->html = ($html != false); $this->Clear(); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function cssparser($html = true) { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct($html); } function finalize() { unset($this->css); } function Clear() { unset($this->css); $this->css = array(); if($this->html) { $this->Add("ADDRESS", ""); $this->Add("APPLET", ""); $this->Add("AREA", ""); $this->Add("A", "text-decoration : underline; color : Blue;"); $this->Add("A:visited", "color : Purple;"); $this->Add("BASE", ""); $this->Add("BASEFONT", ""); $this->Add("BIG", ""); $this->Add("BLOCKQUOTE", ""); $this->Add("BODY", ""); $this->Add("BR", ""); $this->Add("B", "font-weight: bold;"); $this->Add("CAPTION", ""); $this->Add("CENTER", ""); $this->Add("CITE", ""); $this->Add("CODE", ""); $this->Add("DD", ""); $this->Add("DFN", ""); $this->Add("DIR", ""); $this->Add("DIV", ""); $this->Add("DL", ""); $this->Add("DT", ""); $this->Add("EM", ""); $this->Add("FONT", ""); $this->Add("FORM", ""); $this->Add("H1", ""); $this->Add("H2", ""); $this->Add("H3", ""); $this->Add("H4", ""); $this->Add("H5", ""); $this->Add("H6", ""); $this->Add("HEAD", ""); $this->Add("HR", ""); $this->Add("HTML", ""); $this->Add("IMG", ""); $this->Add("INPUT", ""); $this->Add("ISINDEX", ""); $this->Add("I", "font-style: italic;"); $this->Add("KBD", ""); $this->Add("LINK", ""); $this->Add("LI", ""); $this->Add("MAP", ""); $this->Add("MENU", ""); $this->Add("META", ""); $this->Add("OL", ""); $this->Add("OPTION", ""); $this->Add("PARAM", ""); $this->Add("PRE", ""); $this->Add("P", ""); $this->Add("SAMP", ""); $this->Add("SCRIPT", ""); $this->Add("SELECT", ""); $this->Add("SMALL", ""); $this->Add("STRIKE", ""); $this->Add("STRONG", ""); $this->Add("STYLE", ""); $this->Add("SUB", ""); $this->Add("SUP", ""); $this->Add("TABLE", ""); $this->Add("TD", ""); $this->Add("TEXTAREA", ""); $this->Add("TH", ""); $this->Add("TITLE", ""); $this->Add("TR", ""); $this->Add("TT", ""); $this->Add("UL", ""); $this->Add("U", "text-decoration : underline;"); $this->Add("VAR", ""); } } function SetHTML($html) { $this->html = ($html != false); } function Add($key, $codestr) { $key = strtolower($key); $codestr = strtolower($codestr); if(!isset($this->css[$key])) { $this->css[$key] = array(); } $codes = explode(";",$codestr); if(count($codes) > 0) { $codekey=''; $codevalue=''; foreach($codes as $code) { $code = trim($code); $this->assignValues(explode(":",$code),$codekey,$codevalue); if(strlen($codekey) > 0) { $this->css[$key][trim($codekey)] = trim($codevalue); } } } } private function assignValues($arr,&$val1,&$val2) { $n = count($arr); if ($n > 0) { $val1=$arr[0]; $val2=($n > 1) ? $arr[1] : ''; } } function Get($key, $property) { $key = strtolower($key); $property = strtolower($property); $tag='';$subtag='';$class='';$id=''; $this->assignValues(explode(":",$key),$tag,$subtag); $this->assignValues(explode(".",$tag),$tag,$class); $this->assignValues(explode("#",$tag),$tag,$id); $result = ""; $_subtag=''; $_class=''; $_id=''; foreach($this->css as $_tag => $value) { $this->assignValues(explode(":",$_tag),$_tag,$_subtag); $this->assignValues(explode(".",$_tag),$_tag,$_class); $this->assignValues(explode("#",$_tag),$_tag,$_id); $tagmatch = (strcmp($tag, $_tag) == 0) | (strlen($_tag) == 0); $subtagmatch = (strcmp($subtag, $_subtag) == 0) | (strlen($_subtag) == 0); $classmatch = (strcmp($class, $_class) == 0) | (strlen($_class) == 0); $idmatch = (strcmp($id, $_id) == 0); if($tagmatch & $subtagmatch & $classmatch & $idmatch) { $temp = $_tag; if((strlen($temp) > 0) & (strlen($_class) > 0)) { $temp .= ".".$_class; } elseif(strlen($temp) == 0) { $temp = ".".$_class; } if((strlen($temp) > 0) & (strlen($_subtag) > 0)) { $temp .= ":".$_subtag; } elseif(strlen($temp) == 0) { $temp = ":".$_subtag; } if(isset($this->css[$temp][$property])) { $result = $this->css[$temp][$property]; } } } return $result; } function GetSection($key) { $key = strtolower($key); $tag='';$subtag='';$class='';$id=''; $_subtag=''; $_class=''; $_id=''; $this->assignValues(explode(":",$key),$tag,$subtag); $this->assignValues(explode(".",$tag),$tag,$class); $this->assignValues(explode("#",$tag),$tag,$id); $result = array(); foreach($this->css as $_tag => $value) { $this->assignValues(explode(":",$_tag),$_tag,$_subtag); $this->assignValues(explode(".",$_tag),$_tag,$_class); $this->assignValues(explode("#",$_tag),$_tag,$_id); $tagmatch = (strcmp($tag, $_tag) == 0) | (strlen($_tag) == 0); $subtagmatch = (strcmp($subtag, $_subtag) == 0) | (strlen($_subtag) == 0); $classmatch = (strcmp($class, $_class) == 0) | (strlen($_class) == 0); $idmatch = (strcmp($id, $_id) == 0); if($tagmatch & $subtagmatch & $classmatch & $idmatch) { $temp = $_tag; if((strlen($temp) > 0) & (strlen($_class) > 0)) { $temp .= ".".$_class; } elseif(strlen($temp) == 0) { $temp = ".".$_class; } if((strlen($temp) > 0) & (strlen($_subtag) > 0)) { $temp .= ":".$_subtag; } elseif(strlen($temp) == 0) { $temp = ":".$_subtag; } foreach($this->css[$temp] as $property => $value) { $result[$property] = $value; } } } return $result; } function ParseStr($str) { $this->Clear(); // Remove comments $str = preg_replace("/\/\*(.*)?\*\//Usi", "", $str); // Parse this damn csscode $parts = explode("}",$str); if(count($parts) > 0) { foreach($parts as $part) { $keystr='';$codestr=''; $this->assignValues(explode("{",$part),$keystr,$codestr); $keys = explode(",",trim($keystr)); if(count($keys) > 0) { foreach($keys as $key) { if(strlen($key) > 0) { $key = str_replace("\n", "", $key); $key = str_replace("\\", "", $key); $this->Add($key, trim($codestr)); } } } } } // return (count($this->css) > 0); } function Parse($filename) { $this->Clear(); if(file_exists($filename)) { return $this->ParseStr(file_get_contents($filename)); } else { return false; } } function GetCSS() { $result = ""; foreach($this->css as $key => $values) { $result .= $key." {\n"; foreach($values as $key => $value) { $result .= " $key: $value;\n"; } $result .= "}\n\n"; } return $result; } } cc/cc_includes.php 0000644 00000004557 15215711721 0010132 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Main include for IMS Common Cartridge export classes * * @package backup-convert * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once($CFG->dirroot .'/backup/cc/cc_lib/xmlbase.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_resources.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_builder_creator.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_manifest.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_metadata.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_metadata_resource.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_metadata_file.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_version11.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/gral_lib/pathutils.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/gral_lib/functions.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_organization.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_basiclti.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_lti.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_forum.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_url.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_resource.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_quiz.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_page.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_label.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_converter_folder.php'); require_once($CFG->dirroot .'/backup/cc/cc_lib/cc_convert_moodle2.php'); cc/validator.php 0000644 00000015671 15215711721 0007643 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Provides validation classes used by the imscc converters * * @package backup-convert * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class error_messages { /** * * @static error_messages */ private static $instance = null; private function __construct(){} private function __clone(){} /** * @return error_messages */ public static function instance() { if (empty(self::$instance)) { $c = __CLASS__; self::$instance = new $c(); } return self::$instance; } /** * @var array */ private $items = array(); /** * @param string $msg */ public function add($msg) { if (!empty($msg)) { $this->items[] = $msg; } } /** * @return array */ public function errors() { $this->items; } /** * Empties the error content */ public function reset() { $this->items = array(); } /** * @param boolean $web * @return string */ public function to_string($web = false) { $result = ''; if ($web) { $result .= '<ol>'.PHP_EOL; } foreach ($this->items as $error) { if ($web) { $result .= '<li>'; } $result .= $error.PHP_EOL; if ($web) { $result .= '</li>'.PHP_EOL; } } if ($web) { $result .= '</ol>'.PHP_EOL; } return $result; } /** * Casting to string method * @return string */ public function __toString() { return $this->to_string(false); } } final class libxml_errors_mgr { /** * @var boolean */ private $previous = false; /** * @param boolean $reset */ public function __construct($reset=false){ if ($reset) { error_messages::instance()->reset(); } $this->previous = libxml_use_internal_errors(true); libxml_clear_errors(); } private function collect_errors($filename=''){ $errors = libxml_get_errors(); static $error_types = array( LIBXML_ERR_ERROR => 'Error' ,LIBXML_ERR_FATAL => 'Fatal Error' ,LIBXML_ERR_WARNING => 'Warning' ); $result = array(); foreach($errors as $error){ $add = ''; if (!empty($filename)) { $add = " in {$filename}"; } elseif (!empty($error->file)) { $add = " in {$error->file}"; } $line = ''; if (!empty($error->line)) { $line = " at line {$error->line}"; } $err = "{$error_types[$error->level]}{$add}: {$error->message}{$line}"; error_messages::instance()->add($err); } libxml_clear_errors(); return $result; } public function __destruct(){ $this->collect_errors(); if (!$this->previous) { libxml_use_internal_errors($this->previous); } } public function collect() { $this->collect_errors(); } } function validate_xml($xml, $schema){ $result = false; $manifest_file = realpath($xml); $schema_file = realpath($schema); if (empty($manifest_file) || empty($schema_file)) { return false; } $xml_error = new libxml_errors_mgr(); $manifest = new DOMDocument(); $doc->validateOnParse = false; $result = $manifest->load($manifest_file, LIBXML_NONET) && $manifest->schemaValidate($schema_file); return $result; } class cc_validate_type { const manifest_validator1 = 'cclibxml2validator.xsd' ; const assesment_validator1 = '/domainProfile_4/ims_qtiasiv1p2_localised.xsd'; const discussion_validator1 = '/domainProfile_6/imsdt_v1p0_localised.xsd' ; const weblink_validator1 = '/domainProfile_5/imswl_v1p0_localised.xsd' ; const manifest_validator11 = 'cc11libxml2validator.xsd' ; const blti_validator11 = 'imslticc_v1p0p1.xsd' ; const assesment_validator11 = 'ccv1p1_qtiasiv1p2p1_v1p0.xsd'; const discussion_validator11 = 'ccv1p1_imsdt_v1p1.xsd' ; const weblink_validator11 = 'ccv1p1_imswl_v1p1.xsd' ; /** * @var string */ protected $type = null; /** * @var string */ protected $location = null; public function __construct($type, $location){ $this->type = $type; $this->location = $location; } /** * Validates the item * @param string $element - File path for the xml * @return boolean */ public function validate($element) { $celement = realpath($element); $cvalidator = realpath($this->location.DIRECTORY_SEPARATOR.$this->type); $result = (empty($celement) || empty($cvalidator)); if (!$result) { $xml_error = new libxml_errors_mgr(); $doc = new DOMDocument(); $doc->validateOnParse = false; $result = $doc->load($celement, LIBXML_NONET) && $doc->schemaValidate($cvalidator); } return $result; } } class manifest_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::manifest_validator11, $location); } } class manifest10_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::manifest_validator1, $location); } } class blti_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::blti_validator11, $location); } } class assesment_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::assesment_validator11, $location); } } class discussion_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::discussion_validator11, $location); } } class weblink_validator extends cc_validate_type { public function __construct($location){ parent::__construct(self::weblink_validator11, $location); } } cc/entity11.basiclti.class.php 0000644 00000011560 15215711721 0012222 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2011 Darko Miletic (dmiletic@moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc11_basiclti extends entities11 { public function generate_node() { cc2moodle::log_action('Creating BasicLTI mods'); $response = ''; if (!empty(cc2moodle::$instances['instances'][MOODLE_TYPE_BASICLTI])) { foreach (cc2moodle::$instances['instances'][MOODLE_TYPE_BASICLTI] as $instance) { $response .= $this->create_node_course_modules_mod_basiclti($instance); } } return $response; } private function create_node_course_modules_mod_basiclti($instance) { $sheet_mod_basiclti = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_BASICLTI); $topic_data = $this->get_basiclti_data($instance); $result = ''; if (!empty($topic_data)) { $find_tags = array('[#mod_instance#]' , '[#mod_basiclti_name#]' , '[#mod_basiclti_intro#]' , '[#mod_basiclti_timec#]' , '[#mod_basiclti_timem#]' , '[#mod_basiclti_toolurl#]', '[#mod_basiclti_orgid#]' , '[#mod_basiclti_orgurl#]' , '[#mod_basiclti_orgdesc#]' ); $replace_values = array($instance['instance'], $topic_data['title'], $topic_data['description'], time(),time(), $topic_data['launchurl'], $topic_data['orgid'], $topic_data['orgurl'], $topic_data['orgdesc'] ); $result = str_replace($find_tags, $replace_values, $sheet_mod_basiclti); } return $result; } protected function getValue($node, $default = '') { $result = $default; if (is_object($node) && ($node->length > 0) && !empty($node->item(0)->nodeValue)) { $result = htmlspecialchars(trim($node->item(0)->nodeValue), ENT_COMPAT, 'UTF-8', false); } return $result; } public function get_basiclti_data($instance) { $topic_data = array(); $basiclti_file = $this->get_external_xml($instance['resource_indentifier']); if (!empty($basiclti_file)) { $basiclti_file_path = cc2moodle::$path_to_manifest_folder . DIRECTORY_SEPARATOR . $basiclti_file; $basiclti_file_dir = dirname($basiclti_file_path); $basiclti = $this->load_xml_resource($basiclti_file_path); if (!empty($basiclti)) { $xpath = cc2moodle::newx_path($basiclti, cc112moodle::$basicltins); $topic_title = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:title'),'Untitled'); $blti_description = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:description')); $launch_url = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:launch_url')); $tool_raw = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:code'),null); $tool_url = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:url'),null); $tool_desc = $this->getValue($xpath->query('/xmlns:cartridge_basiclti_link/blti:vendor/lticp:description'),null); $topic_data['title' ] = $topic_title; $topic_data['description'] = $blti_description; $topic_data['launchurl' ] = $launch_url; $topic_data['orgid' ] = $tool_raw; $topic_data['orgurl' ] = $tool_url; $topic_data['orgdesc' ] = $tool_desc; } } return $topic_data; } } cc/includes/constants.php 0000644 00000014213 15215711721 0011467 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2009 Mauro Rondinelli (mauro.rondinelli [AT] uvcms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); // GENERAL PARAMETERS ************************************************************************************************* // define('ROOT_DEEP', 2); // PACKAGES FORMATS *************************************************************************************************** // define('FORMAT_UNKNOWN', 'NA'); define('FORMAT_COMMON_CARTRIDGE', 'CC'); define('FORMAT_BLACK_BOARD', 'BB'); // FORMATS NAMESPACES ************************************************************************************************* // define('NS_COMMON_CARTRIDGE', 'http://www.imsglobal.org/xsd/imscc/imscp_v1p1'); define('NS_BLACK_BOARD', 'http://www.blackboard.com/content-packaging'); // SHEET FILES ******************************************************************************************************** // define('SHEET_BASE', 'cc/sheets/base.xml'); define('SHEET_INFO_DETAILS_MOD', 'cc/sheets/info_details_mod.xml'); define('SHEET_INFO_DETAILS_MOD_INSTANCE', 'cc/sheets/info_details_mod_instance.xml'); define('SHEET_COURSE_BLOCKS_BLOCK', 'cc/sheets/course_blocks_block.xml'); define('SHEET_COURSE_HEADER', 'cc/sheets/course_header.xml'); define('SHEET_COURSE_SECTIONS_SECTION', 'cc/sheets/course_sections_section.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD', 'cc/sheets/course_sections_section_mods_mod.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_FORUM', 'cc/sheets/course_modules_mod_forum.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_LABEL', 'cc/sheets/course_modules_mod_label.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_RESOURCE', 'cc/sheets/course_modules_mod_resource.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ', 'cc/sheets/course_modules_mod_quiz.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_QUESTION_INSTANCE', 'cc/sheets/course_modules_mod_quiz_question_instance.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_FEEDBACK', 'cc/sheets/course_modules_mod_quiz_feedback.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES', 'cc/sheets/course_question_categories.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY', 'cc/sheets/course_question_categories_question_category.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION', 'cc/sheets/course_question_categories_question_category_question.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_MULTIPLE_CHOICE', 'cc/sheets/course_question_categories_question_category_question_multiple_choice.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE', 'cc/sheets/course_question_categories_question_category_question_true_false.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_EESAY', 'cc/sheets/course_question_categories_question_category_question_eesay.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_SHORTANSWER', 'cc/sheets/course_question_categories_question_category_question_shortanswer.xml'); define('SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_ANSWER', 'cc/sheets/course_question_categories_question_category_question_answer.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_BASICLTI', 'cc/sheets/course_modules_mod_basiclti.xml'); define('SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_LTI', 'cc/sheets/course_modules_mod_lti.xml'); // CC RESOURCES TYPE ************************************************************************************************** // define('CC_TYPE_FORUM', 'imsdt_xmlv1p0'); define('CC_TYPE_QUIZ', 'imsqti_xmlv1p2/imscc_xmlv1p0/assessment'); define('CC_TYPE_QUESTION_BANK', 'imsqti_xmlv1p2/imscc_xmlv1p0/question-bank'); define('CC_TYPE_WEBLINK', 'imswl_xmlv1p0'); define('CC_TYPE_WEBCONTENT', 'webcontent'); define('CC_TYPE_ASSOCIATED_CONTENT', 'associatedcontent/imscc_xmlv1p0/learning-application-resource'); define('CC_TYPE_EMPTY', ''); // MOODLE RESOURCES TYPE ********************************************************************************************** // define('MOODLE_TYPE_FORUM', 'forum'); define('MOODLE_TYPE_QUIZ', 'quiz'); define('MOODLE_TYPE_QUESTION_BANK', 'question_bank'); define('MOODLE_TYPE_RESOURCE', 'resource'); define('MOODLE_TYPE_LABEL', 'label'); define('MOODLE_TYPE_BASICLTI', 'basiclti'); define('MOODLE_TYPE_LTI', 'lti'); // UNKNOWN TYPE ******************************************************************************************************* // define('TYPE_UNKNOWN', '[UNKNOWN]'); // CC QUESTIONS TYPES ************************************************************************************************* // define('CC_QUIZ_MULTIPLE_CHOICE', 'cc.multiple_choice.v0p1'); define('CC_QUIZ_TRUE_FALSE', 'cc.true_false.v0p1'); define('CC_QUIZ_FIB', 'cc.fib.v0p1'); define('CC_QUIZ_MULTIPLE_RESPONSE', 'cc.multiple_response.v0p1'); define('CC_QUIZ_PATTERN_MACHT', 'cc.pattern_match.v0p1'); define('CC_QUIZ_ESSAY', 'cc.essay.v0p1'); //MOODLE QUESTIONS TYPES ********************************************************************************************** // define('MOODLE_QUIZ_MULTIPLE_CHOICE', 'multichoice'); define('MOODLE_QUIZ_TRUE_FALSE', 'truefalse'); define('MOODLE_QUIZ_MULTIANSWER', 'multianswer'); define('MOODLE_QUIZ_MULTIPLE_RESPONSE', 'multichoice'); define('MOODLE_QUIZ_MACHT', 'match'); define('MOODLE_QUIZ_ESSAY', 'essay'); define('MOODLE_QUIZ_SHORTANSWER', 'shortanswer'); cc/cc112moodle.php 0000644 00000022404 15215711721 0007657 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2011 Darko Miletic (dmiletic@moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once($CFG->dirroot . '/backup/cc/cc2moodle.php'); require_once($CFG->dirroot . '/backup/cc/entities11.class.php'); require_once($CFG->dirroot . '/backup/cc/entity11.resource.class.php'); require_once($CFG->dirroot . '/backup/cc/entity11.forum.class.php'); require_once($CFG->dirroot . '/backup/cc/entity11.quiz.class.php'); require_once($CFG->dirroot . '/backup/cc/entity11.lti.class.php'); class cc112moodle extends cc2moodle { const CC_TYPE_FORUM = 'imsdt_xmlv1p1'; const CC_TYPE_QUIZ = 'imsqti_xmlv1p2/imscc_xmlv1p1/assessment'; const CC_TYPE_QUESTION_BANK = 'imsqti_xmlv1p2/imscc_xmlv1p1/question-bank'; const CC_TYPE_WEBLINK = 'imswl_xmlv1p1'; const CC_TYPE_ASSOCIATED_CONTENT = 'associatedcontent/imscc_xmlv1p1/learning-application-resource'; const CC_TYPE_BASICLTI = 'imsbasiclti_xmlv1p0'; public static $namespaces = array('imscc' => 'http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', 'lomimscc' => 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/manifest', 'lom' => 'http://ltsc.ieee.org/xsd/imsccv1p1/LOM/resource', 'xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'cc' => 'http://www.imsglobal.org/xsd/imsccv1p1/imsccauth_v1p1'); public static $restypes = array('associatedcontent/imscc_xmlv1p1/learning-application-resource', 'webcontent'); public static $forumns = array('dt' => 'http://www.imsglobal.org/xsd/imsccv1p1/imsdt_v1p1'); public static $quizns = array('xmlns' => 'http://www.imsglobal.org/xsd/ims_qtiasiv1p2'); public static $resourcens = array('wl' => 'http://www.imsglobal.org/xsd/imsccv1p1/imswl_v1p1'); public static $basicltins = array( 'xmlns' => 'http://www.imsglobal.org/xsd/imslticc_v1p0', 'blti' => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0', 'lticm' => 'http://www.imsglobal.org/xsd/imslticm_v1p0', 'lticp' => 'http://www.imsglobal.org/xsd/imslticp_v1p0' ); public function __construct($path_to_manifest) { parent::__construct($path_to_manifest); } public function generate_moodle_xml() { global $CFG; $cdir = static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'course_files'; if (!file_exists($cdir)) { mkdir($cdir, $CFG->directorypermissions, true); } $sheet_base = static::loadsheet(SHEET_BASE); // MOODLE_BACKUP / INFO / DETAILS / MOD $node_info_details_mod = $this->create_code_info_details_mod(); // MOODLE_BACKUP / BLOCKS / BLOCK $node_course_blocks_block = $this->create_node_course_blocks_block(); // MOODLE_BACKUP / COURSES / SECTIONS / SECTION $node_course_sections_section = $this->create_node_course_sections_section(); // MOODLE_BACKUP / COURSES / QUESTION_CATEGORIES $node_course_question_categories = $this->create_node_question_categories(); // MOODLE_BACKUP / COURSES / MODULES / MOD $node_course_modules_mod = $this->create_node_course_modules_mod(); // MOODLE_BACKUP / COURSE / HEADER $node_course_header = $this->create_node_course_header(); // GENERAL INFO $filename = optional_param('file', 'not_available.zip', PARAM_RAW); $filename = basename($filename); $www_root = $CFG->wwwroot; $find_tags = array('[#zip_filename#]', '[#www_root#]', '[#node_course_header#]', '[#node_info_details_mod#]', '[#node_course_blocks_block#]', '[#node_course_sections_section#]', '[#node_course_question_categories#]', '[#node_course_modules#]'); $replace_values = array($filename, $www_root, $node_course_header, $node_info_details_mod, $node_course_blocks_block, $node_course_sections_section, $node_course_question_categories, $node_course_modules_mod); $result_xml = str_replace($find_tags, $replace_values, $sheet_base); // COPY RESOURSE FILES $entities = new entities11(); $entities->move_all_files(); if (array_key_exists("index", self::$instances)) { if (!file_put_contents(static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'moodle.xml', $result_xml)) { static::log_action('Cannot save the moodle manifest file: ' . static::$path_to_manifest_folder . DIRECTORY_SEPARATOR . 'moodle.xml', true); } else { $status = true; } } else { $status = false; static::log_action('The course is empty', false); } return $status; } public function convert_to_moodle_type($cc_type) { $type = parent::convert_to_moodle_type($cc_type); if ($type == TYPE_UNKNOWN) { if ($cc_type == static::CC_TYPE_BASICLTI) { $type = MOODLE_TYPE_LTI; } } return $type; } protected function create_node_question_categories() { $quiz = new cc11_quiz(); static::log_action('Creating node: QUESTION_CATEGORIES'); $node_course_question_categories = $quiz->generate_node_question_categories(); return $node_course_question_categories; } protected function create_code_info_details_mod() { $result = parent::create_code_info_details_mod(); $count_blti = $this->count_instances(MOODLE_TYPE_LTI); $sheet_info_details_mod_instances_instance = static::loadsheet(SHEET_INFO_DETAILS_MOD_INSTANCE); $blti_mod = ''; if ($count_blti > 0) { $blti_instance = $this->create_mod_info_details_mod_instances_instance($sheet_info_details_mod_instances_instance, $count_blti, static::$instances['instances'][MOODLE_TYPE_LTI]); $blti_mod = $blti_instance ? $this->create_mod_info_details_mod(MOODLE_TYPE_LTI, $blti_instance) : ''; } return $result . $blti_mod; } /** * (non-PHPdoc) * @see cc2moodle::get_module_visible() */ protected function get_module_visible($identifier) { //Should item be hidden or not $mod_visible = 1; if (!empty($identifier)) { $xpath = static::newx_path(static::$manifest, static::$namespaces); $query = '/imscc:manifest/imscc:resources/imscc:resource[@identifier="' . $identifier . '"]'; $query .= '//lom:intendedEndUserRole/lom:value'; $intendeduserrole = $xpath->query($query); if (!empty($intendeduserrole) && ($intendeduserrole->length > 0)) { $role = trim($intendeduserrole->item(0)->nodeValue); if ((strcasecmp('Instructor', $role) === 0) || (strcasecmp('Mentor', $role) === 0)) { $mod_visible = 0; } } } return $mod_visible; } protected function create_node_course_modules_mod() { $labels = new cc_label(); $resources = new cc11_resource(); $forums = new cc11_forum(); $quiz = new cc11_quiz(); $basiclti = new cc11_lti(); static::log_action('Creating node: COURSE/MODULES/MOD'); // LABELS $node_course_modules_mod_label = $labels->generate_node(); // RESOURCES (WEB CONTENT AND WEB LINK) $node_course_modules_mod_resource = $resources->generate_node(); // FORUMS $node_course_modules_mod_forum = $forums->generate_node(); // QUIZ $node_course_modules_mod_quiz = $quiz->generate_node_course_modules_mod(); //BasicLTI $node_course_modules_mod_basiclti = $basiclti->generate_node(); $node_course_modules = $node_course_modules_mod_label. $node_course_modules_mod_resource . $node_course_modules_mod_forum . $node_course_modules_mod_quiz . $node_course_modules_mod_basiclti; return $node_course_modules; } } cc/entity11.forum.class.php 0000644 00000014477 15215711721 0011572 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2011 Darko Miletic (dmiletic@moodlerooms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc11_forum extends entities11 { public function full_path($path, $dir_sep = DIRECTORY_SEPARATOR) { $token = '$IMS-CC-FILEBASE$'; $path = str_replace($token, '', $path); if (is_string($path) && ($path != '')) { $dir_sep; $dot_dir = '.'; $up_dir = '..'; $length = strlen($path); $rtemp = trim($path); $start = strrpos($path, $dir_sep); $can_continue = ($start !== false); $result = $can_continue ? '' : $path; $rcount = 0; while ($can_continue) { $dir_part = ($start !== false) ? substr($rtemp, $start + 1, $length - $start) : $rtemp; $can_continue = ($dir_part !== false); if ($can_continue) { if ($dir_part != $dot_dir) { if ($dir_part == $up_dir) { $rcount++; } else { if ($rcount > 0) { $rcount --; } else { $result = ($result == '') ? $dir_part : $dir_part . $dir_sep . $result; } } } $rtemp = substr($path, 0, $start); $start = strrpos($rtemp, $dir_sep); $can_continue = (($start !== false) || (strlen($rtemp) > 0)); } } } return $result; } public function generate_node() { cc2moodle::log_action('Creating Forum mods'); $response = ''; if (!empty(cc2moodle::$instances['instances'][MOODLE_TYPE_FORUM])) { foreach (cc2moodle::$instances['instances'][MOODLE_TYPE_FORUM] as $instance) { $response .= $this->create_node_course_modules_mod_forum($instance); } } return $response; } private function create_node_course_modules_mod_forum($instance) { $sheet_mod_forum = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_FORUM); $topic_data = $this->get_topic_data($instance); $result = ''; if (!empty($topic_data)) { $find_tags = array('[#mod_instance#]', '[#mod_forum_title#]', '[#mod_forum_intro#]', '[#date_now#]'); $replace_values = array($instance['instance'], //To be more true to the actual forum name we use only forum title self::safexml($topic_data['title']), self::safexml($topic_data['description']), time()); $result = str_replace($find_tags, $replace_values, $sheet_mod_forum); } return $result; } public function get_topic_data($instance) { $topic_data = array(); $topic_file = $this->get_external_xml($instance['resource_indentifier']); if (!empty($topic_file)) { $topic_file_path = cc2moodle::$path_to_manifest_folder . DIRECTORY_SEPARATOR . $topic_file; $topic_file_dir = dirname($topic_file_path); $topic = $this->load_xml_resource($topic_file_path); if (!empty($topic)) { $xpath = cc2moodle::newx_path($topic, cc112moodle::$forumns); $topic_title = $xpath->query('/dt:topic/dt:title'); if ($topic_title->length > 0 && !empty($topic_title->item(0)->nodeValue)) { $topic_title = $topic_title->item(0)->nodeValue; } else { $topic_title = 'Untitled Topic'; } $topic_text = $xpath->query('/dt:topic/dt:text'); $topic_text = !empty($topic_text->item(0)->nodeValue) ? $this->update_sources($topic_text->item(0)->nodeValue, dirname($topic_file)) : ''; $topic_text = !empty($topic_text) ? str_replace("%24", "\$", $this->include_titles($topic_text)) : ''; if (!empty($topic_title)) { $topic_data['title'] = $topic_title; $topic_data['description'] = $topic_text; } } $topic_attachments = $xpath->query('/dt:topic/dt:attachments/dt:attachment/@href'); if ($topic_attachments->length > 0) { $attachment_html = ''; foreach ($topic_attachments as $file) { $attachment_html .= $this->generate_attachment_html($this->full_path($file->nodeValue,'/')); } $topic_data['description'] = !empty($attachment_html) ? $topic_text . '<p>Attachments:</p>' . $attachment_html : $topic_text; } } return $topic_data; } private function generate_attachment_html($filename) { $images_extensions = array('gif' , 'jpeg' , 'jpg' , 'jif' , 'jfif' , 'png' , 'bmp'); $fileinfo = pathinfo($filename); if (in_array($fileinfo['extension'], $images_extensions)) { return '<img src="$@FILEPHP@$/' . $filename . '" title="' . $fileinfo['basename'] . '" alt="' . $fileinfo['basename'] . '" /><br />'; } else { return '<a href="$@FILEPHP@$/' . $filename . '" title="' . $fileinfo['basename'] . '" alt="' . $fileinfo['basename'] . '">' . $fileinfo['basename'] . '</a><br />'; } return ''; } } cc/entity11.quiz.class.php 0000644 00000130125 15215711721 0011417 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2009 Mauro Rondinelli (mauro.rondinelli [AT] uvcms.com) * @copyright 2011 Darko Miletic <dmiletic@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); class cc11_quiz extends entities11 { public function generate_node_question_categories() { $instances = $this->generate_instances(); $node_course_question_categories = $this->create_node_course_question_categories($instances); $node_course_question_categories = empty($node_course_question_categories) ? '' : $node_course_question_categories; return $node_course_question_categories; } public function generate_node_course_modules_mod() { cc112moodle::log_action('Creating Quiz mods'); $node_course_modules_mod = ''; $instances = $this->generate_instances(); if (!empty($instances)) { foreach ($instances as $instance) { if ($instance['is_question_bank'] == 0) { $node_course_modules_mod .= $this->create_node_course_modules_mod($instance); } } } return $node_course_modules_mod; } private function create_node_course_modules_mod_quiz_feedback() { $sheet_question_mod_feedback = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_FEEDBACK); return $sheet_question_mod_feedback; } private function generate_instances() { $last_instance_id = 0; $last_question_id = 0; $last_answer_id = 0; $instances = array(); $types = array(MOODLE_TYPE_QUIZ, MOODLE_TYPE_QUESTION_BANK); foreach ($types as $type) { if (!empty(cc112moodle::$instances['instances'][$type])) { foreach (cc112moodle::$instances['instances'][$type] as $instance) { if ($type == MOODLE_TYPE_QUIZ) { $is_question_bank = 0; } else { $is_question_bank = 1; } $assessment_file = $this->get_external_xml($instance['resource_indentifier']); if (!empty($assessment_file)) { $assessment = $this->load_xml_resource(cc112moodle::$path_to_manifest_folder . DIRECTORY_SEPARATOR . $assessment_file); if (!empty($assessment)) { $replace_values = array('unlimited' => 0); $questions = $this->get_questions($assessment, $last_question_id, $last_answer_id, dirname($assessment_file), $is_question_bank); $question_count = count($questions); if (!empty($question_count)) { $last_instance_id++; $instances[$instance['resource_indentifier']]['questions'] = $questions; $instances[$instance['resource_indentifier']]['id'] = $last_instance_id; $instances[$instance['resource_indentifier']]['title'] = $instance['title']; $instances[$instance['resource_indentifier']]['is_question_bank'] = $is_question_bank; $instances[$instance['resource_indentifier']]['options']['timelimit'] = $this->get_global_config($assessment, 'qmd_timelimit', 0); $instances[$instance['resource_indentifier']]['options']['max_attempts'] = $this->get_global_config($assessment, 'cc_maxattempts', 0, $replace_values); } } } } } } return $instances; } private function create_node_course_modules_mod($instance) { $sheet_question_mod = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ); $node_course_modules_quiz_question_instances = $this->create_node_course_modules_mod_quiz_question_instances($instance); $node_course_modules_quiz_feedback = $this->create_node_course_modules_mod_quiz_feedback($instance); $questions_strings = $this->get_questions_string($instance); $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $find_tags = array('[#mod_id#]', '[#mod_name#]', '[#mod_intro#]', '[#mod_stamp#]', '[#question_string#]', '[#date_now#]', '[#mod_max_attempts#]', '[#mod_timelimit#]', '[#node_question_instance#]', '[#node_questions_feedback#]'); $replace_values = array($instance['id'], self::safexml($instance['title']), self::safexml($instance['title']), self::safexml($quiz_stamp), self::safexml($questions_strings), time(), $instance['options']['max_attempts'], $instance['options']['timelimit'], $node_course_modules_quiz_question_instances, $node_course_modules_quiz_feedback); //this one has tags $node_question_mod = str_replace($find_tags, $replace_values, $sheet_question_mod); return $node_question_mod; } private function get_global_config($assessment, $option, $default_value, $replace_values = '') { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $metadata = $xpath->query('/xmlns:questestinterop/xmlns:assessment/xmlns:qtimetadata/xmlns:qtimetadatafield'); foreach ($metadata as $field) { $field_label = $xpath->query('xmlns:fieldlabel', $field); $field_label = !empty($field_label->item(0)->nodeValue) ? $field_label->item(0)->nodeValue : ''; if (strtolower($field_label) == strtolower($option)) { $field_entry = $xpath->query('xmlns:fieldentry', $field); $response = !empty($field_entry->item(0)->nodeValue) ? $field_entry->item(0)->nodeValue : ''; } } $response = !empty($response) ? trim($response) : ''; if (!empty($replace_values)) { foreach ($replace_values as $key => $value) { $response = ($key == $response) ? $value : $response; } } $response = empty($response) ? $default_value : $response; return $response; } private function create_node_course_modules_mod_quiz_question_instances($instance) { $node_course_module_mod_quiz_questions_instances = ''; $sheet_question_mod_instance = cc112moodle::loadsheet(SHEET_COURSE_SECTIONS_SECTION_MODS_MOD_QUIZ_QUESTION_INSTANCE); $find_tags = array('[#question_id#]' , '[#instance_id#]'); if (!empty($instance['questions'])) { foreach ($instance['questions'] as $question) { $replace_values = array($question['id'] , $question['id']); $node_course_module_mod_quiz_questions_instances .= str_replace($find_tags, $replace_values, $sheet_question_mod_instance); } $node_course_module_mod_quiz_questions_instances = str_replace($find_tags, $replace_values, $node_course_module_mod_quiz_questions_instances); } return $node_course_module_mod_quiz_questions_instances; } private function get_questions_string($instance) { $questions_string = ''; if (!empty($instance['questions'])) { foreach ($instance['questions'] as $question) { $questions_string .= $question['id'] . ','; } } $questions_string = !empty($questions_string) ? substr($questions_string, 0, strlen($questions_string) - 1) : ''; return $questions_string; } private function create_node_course_question_categories($instances) { $sheet_question_categories = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES); if (!empty($instances)) { $node_course_question_categories_question_category = ''; foreach ($instances as $instance) { $node_course_question_categories_question_category .= $this->create_node_course_question_categories_question_category($instance); } $find_tags = array('[#node_course_question_categories_question_category#]'); $replace_values = array($node_course_question_categories_question_category); $node_course_question_categories = str_replace($find_tags, $replace_values, $sheet_question_categories); } $node_course_question_categories = empty($node_course_question_categories) ? '' : $node_course_question_categories; return $node_course_question_categories; } private function create_node_course_question_categories_question_category($instance) { $sheet_question_categories_quetion_category = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY); $find_tags = array('[#quiz_id#]', '[#quiz_name#]', '[#quiz_stamp#]', '[#node_course_question_categories_question_category_questions#]'); $node_course_question_categories_questions = $this->create_node_course_question_categories_question_category_question($instance); $node_course_question_categories_questions = empty($node_course_question_categories_questions) ? '' : $node_course_question_categories_questions; $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $replace_values = array($instance['id'], self::safexml($instance['title']), $quiz_stamp, $node_course_question_categories_questions); $node_question_categories = str_replace($find_tags, $replace_values, $sheet_question_categories_quetion_category); return $node_question_categories; } private function create_node_course_question_categories_question_category_question($instance) { global $USER; $node_course_question_categories_question = ''; $find_tags = array('[#question_id#]', '[#question_title#]', '[#question_text#]', '[#question_type#]', '[#question_general_feedback#]', '[#question_defaultgrade#]', '[#date_now#]', '[#question_type_nodes#]', '[#question_stamp#]', '[#question_version#]', '[#logged_user#]'); $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION); $questions = $instance['questions']; if (!empty($questions)) { foreach ($questions as $question) { $quiz_stamp = 'localhost+' . time() . '+' . $this->generate_random_string(6); $quiz_version = 'localhost+' . time() . '+' . $this->generate_random_string(6); $question_moodle_type = $question['moodle_type']; $question_cc_type = $question['cc_type']; $question_type_node = ''; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_MULTIPLE_CHOICE) ? $this->create_node_course_question_categories_question_category_question_multiple_choice($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_TRUE_FALSE) ? $this->create_node_course_question_categories_question_category_question_true_false($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_ESSAY) ? $this->create_node_course_question_categories_question_category_question_eesay($question) : $question_type_node; $question_type_node = ($question_moodle_type == MOODLE_QUIZ_SHORTANSWER) ? $this->create_node_course_question_categories_question_category_question_shortanswer($question) : $question_type_node; $questionname = !empty($question['name']) ? self::safexml($question['name']) : self::safexml($this->truncate_text($question['title'], 255, true)); $replace_values = array($question['id'], $questionname, self::safexml($question['title']), $question_moodle_type, self::safexml($question['feedback']), $question['defaultgrade'], time(), $question_type_node, $quiz_stamp, $quiz_version, $USER->id); $node_course_question_categories_question .= str_replace($find_tags, $replace_values, $sheet_question_categories_question); } } $node_course_question_categories_question = empty($node_course_question_categories_question) ? '' : $node_course_question_categories_question; return $node_course_question_categories_question; } private function get_questions($assessment, &$last_question_id, &$last_answer_id, $root_path, $is_question_bank) { $questions = array(); $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); if (!$is_question_bank) { $questions_items = $xpath->query('/xmlns:questestinterop/xmlns:assessment/xmlns:section/xmlns:item'); } else { $questions_items = $xpath->query('/xmlns:questestinterop/xmlns:objectbank/xmlns:item'); } foreach ($questions_items as $question_item) { $count_questions = $xpath->evaluate('count(xmlns:presentation/xmlns:flow/xmlns:material/xmlns:mattext)', $question_item); if ($count_questions == 0) { $question_title = $xpath->query('xmlns:presentation/xmlns:material/xmlns:mattext', $question_item); } else { $question_title = $xpath->query('xmlns:presentation/xmlns:flow/xmlns:material/xmlns:mattext', $question_item); } $question_title = !empty($question_title->item(0)->nodeValue) ? $question_title->item(0)->nodeValue : ''; $question_identifier = $xpath->query('@ident', $question_item); $question_identifier = !empty($question_identifier->item(0)->nodeValue) ? $question_identifier->item(0)->nodeValue : ''; if (!empty($question_identifier)) { $question_type = $this->get_question_type($question_identifier, $assessment); if (!empty($question_type['moodle'])) { $last_question_id++; $questions[$question_identifier]['id'] = $last_question_id; $question_title = $this->update_sources($question_title, $root_path); $question_title = !empty($question_title) ? str_replace("%24", "\$", $this->include_titles($question_title)) : ''; // This attribute is not IMSCC spec, but it is included in Moodle 2.x export of IMS1.1 $questionname = $xpath->query('@title', $question_item); $questionname = !empty($questionname->item(0)->nodeValue) ? $questionname->item(0)->nodeValue : ''; $questions[$question_identifier]['title'] = $question_title; $questions[$question_identifier]['name'] = $questionname; $questions[$question_identifier]['identifier'] = $question_identifier; $questions[$question_identifier]['moodle_type'] = $question_type['moodle']; $questions[$question_identifier]['cc_type'] = $question_type['cc']; $questions[$question_identifier]['feedback'] = $this->get_general_feedback($assessment, $question_identifier); $questions[$question_identifier]['defaultgrade'] = $this->get_defaultgrade($assessment, $question_identifier); $questions[$question_identifier]['answers'] = $this->get_answers($question_identifier, $assessment, $last_answer_id); } } } $questions = !empty($questions) ? $questions : ''; return $questions; } private function str_replace_once($search, $replace, $subject) { $first_char = strpos($subject, $search); if ($first_char !== false) { $before_str = substr($subject, 0, $first_char); $after_str = substr($subject, $first_char + strlen($search)); return $before_str . $replace . $after_str; } else { return $subject; } } private function get_defaultgrade($assessment, $question_identifier) { $result = 1; $xpath = cc2moodle::newx_path($assessment, cc2moodle::getquizns()); $query = '//xmlns:item[@ident="' . $question_identifier . '"]'; $query .= '//xmlns:qtimetadatafield[xmlns:fieldlabel="cc_weighting"]/xmlns:fieldentry'; $defgrade = $xpath->query($query); if (!empty($defgrade) && ($defgrade->length > 0)) { $resp = (int)$defgrade->item(0)->nodeValue; if ($resp >= 0 && $resp <= 99) { $result = $resp; } } return $result; } private function get_general_feedback($assessment, $question_identifier) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $respconditions = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($respconditions)) { foreach ($respconditions as $respcondition) { $continue = $respcondition->getAttributeNode('continue'); $continue = !empty($continue->nodeValue) ? strtolower($continue->nodeValue) : ''; if ($continue == 'yes') { $display_feedback = $xpath->query('xmlns:displayfeedback', $respcondition); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } return $feedback; } private function get_feedback($assessment, $identifier, $item_identifier, $question_type) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $resource_processing = $xpath->query('//xmlns:item[@ident="' . $item_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($resource_processing)) { foreach ($resource_processing as $response) { $varequal = $xpath->query('xmlns:conditionvar/xmlns:varequal', $response); $varequal = !empty($varequal->item(0)->nodeValue) ? $varequal->item(0)->nodeValue : ''; if (strtolower($varequal) == strtolower($identifier) || ($question_type == CC_QUIZ_ESSAY)) { $display_feedback = $xpath->query('xmlns:displayfeedback', $response); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $item_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } return $feedback; } private function get_answers_fib($question_identifier, $identifier, $assessment, &$last_answer_id) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $correctanswersfib = array(); $incorrectanswersfib = array(); $response_items = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); $correctrespcond = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition/xmlns:setvar[text()="100"]/..'); $correctanswers = $xpath->query('xmlns:conditionvar/xmlns:varequal', $correctrespcond->item(0)); // Correct answers. foreach ($correctanswers as $correctans) { $answertitle = !empty($correctans->nodeValue) ? $correctans->nodeValue : ''; if (empty($answertitle)) { continue; } $last_answer_id++; $correctanswersfib[$answertitle] = array( 'id' => $last_answer_id, 'title' => $answertitle, 'score' => 1, 'feedback' => '', 'case' => 0); } // Handle incorrect answers and feedback for all items. foreach ($response_items as $response_item) { $setvar = $xpath->query('xmlns:setvar', $response_item); if (!empty($setvar->length) && $setvar->item(0)->nodeValue == '100') { // Skip the correct answer responsecondition. continue; } $varequal = $xpath->query('xmlns:conditionvar/xmlns:varequal', $response_item); if (empty($varequal->length)) { // Skip respcondition elements that don't have varequal containing an answer continue; } $answer_title = !empty($varequal->item(0)->nodeValue) ? $varequal->item(0)->nodeValue : ''; $display_feedback = $xpath->query('xmlns:displayfeedback', $response_item); unset($feedbacks_identifiers); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } if (array_key_exists($answer_title, $correctanswersfib)) { // Already a correct answer, just need the feedback for the correct answer. $correctanswerfib[$answer_title]['feedback'] = $feedback; } else { // Need to add an incorrect answer. $last_answer_id++; $incorrectanswersfib[] = array( 'id' => $last_answer_id, 'title' => $answer_title, 'score' => 0, 'feedback' => $feedback, 'case' => 0); } } $answers_fib = array_merge($correctanswersfib, $incorrectanswersfib); $answers_fib = empty($answers_fib) ? '' : $answers_fib; return $answers_fib; } private function get_answers_pattern_match($question_identifier, $identifier, $assessment, &$last_answer_id) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $answers_fib = array(); $response_items = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); foreach ($response_items as $response_item) { $setvar = $xpath->query('xmlns:setvar', $response_item); $setvar = is_object($setvar->item(0)) ? $setvar->item(0)->nodeValue : ''; if ($setvar != '') { $last_answer_id++; $answer_title = $xpath->query('xmlns:conditionvar/xmlns:varequal[@respident="' . $identifier . '"]', $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? $answer_title->item(0)->nodeValue : ''; if (empty($answer_title)) { $answer_title = $xpath->query('xmlns:conditionvar/xmlns:varsubstring[@respident="' . $identifier . '"]', $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? '*' . $answer_title->item(0)->nodeValue . '*' : ''; } if (empty($answer_title)) { $answer_title = '*'; } $case = $xpath->query('xmlns:conditionvar/xmlns:varequal/@case', $response_item); $case = is_object($case->item(0)) ? $case->item(0)->nodeValue : 'no' ; $case = strtolower($case) == 'yes' ? 1 : 0; $display_feedback = $xpath->query('xmlns:displayfeedback', $response_item); unset($feedbacks_identifiers); if (!empty($display_feedback)) { foreach ($display_feedback as $feedback) { $feedback_identifier = $feedback->getAttributeNode('linkrefid'); $feedback_identifier = !empty($feedback_identifier->nodeValue) ? $feedback_identifier->nodeValue : ''; if (!empty($feedback_identifier)) { $feedbacks_identifiers[] = $feedback_identifier; } } } $feedback = ''; $feedbacks_identifiers = empty($feedbacks_identifiers) ? '' : $feedbacks_identifiers; if (!empty($feedbacks_identifiers)) { foreach ($feedbacks_identifiers as $feedback_identifier) { $feedbacks = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:itemfeedback[@ident="' . $feedback_identifier . '"]/xmlns:flow_mat/xmlns:material/xmlns:mattext'); $feedback .= !empty($feedbacks->item(0)->nodeValue) ? $feedbacks->item(0)->nodeValue . ' ' : ''; } } $answers_fib[] = array('id' => $last_answer_id, 'title' => $answer_title, 'score' => $setvar, 'feedback' => $feedback, 'case' => $case); } } $answers_fib = empty($answers_fib) ? '' : $answers_fib; return $answers_fib; } private function get_answers($identifier, $assessment, &$last_answer_id) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $answers = array(); $question_cc_type = $this->get_question_type($identifier, $assessment); $question_cc_type = $question_cc_type['cc']; $is_multiresponse = ($question_cc_type == CC_QUIZ_MULTIPLE_RESPONSE); if ($question_cc_type == CC_QUIZ_MULTIPLE_CHOICE || $is_multiresponse || $question_cc_type == CC_QUIZ_TRUE_FALSE) { $query_answers = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_lid/xmlns:render_choice/xmlns:response_label'; $query_answers_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_lid/xmlns:render_choice/xmlns:response_label'; $query_indentifer = '@ident'; $query_title = 'xmlns:material/xmlns:mattext'; } if ($question_cc_type == CC_QUIZ_ESSAY) { $query_answers = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_str'; $query_answers_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_str'; $query_indentifer = '@ident'; $query_title = 'xmlns:render_fib'; } if ($question_cc_type == CC_QUIZ_FIB || $question_cc_type == CC_QUIZ_PATTERN_MACHT) { $xpath_query = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:response_str/@ident'; $xpath_query_with_flow = '//xmlns:item[@ident="' . $identifier . '"]/xmlns:presentation/xmlns:flow/xmlns:response_str/@ident'; $count_response = $xpath->evaluate('count(' . $xpath_query_with_flow . ')'); if ($count_response == 0) { $answer_identifier = $xpath->query($xpath_query); } else { $answer_identifier = $xpath->query($xpath_query_with_flow); } $answer_identifier = !empty($answer_identifier->item(0)->nodeValue) ? $answer_identifier->item(0)->nodeValue : ''; if ($question_cc_type == CC_QUIZ_FIB) { $answers = $this->get_answers_fib ($identifier, $answer_identifier, $assessment, $last_answer_id); } else { $answers = $this->get_answers_pattern_match ($identifier, $answer_identifier, $assessment, $last_answer_id); } } else { $count_response = $xpath->evaluate('count(' . $query_answers_with_flow . ')'); if ($count_response == 0) { $response_items = $xpath->query($query_answers); } else { $response_items = $xpath->query($query_answers_with_flow); } if (!empty($response_items)) { if ($is_multiresponse) { $correct_answer_score = 0; //get the correct answers count $canswers_query = "//xmlns:item[@ident='{$identifier}']//xmlns:setvar[@varname='SCORE'][.=100]/../xmlns:conditionvar//xmlns:varequal[@case='Yes'][not(parent::xmlns:not)]"; $canswers = $xpath->query($canswers_query); if ($canswers->length > 0) { $correct_answer_score = round(1.0 / (float)$canswers->length, 7); //weird $correct_answers_ident = array(); foreach ($canswers as $cnode) { $correct_answers_ident[$cnode->nodeValue] = true; } } } foreach ($response_items as $response_item) { $last_answer_id++; $answer_identifier = $xpath->query($query_indentifer, $response_item); $answer_identifier = !empty($answer_identifier->item(0)->nodeValue) ? $answer_identifier->item(0)->nodeValue : ''; $answer_title = $xpath->query($query_title, $response_item); $answer_title = !empty($answer_title->item(0)->nodeValue) ? $answer_title->item(0)->nodeValue : ''; $answer_feedback = $this->get_feedback($assessment, $answer_identifier, $identifier, $question_cc_type); $answer_score = $this->get_score($assessment, $answer_identifier, $identifier); if ($is_multiresponse && isset($correct_answers_ident[$answer_identifier])) { $answer_score = $correct_answer_score; } $answers[] = array('id' => $last_answer_id, 'title' => $answer_title, 'score' => $answer_score, 'identifier' => $answer_identifier, 'feedback' => $answer_feedback); } } } $answers = empty($answers) ? '' : $answers; return $answers; } private function get_score($assessment, $identifier, $question_identifier) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $resource_processing = $xpath->query('//xmlns:item[@ident="' . $question_identifier . '"]/xmlns:resprocessing/xmlns:respcondition'); if (!empty($resource_processing)) { foreach ($resource_processing as $response) { $question_cc_type = $this->get_question_type($question_identifier, $assessment); $question_cc_type = $question_cc_type['cc']; $varequal = $xpath->query('xmlns:conditionvar/xmlns:varequal', $response); $varequal = !empty($varequal->item(0)->nodeValue) ? $varequal->item(0)->nodeValue : ''; if (strtolower($varequal) == strtolower($identifier)) { $score = $xpath->query('xmlns:setvar', $response); $score = !empty($score->item(0)->nodeValue) ? $score->item(0)->nodeValue : ''; } } } // This method (get_score) is only used by T/F & M/C questions in CC, therefore it's either 0 or 1 in Moodle. $score = empty($score) ? "0.0000000" : '1.0000000'; return $score; } private function create_node_course_question_categories_question_category_question_multiple_choice($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_MULTIPLE_CHOICE); if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $answer_string = $this->get_answers_string($question['answers']); $is_single = ($question['cc_type'] == CC_QUIZ_MULTIPLE_CHOICE) ? 1 : 0; $find_tags = array('[#node_course_question_categories_question_category_question_answer#]', '[#answer_string#]', '[#is_single#]'); $replace_values = array($node_course_question_categories_question_answer, self::safexml($answer_string), $is_single); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_eesay($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_EESAY); if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $find_tags = array('[#node_course_question_categories_question_category_question_answer#]'); $replace_values = array($node_course_question_categories_question_answer); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_shortanswer($question) { //, &$fib_questions) { $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_SHORTANSWER); $node_course_question_categories_question_answer = ''; if (!empty($question['answers'])) { foreach ($question['answers'] as $answer) { $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } } $answers_string = $this->get_answers_string($question['answers']); $use_case = 0; foreach ($question['answers'] as $answer) { if ($answer['case'] == 1) { $use_case = 1; } } $find_tags = array('[#answers_string#]', '[#use_case#]', '[#node_course_question_categories_question_category_question_answer#]'); $replace_values = array(self::safexml($answers_string), self::safexml($use_case), $node_course_question_categories_question_answer); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function create_node_course_question_categories_question_category_question_true_false($question) { $node_course_question_categories_question_answer = ''; $sheet_question_categories_question = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_TRUE_FALSE); $trueanswer = null; $falseanswer = null; if (!empty($question['answers'])) { // Identify the true and false answers. foreach ($question['answers'] as $answer) { if ($answer['identifier'] == 'true') { $trueanswer = $answer; } else if ($answer['identifier'] == 'false') { $falseanswer = $answer; } else { // Should not happen, but just in case. throw new coding_exception("Unknown answer identifier detected " . "in true/false quiz question with id {$question['id']}."); } $node_course_question_categories_question_answer .= $this->create_node_course_question_categories_question_category_question_answer($answer); } // Make sure the true and false answer was found. if (is_null($trueanswer) || is_null($falseanswer)) { throw new coding_exception("Unable to correctly identify the " . "true and false answers in the question with id {$question['id']}."); } } $find_tags = array('[#node_course_question_categories_question_category_question_answer#]', '[#true_answer_id#]', '[#false_answer_id#]'); $replace_values = array($node_course_question_categories_question_answer, $trueanswer['id'], $falseanswer['id']); $node_question_categories_question = str_replace($find_tags, $replace_values, $sheet_question_categories_question); return $node_question_categories_question; } private function get_answers_string($answers) { $answer_string = ''; if (!empty($answers)) { foreach ($answers as $answer) { $answer_string .= $answer['id'] . ','; } } $answer_string = !empty($answer_string) ? substr($answer_string, 0, strlen($answer_string) - 1) : ''; return $answer_string; } private function create_node_course_question_categories_question_category_question_answer($answer) { $sheet_question_categories_question_answer = cc112moodle::loadsheet(SHEET_COURSE_QUESTION_CATEGORIES_QUESTION_CATEGORY_QUESTION_ANSWER); $find_tags = array('[#answer_id#]', '[#answer_text#]', '[#answer_score#]', '[#answer_feedback#]'); $replace_values = array($answer['id'], self::safexml($answer['title']), $answer['score'], self::safexml($answer['feedback'])); $node_question_categories_question_answer = str_replace($find_tags, $replace_values, $sheet_question_categories_question_answer); return $node_question_categories_question_answer; } private function get_question_type($identifier, $assessment) { $xpath = cc112moodle::newx_path($assessment, cc112moodle::getquizns()); $metadata = $xpath->query('//xmlns:item[@ident="' . $identifier . '"]/xmlns:itemmetadata/xmlns:qtimetadata/xmlns:qtimetadatafield'); foreach ($metadata as $field) { $field_label = $xpath->query('xmlns:fieldlabel', $field); $field_label = !empty($field_label->item(0)->nodeValue) ? $field_label->item(0)->nodeValue : ''; if ($field_label == 'cc_profile') { $field_entry = $xpath->query('xmlns:fieldentry', $field); $type = !empty($field_entry->item(0)->nodeValue) ? $field_entry->item(0)->nodeValue : ''; } } $return_type = array(); $return_type['moodle'] = ''; $return_type['cc'] = $type; if ($type == CC_QUIZ_MULTIPLE_CHOICE) { $return_type['moodle'] = MOODLE_QUIZ_MULTIPLE_CHOICE; } if ($type == CC_QUIZ_MULTIPLE_RESPONSE) { $return_type['moodle'] = MOODLE_QUIZ_MULTIPLE_CHOICE; } if ($type == CC_QUIZ_TRUE_FALSE) { $return_type['moodle'] = MOODLE_QUIZ_TRUE_FALSE; } if ($type == CC_QUIZ_ESSAY) { $return_type['moodle'] = MOODLE_QUIZ_ESSAY; } if ($type == CC_QUIZ_FIB) { $return_type['moodle'] = MOODLE_QUIZ_SHORTANSWER; } if ($type == CC_QUIZ_PATTERN_MACHT) { $return_type['moodle'] = MOODLE_QUIZ_SHORTANSWER; } return $return_type; } } cc/restore_cc.php 0000644 00000011203 15215711721 0007771 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * @package moodlecore * @subpackage backup-imscc * @copyright 2009 Mauro Rondinelli (mauro.rondinelli [AT] uvcms.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') or die('Direct access to this script is forbidden.'); require_once($CFG->dirroot . '/backup/cc/includes/constants.php'); require_once($CFG->dirroot . '/backup/cc/cc2moodle.php'); function cc_convert ($dir) { global $OUTPUT; $manifest_file = $dir . DIRECTORY_SEPARATOR . 'imsmanifest.xml'; $moodle_file = $dir . DIRECTORY_SEPARATOR . 'moodle.xml'; $schema_file = 'cc' . DIRECTORY_SEPARATOR . '' . DIRECTORY_SEPARATOR . 'schemas' . DIRECTORY_SEPARATOR . 'cclibxml2validator.xsd'; if (is_readable($manifest_file) && !is_readable($moodle_file)) { $is_cc = detect_cc_format($manifest_file); if ($is_cc) { $detected_requirements = detect_requirements(); if (!$detected_requirements["php5"]) { echo $OUTPUT->notification(get_string('cc_import_req_php5', 'imscc')); return false; } if (!$detected_requirements["dom"]) { echo $OUTPUT->notification(get_string('cc_import_req_dom', 'imscc')); return false; } if (!$detected_requirements["libxml"]) { echo $OUTPUT->notification(get_string('cc_import_req_libxml', 'imscc')); return false; } if (!$detected_requirements["libxmlminversion"]) { echo $OUTPUT->notification(get_string('cc_import_req_libxmlminversion', 'imscc')); return false; } if (!$detected_requirements["xsl"]) { echo $OUTPUT->notification(get_string('cc_import_req_xsl', 'imscc')); return false; } echo get_string('cc2moodle_checking_schema', 'imscc') . '<br />'; $cc_manifest = new DOMDocument(); if ($cc_manifest->load($manifest_file)) { if ($cc_manifest->schemaValidate($schema_file)) { echo get_string('cc2moodle_valid_schema', 'imscc') . '<br />'; $cc2moodle = new cc2moodle($manifest_file); if (!$cc2moodle->is_auth()) { return $cc2moodle->generate_moodle_xml(); } else { echo $OUTPUT->notification(get_string('cc2moodle_req_auth', 'imscc')); return false; } } else { echo $OUTPUT->notification(get_string('cc2moodle_invalid_schema', 'imscc')); return false; } } else { echo $OUTPUT->notification(get_string('cc2moodle_manifest_dont_load', 'imscc')); return false; } } } return true; } function detect_requirements () { if (floor(phpversion()) >= 5) { $detected["php5"] = true; } else { $detected["php5"] = false; } $detected["xsl"] = extension_loaded('xsl'); $detected['dom'] = extension_loaded('dom'); $detected['libxml'] = extension_loaded('libxml'); $detected['libxmlminversion'] = extension_loaded('libxml') && version_compare(LIBXML_DOTTED_VERSION, '2.6.30', '>='); return $detected; } function detect_cc_format ($xml_file) { $inpos = 0; $xml_snippet = file_get_contents($xml_file, 0, NULL, 0, 500); if (!empty($xml_snippet)) { $xml_snippet = strtolower($xml_snippet); $xml_snippet = preg_replace('/\s*/m', '', $xml_snippet); $xml_snippet = str_replace("'", '', $xml_snippet); $xml_snippet = str_replace('"', '', $xml_snippet); $search_string = "xmlns=" . NS_COMMON_CARTRIDGE; $inpos = strpos($xml_snippet, $search_string); if ($inpos) { return true; } else { return false; } } else { return false; } } cc/sheets/course_question_categories_question_category_question.xml 0000644 00000001635 15215711721 0022364 0 ustar 00 <QUESTION> <ID>[#question_id#]</ID> <PARENT>0</PARENT> <NAME>[#question_title#]</NAME> <QUESTIONTEXT>[#question_text#]</QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK>[#question_general_feedback#]</GENERALFEEDBACK> <DEFAULTGRADE>[#question_defaultgrade#]</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>[#question_type#]</QTYPE> <LENGTH>1</LENGTH> <STAMP>[#question_stamp#]</STAMP> <VERSION>[#question_version#]</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>[#date_now#]</TIMECREATED> <TIMEMODIFIED>[#date_now#]</TIMEMODIFIED> <CREATEDBY>[#logged_user#]</CREATEDBY> <MODIFIEDBY>[#logged_user#]</MODIFIEDBY> [#question_type_nodes#] </QUESTION> cc/sheets/info_details_mod.xml 0000644 00000000340 15215711721 0012444 0 ustar 00 <MOD> <NAME>[#mod_type#]</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> <INSTANCES> [#node_info_details_mod_instances_instance#] </INSTANCES> </MOD> cc/sheets/course_question_categories_question_category_question_eesay.xml 0000644 00000000201 15215711721 0023536 0 ustar 00 <ANSWERS> [#node_course_question_categories_question_category_question_answer#] </ANSWERS> cc/sheets/course_question_categories_question_category_question_answer.xml 0000644 00000000400 15215711721 0023730 0 ustar 00 <ANSWER> <ID>[#answer_id#]</ID> <ANSWER_TEXT>[#answer_text#]</ANSWER_TEXT> <FRACTION>[#answer_score#]</FRACTION> <FEEDBACK>[#answer_feedback#]</FEEDBACK> </ANSWER> cc/sheets/course_sections_section.xml 0000644 00000000414 15215711721 0014102 0 ustar 00 <SECTION> <ID>[#section_id#]</ID> <NUMBER>[#section_number#]</NUMBER> <SUMMARY>[#section_summary#]</SUMMARY> <VISIBLE>1</VISIBLE> <MODS> [#node_course_sections_section_mods_mod#] </MODS> </SECTION> cc/sheets/course_modules_mod_label.xml 0000644 00000000333 15215711721 0014175 0 ustar 00 <MOD> <ID>[#mod_instance#]</ID> <MODTYPE>label</MODTYPE> <NAME>[#mod_name#]</NAME> <CONTENT>[#mod_content#]</CONTENT> <TIMEMODIFIED>[#date_now#]</TIMEMODIFIED> </MOD> cc/sheets/course_header.xml 0000644 00000002752 15215711721 0011766 0 ustar 00 <ID>1</ID> <CATEGORY> <ID>1</ID> <NAME>Category 1</NAME> </CATEGORY> <PASSWORD></PASSWORD> <FULLNAME>[#course_name#]</FULLNAME> <SHORTNAME>[#course_short_name#]</SHORTNAME> <IDNUMBER></IDNUMBER> <SUMMARY>[#course_description#]</SUMMARY> <FORMAT>topics</FORMAT> <SHOWGRADES>1</SHOWGRADES> <NEWSITEMS>5</NEWSITEMS> <TEACHER>Teacher</TEACHER> <TEACHERS>Teachers</TEACHERS> <STUDENT>Student</STUDENT> <STUDENTS>Students</STUDENTS> <GUEST>0</GUEST> <STARTDATE>[#date_now#]</STARTDATE> <NUMSECTIONS>[#section_count#]</NUMSECTIONS> <MAXBYTES>268435456</MAXBYTES> <SHOWREPORTS>0</SHOWREPORTS> <GROUPMODE>0</GROUPMODE> <GROUPMODEFORCE>0</GROUPMODEFORCE> <DEFAULTGROUPINGID>0</DEFAULTGROUPINGID> <LANG></LANG> <THEME></THEME> <COST>0</COST> <CURRENCY>USD</CURRENCY> <MARKER>0</MARKER> <VISIBLE>1</VISIBLE> <HIDDENSECTIONS>0</HIDDENSECTIONS> <TIMECREATED>[#date_now#]</TIMECREATED> <TIMEMODIFIED>[#date_now#]</TIMEMODIFIED> <METACOURSE>0</METACOURSE> <EXPIRENOTIFY>0</EXPIRENOTIFY> <NOTIFYSTUDENTS>0</NOTIFYSTUDENTS> <EXPIRYTHRESHOLD>864000</EXPIRYTHRESHOLD> <ENROLLABLE>1</ENROLLABLE> <ENROLSTARTDATE>0</ENROLSTARTDATE> <ENROLENDDATE>0</ENROLENDDATE> <ENROLPERIOD>0</ENROLPERIOD> <ROLES_OVERRIDES></ROLES_OVERRIDES> <ROLES_ASSIGNMENTS></ROLES_ASSIGNMENTS> cc/sheets/course_blocks_block.xml 0000644 00000000634 15215711721 0013162 0 ustar 00 <BLOCK> <ID>[#block_id#]</ID> <NAME>[#block_name#]</NAME> <PAGEID>2</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>[#block_position#]</POSITION> <WEIGHT>[#block_weight#]</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES></ROLES_OVERRIDES> <ROLES_ASSIGNMENTS></ROLES_ASSIGNMENTS> </BLOCK> cc/sheets/course_modules_mod_quiz.xml 0000644 00000002617 15215711721 0014115 0 ustar 00 <MOD> <ID>[#mod_id#]</ID> <MODTYPE>quiz</MODTYPE> <NAME>[#mod_name#]</NAME> <INTRO>[#mod_intro#]</INTRO> <TIMEOPEN>0</TIMEOPEN> <TIMECLOSE>0</TIMECLOSE> <OPTIONFLAGS>1</OPTIONFLAGS> <PENALTYSCHEME>0</PENALTYSCHEME> <ATTEMPTS_NUMBER>[#mod_max_attempts#]</ATTEMPTS_NUMBER> <ATTEMPTONLAST>0</ATTEMPTONLAST> <GRADEMETHOD>1</GRADEMETHOD> <DECIMALPOINTS>0</DECIMALPOINTS> <REVIEW>0</REVIEW> <QUESTIONSPERPAGE>30</QUESTIONSPERPAGE> <SHUFFLEQUESTIONS>0</SHUFFLEQUESTIONS> <SHUFFLEANSWERS>0</SHUFFLEANSWERS> <QUESTIONS>[#question_string#]</QUESTIONS> <SUMGRADES>4</SUMGRADES> <GRADE>10</GRADE> <TIMECREATED>[#date_now#]</TIMECREATED> <TIMEMODIFIED>[#date_now#]</TIMEMODIFIED> <TIMELIMIT>[#mod_timelimit#]</TIMELIMIT> <PASSWORD></PASSWORD> <SUBNET></SUBNET> <POPUP>0</POPUP> <DELAY1>0</DELAY1> <DELAY2>0</DELAY2> <QUESTION_INSTANCES> [#node_question_instance#] </QUESTION_INSTANCES> <FEEDBACKS> [#node_questions_feedback#] </FEEDBACKS> </MOD> cc/sheets/course_question_categories_question_category_question_true_false.xml 0000644 00000000451 15215711721 0024570 0 ustar 00 <TRUEFALSE> <TRUEANSWER>[#true_answer_id#]</TRUEANSWER> <FALSEANSWER>[#false_answer_id#]</FALSEANSWER> </TRUEFALSE> <ANSWERS> [#node_course_question_categories_question_category_question_answer#] </ANSWERS> cc/sheets/info_details_mod_instance.xml 0000644 00000000307 15215711721 0014333 0 ustar 00 <INSTANCE> <ID>[#mod_instance_id#]</ID> <NAME>[#mod_name#]</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> </INSTANCE> cc/sheets/course_modules_mod_lti.xml 0000644 00000001564 15215711721 0013715 0 ustar 00 <MOD> <ID>[#mod_instance#]</ID> <MODTYPE>lti</MODTYPE> <NAME>[#mod_basiclti_name#]</NAME> <INTRO>[#mod_basiclti_intro#]</INTRO> <INTROFORMAT>1</INTROFORMAT> <TIMECREATED>[#mod_basiclti_timec#]</TIMECREATED> <TIMEMODIFIED>[#mod_basiclti_timem#]</TIMEMODIFIED> <TYPEID>1</TYPEID> <TOOLURL>[#mod_basiclti_toolurl#]</TOOLURL> <INSTRUCTORCHOICESENDEMAILADDR>1</INSTRUCTORCHOICESENDEMAILADDR> <DEBUGLAUNCH>0</DEBUGLAUNCH> <INSTRUCTORCHOICEACCEPTGRADES>1</INSTRUCTORCHOICEACCEPTGRADES> <INSTRUCTORCHOICEALLOWROSTER>1</INSTRUCTORCHOICEALLOWROSTER> <INSTRUCTORCHOICEALLOWSETTING>$@NULL@$</INSTRUCTORCHOICEALLOWSETTING> <GRADE>100.00000</GRADE> <INSTRUCTORCUSTOMPARAMETERS>0</INSTRUCTORCUSTOMPARAMETERS> <ICON>[#mod_basiclti_icon#]</ICON> </MOD> cc/sheets/course_modules_mod_quiz_feedback.xml 0000644 00000000322 15215711721 0015710 0 ustar 00 <FEEDBACK> <ID>1</ID> <QUIZID>1</QUIZID> <FEEDBACKTEXT></FEEDBACKTEXT> <MINGRADE>0</MINGRADE> <MAXGRADE>11</MAXGRADE> </FEEDBACK> cc/sheets/course_modules_mod_basiclti.xml 0000644 00000002716 15215711721 0014717 0 ustar 00 <MOD> <ID>[#mod_instance#]</ID> <MODTYPE>basiclti</MODTYPE> <NAME>[#mod_basiclti_name#]</NAME> <INTRO>[#mod_basiclti_intro#]</INTRO> <INTROFORMAT>1</INTROFORMAT> <TIMECREATED>[#mod_basiclti_timec#]</TIMECREATED> <TIMEMODIFIED>[#mod_basiclti_timem#]</TIMEMODIFIED> <TYPEID>0</TYPEID> <TOOLURL>[#mod_basiclti_toolurl#]</TOOLURL> <PREFERHEIGHT>0</PREFERHEIGHT> <INSTRUCTORCHOICESENDNAME>0</INSTRUCTORCHOICESENDNAME> <INSTRUCTORCHOICESENDEMAILADDR>0</INSTRUCTORCHOICESENDEMAILADDR> <INSTRUCTORCHOICEALLOWROSTER>0</INSTRUCTORCHOICEALLOWROSTER> <INSTRUCTORCHOICEALLOWSETTING>0</INSTRUCTORCHOICEALLOWSETTING> <SETTING>$@NULL@$</SETTING> <INSTRUCTORCUSTOMPARAMETERS>0</INSTRUCTORCUSTOMPARAMETERS> <INSTRUCTORCHOICEACCEPTGRADES>0</INSTRUCTORCHOICEACCEPTGRADES> <GRADE>100</GRADE> <PLACEMENTSECRET>$@NULL@$</PLACEMENTSECRET> <TIMEPLACEMENTSECRET>$@NULL@$</TIMEPLACEMENTSECRET> <OLDPLACEMENTSECRET>$@NULL@$</OLDPLACEMENTSECRET> <ORGANIZATIONID>[#mod_basiclti_orgid#]</ORGANIZATIONID> <ORGANIZATIONURL>[#mod_basiclti_orgurl#]</ORGANIZATIONURL> <ORGANIZATIONDESCR>[#mod_basiclti_orgdesc#]</ORGANIZATIONDESCR> <LAUNCHINPOPUP>0</LAUNCHINPOPUP> <DEBUGLAUNCH>0</DEBUGLAUNCH> <MOODLE_COURSE_FIELD>0</MOODLE_COURSE_FIELD> <MODULE_CLASS_TYPE>0</MODULE_CLASS_TYPE> </MOD>