Файловый менеджер - Редактировать - /home/harasnat/www/mf/converter.zip
Назад
PK ��/[V��� � moodle1/backuplib.phpnu &1i� <?php require_once($CFG->dirroot . '/backup/converter/convertlib.php'); class moodle1_export_converter extends base_converter { public static function is_available() { return false; } protected function execute() { } }PK ��/[;����� �� moodle1/lib.phpnu &1i� <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more 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 classes used by the moodle1 converter * * @package backup-convert * @subpackage moodle1 * @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/converter/convertlib.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php'); require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/dbops/restore_dbops.class.php'); require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php'); require_once(__DIR__ . '/handlerlib.php'); /** * Converter of Moodle 1.9 backup into Moodle 2.x format */ class moodle1_converter extends base_converter { /** @var progressive_parser moodle.xml file parser */ protected $xmlparser; /** @var moodle1_parser_processor */ protected $xmlprocessor; /** @var array of {@link convert_path} to process */ protected $pathelements = array(); /** @var null|string the current module being processed - used to expand the MOD paths */ protected $currentmod = null; /** @var null|string the current block being processed - used to expand the BLOCK paths */ protected $currentblock = null; /** @var string path currently locking processing of children */ protected $pathlock; /** @var int used by the serial number {@link get_nextid()} */ private $nextid = 1; /** * Instructs the dispatcher to ignore all children below path processor returning it */ const SKIP_ALL_CHILDREN = -991399; /** * Log a message * * @see parent::log() * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { parent::log('(moodle1) '.$message, $level, $a, $depth, $display); } /** * Detects the Moodle 1.9 format of the backup directory * * @param string $tempdir the name of the backup directory * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise */ public static function detect_format($tempdir) { global $CFG; $tempdirpath = make_backup_temp_directory($tempdir, false); $filepath = $tempdirpath . '/moodle.xml'; if (file_exists($filepath)) { // looks promising, lets load some information $handle = fopen($filepath, 'r'); $first_chars = fread($handle, 200); fclose($handle); // check if it has the required strings if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and strpos($first_chars,'<MOODLE_BACKUP>') !== false and strpos($first_chars,'<INFO>') !== false) { return backup::FORMAT_MOODLE1; } } return null; } /** * Initialize the instance if needed, called by the constructor * * Here we create objects we need before the execution. */ protected function init() { // ask your mother first before going out playing with toys parent::init(); $this->log('initializing '.$this->get_name().' converter', backup::LOG_INFO); // good boy, prepare XML parser and processor $this->log('setting xml parser', backup::LOG_DEBUG, null, 1); $this->xmlparser = new progressive_parser(); $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml'); $this->log('setting xml processor', backup::LOG_DEBUG, null, 1); $this->xmlprocessor = new moodle1_parser_processor($this); $this->xmlparser->set_processor($this->xmlprocessor); // make sure that MOD and BLOCK paths are visited $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/MODULES/MOD'); $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK'); // register the conversion handlers foreach (moodle1_handlers_factory::get_handlers($this) as $handler) { $this->log('registering handler', backup::LOG_DEBUG, get_class($handler), 1); $this->register_handler($handler, $handler->get_paths()); } } /** * Converts the contents of the tempdir into the target format in the workdir */ protected function execute() { $this->log('creating the stash storage', backup::LOG_DEBUG); $this->create_stash_storage(); $this->log('parsing moodle.xml starts', backup::LOG_DEBUG); $this->xmlparser->process(); $this->log('parsing moodle.xml done', backup::LOG_DEBUG); $this->log('dropping the stash storage', backup::LOG_DEBUG); $this->drop_stash_storage(); } /** * Register a handler for the given path elements */ protected function register_handler(moodle1_handler $handler, array $elements) { // first iteration, push them to new array, indexed by name // to detect duplicates in names or paths $names = array(); $paths = array(); foreach($elements as $element) { if (!$element instanceof convert_path) { throw new convert_exception('path_element_wrong_class', get_class($element)); } if (array_key_exists($element->get_name(), $names)) { throw new convert_exception('path_element_name_alreadyexists', $element->get_name()); } if (array_key_exists($element->get_path(), $paths)) { throw new convert_exception('path_element_path_alreadyexists', $element->get_path()); } $names[$element->get_name()] = true; $paths[$element->get_path()] = $element; } // now, for each element not having a processing object yet, assign the handler // if the element is not a memeber of a group foreach($paths as $key => $element) { if (is_null($element->get_processing_object()) and !$this->grouped_parent_exists($element, $paths)) { $paths[$key]->set_processing_object($handler); } // add the element path to the processor $this->xmlprocessor->add_path($element->get_path(), $element->is_grouped()); } // done, store the paths (duplicates by path are discarded) $this->pathelements = array_merge($this->pathelements, $paths); // remove the injected plugin name element from the MOD and BLOCK paths // and register such collapsed path, too foreach ($elements as $element) { $path = $element->get_path(); $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/MODULES\/MOD\/(\w+)\//', '/MOODLE_BACKUP/COURSE/MODULES/MOD/', $path); $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/BLOCKS\/BLOCK\/(\w+)\//', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/', $path); if (!empty($path) and $path != $element->get_path()) { $this->xmlprocessor->add_path($path, false); } } } /** * Helper method used by {@link self::register_handler()} * * @param convert_path $pelement path element * @param array of convert_path instances * @return bool true if grouped parent was found, false otherwise */ protected function grouped_parent_exists($pelement, $elements) { foreach ($elements as $element) { if ($pelement->get_path() == $element->get_path()) { // don't compare against itself continue; } // if the element is grouped and it is a parent of pelement, return true if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) { return true; } } // no grouped parent found return false; } /** * Process the data obtained from the XML parser processor * * This methods receives one chunk of information from the XML parser * processor and dispatches it, following the naming rules. * We are expanding the modules and blocks paths here to include the plugin's name. * * @param array $data */ public function process_chunk($data) { $path = $data['path']; // expand the MOD paths so that they contain the module name if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') { $this->currentmod = strtoupper($data['tags']['MODTYPE']); $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) { $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path); } // expand the BLOCK paths so that they contain the module name if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') { $this->currentblock = strtoupper($data['tags']['NAME']); $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) { $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path); } if ($path !== $data['path']) { if (!array_key_exists($path, $this->pathelements)) { // no handler registered for the transformed MOD or BLOCK path $this->log('no handler attached', backup::LOG_WARNING, $path); return; } else { // pretend as if the original $data contained the tranformed path $data['path'] = $path; } } if (!array_key_exists($data['path'], $this->pathelements)) { // path added to the processor without the handler throw new convert_exception('missing_path_handler', $data['path']); } $element = $this->pathelements[$data['path']]; $object = $element->get_processing_object(); $method = $element->get_processing_method(); $returned = null; // data returned by the processing method, if any if (empty($object)) { throw new convert_exception('missing_processing_object', null, $data['path']); } // 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 the path is not locked, apply the element's recipes and dispatch // the cooked tags to the processing method if (is_null($this->pathlock)) { $rawdatatags = $data['tags']; $data['tags'] = $element->apply_recipes($data['tags']); // if the processing method exists, give it a chance to modify data if (method_exists($object, $method)) { $returned = $object->$method($data['tags'], $rawdatatags); } } // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path // and lock it so that its children are not dispatched if ($returned === self::SKIP_ALL_CHILDREN) { // check we haven't any previous lock if (!is_null($this->pathlock)) { throw new convert_exception('already_locked_path', $data['path']); } // set the lock - nothing below the current path will be dispatched $this->pathlock = $data['path'] . '/'; // if the method has returned any info, set element data to it } else if (!is_null($returned)) { $element->set_tags($returned); // use just the cooked parsed data otherwise } else { $element->set_tags($data['tags']); } } /** * Executes operations required at the start of a watched path * * For MOD and BLOCK paths, this is supported only for the sub-paths, not the root * module/block element. For the illustration: * * You CAN'T attach on_xxx_start() listener to a path like * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP because the <MOD> must * be processed first in {@link self::process_chunk()} where $this->currentmod * is set. * * You CAN attach some on_xxx_start() listener to a path like * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP/SUBMISSIONS because it is * a sub-path under <MOD> and we have $this->currentmod already set when the * <SUBMISSIONS> is reached. * * @param string $path in the original file */ public function path_start_reached($path) { if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') { $this->currentmod = null; $forbidden = true; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) { // expand the MOD paths so that they contain the module name $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path); } if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') { $this->currentblock = null; $forbidden = true; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) { // expand the BLOCK paths so that they contain the module name $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path); } if (empty($this->pathelements[$path])) { return; } $element = $this->pathelements[$path]; $pobject = $element->get_processing_object(); $method = $element->get_start_method(); if (method_exists($pobject, $method)) { if (empty($forbidden)) { $pobject->$method(); } else { // this path is not supported because we do not know the module/block yet throw new coding_exception('Attaching the on-start event listener to the root MOD or BLOCK element is forbidden.'); } } } /** * Executes operations required at the end of a watched path * * @param string $path in the original file */ public function path_end_reached($path) { // expand the MOD paths so that they contain the current module name if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') { $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) { $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path); } // expand the BLOCK paths so that they contain the module name if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') { $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock; } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) { $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path); } if (empty($this->pathelements[$path])) { return; } $element = $this->pathelements[$path]; $pobject = $element->get_processing_object(); $method = $element->get_end_method(); $tags = $element->get_tags(); if (method_exists($pobject, $method)) { $pobject->$method($tags); } } /** * Creates the temporary storage for stashed data * * This implementation uses backup_ids_temp table. */ public function create_stash_storage() { backup_controller_dbops::create_backup_ids_temp_table($this->get_id()); } /** * Drops the temporary storage of stashed data * * This implementation uses backup_ids_temp table. */ public function drop_stash_storage() { backup_controller_dbops::drop_backup_ids_temp_table($this->get_id()); } /** * Stores some information for later processing * * This implementation uses backup_ids_temp table to store data. Make * sure that the $stashname + $itemid combo is unique. * * @param string $stashname name of the stash * @param mixed $info information to stash * @param int $itemid optional id for multiple infos within the same stashname */ public function set_stash($stashname, $info, $itemid = 0) { try { restore_dbops::set_backup_ids_record($this->get_id(), $stashname, $itemid, 0, null, $info); } catch (dml_exception $e) { throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage()); } } /** * Restores a given stash stored previously by {@link self::set_stash()} * * @param string $stashname name of the stash * @param int $itemid optional id for multiple infos within the same stashname * @throws moodle1_convert_empty_storage_exception if the info has not been stashed previously * @return mixed stashed data */ public function get_stash($stashname, $itemid = 0) { $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid); if (empty($record)) { throw new moodle1_convert_empty_storage_exception('required_not_stashed_data', array($stashname, $itemid)); } else { if (empty($record->info)) { return array(); } return $record->info; } } /** * Restores a given stash or returns the given default if there is no such stash * * @param string $stashname name of the stash * @param int $itemid optional id for multiple infos within the same stashname * @param mixed $default information to return if the info has not been stashed previously * @return mixed stashed data or the default value */ public function get_stash_or_default($stashname, $itemid = 0, $default = null) { try { return $this->get_stash($stashname, $itemid); } catch (moodle1_convert_empty_storage_exception $e) { return $default; } } /** * Returns the list of existing stashes * * @return array */ public function get_stash_names() { global $DB; $search = array( 'backupid' => $this->get_id(), ); return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemname')); } /** * Returns the list of stashed $itemids in the given stash * * @param string $stashname * @return array */ public function get_stash_itemids($stashname) { global $DB; $search = array( 'backupid' => $this->get_id(), 'itemname' => $stashname ); return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemid')); } /** * Generates an artificial context id * * Moodle 1.9 backups do not contain any context information. But we need them * in Moodle 2.x format so here we generate fictive context id for every given * context level + instance combo. * * CONTEXT_SYSTEM and CONTEXT_COURSE ignore the $instance as they represent a * single system or the course being restored. * * @see context_system::instance() * @see context_course::instance() * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules * @return int the context id */ public function get_contextid($level, $instance = 0) { $stashname = 'context' . $level; if ($level == CONTEXT_SYSTEM or $level == CONTEXT_COURSE) { $instance = 0; } try { // try the previously stashed id return $this->get_stash($stashname, $instance); } catch (moodle1_convert_empty_storage_exception $e) { // this context level + instance is required for the first time $newid = $this->get_nextid(); $this->set_stash($stashname, $newid, $instance); return $newid; } } /** * Simple autoincrement generator * * @return int the next number in a row of numbers */ public function get_nextid() { return $this->nextid++; } /** * Creates and returns new instance of the file manager * * @param int $contextid the default context id of the files being migrated * @param string $component the default component name of the files being migrated * @param string $filearea the default file area of the files being migrated * @param int $itemid the default item id of the files being migrated * @param int $userid initial user id of the files being migrated * @return moodle1_file_manager */ public function get_file_manager($contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) { return new moodle1_file_manager($this, $contextid, $component, $filearea, $itemid, $userid); } /** * Creates and returns new instance of the inforef manager * * @param string $name the name of the annotator (like course, section, activity, block) * @param int $id the id of the annotator if required * @return moodle1_inforef_manager */ public function get_inforef_manager($name, $id = 0) { return new moodle1_inforef_manager($this, $name, $id); } /** * Migrates all course files referenced from the hypertext using the given filemanager * * This is typically used to convert images embedded into the intro fields. * * @param string $text hypertext containing $@FILEPHP@$ referenced * @param moodle1_file_manager $fileman file manager to use for the file migration * @return string the original $text with $@FILEPHP@$ references replaced with the new @@PLUGINFILE@@ */ public static function migrate_referenced_files($text, moodle1_file_manager $fileman) { $files = self::find_referenced_files($text); if (!empty($files)) { foreach ($files as $file) { try { $fileman->migrate_file('course_files'.$file, dirname($file)); } catch (moodle1_convert_exception $e) { // file probably does not exist $fileman->log('error migrating file', backup::LOG_WARNING, 'course_files'.$file); } } $text = self::rewrite_filephp_usage($text, $files); } return $text; } /** * Detects all links to file.php encoded via $@FILEPHP@$ and returns the files to migrate * * @see self::migrate_referenced_files() * @param string $text * @return array */ public static function find_referenced_files($text) { $files = array(); if (empty($text) or is_numeric($text)) { return $files; } $matches = array(); $pattern = '|(["\'])(\$@FILEPHP@\$.+?)\1|'; $result = preg_match_all($pattern, $text, $matches); if ($result === false) { throw new moodle1_convert_exception('error_while_searching_for_referenced_files'); } if ($result == 0) { return $files; } foreach ($matches[2] as $match) { $file = str_replace(array('$@FILEPHP@$', '$@SLASH@$', '$@FORCEDOWNLOAD@$'), array('', '/', ''), $match); if ($file === clean_param($file, PARAM_PATH)) { $files[] = rawurldecode($file); } } return array_unique($files); } /** * Given the list of migrated files, rewrites references to them from $@FILEPHP@$ form to the @@PLUGINFILE@@ one * * @see self::migrate_referenced_files() * @param string $text * @param array $files * @return string */ public static function rewrite_filephp_usage($text, array $files) { foreach ($files as $file) { // Expect URLs properly encoded by default. $parts = explode('/', $file); $encoded = implode('/', array_map('rawurlencode', $parts)); $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $encoded); $text = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text); $text = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text); // Add support for URLs without any encoding. $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $file); $text = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text); $text = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text); } return $text; } /** * @see parent::description() */ public static function description() { return array( 'from' => backup::FORMAT_MOODLE1, 'to' => backup::FORMAT_MOODLE, 'cost' => 10, ); } } /** * Exception thrown by this converter */ class moodle1_convert_exception extends convert_exception { } /** * Exception thrown by the temporary storage subsystem of moodle1_converter */ class moodle1_convert_storage_exception extends moodle1_convert_exception { } /** * Exception thrown by the temporary storage subsystem of moodle1_converter */ class moodle1_convert_empty_storage_exception extends moodle1_convert_exception { } /** * XML parser processor used for processing parsed moodle.xml */ class moodle1_parser_processor extends grouped_parser_processor { /** @var moodle1_converter */ protected $converter; public function __construct(moodle1_converter $converter) { $this->converter = $converter; parent::__construct(); } /** * Provides NULL decoding * * Note that we do not decode $@FILEPHP@$ and friends here as we are going to write them * back immediately into another XML file. */ public function process_cdata($cdata) { if ($cdata === '$@NULL@$') { return null; } return $cdata; } /** * Dispatches the data chunk to the converter class * * @param array $data the chunk of parsed data */ protected function dispatch_chunk($data) { $this->converter->process_chunk($data); } /** * Informs the converter at the start of a watched path * * @param string $path */ protected function notify_path_start($path) { $this->converter->path_start_reached($path); } /** * Informs the converter at the end of a watched path * * @param string $path */ protected function notify_path_end($path) { $this->converter->path_end_reached($path); } } /** * XML transformer that modifies the content of the files being written during the conversion * * @see backup_xml_transformer */ class moodle1_xml_transformer extends xml_contenttransformer { /** * Modify the content before it is writter to a file * * @param string|mixed $content */ public function process($content) { // the content should be a string. If array or object is given, try our best recursively // but inform the developer if (is_array($content)) { debugging('Moodle1 XML transformer should not process arrays but plain content always', DEBUG_DEVELOPER); foreach($content as $key => $plaincontent) { $content[$key] = $this->process($plaincontent); } return $content; } else if (is_object($content)) { debugging('Moodle1 XML transformer should not process objects but plain content always', DEBUG_DEVELOPER); foreach((array)$content as $key => $plaincontent) { $content[$key] = $this->process($plaincontent); } return (object)$content; } // try to deal with some trivial cases first if (is_null($content)) { return '$@NULL@$'; } else if ($content === '') { return ''; } else if (is_numeric($content)) { return $content; } else if (strlen($content) < 32) { return $content; } return $content; } } /** * Class representing a path to be converted from XML file * * This was created as a copy of {@link restore_path_element} and should be refactored * probably. */ class convert_path { /** @var string name of the element */ protected $name; /** @var string path within the XML file this element will handle */ protected $path; /** @var bool flag to define if this element will get child ones grouped or no */ protected $grouped; /** @var object object instance in charge of processing this element. */ protected $pobject = null; /** @var string the name of the processing method */ protected $pmethod = null; /** @var string the name of the path start event handler */ protected $smethod = null; /** @var string the name of the path end event handler */ protected $emethod = null; /** @var mixed last data read for this element or returned data by processing method */ protected $tags = null; /** @var array of deprecated fields that are dropped */ protected $dropfields = array(); /** @var array of fields renaming */ protected $renamefields = array(); /** @var array of new fields to add and their initial values */ protected $newfields = array(); /** * Constructor * * The optional recipe array can have three keys, and for each key, the value is another array. * - newfields => array fieldname => defaultvalue indicates fields that have been added to the table, * and so should be added to the XML. * - dropfields => array fieldname indicates fieldsthat have been dropped from the table, * and so can be dropped from the XML. * - renamefields => array oldname => newname indicates fieldsthat have been renamed in the table, * and so should be renamed in the XML. * {@line moodle1_course_outline_handler} is a good example that uses all of these. * * @param string $name name of the element * @param string $path path of the element * @param array $recipe basic description of the structure conversion * @param bool $grouped to gather information in grouped mode or no */ public function __construct($name, $path, array $recipe = array(), $grouped = false) { $this->validate_name($name); $this->name = $name; $this->path = $path; $this->grouped = $grouped; // set the default method names $this->set_processing_method('process_' . $name); $this->set_start_method('on_'.$name.'_start'); $this->set_end_method('on_'.$name.'_end'); if ($grouped and !empty($recipe)) { throw new convert_path_exception('recipes_not_supported_for_grouped_elements'); } if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) { $this->set_dropped_fields($recipe['dropfields']); } if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) { $this->set_renamed_fields($recipe['renamefields']); } if (isset($recipe['newfields']) and is_array($recipe['newfields'])) { $this->set_new_fields($recipe['newfields']); } } /** * Validates and sets the given processing object * * @param object $pobject processing object, must provide a method to be called */ public function set_processing_object($pobject) { $this->validate_pobject($pobject); $this->pobject = $pobject; } /** * Sets the name of the processing method * * @param string $pmethod */ public function set_processing_method($pmethod) { $this->pmethod = $pmethod; } /** * Sets the name of the path start event listener * * @param string $smethod */ public function set_start_method($smethod) { $this->smethod = $smethod; } /** * Sets the name of the path end event listener * * @param string $emethod */ public function set_end_method($emethod) { $this->emethod = $emethod; } /** * Sets the element tags * * @param array $tags */ public function set_tags($tags) { $this->tags = $tags; } /** * Sets the list of deprecated fields to drop * * @param array $fields */ public function set_dropped_fields(array $fields) { $this->dropfields = $fields; } /** * Sets the required new names of the current fields * * @param array $fields (string)$currentname => (string)$newname */ public function set_renamed_fields(array $fields) { $this->renamefields = $fields; } /** * Sets the new fields and their values * * @param array $fields (string)$field => (mixed)value */ public function set_new_fields(array $fields) { $this->newfields = $fields; } /** * Cooks the parsed tags data by applying known recipes * * Recipes are used for common trivial operations like adding new fields * or renaming fields. The handler's processing method receives cooked * data. * * @param array $data the contents of the element * @return array */ public function apply_recipes(array $data) { $cooked = array(); foreach ($data as $name => $value) { // lower case rocks! $name = strtolower($name); if (is_array($value)) { if ($this->is_grouped()) { $value = $this->apply_recipes($value); } else { throw new convert_path_exception('non_grouped_path_with_array_values'); } } // drop legacy fields if (in_array($name, $this->dropfields)) { continue; } // fields renaming if (array_key_exists($name, $this->renamefields)) { $name = $this->renamefields[$name]; } $cooked[$name] = $value; } // adding new fields foreach ($this->newfields as $name => $value) { $cooked[$name] = $value; } return $cooked; } /** * @return string the element given name */ public function get_name() { return $this->name; } /** * @return string the path to the element */ public function get_path() { return $this->path; } /** * @return bool flag to define if this element will get child ones grouped or no */ public function is_grouped() { return $this->grouped; } /** * @return object the processing object providing the processing method */ public function get_processing_object() { return $this->pobject; } /** * @return string the name of the method to call to process the element */ public function get_processing_method() { return $this->pmethod; } /** * @return string the name of the path start event listener */ public function get_start_method() { return $this->smethod; } /** * @return string the name of the path end event listener */ public function get_end_method() { return $this->emethod; } /** * @return mixed the element data */ public function get_tags() { return $this->tags; } /// end of public API ////////////////////////////////////////////////////// /** * Makes sure the given name is a valid element name * * Note it may look as if we used exceptions for code flow control here. That's not the case * as we actually validate the code, not the user data. And the code is supposed to be * correct. * * @param string @name the element given name * @throws convert_path_exception * @return void */ protected function validate_name($name) { // Validate various name constraints, throwing exception if needed if (empty($name)) { throw new convert_path_exception('convert_path_emptyname', $name); } if (preg_replace('/\s/', '', $name) != $name) { throw new convert_path_exception('convert_path_whitespace', $name); } if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) { throw new convert_path_exception('convert_path_notasciiname', $name); } } /** * Makes sure that the given object is a valid processing object * * The processing object must be an object providing at least element's processing method * or path-reached-end event listener or path-reached-start listener method. * * Note it may look as if we used exceptions for code flow control here. That's not the case * as we actually validate the code, not the user data. And the code is supposed to be * correct. * * @param object $pobject * @throws convert_path_exception * @return void */ protected function validate_pobject($pobject) { if (!is_object($pobject)) { throw new convert_path_exception('convert_path_no_object', get_class($pobject)); } if (!method_exists($pobject, $this->get_processing_method()) and !method_exists($pobject, $this->get_end_method()) and !method_exists($pobject, $this->get_start_method())) { throw new convert_path_exception('convert_path_missing_method', get_class($pobject)); } } } /** * Exception being thrown by {@link convert_path} methods */ class convert_path_exception extends moodle_exception { /** * Constructor * * @param string $errorcode key for the corresponding error string * @param mixed $a extra words and phrases that might be required by the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $a = null, $debuginfo = null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } } /** * The class responsible for files migration * * The files in Moodle 1.9 backup are stored in moddata, user_files, group_files, * course_files and site_files folders. */ class moodle1_file_manager implements loggable { /** @var moodle1_converter instance we serve to */ public $converter; /** @var int context id of the files being migrated */ public $contextid; /** @var string component name of the files being migrated */ public $component; /** @var string file area of the files being migrated */ public $filearea; /** @var int item id of the files being migrated */ public $itemid = 0; /** @var int user id */ public $userid; /** @var string the root of the converter temp directory */ protected $basepath; /** @var array of file ids that were migrated by this instance */ protected $fileids = array(); /** * Constructor optionally accepting some default values for the migrated files * * @param moodle1_converter $converter the converter instance we serve to * @param int $contextid initial context id of the files being migrated * @param string $component initial component name of the files being migrated * @param string $filearea initial file area of the files being migrated * @param int $itemid initial item id of the files being migrated * @param int $userid initial user id of the files being migrated */ public function __construct(moodle1_converter $converter, $contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) { // set the initial destination of the migrated files $this->converter = $converter; $this->contextid = $contextid; $this->component = $component; $this->filearea = $filearea; $this->itemid = $itemid; $this->userid = $userid; // set other useful bits $this->basepath = $converter->get_tempdir_path(); } /** * Migrates one given file stored on disk * * @param string $sourcepath the path to the source local file within the backup archive {@example 'moddata/foobar/file.ext'} * @param string $filepath the file path of the migrated file, defaults to the root directory '/' {@example '/sub/dir/'} * @param string $filename the name of the migrated file, defaults to the same as the source file has * @param int $sortorder the sortorder of the file (main files have sortorder set to 1) * @param int $timecreated override the timestamp of when the migrated file should appear as created * @param int $timemodified override the timestamp of when the migrated file should appear as modified * @return int id of the migrated file */ public function migrate_file($sourcepath, $filepath = '/', $filename = null, $sortorder = 0, $timecreated = null, $timemodified = null) { // Normalise Windows paths a bit. $sourcepath = str_replace('\\', '/', $sourcepath); // PARAM_PATH must not be used on full OS path! if ($sourcepath !== clean_param($sourcepath, PARAM_PATH)) { throw new moodle1_convert_exception('file_invalid_path', $sourcepath); } $sourcefullpath = $this->basepath.'/'.$sourcepath; if (!is_readable($sourcefullpath)) { throw new moodle1_convert_exception('file_not_readable', $sourcefullpath); } // sanitize filepath if (empty($filepath)) { $filepath = '/'; } if (substr($filepath, -1) !== '/') { $filepath .= '/'; } $filepath = clean_param($filepath, PARAM_PATH); if (core_text::strlen($filepath) > 255) { throw new moodle1_convert_exception('file_path_longer_than_255_chars'); } if (is_null($filename)) { $filename = basename($sourcefullpath); } $filename = clean_param($filename, PARAM_FILE); if ($filename === '') { throw new moodle1_convert_exception('unsupported_chars_in_filename'); } if (is_null($timecreated)) { $timecreated = filectime($sourcefullpath); } if (is_null($timemodified)) { $timemodified = filemtime($sourcefullpath); } $filerecord = $this->make_file_record(array( 'filepath' => $filepath, 'filename' => $filename, 'sortorder' => $sortorder, 'mimetype' => mimeinfo('type', $sourcefullpath), 'timecreated' => $timecreated, 'timemodified' => $timemodified, )); list($filerecord['contenthash'], $filerecord['filesize'], $newfile) = $this->add_file_to_pool($sourcefullpath); $this->stash_file($filerecord); return $filerecord['id']; } /** * Migrates all files in the given directory * * @param string $rootpath path within the backup archive to the root directory containing the files {@example 'course_files'} * @param string $relpath relative path used during the recursion - do not provide when calling this! * @return array ids of the migrated files, empty array if the $rootpath not found */ public function migrate_directory($rootpath, $relpath='/') { // Check the trailing slash in the $rootpath if (substr($rootpath, -1) === '/') { debugging('moodle1_file_manager::migrate_directory() expects $rootpath without the trailing slash', DEBUG_DEVELOPER); $rootpath = substr($rootpath, 0, strlen($rootpath) - 1); } if (!file_exists($this->basepath.'/'.$rootpath.$relpath)) { return array(); } $fileids = array(); // make the fake file record for the directory itself $filerecord = $this->make_file_record(array('filepath' => $relpath, 'filename' => '.')); $this->stash_file($filerecord); $fileids[] = $filerecord['id']; $items = new DirectoryIterator($this->basepath.'/'.$rootpath.$relpath); foreach ($items as $item) { if ($item->isDot()) { continue; } if ($item->isLink()) { throw new moodle1_convert_exception('unexpected_symlink'); } if ($item->isFile()) { $fileids[] = $this->migrate_file(substr($item->getPathname(), strlen($this->basepath.'/')), $relpath, $item->getFilename(), 0, $item->getCTime(), $item->getMTime()); } else { $dirname = clean_param($item->getFilename(), PARAM_PATH); if ($dirname === '') { throw new moodle1_convert_exception('unsupported_chars_in_filename'); } // migrate subdirectories recursively $fileids = array_merge($fileids, $this->migrate_directory($rootpath, $relpath.$item->getFilename().'/')); } } return $fileids; } /** * Returns the list of all file ids migrated by this instance so far * * @return array of int */ public function get_fileids() { return $this->fileids; } /** * Explicitly clear the list of file ids migrated by this instance so far */ public function reset_fileids() { $this->fileids = array(); } /** * Log a message using the converter's logging mechanism * * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { $this->converter->log($message, $level, $a, $depth, $display); } /// internal implementation details //////////////////////////////////////// /** * Prepares a fake record from the files table * * @param array $fileinfo explicit file data * @return array */ protected function make_file_record(array $fileinfo) { $defaultrecord = array( 'contenthash' => file_storage::hash_from_string(''), 'contextid' => $this->contextid, 'component' => $this->component, 'filearea' => $this->filearea, 'itemid' => $this->itemid, 'filepath' => null, 'filename' => null, 'filesize' => 0, 'userid' => $this->userid, 'mimetype' => null, 'status' => 0, 'timecreated' => $now = time(), 'timemodified' => $now, 'source' => null, 'author' => null, 'license' => null, 'sortorder' => 0, ); if (!array_key_exists('id', $fileinfo)) { $defaultrecord['id'] = $this->converter->get_nextid(); } // override the default values with the explicit data provided and return return array_merge($defaultrecord, $fileinfo); } /** * Copies the given file to the pool directory * * Returns an array containing SHA1 hash of the file contents, the file size * and a flag indicating whether the file was actually added to the pool or whether * it was already there. * * @param string $pathname the full path to the file * @return array with keys (string)contenthash, (int)filesize, (bool)newfile */ protected function add_file_to_pool($pathname) { if (!is_readable($pathname)) { throw new moodle1_convert_exception('file_not_readable'); } $contenthash = file_storage::hash_from_path($pathname); $filesize = filesize($pathname); $hashpath = $this->converter->get_workdir_path().'/files/'.substr($contenthash, 0, 2); $hashfile = "$hashpath/$contenthash"; if (file_exists($hashfile)) { if (filesize($hashfile) !== $filesize) { // congratulations! you have found two files with different size and the same // content hash. or, something were wrong (which is more likely) throw new moodle1_convert_exception('same_hash_different_size'); } $newfile = false; } else { check_dir_exists($hashpath); $newfile = true; if (!copy($pathname, $hashfile)) { throw new moodle1_convert_exception('unable_to_copy_file'); } if (filesize($hashfile) !== $filesize) { throw new moodle1_convert_exception('filesize_different_after_copy'); } } return array($contenthash, $filesize, $newfile); } /** * Stashes the file record into 'files' stash and adds the record id to list of migrated files * * @param array $filerecord */ protected function stash_file(array $filerecord) { $this->converter->set_stash('files', $filerecord, $filerecord['id']); $this->fileids[] = $filerecord['id']; } } /** * Helper class that handles ids annotations for inforef.xml files */ class moodle1_inforef_manager { /** @var string the name of the annotator we serve to (like course, section, activity, block) */ protected $annotator = null; /** @var int the id of the annotator if it can have multiple instances */ protected $annotatorid = null; /** @var array the actual storage of references, currently implemented as a in-memory structure */ private $refs = array(); /** * Creates new instance of the manager for the given annotator * * The identification of the annotator we serve to may be important in the future * when we move the actual storage of the references from memory to a persistent storage. * * @param moodle1_converter $converter * @param string $name the name of the annotator (like course, section, activity, block) * @param int $id the id of the annotator if required */ public function __construct(moodle1_converter $converter, $name, $id = 0) { $this->annotator = $name; $this->annotatorid = $id; } /** * Adds a reference * * @param string $item the name of referenced item (like user, file, scale, outcome or grade_item) * @param int $id the value of the reference */ public function add_ref($item, $id) { $this->validate_item($item); $this->refs[$item][$id] = true; } /** * Adds a bulk of references * * @param string $item the name of referenced item (like user, file, scale, outcome or grade_item) * @param array $ids the list of referenced ids */ public function add_refs($item, array $ids) { $this->validate_item($item); foreach ($ids as $id) { $this->refs[$item][$id] = true; } } /** * Writes the current references using a given opened xml writer * * @param xml_writer $xmlwriter */ public function write_refs(xml_writer $xmlwriter) { $xmlwriter->begin_tag('inforef'); foreach ($this->refs as $item => $ids) { $xmlwriter->begin_tag($item.'ref'); foreach (array_keys($ids) as $id) { $xmlwriter->full_tag($item, $id); } $xmlwriter->end_tag($item.'ref'); } $xmlwriter->end_tag('inforef'); } /** * Makes sure that the given name is a valid citizen of inforef.xml file * * @see backup_helper::get_inforef_itemnames() * @param string $item the name of reference (like user, file, scale, outcome or grade_item) * @throws coding_exception */ protected function validate_item($item) { $allowed = array( 'user' => true, 'grouping' => true, 'group' => true, 'role' => true, 'file' => true, 'scale' => true, 'outcome' => true, 'grade_item' => true, 'question_category' => true ); if (!isset($allowed[$item])) { throw new coding_exception('Invalid inforef item type'); } } } PK ��/[�� ]�C �C moodle1/handlerlib.phpnu &1i� <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more 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 Moodle 1.9 backup conversion handlers * * Handlers are classes responsible for the actual conversion work. Their logic * is similar to the functionality provided by steps in plan based restore process. * * @package backup-convert * @subpackage moodle1 * @copyright 2011 David Mudrak <david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); 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'); /** * Handlers factory class */ abstract class moodle1_handlers_factory { /** * @param moodle1_converter the converter requesting the converters * @return list of all available conversion handlers */ public static function get_handlers(moodle1_converter $converter) { $handlers = array( new moodle1_root_handler($converter), new moodle1_info_handler($converter), new moodle1_course_header_handler($converter), new moodle1_course_outline_handler($converter), new moodle1_roles_definition_handler($converter), new moodle1_question_bank_handler($converter), new moodle1_scales_handler($converter), new moodle1_outcomes_handler($converter), new moodle1_gradebook_handler($converter), ); $handlers = array_merge($handlers, self::get_plugin_handlers('mod', $converter)); $handlers = array_merge($handlers, self::get_plugin_handlers('block', $converter)); // make sure that all handlers have expected class foreach ($handlers as $handler) { if (!$handler instanceof moodle1_handler) { throw new moodle1_convert_exception('wrong_handler_class', get_class($handler)); } } return $handlers; } /// public API ends here /////////////////////////////////////////////////// /** * Runs through all plugins of a specific type and instantiates their handlers * * @todo ask mod's subplugins * @param string $type the plugin type * @param moodle1_converter $converter the converter requesting the handler * @throws moodle1_convert_exception * @return array of {@link moodle1_handler} instances */ protected static function get_plugin_handlers($type, moodle1_converter $converter) { global $CFG; $handlers = array(); $plugins = core_component::get_plugin_list($type); foreach ($plugins as $name => $dir) { $handlerfile = $dir . '/backup/moodle1/lib.php'; $handlerclass = "moodle1_{$type}_{$name}_handler"; if (file_exists($handlerfile)) { require_once($handlerfile); } elseif ($type == 'block') { $handlerclass = "moodle1_block_generic_handler"; } else { continue; } if (!class_exists($handlerclass)) { throw new moodle1_convert_exception('missing_handler_class', $handlerclass); } $handlers[] = new $handlerclass($converter, $type, $name); } return $handlers; } } /** * Base backup conversion handler */ abstract class moodle1_handler implements loggable { /** @var moodle1_converter */ protected $converter; /** * @param moodle1_converter $converter the converter that requires us */ public function __construct(moodle1_converter $converter) { $this->converter = $converter; } /** * @return moodle1_converter the converter that required this handler */ public function get_converter() { return $this->converter; } /** * Log a message using the converter's logging mechanism * * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { $this->converter->log($message, $level, $a, $depth, $display); } } /** * Base backup conversion handler that generates an XML file */ abstract class moodle1_xml_handler extends moodle1_handler { /** @var null|string the name of file we are writing to */ protected $xmlfilename; /** @var null|xml_writer */ protected $xmlwriter; /** * Opens the XML writer - after calling, one is free to use $xmlwriter * * @param string $filename XML file name to write into * @return void */ protected function open_xml_writer($filename) { if (!is_null($this->xmlfilename) and $filename !== $this->xmlfilename) { throw new moodle1_convert_exception('xml_writer_already_opened_for_other_file', $this->xmlfilename); } if (!$this->xmlwriter instanceof xml_writer) { $this->xmlfilename = $filename; $fullpath = $this->converter->get_workdir_path() . '/' . $this->xmlfilename; $directory = pathinfo($fullpath, PATHINFO_DIRNAME); if (!check_dir_exists($directory)) { throw new moodle1_convert_exception('unable_create_target_directory', $directory); } $this->xmlwriter = new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer()); $this->xmlwriter->start(); } } /** * Close the XML writer * * At the moment, the caller must close all tags before calling * * @return void */ protected function close_xml_writer() { if ($this->xmlwriter instanceof xml_writer) { $this->xmlwriter->stop(); } unset($this->xmlwriter); $this->xmlwriter = null; $this->xmlfilename = null; } /** * Checks if the XML writer has been opened by {@link self::open_xml_writer()} * * @return bool */ protected function has_xml_writer() { if ($this->xmlwriter instanceof xml_writer) { return true; } else { return false; } } /** * Writes the given XML tree data into the currently opened file * * @param string $element the name of the root element of the tree * @param array $data the associative array of data to write * @param array $attribs list of additional fields written as attributes instead of nested elements * @param string $parent used internally during the recursion, do not set yourself */ protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') { if (!$this->has_xml_writer()) { throw new moodle1_convert_exception('write_xml_without_writer'); } $mypath = $parent . $element; $myattribs = array(); // detect properties that should be rendered as element's attributes instead of children foreach ($data as $name => $value) { if (!is_array($value)) { if (in_array($mypath . '/' . $name, $attribs)) { $myattribs[$name] = $value; unset($data[$name]); } } } // reorder the $data so that all sub-branches are at the end (needed by our parser) $leaves = array(); $branches = array(); foreach ($data as $name => $value) { if (is_array($value)) { $branches[$name] = $value; } else { $leaves[$name] = $value; } } $data = array_merge($leaves, $branches); $this->xmlwriter->begin_tag($element, $myattribs); foreach ($data as $name => $value) { if (is_array($value)) { // recursively call self $this->write_xml($name, $value, $attribs, $mypath.'/'); } else { $this->xmlwriter->full_tag($name, $value); } } $this->xmlwriter->end_tag($element); } /** * Makes sure that a new XML file exists, or creates it itself * * This is here so we can check that all XML files that the restore process relies on have * been created by an executed handler. If the file is not found, this method can create it * using the given $rootelement as an empty root container in the file. * * @param string $filename relative file name like 'course/course.xml' * @param string|bool $rootelement root element to use, false to not create the file * @param array $content content of the root element * @return bool true is the file existed, false if it did not */ protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) { $existed = file_exists($this->converter->get_workdir_path().'/'.$filename); if ($existed) { return true; } if ($rootelement !== false) { $this->open_xml_writer($filename); $this->write_xml($rootelement, $content); $this->close_xml_writer(); } return false; } } /** * Process the root element of the backup file */ class moodle1_root_handler extends moodle1_xml_handler { public function get_paths() { return array(new convert_path('root_element', '/MOODLE_BACKUP')); } /** * Converts course_files and site_files */ public function on_root_element_start() { // convert course files $fileshandler = new moodle1_files_handler($this->converter); $fileshandler->process(); } /** * This is executed at the end of the moodle.xml parsing */ public function on_root_element_end() { global $CFG; // restore the stashes prepared by other handlers for us $backupinfo = $this->converter->get_stash('backup_info'); $originalcourseinfo = $this->converter->get_stash('original_course_info'); //////////////////////////////////////////////////////////////////////// // write moodle_backup.xml //////////////////////////////////////////////////////////////////////// $this->open_xml_writer('moodle_backup.xml'); $this->xmlwriter->begin_tag('moodle_backup'); $this->xmlwriter->begin_tag('information'); // moodle_backup/information $this->xmlwriter->full_tag('name', $backupinfo['name']); $this->xmlwriter->full_tag('moodle_version', $backupinfo['moodle_version']); $this->xmlwriter->full_tag('moodle_release', $backupinfo['moodle_release']); $this->xmlwriter->full_tag('backup_version', $CFG->backup_version); // {@see restore_prechecks_helper::execute_prechecks} $this->xmlwriter->full_tag('backup_release', $CFG->backup_release); $this->xmlwriter->full_tag('backup_date', $backupinfo['date']); // see the commit c0543b - all backups created in 1.9 and later declare the // information or it is considered as false if (isset($backupinfo['mnet_remoteusers'])) { $this->xmlwriter->full_tag('mnet_remoteusers', $backupinfo['mnet_remoteusers']); } else { $this->xmlwriter->full_tag('mnet_remoteusers', false); } $this->xmlwriter->full_tag('original_wwwroot', $backupinfo['original_wwwroot']); // {@see backup_general_helper::backup_is_samesite()} if (isset($backupinfo['original_site_identifier_hash'])) { $this->xmlwriter->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']); } else { $this->xmlwriter->full_tag('original_site_identifier_hash', null); } $this->xmlwriter->full_tag('original_course_id', $originalcourseinfo['original_course_id']); $this->xmlwriter->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']); $this->xmlwriter->full_tag('original_course_shortname', $originalcourseinfo['original_course_shortname']); $this->xmlwriter->full_tag('original_course_startdate', $originalcourseinfo['original_course_startdate']); $this->xmlwriter->full_tag('original_system_contextid', $this->converter->get_contextid(CONTEXT_SYSTEM)); // note that even though we have original_course_contextid available, we regenerate the // original course contextid using our helper method to be sure that the data are consistent // within the MBZ file $this->xmlwriter->full_tag('original_course_contextid', $this->converter->get_contextid(CONTEXT_COURSE)); // moodle_backup/information/details $this->xmlwriter->begin_tag('details'); $this->write_xml('detail', array( 'backup_id' => $this->converter->get_id(), 'type' => backup::TYPE_1COURSE, 'format' => backup::FORMAT_MOODLE, 'interactive' => backup::INTERACTIVE_YES, 'mode' => backup::MODE_CONVERTED, 'execution' => backup::EXECUTION_INMEDIATE, 'executiontime' => 0, ), array('/detail/backup_id')); $this->xmlwriter->end_tag('details'); // moodle_backup/information/contents $this->xmlwriter->begin_tag('contents'); // moodle_backup/information/contents/activities $this->xmlwriter->begin_tag('activities'); $activitysettings = array(); foreach ($this->converter->get_stash('coursecontents') as $activity) { $modinfo = $this->converter->get_stash('modinfo_'.$activity['modulename']); $modinstance = $modinfo['instances'][$activity['instanceid']]; $this->write_xml('activity', array( 'moduleid' => $activity['cmid'], 'sectionid' => $activity['sectionid'], 'modulename' => $activity['modulename'], 'title' => $modinstance['name'], 'directory' => 'activities/'.$activity['modulename'].'_'.$activity['cmid'] )); $activitysettings[] = array( 'level' => 'activity', 'activity' => $activity['modulename'].'_'.$activity['cmid'], 'name' => $activity['modulename'].'_'.$activity['cmid'].'_included', 'value' => (($modinfo['included'] === 'true' and $modinstance['included'] === 'true') ? 1 : 0)); $activitysettings[] = array( 'level' => 'activity', 'activity' => $activity['modulename'].'_'.$activity['cmid'], 'name' => $activity['modulename'].'_'.$activity['cmid'].'_userinfo', //'value' => (($modinfo['userinfo'] === 'true' and $modinstance['userinfo'] === 'true') ? 1 : 0)); 'value' => 0); // todo hardcoded non-userinfo for now } $this->xmlwriter->end_tag('activities'); // moodle_backup/information/contents/sections $this->xmlwriter->begin_tag('sections'); $sectionsettings = array(); foreach ($this->converter->get_stash_itemids('sectioninfo') as $sectionid) { $sectioninfo = $this->converter->get_stash('sectioninfo', $sectionid); $sectionsettings[] = array( 'level' => 'section', 'section' => 'section_'.$sectionid, 'name' => 'section_'.$sectionid.'_included', 'value' => 1); $sectionsettings[] = array( 'level' => 'section', 'section' => 'section_'.$sectionid, 'name' => 'section_'.$sectionid.'_userinfo', 'value' => 0); // @todo how to detect this from moodle.xml? $this->write_xml('section', array( 'sectionid' => $sectionid, 'title' => $sectioninfo['number'], // because the title is not available 'directory' => 'sections/section_'.$sectionid)); } $this->xmlwriter->end_tag('sections'); // moodle_backup/information/contents/course $this->write_xml('course', array( 'courseid' => $originalcourseinfo['original_course_id'], 'title' => $originalcourseinfo['original_course_shortname'], 'directory' => 'course')); unset($originalcourseinfo); $this->xmlwriter->end_tag('contents'); // moodle_backup/information/settings $this->xmlwriter->begin_tag('settings'); // fake backup root seetings $rootsettings = array( 'filename' => $backupinfo['name'], 'users' => 0, // @todo how to detect this from moodle.xml? 'anonymize' => 0, 'role_assignments' => 0, 'activities' => 1, 'blocks' => 1, 'filters' => 0, 'comments' => 0, 'userscompletion' => 0, 'logs' => 0, 'grade_histories' => 0, ); unset($backupinfo); foreach ($rootsettings as $name => $value) { $this->write_xml('setting', array( 'level' => 'root', 'name' => $name, 'value' => $value)); } unset($rootsettings); // activity settings populated above foreach ($activitysettings as $activitysetting) { $this->write_xml('setting', $activitysetting); } unset($activitysettings); // section settings populated above foreach ($sectionsettings as $sectionsetting) { $this->write_xml('setting', $sectionsetting); } unset($sectionsettings); $this->xmlwriter->end_tag('settings'); $this->xmlwriter->end_tag('information'); $this->xmlwriter->end_tag('moodle_backup'); $this->close_xml_writer(); //////////////////////////////////////////////////////////////////////// // write files.xml //////////////////////////////////////////////////////////////////////// $this->open_xml_writer('files.xml'); $this->xmlwriter->begin_tag('files'); foreach ($this->converter->get_stash_itemids('files') as $fileid) { $this->write_xml('file', $this->converter->get_stash('files', $fileid), array('/file/id')); } $this->xmlwriter->end_tag('files'); $this->close_xml_writer('files.xml'); //////////////////////////////////////////////////////////////////////// // write scales.xml //////////////////////////////////////////////////////////////////////// $this->open_xml_writer('scales.xml'); $this->xmlwriter->begin_tag('scales_definition'); foreach ($this->converter->get_stash_itemids('scales') as $scaleid) { $this->write_xml('scale', $this->converter->get_stash('scales', $scaleid), array('/scale/id')); } $this->xmlwriter->end_tag('scales_definition'); $this->close_xml_writer('scales.xml'); //////////////////////////////////////////////////////////////////////// // write course/inforef.xml //////////////////////////////////////////////////////////////////////// $this->open_xml_writer('course/inforef.xml'); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); // legacy course files $fileids = $this->converter->get_stash('course_files_ids'); if (is_array($fileids)) { foreach ($fileids as $fileid) { $this->write_xml('file', array('id' => $fileid)); } } // todo site files // course summary files $fileids = $this->converter->get_stash('course_summary_files_ids'); if (is_array($fileids)) { foreach ($fileids as $fileid) { $this->write_xml('file', array('id' => $fileid)); } } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->begin_tag('question_categoryref'); foreach ($this->converter->get_stash_itemids('question_categories') as $questioncategoryid) { $this->write_xml('question_category', array('id' => $questioncategoryid)); } $this->xmlwriter->end_tag('question_categoryref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); // make sure that the files required by the restore process have been generated. // missing file may happen if the watched tag is not present in moodle.xml (for example // QUESTION_CATEGORIES is optional in moodle.xml but questions.xml must exist in // moodle2 format) or the handler has not been implemented yet. // apparently this must be called after the handler had a chance to create the file. $this->make_sure_xml_exists('questions.xml', 'question_categories'); $this->make_sure_xml_exists('groups.xml', 'groups'); $this->make_sure_xml_exists('outcomes.xml', 'outcomes_definition'); $this->make_sure_xml_exists('users.xml', 'users'); $this->make_sure_xml_exists('course/roles.xml', 'roles', array('role_assignments' => array(), 'role_overrides' => array())); $this->make_sure_xml_exists('course/enrolments.xml', 'enrolments', array('enrols' => array())); } } /** * The class responsible for course and site files migration * * @todo migrate site_files */ class moodle1_files_handler extends moodle1_xml_handler { /** * Migrates course_files and site_files in the converter workdir */ public function process() { $this->migrate_course_files(); // todo $this->migrate_site_files(); } /** * Migrates course_files in the converter workdir */ protected function migrate_course_files() { $ids = array(); $fileman = $this->converter->get_file_manager($this->converter->get_contextid(CONTEXT_COURSE), 'course', 'legacy'); $this->converter->set_stash('course_files_ids', array()); if (file_exists($this->converter->get_tempdir_path().'/course_files')) { $ids = $fileman->migrate_directory('course_files'); $this->converter->set_stash('course_files_ids', $ids); } $this->log('course files migrated', backup::LOG_INFO, count($ids)); } } /** * Handles the conversion of /MOODLE_BACKUP/INFO paths * * We do not produce any XML file here, just storing the data in the temp * table so thay can be used by a later handler. */ class moodle1_info_handler extends moodle1_handler { /** @var array list of mod names included in info_details */ protected $modnames = array(); /** @var array the in-memory cache of the currently parsed info_details_mod element */ protected $currentmod; public function get_paths() { return array( new convert_path('info', '/MOODLE_BACKUP/INFO'), new convert_path('info_details', '/MOODLE_BACKUP/INFO/DETAILS'), new convert_path('info_details_mod', '/MOODLE_BACKUP/INFO/DETAILS/MOD'), new convert_path('info_details_mod_instance', '/MOODLE_BACKUP/INFO/DETAILS/MOD/INSTANCES/INSTANCE'), ); } /** * Stashes the backup info for later processing by {@link moodle1_root_handler} */ public function process_info($data) { $this->converter->set_stash('backup_info', $data); } /** * Initializes the in-memory cache for the current mod */ public function process_info_details_mod($data) { $this->currentmod = $data; $this->currentmod['instances'] = array(); } /** * Appends the current instance data to the temporary in-memory cache */ public function process_info_details_mod_instance($data) { $this->currentmod['instances'][$data['id']] = $data; } /** * Stashes the backup info for later processing by {@link moodle1_root_handler} */ public function on_info_details_mod_end($data) { global $CFG; // keep only such modules that seem to have the support for moodle1 implemented $modname = $this->currentmod['name']; if (file_exists($CFG->dirroot.'/mod/'.$modname.'/backup/moodle1/lib.php')) { $this->converter->set_stash('modinfo_'.$modname, $this->currentmod); $this->modnames[] = $modname; } else { $this->log('unsupported activity module', backup::LOG_WARNING, $modname); } $this->currentmod = array(); } /** * Stashes the list of activity module types for later processing by {@link moodle1_root_handler} */ public function on_info_details_end() { $this->converter->set_stash('modnameslist', $this->modnames); } } /** * Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths */ class moodle1_course_header_handler extends moodle1_xml_handler { /** @var array we need to merge course information because it is dispatched twice */ protected $course = array(); /** @var array we need to merge course information because it is dispatched twice */ protected $courseraw = array(); /** @var array */ protected $category; public function get_paths() { return array( new convert_path( 'course_header', '/MOODLE_BACKUP/COURSE/HEADER', array( 'newfields' => array( 'summaryformat' => 1, 'legacyfiles' => 2, 'requested' => 0, // @todo not really new, but maybe never backed up? 'restrictmodules' => 0, 'enablecompletion' => 0, 'completionstartonenrol' => 0, 'completionnotify' => 0, 'tags' => array(), 'allowed_modules' => array(), ), 'dropfields' => array( 'roles_overrides', 'roles_assignments', 'cost', 'currancy', 'defaultrole', 'enrol', 'enrolenddate', 'enrollable', 'enrolperiod', 'enrolstartdate', 'expirynotify', 'expirythreshold', 'guest', 'notifystudents', 'password', 'student', 'students', 'teacher', 'teachers', 'metacourse', ) ) ), new convert_path( 'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY', array( 'newfields' => array( 'description' => null, ) ) ), ); } /** * Because there is the CATEGORY branch in the middle of the COURSE/HEADER * branch, this is dispatched twice. We use $this->coursecooked to merge * the result. Once the parser is fixed, it can be refactored. */ public function process_course_header($data, $raw) { $this->course = array_merge($this->course, $data); $this->courseraw = array_merge($this->courseraw, $raw); } public function process_course_header_category($data) { $this->category = $data; } public function on_course_header_end() { $contextid = $this->converter->get_contextid(CONTEXT_COURSE); // stash the information needed by other handlers $info = array( 'original_course_id' => $this->course['id'], 'original_course_fullname' => $this->course['fullname'], 'original_course_shortname' => $this->course['shortname'], 'original_course_startdate' => $this->course['startdate'], 'original_course_contextid' => $contextid ); $this->converter->set_stash('original_course_info', $info); $this->course['contextid'] = $contextid; $this->course['category'] = $this->category; // migrate files embedded into the course summary and stash their ids $fileman = $this->converter->get_file_manager($contextid, 'course', 'summary'); $this->course['summary'] = moodle1_converter::migrate_referenced_files($this->course['summary'], $fileman); $this->converter->set_stash('course_summary_files_ids', $fileman->get_fileids()); // write course.xml $this->open_xml_writer('course/course.xml'); $this->write_xml('course', $this->course, array('/course/id', '/course/contextid')); $this->close_xml_writer(); } } /** * Handles the conversion of course sections and course modules */ class moodle1_course_outline_handler extends moodle1_xml_handler { /** @var array ordered list of the course contents */ protected $coursecontents = array(); /** @var array current section data */ protected $currentsection; /** * This handler is interested in course sections and course modules within them */ public function get_paths() { return array( new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'), new convert_path( 'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION', array( 'newfields' => array( 'name' => null, 'summaryformat' => 1, 'sequence' => null, ), ) ), new convert_path( 'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD', array( 'newfields' => array( 'completion' => 0, 'completiongradeitemnumber' => null, 'completionview' => 0, 'completionexpected' => 0, 'availability' => null, 'visibleold' => 1, 'showdescription' => 0, ), 'dropfields' => array( 'instance', 'roles_overrides', 'roles_assignments', ), 'renamefields' => array( 'type' => 'modulename', ), ) ), new convert_path('course_modules', '/MOODLE_BACKUP/COURSE/MODULES'), // todo new convert_path('course_module_roles_overrides', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'), // todo new convert_path('course_module_roles_assignments', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_ASSIGNMENTS'), ); } public function process_course_section($data) { $this->currentsection = $data; } /** * Populates the section sequence field (order of course modules) and stashes the * course module info so that is can be dumped to activities/xxxx_x/module.xml later */ public function process_course_module($data, $raw) { global $CFG; // check that this type of module should be included in the mbz $modinfo = $this->converter->get_stash_itemids('modinfo_'.$data['modulename']); if (empty($modinfo)) { return; } // add the course module into the course contents list $this->coursecontents[$data['id']] = array( 'cmid' => $data['id'], 'instanceid' => $raw['INSTANCE'], 'sectionid' => $this->currentsection['id'], 'modulename' => $data['modulename'], 'title' => null ); // add the course module id into the section's sequence if (is_null($this->currentsection['sequence'])) { $this->currentsection['sequence'] = $data['id']; } else { $this->currentsection['sequence'] .= ',' . $data['id']; } // add the sectionid and sectionnumber $data['sectionid'] = $this->currentsection['id']; $data['sectionnumber'] = $this->currentsection['number']; // generate the module version - this is a bit tricky as this information // is not present in 1.9 backups. we will use the currently installed version // whenever we can but that might not be accurate for some modules. // also there might be problem with modules that are not present at the target // host... $versionfile = $CFG->dirroot.'/mod/'.$data['modulename'].'/version.php'; if (file_exists($versionfile)) { $plugin = new stdClass(); $plugin->version = null; $module = $plugin; include($versionfile); // Have to hardcode - since quiz uses some hardcoded version numbers when restoring. // This is the lowest number used minus one. $data['version'] = 2011010099; } else { $data['version'] = null; } // stash the course module info in stashes like 'cminfo_forum' with // itemid set to the instance id. this is needed so that module handlers // can later obtain information about the course module and dump it into // the module.xml file $this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']); } /** * Writes sections/section_xxx/section.xml file and stashes it, too */ public function on_course_section_end() { // migrate files embedded into the section summary field $contextid = $this->converter->get_contextid(CONTEXT_COURSE); $fileman = $this->converter->get_file_manager($contextid, 'course', 'section', $this->currentsection['id']); $this->currentsection['summary'] = moodle1_converter::migrate_referenced_files($this->currentsection['summary'], $fileman); // write section's inforef.xml with the file references $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/inforef.xml'); $this->xmlwriter->begin_tag('inforef'); $this->xmlwriter->begin_tag('fileref'); $fileids = $fileman->get_fileids(); if (is_array($fileids)) { foreach ($fileids as $fileid) { $this->write_xml('file', array('id' => $fileid)); } } $this->xmlwriter->end_tag('fileref'); $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); // stash the section info and write section.xml $this->converter->set_stash('sectioninfo', $this->currentsection, $this->currentsection['id']); $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml'); $this->write_xml('section', $this->currentsection); $this->close_xml_writer(); unset($this->currentsection); } /** * Stashes the course contents */ public function on_course_sections_end() { $this->converter->set_stash('coursecontents', $this->coursecontents); } /** * Writes the information collected by mod handlers */ public function on_course_modules_end() { foreach ($this->converter->get_stash('modnameslist') as $modname) { $modinfo = $this->converter->get_stash('modinfo_'.$modname); foreach ($modinfo['instances'] as $modinstanceid => $modinstance) { $cminfo = $this->converter->get_stash('cminfo_'.$modname, $modinstanceid); $directory = 'activities/'.$modname.'_'.$cminfo['id']; // write module.xml $this->open_xml_writer($directory.'/module.xml'); $this->write_xml('module', $cminfo, array('/module/id', '/module/version')); $this->close_xml_writer(); // write grades.xml $this->open_xml_writer($directory.'/grades.xml'); $this->xmlwriter->begin_tag('activity_gradebook'); $gradeitems = $this->converter->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array()); if (!empty($gradeitems)) { $this->xmlwriter->begin_tag('grade_items'); foreach ($gradeitems as $gradeitem) { $this->write_xml('grade_item', $gradeitem, array('/grade_item/id')); } $this->xmlwriter->end_tag('grade_items'); } $this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9 $this->xmlwriter->end_tag('activity_gradebook'); $this->close_xml_writer(); // todo: write proper roles.xml, for now we just make sure the file is present $this->make_sure_xml_exists($directory.'/roles.xml', 'roles'); } } } } /** * Handles the conversion of the defined roles */ class moodle1_roles_definition_handler extends moodle1_xml_handler { /** * Where the roles are defined in the source moodle.xml */ public function get_paths() { return array( new convert_path('roles', '/MOODLE_BACKUP/ROLES'), new convert_path( 'roles_role', '/MOODLE_BACKUP/ROLES/ROLE', array( 'newfields' => array( 'description' => '', 'sortorder' => 0, 'archetype' => '' ) ) ) ); } /** * If there are any roles defined in moodle.xml, convert them to roles.xml */ public function process_roles_role($data) { if (!$this->has_xml_writer()) { $this->open_xml_writer('roles.xml'); $this->xmlwriter->begin_tag('roles_definition'); } if (!isset($data['nameincourse'])) { $data['nameincourse'] = null; } $this->write_xml('role', $data, array('role/id')); } /** * Finishes writing roles.xml */ public function on_roles_end() { if (!$this->has_xml_writer()) { // no roles defined in moodle.xml so {link self::process_roles_role()} // was never executed $this->open_xml_writer('roles.xml'); $this->write_xml('roles_definition', array()); } else { // some roles were dumped into the file, let us close their wrapper now $this->xmlwriter->end_tag('roles_definition'); } $this->close_xml_writer(); } } /** * Handles the conversion of the question bank included in the moodle.xml file */ class moodle1_question_bank_handler extends moodle1_xml_handler { /** @var array the current question category being parsed */ protected $currentcategory = null; /** @var array of the raw data for the current category */ protected $currentcategoryraw = null; /** @var moodle1_file_manager instance used to convert question images */ protected $fileman = null; /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */ private $currentcategorywritten = false; /** @var bool was the <questions> tag already written (work around MDL-27693) */ private $questionswrapperwritten = false; /** @var array holds the instances of qtype specific conversion handlers */ private $qtypehandlers = null; /** * Return the file manager instance used. * * @return moodle1_file_manager */ public function get_file_manager() { return $this->fileman; } /** * Returns the information about the question category context being currently parsed * * @return array with keys contextid, contextlevel and contextinstanceid */ public function get_current_category_context() { return $this->currentcategory; } /** * Registers path that are not qtype-specific */ public function get_paths() { $paths = array( new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'), new convert_path( 'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY', array( 'newfields' => array( 'infoformat' => 0 ) )), new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'), new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'), // the question element must be grouped so we can re-dispatch it to the qtype handler as a whole new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true), ); // annotate all question subpaths required by the qtypes subplugins $subpaths = array(); foreach ($this->get_qtype_handler('*') as $qtypehandler) { foreach ($qtypehandler->get_question_subpaths() as $subpath) { $subpaths[$subpath] = true; } } foreach (array_keys($subpaths) as $subpath) { $name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath)); $path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath; $paths[] = new convert_path($name, $path); } return $paths; } /** * Starts writing questions.xml and prepares the file manager instance */ public function on_question_categories_start() { $this->open_xml_writer('questions.xml'); $this->xmlwriter->begin_tag('question_categories'); if (is_null($this->fileman)) { $this->fileman = $this->converter->get_file_manager(); } } /** * Initializes the current category cache */ public function on_question_category_start() { $this->currentcategory = array(); $this->currentcategoryraw = array(); $this->currentcategorywritten = false; $this->questionswrapperwritten = false; } /** * Populates the current question category data * * Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually * called twice for both halves of the data. We merge them here into the currentcategory array. */ public function process_question_category($data, $raw) { $this->currentcategory = array_merge($this->currentcategory, $data); $this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw); } /** * Inject the context related information into the current category */ public function process_question_category_context($data) { switch ($data['level']) { case 'module': $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']); $this->currentcategory['contextlevel'] = CONTEXT_MODULE; $this->currentcategory['contextinstanceid'] = $data['instance']; break; case 'course': $originalcourseinfo = $this->converter->get_stash('original_course_info'); $originalcourseid = $originalcourseinfo['original_course_id']; $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSE); $this->currentcategory['contextlevel'] = CONTEXT_COURSE; $this->currentcategory['contextinstanceid'] = $originalcourseid; break; case 'coursecategory': // this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance // of the course category (1 = parent category, 2 = grand-parent category etc). We pretend // that this level*10 is the id of that category and create an artifical contextid for it $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSECAT, $data['coursecategorylevel'] * 10); $this->currentcategory['contextlevel'] = CONTEXT_COURSECAT; $this->currentcategory['contextinstanceid'] = $data['coursecategorylevel'] * 10; break; case 'system': $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM); $this->currentcategory['contextlevel'] = CONTEXT_SYSTEM; $this->currentcategory['contextinstanceid'] = 0; break; } } /** * Writes the common <question> data and re-dispateches the whole grouped * <QUESTION> data to the qtype for appending its qtype specific data processing * * @param array $data * @param array $raw * @return array */ public function process_question(array $data, array $raw) { global $CFG; // firstly make sure that the category data and the <questions> wrapper are written // note that because of MDL-27693 we can't use {@link self::process_question_category()} // and {@link self::on_questions_start()} to do so if (empty($this->currentcategorywritten)) { $this->xmlwriter->begin_tag('question_category', array('id' => $this->currentcategory['id'])); foreach ($this->currentcategory as $name => $value) { if ($name === 'id') { continue; } $this->xmlwriter->full_tag($name, $value); } $this->currentcategorywritten = true; } if (empty($this->questionswrapperwritten)) { $this->xmlwriter->begin_tag('questions'); $this->questionswrapperwritten = true; } $qtype = $data['qtype']; // replay the upgrade step 2008050700 {@see question_fix_random_question_parents()} if ($qtype == 'random' and $data['parent'] <> $data['id']) { $data['parent'] = $data['id']; } // replay the upgrade step 2010080900 and part of 2010080901 $data['generalfeedbackformat'] = $data['questiontextformat']; $data['oldquestiontextformat'] = $data['questiontextformat']; if ($CFG->texteditors !== 'textarea') { $data['questiontext'] = text_to_html($data['questiontext'], false, false, true); $data['questiontextformat'] = FORMAT_HTML; $data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true); $data['generalfeedbackformat'] = FORMAT_HTML; } // Migrate files in questiontext. $this->fileman->contextid = $this->currentcategory['contextid']; $this->fileman->component = 'question'; $this->fileman->filearea = 'questiontext'; $this->fileman->itemid = $data['id']; $data['questiontext'] = moodle1_converter::migrate_referenced_files($data['questiontext'], $this->fileman); // Migrate files in generalfeedback. $this->fileman->filearea = 'generalfeedback'; $data['generalfeedback'] = moodle1_converter::migrate_referenced_files($data['generalfeedback'], $this->fileman); // replay the upgrade step 2010080901 - updating question image if (!empty($data['image'])) { if (core_text::substr(core_text::strtolower($data['image']), 0, 7) == 'http://') { // it is a link, appending to existing question text $data['questiontext'] .= ' <img src="' . $data['image'] . '" />'; } else { // it is a file in course_files $filename = basename($data['image']); $filepath = dirname($data['image']); if (empty($filepath) or $filepath == '.' or $filepath == '/') { $filepath = '/'; } else { // append / $filepath = '/'.trim($filepath, './@#$ ').'/'; } if (file_exists($this->converter->get_tempdir_path().'/course_files'.$filepath.$filename)) { $this->fileman->contextid = $this->currentcategory['contextid']; $this->fileman->component = 'question'; $this->fileman->filearea = 'questiontext'; $this->fileman->itemid = $data['id']; $this->fileman->migrate_file('course_files'.$filepath.$filename, '/', $filename); // note this is slightly different from the upgrade code as we put the file into the // root folder here. this makes our life easier as we do not need to create all the // directories within the specified filearea/itemid $data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />'; } else { $this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename)); } } } unset($data['image']); // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark $data['defaultmark'] = $data['defaultgrade']; // write the common question data $this->xmlwriter->begin_tag('question', array('id' => $data['id'])); foreach (array( 'parent', 'name', 'questiontext', 'questiontextformat', 'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified', 'createdby', 'modifiedby' ) as $fieldname) { if (!array_key_exists($fieldname, $data)) { throw new moodle1_convert_exception('missing_common_question_field', $fieldname); } $this->xmlwriter->full_tag($fieldname, $data[$fieldname]); } // unless we know that the given qtype does not append any own structures, // give the handler a chance to do so now if (!in_array($qtype, array('description', 'random'))) { $handler = $this->get_qtype_handler($qtype); if ($handler === false) { $this->log('question type converter not found', backup::LOG_ERROR, $qtype); } else { $this->xmlwriter->begin_tag('plugin_qtype_'.$qtype.'_question'); $handler->use_xml_writer($this->xmlwriter); $handler->process_question($data, $raw); $this->xmlwriter->end_tag('plugin_qtype_'.$qtype.'_question'); } } $this->xmlwriter->end_tag('question'); } /** * Closes the questions wrapper */ public function on_questions_end() { if ($this->questionswrapperwritten) { $this->xmlwriter->end_tag('questions'); } } /** * Closes the question_category and annotates the category id * so that it can be dumped into course/inforef.xml */ public function on_question_category_end() { // make sure that the category data were written by {@link self::process_question()} // if not, write it now. this may happen when the current category does not contain any // questions so the subpaths is missing completely if (empty($this->currentcategorywritten)) { $this->write_xml('question_category', $this->currentcategory, array('/question_category/id')); } else { $this->xmlwriter->end_tag('question_category'); } $this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']); } /** * Stops writing questions.xml */ public function on_question_categories_end() { $this->xmlwriter->end_tag('question_categories'); $this->close_xml_writer(); } /** * Provides access to the qtype handlers * * Returns either list of all qtype handler instances (if passed '*') or a particular handler * for the given qtype or false if the qtype is not supported. * * @throws moodle1_convert_exception * @param string $qtype the name of the question type or '*' for returning all * @return array|moodle1_qtype_handler|bool */ protected function get_qtype_handler($qtype) { if (is_null($this->qtypehandlers)) { // initialize the list of qtype handler instances $this->qtypehandlers = array(); foreach (core_component::get_plugin_list('qtype') as $qtypename => $qtypelocation) { $filename = $qtypelocation.'/backup/moodle1/lib.php'; if (file_exists($filename)) { $classname = 'moodle1_qtype_'.$qtypename.'_handler'; require_once($filename); if (!class_exists($classname)) { throw new moodle1_convert_exception('missing_handler_class', $classname); } $this->log('registering handler', backup::LOG_DEBUG, $classname, 2); $this->qtypehandlers[$qtypename] = new $classname($this, $qtypename); } } } if ($qtype === '*') { return $this->qtypehandlers; } else if (isset($this->qtypehandlers[$qtype])) { return $this->qtypehandlers[$qtype]; } else { return false; } } } /** * Handles the conversion of the scales included in the moodle.xml file */ class moodle1_scales_handler extends moodle1_handler { /** @var moodle1_file_manager instance used to convert question images */ protected $fileman = null; /** * Registers paths */ public function get_paths() { return array( new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'), new convert_path( 'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE', array( 'renamefields' => array( 'scaletext' => 'scale', ), 'addfields' => array( 'descriptionformat' => 0, ) ) ), ); } /** * Prepare the file manager for the files embedded in the scale description field */ public function on_scales_start() { $syscontextid = $this->converter->get_contextid(CONTEXT_SYSTEM); $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'scale'); } /** * This is executed every time we have one <SCALE> data available * * @param array $data * @param array $raw * @return array */ public function process_scale(array $data, array $raw) { global $CFG; // replay upgrade step 2009110400 if ($CFG->texteditors !== 'textarea') { $data['description'] = text_to_html($data['description'], false, false, true); $data['descriptionformat'] = FORMAT_HTML; } // convert course files embedded into the scale description field $this->fileman->itemid = $data['id']; $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman); // stash the scale $this->converter->set_stash('scales', $data, $data['id']); } } /** * Handles the conversion of the outcomes */ class moodle1_outcomes_handler extends moodle1_xml_handler { /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */ protected $fileman = null; /** * Registers paths */ public function get_paths() { return array( new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'), new convert_path( 'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME', array( 'addfields' => array( 'descriptionformat' => FORMAT_MOODLE, ), ) ), ); } /** * Prepares the file manager and starts writing outcomes.xml */ public function on_gradebook_grade_outcomes_start() { $syscontextid = $this->converter->get_contextid(CONTEXT_SYSTEM); $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome'); $this->open_xml_writer('outcomes.xml'); $this->xmlwriter->begin_tag('outcomes_definition'); } /** * Processes GRADE_OUTCOME tags progressively */ public function process_gradebook_grade_outcome(array $data, array $raw) { global $CFG; // replay the upgrade step 2009110400 if ($CFG->texteditors !== 'textarea') { $data['description'] = text_to_html($data['description'], false, false, true); $data['descriptionformat'] = FORMAT_HTML; } // convert course files embedded into the outcome description field $this->fileman->itemid = $data['id']; $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman); // write the outcome data $this->write_xml('outcome', $data, array('/outcome/id')); return $data; } /** * Closes outcomes.xml */ public function on_gradebook_grade_outcomes_end() { $this->xmlwriter->end_tag('outcomes_definition'); $this->close_xml_writer(); } } /** * Handles the conversion of the gradebook structures in the moodle.xml file */ class moodle1_gradebook_handler extends moodle1_xml_handler { /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */ protected $categoryparent = array(); /** * Registers paths */ public function get_paths() { return array( new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'), new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'), new convert_path( 'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY', array( 'addfields' => array( 'hidden' => 0, // upgrade step 2010011200 ), ) ), new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'), new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'), ); } /** * Initializes the in-memory structures * * This should not be needed actually as the moodle.xml contains just one GRADEBOOK * element. But who knows - maybe someone will want to write a mass conversion * tool in the future (not me definitely ;-) */ public function on_gradebook_start() { $this->categoryparent = array(); } /** * Processes one GRADE_LETTER data * * In Moodle 1.9, all grade_letters are from course context only. Therefore * we put them here. */ public function process_gradebook_grade_letter(array $data, array $raw) { $this->converter->set_stash('gradebook_gradeletter', $data, $data['id']); } /** * Processes one GRADE_CATEGORY data */ public function process_gradebook_grade_category(array $data, array $raw) { $this->categoryparent[$data['id']] = $data['parent']; $this->converter->set_stash('gradebook_gradecategory', $data, $data['id']); } /** * Processes one GRADE_ITEM data */ public function process_gradebook_grade_item(array $data, array $raw) { // here we use get_nextid() to get a nondecreasing sequence $data['sortorder'] = $this->converter->get_nextid(); if ($data['itemtype'] === 'mod') { return $this->process_mod_grade_item($data, $raw); } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) { return $this->process_nonmod_grade_item($data, $raw); } else { $this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']); } } /** * Processes one GRADE_ITEM of the type 'mod' */ protected function process_mod_grade_item(array $data, array $raw) { $stashname = 'gradebook_modgradeitem_'.$data['itemmodule']; $stashitemid = $data['iteminstance']; $gradeitems = $this->converter->get_stash_or_default($stashname, $stashitemid, array()); // typically there will be single item with itemnumber 0 $gradeitems[$data['itemnumber']] = $data; $this->converter->set_stash($stashname, $gradeitems, $stashitemid); return $data; } /** * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category' */ protected function process_nonmod_grade_item(array $data, array $raw) { $stashname = 'gradebook_nonmodgradeitem'; $stashitemid = $data['id']; $this->converter->set_stash($stashname, $data, $stashitemid); return $data; } /** * @todo */ public function on_gradebook_grade_item_grades_start() { } /** * Writes the collected information into gradebook.xml */ public function on_gradebook_end() { $this->open_xml_writer('gradebook.xml'); $this->xmlwriter->begin_tag('gradebook'); $this->write_grade_categories(); $this->write_grade_items(); $this->write_grade_letters(); $this->xmlwriter->end_tag('gradebook'); $this->close_xml_writer(); } /** * Writes grade_categories */ protected function write_grade_categories() { $this->xmlwriter->begin_tag('grade_categories'); foreach ($this->converter->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) { $gradecategory = $this->converter->get_stash('gradebook_gradecategory', $gradecategoryid); $path = $this->calculate_category_path($gradecategoryid); $gradecategory['depth'] = count($path); $gradecategory['path'] = '/'.implode('/', $path).'/'; $this->write_xml('grade_category', $gradecategory, array('/grade_category/id')); } $this->xmlwriter->end_tag('grade_categories'); } /** * Calculates the path to the grade_category * * Moodle 1.9 backup does not store the grade_category's depth and path. This method is used * to repopulate this information using the $this->categoryparent values. * * @param int $categoryid * @return array of ids including the categoryid */ protected function calculate_category_path($categoryid) { if (!array_key_exists($categoryid, $this->categoryparent)) { throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid); } $path = array($categoryid); $parent = $this->categoryparent[$categoryid]; while (!is_null($parent)) { array_unshift($path, $parent); $parent = $this->categoryparent[$parent]; if (in_array($parent, $path)) { throw new moodle1_convert_exception('circular_reference_in_categories_tree'); } } return $path; } /** * Writes grade_items */ protected function write_grade_items() { $this->xmlwriter->begin_tag('grade_items'); foreach ($this->converter->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) { $gradeitem = $this->converter->get_stash('gradebook_nonmodgradeitem', $gradeitemid); $this->write_xml('grade_item', $gradeitem, array('/grade_item/id')); } $this->xmlwriter->end_tag('grade_items'); } /** * Writes grade_letters */ protected function write_grade_letters() { $this->xmlwriter->begin_tag('grade_letters'); foreach ($this->converter->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) { $gradeletter = $this->converter->get_stash('gradebook_gradeletter', $gradeletterid); $this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id')); } $this->xmlwriter->end_tag('grade_letters'); } } /** * Shared base class for activity modules, blocks and qtype handlers */ abstract class moodle1_plugin_handler extends moodle1_xml_handler { /** @var string */ protected $plugintype; /** @var string */ protected $pluginname; /** * @param moodle1_converter $converter the converter that requires us * @param string $plugintype * @param string $pluginname */ public function __construct(moodle1_converter $converter, $plugintype, $pluginname) { parent::__construct($converter); $this->plugintype = $plugintype; $this->pluginname = $pluginname; } /** * Returns the normalized name of the plugin, eg mod_workshop * * @return string */ public function get_component_name() { return $this->plugintype.'_'.$this->pluginname; } } /** * Base class for all question type handlers */ abstract class moodle1_qtype_handler extends moodle1_plugin_handler { /** @var moodle1_question_bank_handler */ protected $qbankhandler; /** * Returns the list of paths within one <QUESTION> that this qtype needs to have included * in the grouped question structure * * @return array of strings */ public function get_question_subpaths() { return array(); } /** * Gives the qtype handler a chance to write converted data into questions.xml * * @param array $data grouped question data * @param array $raw grouped raw QUESTION data */ public function process_question(array $data, array $raw) { } /** * Converts the answers and writes them into the questions.xml * * The structure "answers" is used by several qtypes. It contains data from {question_answers} table. * * @param array $answers as parsed by the grouped parser in moodle.xml * @param string $qtype containing the answers */ protected function write_answers(array $answers, $qtype) { $this->xmlwriter->begin_tag('answers'); foreach ($answers as $elementname => $elements) { foreach ($elements as $element) { $answer = $this->convert_answer($element, $qtype); // Migrate images in answertext. if ($answer['answerformat'] == FORMAT_HTML) { $answer['answertext'] = $this->migrate_files($answer['answertext'], 'question', 'answer', $answer['id']); } // Migrate images in feedback. if ($answer['feedbackformat'] == FORMAT_HTML) { $answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']); } $this->write_xml('answer', $answer, array('/answer/id')); } } $this->xmlwriter->end_tag('answers'); } /** * Migrate files belonging to one qtype plugin text field. * * @param array $text the html fragment containing references to files * @param string $component the component for restored files * @param string $filearea the file area for restored files * @param int $itemid the itemid for restored files * * @return string the text for this field, after files references have been processed */ protected function migrate_files($text, $component, $filearea, $itemid) { $context = $this->qbankhandler->get_current_category_context(); $fileman = $this->qbankhandler->get_file_manager(); $fileman->contextid = $context['contextid']; $fileman->component = $component; $fileman->filearea = $filearea; $fileman->itemid = $itemid; $text = moodle1_converter::migrate_referenced_files($text, $fileman); return $text; } /** * Writes the grouped numerical_units structure * * @param array $numericalunits */ protected function write_numerical_units(array $numericalunits) { $this->xmlwriter->begin_tag('numerical_units'); foreach ($numericalunits as $elementname => $elements) { foreach ($elements as $element) { $element['id'] = $this->converter->get_nextid(); $this->write_xml('numerical_unit', $element, array('/numerical_unit/id')); } } $this->xmlwriter->end_tag('numerical_units'); } /** * Writes the numerical_options structure * * @see get_default_numerical_options() * @param array $numericaloption */ protected function write_numerical_options(array $numericaloption) { $this->xmlwriter->begin_tag('numerical_options'); if (!empty($numericaloption)) { $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id')); } $this->xmlwriter->end_tag('numerical_options'); } /** * Returns default numerical_option structure * * This structure is not present in moodle.xml, we create a new artificial one here. * * @see write_numerical_options() * @param int $oldquestiontextformat * @return array */ protected function get_default_numerical_options($oldquestiontextformat, $units) { global $CFG; // replay the upgrade step 2009100100 - new table $options = array( 'id' => $this->converter->get_nextid(), 'instructions' => null, 'instructionsformat' => 0, 'showunits' => 0, 'unitsleft' => 0, 'unitgradingtype' => 0, 'unitpenalty' => 0.1 ); // replay the upgrade step 2009100101 if ($CFG->texteditors !== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE) { $options['instructionsformat'] = FORMAT_HTML; } else { $options['instructionsformat'] = $oldquestiontextformat; } // Set a good default, depending on whether there are any units defined. if (empty($units)) { $options['showunits'] = 3; } return $options; } /** * Writes the dataset_definitions structure * * @param array $datasetdefinitions array of dataset_definition structures */ protected function write_dataset_definitions(array $datasetdefinitions) { $this->xmlwriter->begin_tag('dataset_definitions'); foreach ($datasetdefinitions as $datasetdefinition) { $this->xmlwriter->begin_tag('dataset_definition', array('id' => $this->converter->get_nextid())); foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) { $this->xmlwriter->full_tag($element, $datasetdefinition[$element]); } $this->xmlwriter->begin_tag('dataset_items'); if (!empty($datasetdefinition['dataset_items']['dataset_item'])) { foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) { $datasetitem['id'] = $this->converter->get_nextid(); $this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id')); } } $this->xmlwriter->end_tag('dataset_items'); $this->xmlwriter->end_tag('dataset_definition'); } $this->xmlwriter->end_tag('dataset_definitions'); } /// implementation details follow ////////////////////////////////////////// public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) { parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype); $this->qbankhandler = $qbankhandler; } /** * @see self::get_question_subpaths() */ final public function get_paths() { throw new moodle1_convert_exception('qtype_handler_get_paths'); } /** * Question type handlers cannot open the xml_writer */ final protected function open_xml_writer($filename) { throw new moodle1_convert_exception('opening_xml_writer_forbidden'); } /** * Question type handlers cannot close the xml_writer */ final protected function close_xml_writer() { throw new moodle1_convert_exception('opening_xml_writer_forbidden'); } /** * Provides a xml_writer instance to this qtype converter * * @param xml_writer $xmlwriter */ public function use_xml_writer(xml_writer $xmlwriter) { $this->xmlwriter = $xmlwriter; } /** * Converts <ANSWER> structure into the new <answer> one * * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0 * * @param array $old the parsed answer array in moodle.xml * @param string $qtype the question type the answer is part of * @return array */ private function convert_answer(array $old, $qtype) { global $CFG; $new = array(); $new['id'] = $old['id']; $new['answertext'] = $old['answer_text']; $new['answerformat'] = 0; // upgrade step 2010080900 $new['fraction'] = $old['fraction']; $new['feedback'] = $old['feedback']; $new['feedbackformat'] = 0; // upgrade step 2010080900 // replay upgrade step 2010080901 if ($qtype !== 'multichoice') { $new['answerformat'] = FORMAT_PLAIN; } else { $new['answertext'] = text_to_html($new['answertext'], false, false, true); $new['answerformat'] = FORMAT_HTML; } if ($CFG->texteditors !== 'textarea') { if ($qtype == 'essay') { $new['feedback'] = text_to_html($new['feedback'], false, false, true); } $new['feedbackformat'] = FORMAT_HTML; } else { $new['feedbackformat'] = FORMAT_MOODLE; } return $new; } } /** * Base class for activity module handlers */ abstract class moodle1_mod_handler extends moodle1_plugin_handler { /** * Returns the name of the module, eg. 'forum' * * @return string */ public function get_modname() { return $this->pluginname; } /** * Returns course module information for the given instance id * * The information for this instance id has been stashed by * {@link moodle1_course_outline_handler::process_course_module()} * * @param int $instance the module instance id * @param string $modname the module type, defaults to $this->pluginname * @return int */ protected function get_cminfo($instance, $modname = null) { if (is_null($modname)) { $modname = $this->pluginname; } return $this->converter->get_stash('cminfo_'.$modname, $instance); } } /** * Base class for all modules that are successors of the 1.9 resource module */ abstract class moodle1_resource_successor_handler extends moodle1_mod_handler { /** * Resource successors do not attach to paths themselves, they are called explicitely * by moodle1_mod_resource_handler * * @return array */ final public function get_paths() { return array(); } /** * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data * * Called by {@link moodle1_mod_resource_handler::process_resource()} * * @param array $data pre-cooked legacy resource data * @param array $raw raw legacy resource data */ public function process_legacy_resource(array $data, array $raw = null) { } /** * Called when the parses reaches the end </MOD> resource tag * * @param array $data the data returned by {@link self::process_resource} or just pre-cooked */ public function on_legacy_resource_end(array $data) { } } /** * Base class for block handlers */ abstract class moodle1_block_handler extends moodle1_plugin_handler { public function get_paths() { $blockname = strtoupper($this->pluginname); return array( new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"), ); } public function process_block(array $data) { $newdata = $this->convert_common_block_data($data); $this->write_block_xml($newdata, $data); $this->write_inforef_xml($newdata, $data); $this->write_roles_xml($newdata, $data); return $data; } protected function convert_common_block_data(array $olddata) { $newdata = array(); $newdata['blockname'] = $olddata['name']; $newdata['parentcontextid'] = $this->converter->get_contextid(CONTEXT_COURSE, 0); $newdata['showinsubcontexts'] = 0; $newdata['pagetypepattern'] = $olddata['pagetype'].='-*'; $newdata['subpagepattern'] = null; $newdata['defaultregion'] = ($olddata['position']=='l')?'side-pre':'side-post'; $newdata['defaultweight'] = $olddata['weight']; $newdata['configdata'] = $this->convert_configdata($olddata); return $newdata; } protected function convert_configdata(array $olddata) { return $olddata['configdata']; } protected function write_block_xml($newdata, $data) { $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']); $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/block.xml"); $this->xmlwriter->begin_tag('block', array('id' => $data['id'], 'contextid' => $contextid)); foreach ($newdata as $field => $value) { $this->xmlwriter->full_tag($field, $value); } $this->xmlwriter->begin_tag('block_positions'); $this->xmlwriter->begin_tag('block_position', array('id' => 1)); $this->xmlwriter->full_tag('contextid', $newdata['parentcontextid']); $this->xmlwriter->full_tag('pagetype', $data['pagetype']); $this->xmlwriter->full_tag('subpage', ''); $this->xmlwriter->full_tag('visible', $data['visible']); $this->xmlwriter->full_tag('region', $newdata['defaultregion']); $this->xmlwriter->full_tag('weight', $newdata['defaultweight']); $this->xmlwriter->end_tag('block_position'); $this->xmlwriter->end_tag('block_positions'); $this->xmlwriter->end_tag('block'); $this->close_xml_writer(); } protected function write_inforef_xml($newdata, $data) { $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml"); $this->xmlwriter->begin_tag('inforef'); // Subclasses may provide inforef contents if needed $this->xmlwriter->end_tag('inforef'); $this->close_xml_writer(); } protected function write_roles_xml($newdata, $data) { // This is an empty shell, as the moodle1 converter doesn't handle user data. $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/roles.xml"); $this->xmlwriter->begin_tag('roles'); $this->xmlwriter->full_tag('role_overrides', ''); $this->xmlwriter->full_tag('role_assignments', ''); $this->xmlwriter->end_tag('roles'); $this->close_xml_writer(); } } /** * Base class for block generic handler */ class moodle1_block_generic_handler extends moodle1_block_handler { } /** * Base class for the activity modules' subplugins */ abstract class moodle1_submod_handler extends moodle1_plugin_handler { /** @var moodle1_mod_handler */ protected $parenthandler; /** * @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of * @param string $subplugintype the type of the subplugin * @param string $subpluginname the name of the subplugin */ public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) { $this->parenthandler = $parenthandler; parent::__construct($parenthandler->converter, $subplugintype, $subpluginname); } /** * Activity module subplugins can't declare any paths to handle * * The paths must be registered by the parent module and then re-dispatched to the * relevant subplugins for eventual processing. * * @return array empty array */ final public function get_paths() { return array(); } } PK ��/[.�j~ moodle1/tests/fixtures/icon.gifnu &1i� GIF89a � ̙f �����̙f3��� !� , D��/�I��E�ݺ�U�۠��0��Ց�t�yu>�@@�_�G�}4�mY�d�i#{ *њ�!� ;PK ��/[���� �� $ moodle1/tests/fixtures/questions.xmlnu &1i� <?xml version="1.0" encoding="UTF-8"?> <MOODLE_BACKUP> <INFO> <NAME>backup-questions19-20110603-2045.zip</NAME> <MOODLE_VERSION>2007101591.03</MOODLE_VERSION> <MOODLE_RELEASE>1.9.12+ (Build: 20110511)</MOODLE_RELEASE> <BACKUP_VERSION>2009111300</BACKUP_VERSION> <BACKUP_RELEASE>1.9.7</BACKUP_RELEASE> <DATE>1307126713</DATE> <ORIGINAL_WWWROOT>http://glum/~mudrd8mz/moodle19</ORIGINAL_WWWROOT> <ORIGINAL_SITE_IDENTIFIER_HASH>26f3fcb01f8a394b100261c626f94462</ORIGINAL_SITE_IDENTIFIER_HASH> <ZIP_METHOD>external</ZIP_METHOD> <DETAILS> <MOD> <NAME>forum</NAME> <INCLUDED>false</INCLUDED> <USERINFO>false</USERINFO> <INSTANCES> </INSTANCES> </MOD> <MOD> <NAME>quiz</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> <INSTANCES> <INSTANCE> <ID>5</ID> <NAME>Quiz 001</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> </INSTANCE> </INSTANCES> </MOD> <METACOURSE>false</METACOURSE> <USERS>none</USERS> <LOGS>false</LOGS> <USERFILES>false</USERFILES> <COURSEFILES>true</COURSEFILES> <SITEFILES>true</SITEFILES> <GRADEBOOKHISTORIES>false</GRADEBOOKHISTORIES> <MESSAGES>false</MESSAGES> <BLOGS>false</BLOGS> <BLOCKFORMAT>instances</BLOCKFORMAT> </DETAILS> </INFO> <ROLES> </ROLES> <COURSE> <HEADER> <ID>6</ID> <CATEGORY> <ID>1</ID> <NAME>Různé</NAME> </CATEGORY> <PASSWORD></PASSWORD> <FULLNAME>Course with Questions</FULLNAME> <SHORTNAME>QUESTIONS19</SHORTNAME> <IDNUMBER></IDNUMBER> <SUMMARY></SUMMARY> <FORMAT>weeks</FORMAT> <SHOWGRADES>1</SHOWGRADES> <NEWSITEMS>5</NEWSITEMS> <TEACHER>Teacher</TEACHER> <TEACHERS>Teachers</TEACHERS> <STUDENT>Student</STUDENT> <STUDENTS>Students</STUDENTS> <GUEST>0</GUEST> <STARTDATE>1306879200</STARTDATE> <NUMSECTIONS>10</NUMSECTIONS> <MAXBYTES>2097152</MAXBYTES> <SHOWREPORTS>0</SHOWREPORTS> <GROUPMODE>0</GROUPMODE> <GROUPMODEFORCE>0</GROUPMODEFORCE> <DEFAULTGROUPINGID>0</DEFAULTGROUPINGID> <LANG></LANG> <THEME></THEME> <COST></COST> <CURRENCY>USD</CURRENCY> <MARKER>0</MARKER> <VISIBLE>1</VISIBLE> <HIDDENSECTIONS>0</HIDDENSECTIONS> <TIMECREATED>1306830996</TIMECREATED> <TIMEMODIFIED>1306830996</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> </HEADER> <BLOCKS> <BLOCK> <ID>39</ID> <NAME>participants</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>0</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>40</ID> <NAME>activity_modules</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>1</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>41</ID> <NAME>search_forums</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>2</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>42</ID> <NAME>admin</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>3</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>43</ID> <NAME>course_list</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>4</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>44</ID> <NAME>news_items</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>0</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>45</ID> <NAME>calendar_upcoming</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>1</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>46</ID> <NAME>recent_activity</NAME> <PAGEID>6</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>2</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> </BLOCKS> <SECTIONS> <SECTION> <ID>51</ID> <NUMBER>0</NUMBER> <SUMMARY>$@NULL@$</SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>52</ID> <NUMBER>1</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> <MODS> <MOD> <ID>18</ID> <TYPE>quiz</TYPE> <INSTANCE>5</INSTANCE> <ADDED>1306833129</ADDED> <SCORE>0</SCORE> <INDENT>0</INDENT> <VISIBLE>1</VISIBLE> <GROUPMODE>0</GROUPMODE> <GROUPINGID>0</GROUPINGID> <GROUPMEMBERSONLY>0</GROUPMEMBERSONLY> <IDNUMBER></IDNUMBER> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </MOD> </MODS> </SECTION> <SECTION> <ID>53</ID> <NUMBER>2</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>54</ID> <NUMBER>3</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>55</ID> <NUMBER>4</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>56</ID> <NUMBER>5</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>57</ID> <NUMBER>6</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>58</ID> <NUMBER>7</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>59</ID> <NUMBER>8</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>60</ID> <NUMBER>9</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>61</ID> <NUMBER>10</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> </SECTIONS> <QUESTION_CATEGORIES> <QUESTION_CATEGORY> <ID>3</ID> <NAME>Course-category level category</NAME> <INFO></INFO> <CONTEXT> <LEVEL>coursecategory</LEVEL> <COURSECATEGORYLEVEL>1</COURSECATEGORYLEVEL> </CONTEXT> <STAMP>glum+110503130242+iB54x2</STAMP> <PARENT>0</PARENT> <SORTORDER>1</SORTORDER> <QUESTIONS> <QUESTION> <ID>9</ID> <PARENT>0</PARENT> <NAME>Essay</NAME> <QUESTIONTEXT>Essay text</QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>essay</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531084941+RkBCPq</STAMP> <VERSION>glum+110531084941+phvlnC</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831781</TIMECREATED> <TIMEMODIFIED>1306831781</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <ANSWERS> <ANSWER> <ID>6</ID> <ANSWER_TEXT></ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>4</ID> <NAME>System level category</NAME> <INFO></INFO> <CONTEXT> <LEVEL>system</LEVEL> </CONTEXT> <STAMP>glum+110503130242+uoZHVL</STAMP> <PARENT>0</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>11</ID> <PARENT>0</PARENT> <NAME>Cloze</NAME> <QUESTIONTEXT><pre>Match the following cities with the correct state: </pre> <ul> <li>San Francisco: {#1}<span style="font-family: monospace;"></span></li> <li><span style="font-family: monospace;"></span>Tucson: {#2}</li> <li>Los Angeles: {#3}<span style="font-family: monospace;"></span></li> <li><span style="font-family: monospace;"></span>Phoenix: {#4}<span style="font-family: monospace;"></span></li> <li><span style="font-family: monospace;"></span>The capital of France is {1:SHORTANSWER:%100%Paris#Congratulations!</li> </ul><pre>~%50%Marseille#No, that is the second largest city in France (after Paris).~*#Wrong answer. The capital of France is Paris, of course.}. </pre></QUESTIONTEXT> <QUESTIONTEXTFORMAT>0</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>4</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>multianswer</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085230+FxUX8d</STAMP> <VERSION>glum+110531085517+0K4ssK</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831950</TIMECREATED> <TIMEMODIFIED>1306832117</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTIANSWERS> <MULTIANSWER> <ID>1</ID> <QUESTION>11</QUESTION> <SEQUENCE>12,13,14,15</SEQUENCE> </MULTIANSWER> </MULTIANSWERS> </QUESTION> <QUESTION> <ID>12</ID> <PARENT>11</PARENT> <NAME>Cloze</NAME> <QUESTIONTEXT>{1:MULTICHOICE:=California#OK~Arizona#Wrong}</QUESTIONTEXT> <QUESTIONTEXTFORMAT>0</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>multichoice</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085230+eFahsT</STAMP> <VERSION>glum+110531085517+Nplir6</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831950</TIMECREATED> <TIMEMODIFIED>1306832117</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTICHOICE> <LAYOUT>0</LAYOUT> <ANSWERS>7,8</ANSWERS> <SINGLE>1</SINGLE> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <CORRECTFEEDBACK></CORRECTFEEDBACK> <PARTIALLYCORRECTFEEDBACK></PARTIALLYCORRECTFEEDBACK> <INCORRECTFEEDBACK></INCORRECTFEEDBACK> <ANSWERNUMBERING>0</ANSWERNUMBERING> </MULTICHOICE> <ANSWERS> <ANSWER> <ID>7</ID> <ANSWER_TEXT>California</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK>OK</FEEDBACK> </ANSWER> <ANSWER> <ID>8</ID> <ANSWER_TEXT>Arizona</ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK>Wrong</FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> <QUESTION> <ID>13</ID> <PARENT>11</PARENT> <NAME>Cloze</NAME> <QUESTIONTEXT>{1:MULTICHOICE:California#Wrong~%100%Arizona#OK}</QUESTIONTEXT> <QUESTIONTEXTFORMAT>0</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>multichoice</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085230+ED2gT0</STAMP> <VERSION>glum+110531085517+dOaA5B</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831950</TIMECREATED> <TIMEMODIFIED>1306832117</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTICHOICE> <LAYOUT>0</LAYOUT> <ANSWERS>9,10</ANSWERS> <SINGLE>1</SINGLE> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <CORRECTFEEDBACK></CORRECTFEEDBACK> <PARTIALLYCORRECTFEEDBACK></PARTIALLYCORRECTFEEDBACK> <INCORRECTFEEDBACK></INCORRECTFEEDBACK> <ANSWERNUMBERING>0</ANSWERNUMBERING> </MULTICHOICE> <ANSWERS> <ANSWER> <ID>9</ID> <ANSWER_TEXT>California</ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK>Wrong</FEEDBACK> </ANSWER> <ANSWER> <ID>10</ID> <ANSWER_TEXT>Arizona</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK>OK</FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> <QUESTION> <ID>14</ID> <PARENT>11</PARENT> <NAME>Cloze</NAME> <QUESTIONTEXT>{1:MULTICHOICE:=California#OK~Arizona#Wrong}</QUESTIONTEXT> <QUESTIONTEXTFORMAT>0</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>multichoice</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085230+g8ulKW</STAMP> <VERSION>glum+110531085517+Um16PG</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831950</TIMECREATED> <TIMEMODIFIED>1306832117</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTICHOICE> <LAYOUT>0</LAYOUT> <ANSWERS>11,12</ANSWERS> <SINGLE>1</SINGLE> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <CORRECTFEEDBACK></CORRECTFEEDBACK> <PARTIALLYCORRECTFEEDBACK></PARTIALLYCORRECTFEEDBACK> <INCORRECTFEEDBACK></INCORRECTFEEDBACK> <ANSWERNUMBERING>0</ANSWERNUMBERING> </MULTICHOICE> <ANSWERS> <ANSWER> <ID>11</ID> <ANSWER_TEXT>California</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK>OK</FEEDBACK> </ANSWER> <ANSWER> <ID>12</ID> <ANSWER_TEXT>Arizona</ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK>Wrong</FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> <QUESTION> <ID>15</ID> <PARENT>11</PARENT> <NAME>Cloze</NAME> <QUESTIONTEXT>{1:MULTICHOICE:%0%California#Wrong~=Arizona#OK}</QUESTIONTEXT> <QUESTIONTEXTFORMAT>0</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>multichoice</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085230+IvvI1p</STAMP> <VERSION>glum+110531085517+9JxglD</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831950</TIMECREATED> <TIMEMODIFIED>1306832117</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTICHOICE> <LAYOUT>0</LAYOUT> <ANSWERS>13,14</ANSWERS> <SINGLE>1</SINGLE> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <CORRECTFEEDBACK></CORRECTFEEDBACK> <PARTIALLYCORRECTFEEDBACK></PARTIALLYCORRECTFEEDBACK> <INCORRECTFEEDBACK></INCORRECTFEEDBACK> <ANSWERNUMBERING>0</ANSWERNUMBERING> </MULTICHOICE> <ANSWERS> <ANSWER> <ID>13</ID> <ANSWER_TEXT>California</ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK>Wrong</FEEDBACK> </ANSWER> <ANSWER> <ID>14</ID> <ANSWER_TEXT>Arizona</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK>OK</FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>8</ID> <NAME>Course level category</NAME> <INFO></INFO> <CONTEXT> <LEVEL>course</LEVEL> </CONTEXT> <STAMP>glum+110531083806+omJiSU</STAMP> <PARENT>0</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>7</ID> <PARENT>0</PARENT> <NAME>Example calculated question</NAME> <QUESTIONTEXT>Example calculated question text {a} {b}<br /></QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>calculated</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531084435+G1PzDs</STAMP> <VERSION>glum+110603184148+BOm8Mf</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831475</TIMECREATED> <TIMEMODIFIED>1307126508</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <CALCULATED> <ANSWER>5</ANSWER> <TOLERANCE>0.01</TOLERANCE> <TOLERANCETYPE>1</TOLERANCETYPE> <CORRECTANSWERLENGTH>2</CORRECTANSWERLENGTH> <CORRECTANSWERFORMAT>1</CORRECTANSWERFORMAT> <NUMERICAL_UNITS> <NUMERICAL_UNIT> <MULTIPLIER>1.00000000000000000000</MULTIPLIER> <UNIT>m</UNIT> </NUMERICAL_UNIT> <NUMERICAL_UNIT> <MULTIPLIER>100.00000000000000000000</MULTIPLIER> <UNIT>cm</UNIT> </NUMERICAL_UNIT> </NUMERICAL_UNITS> <DATASET_DEFINITIONS> <DATASET_DEFINITION> <CATEGORY>0</CATEGORY> <NAME>b</NAME> <TYPE>1</TYPE> <OPTIONS>loguniform:1.0:10.0:1</OPTIONS> <ITEMCOUNT>12</ITEMCOUNT> <DATASET_ITEMS> <DATASET_ITEM> <NUMBER>1</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>2</NUMBER> <VALUE>5.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>3</NUMBER> <VALUE>4.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>4</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>5</NUMBER> <VALUE>9.0</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>6</NUMBER> <VALUE>2.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>7</NUMBER> <VALUE>3.1</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>8</NUMBER> <VALUE>8.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>9</NUMBER> <VALUE>6.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>10</NUMBER> <VALUE>4.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>11</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>12</NUMBER> <VALUE>7.4</VALUE> </DATASET_ITEM> </DATASET_ITEMS> </DATASET_DEFINITION> <DATASET_DEFINITION> <CATEGORY>8</CATEGORY> <NAME>a</NAME> <TYPE>1</TYPE> <OPTIONS>loguniform:1.0:10.0:1</OPTIONS> <ITEMCOUNT>12</ITEMCOUNT> <DATASET_ITEMS> <DATASET_ITEM> <NUMBER>1</NUMBER> <VALUE>9.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>2</NUMBER> <VALUE>2.8</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>3</NUMBER> <VALUE>1.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>4</NUMBER> <VALUE>4.2</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>5</NUMBER> <VALUE>6.0</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>6</NUMBER> <VALUE>1.4</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>7</NUMBER> <VALUE>8.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>8</NUMBER> <VALUE>6.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>9</NUMBER> <VALUE>3.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>10</NUMBER> <VALUE>3.2</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>11</NUMBER> <VALUE>2.3</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>12</NUMBER> <VALUE>2.3</VALUE> </DATASET_ITEM> </DATASET_ITEMS> </DATASET_DEFINITION> </DATASET_DEFINITIONS> </CALCULATED> <CALCULATED> <ANSWER>26</ANSWER> <TOLERANCE>0.05</TOLERANCE> <TOLERANCETYPE>2</TOLERANCETYPE> <CORRECTANSWERLENGTH>3</CORRECTANSWERLENGTH> <CORRECTANSWERFORMAT>2</CORRECTANSWERFORMAT> <NUMERICAL_UNITS> <NUMERICAL_UNIT> <MULTIPLIER>1.00000000000000000000</MULTIPLIER> <UNIT>m</UNIT> </NUMERICAL_UNIT> <NUMERICAL_UNIT> <MULTIPLIER>100.00000000000000000000</MULTIPLIER> <UNIT>cm</UNIT> </NUMERICAL_UNIT> </NUMERICAL_UNITS> <DATASET_DEFINITIONS> <DATASET_DEFINITION> <CATEGORY>0</CATEGORY> <NAME>b</NAME> <TYPE>1</TYPE> <OPTIONS>loguniform:1.0:10.0:1</OPTIONS> <ITEMCOUNT>12</ITEMCOUNT> <DATASET_ITEMS> <DATASET_ITEM> <NUMBER>1</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>2</NUMBER> <VALUE>5.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>3</NUMBER> <VALUE>4.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>4</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>5</NUMBER> <VALUE>9.0</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>6</NUMBER> <VALUE>2.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>7</NUMBER> <VALUE>3.1</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>8</NUMBER> <VALUE>8.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>9</NUMBER> <VALUE>6.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>10</NUMBER> <VALUE>4.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>11</NUMBER> <VALUE>5.6</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>12</NUMBER> <VALUE>7.4</VALUE> </DATASET_ITEM> </DATASET_ITEMS> </DATASET_DEFINITION> <DATASET_DEFINITION> <CATEGORY>8</CATEGORY> <NAME>a</NAME> <TYPE>1</TYPE> <OPTIONS>loguniform:1.0:10.0:1</OPTIONS> <ITEMCOUNT>12</ITEMCOUNT> <DATASET_ITEMS> <DATASET_ITEM> <NUMBER>1</NUMBER> <VALUE>9.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>2</NUMBER> <VALUE>2.8</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>3</NUMBER> <VALUE>1.5</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>4</NUMBER> <VALUE>4.2</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>5</NUMBER> <VALUE>6.0</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>6</NUMBER> <VALUE>1.4</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>7</NUMBER> <VALUE>8.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>8</NUMBER> <VALUE>6.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>9</NUMBER> <VALUE>3.7</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>10</NUMBER> <VALUE>3.2</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>11</NUMBER> <VALUE>2.3</VALUE> </DATASET_ITEM> <DATASET_ITEM> <NUMBER>12</NUMBER> <VALUE>2.3</VALUE> </DATASET_ITEM> </DATASET_ITEMS> </DATASET_DEFINITION> </DATASET_DEFINITIONS> </CALCULATED> <ANSWERS> <ANSWER> <ID>5</ID> <ANSWER_TEXT>{a}*{b}</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> <ANSWER> <ID>26</ID> <ANSWER_TEXT>2*{a}*{b}</ANSWER_TEXT> <FRACTION>0.8</FRACTION> <FEEDBACK>Feedback</FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> <QUESTION> <ID>18</ID> <PARENT>0</PARENT> <NAME>Short answer</NAME> <QUESTIONTEXT></QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>shortanswer</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085701+rXS4MV</STAMP> <VERSION>glum+110531085701+PhXvCL</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306832221</TIMECREATED> <TIMEMODIFIED>1306832221</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <SHORTANSWER> <ANSWERS>20,21</ANSWERS> <USECASE>0</USECASE> </SHORTANSWER> <ANSWERS> <ANSWER> <ID>20</ID> <ANSWER_TEXT>AAA</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> <ANSWER> <ID>21</ID> <ANSWER_TEXT>BBB</ANSWER_TEXT> <FRACTION>0.9</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> <QUESTION> <ID>19</ID> <PARENT>0</PARENT> <NAME>Numerical</NAME> <QUESTIONTEXT>Correct answer is 2 meters or 5 metres. Can be in centimetres, too.</QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>numerical</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085745+IDs7qb</STAMP> <VERSION>glum+110602145234+gfl7nu</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306832265</TIMECREATED> <TIMEMODIFIED>1307026354</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <NUMERICAL> <ANSWER>22</ANSWER> <TOLERANCE>0</TOLERANCE> <NUMERICAL_UNITS> <NUMERICAL_UNIT> <MULTIPLIER>1.00000000000000000000</MULTIPLIER> <UNIT>m</UNIT> </NUMERICAL_UNIT> <NUMERICAL_UNIT> <MULTIPLIER>100.00000000000000000000</MULTIPLIER> <UNIT>cm</UNIT> </NUMERICAL_UNIT> </NUMERICAL_UNITS> </NUMERICAL> <NUMERICAL> <ANSWER>25</ANSWER> <TOLERANCE>0</TOLERANCE> <NUMERICAL_UNITS> <NUMERICAL_UNIT> <MULTIPLIER>1.00000000000000000000</MULTIPLIER> <UNIT>m</UNIT> </NUMERICAL_UNIT> <NUMERICAL_UNIT> <MULTIPLIER>100.00000000000000000000</MULTIPLIER> <UNIT>cm</UNIT> </NUMERICAL_UNIT> </NUMERICAL_UNITS> </NUMERICAL> <ANSWERS> <ANSWER> <ID>22</ID> <ANSWER_TEXT>2</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> <ANSWER> <ID>25</ID> <ANSWER_TEXT>5</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>9</ID> <NAME>Course level subcategory</NAME> <INFO></INFO> <CONTEXT> <LEVEL>course</LEVEL> </CONTEXT> <STAMP>glum+110531083816+9h2ltz</STAMP> <PARENT>8</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>8</ID> <PARENT>0</PARENT> <NAME>Description question</NAME> <QUESTIONTEXT>Description text</QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>0</DEFAULTGRADE> <PENALTY>0</PENALTY> <QTYPE>description</QTYPE> <LENGTH>0</LENGTH> <STAMP>glum+110531084921+7d3EsT</STAMP> <VERSION>glum+110531084921+tCXWjW</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831761</TIMECREATED> <TIMEMODIFIED>1306831761</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>10</ID> <NAME>Course-category level subcategory</NAME> <INFO></INFO> <CONTEXT> <LEVEL>coursecategory</LEVEL> <COURSECATEGORYLEVEL>1</COURSECATEGORYLEVEL> </CONTEXT> <STAMP>glum+110531083918+J5b4nH</STAMP> <PARENT>3</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>10</ID> <PARENT>0</PARENT> <NAME>Matching</NAME> <QUESTIONTEXT></QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>match</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085011+MFOyGv</STAMP> <VERSION>glum+110531085011+A7Rd8x</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306831811</TIMECREATED> <TIMEMODIFIED>1306831811</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MATCHOPTIONS> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> </MATCHOPTIONS> <MATCHS> <MATCH> <ID>1</ID> <CODE>338309139</CODE> <QUESTIONTEXT>AAA</QUESTIONTEXT> <ANSWERTEXT>aaa</ANSWERTEXT> </MATCH> <MATCH> <ID>2</ID> <CODE>390311826</CODE> <QUESTIONTEXT>BBB</QUESTIONTEXT> <ANSWERTEXT>bbb</ANSWERTEXT> </MATCH> <MATCH> <ID>3</ID> <CODE>597270871</CODE> <QUESTIONTEXT>CCC</QUESTIONTEXT> <ANSWERTEXT>ccc</ANSWERTEXT> </MATCH> </MATCHS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>11</ID> <NAME>System level subcategory</NAME> <INFO></INFO> <CONTEXT> <LEVEL>system</LEVEL> </CONTEXT> <STAMP>glum+110531083941+gYNTQn</STAMP> <PARENT>4</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>17</ID> <PARENT>0</PARENT> <NAME>Multichoice</NAME> <QUESTIONTEXT></QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>0.1</PENALTY> <QTYPE>multichoice</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085614+ltkItU</STAMP> <VERSION>glum+110531085614+go7lJo</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306832174</TIMECREATED> <TIMEMODIFIED>1306832174</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <MULTICHOICE> <LAYOUT>0</LAYOUT> <ANSWERS>18,19</ANSWERS> <SINGLE>1</SINGLE> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <CORRECTFEEDBACK></CORRECTFEEDBACK> <PARTIALLYCORRECTFEEDBACK></PARTIALLYCORRECTFEEDBACK> <INCORRECTFEEDBACK></INCORRECTFEEDBACK> <ANSWERNUMBERING>abc</ANSWERNUMBERING> </MULTICHOICE> <ANSWERS> <ANSWER> <ID>18</ID> <ANSWER_TEXT>AAA</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK>aaa </FEEDBACK> </ANSWER> <ANSWER> <ID>19</ID> <ANSWER_TEXT>BBB</ANSWER_TEXT> <FRACTION>0.9</FRACTION> <FEEDBACK>bbb </FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>12</ID> <NAME>Default for Choice 001</NAME> <INFO>The default category for questions shared in context 'Choice 001'.</INFO> <CONTEXT> <LEVEL>module</LEVEL> <INSTANCE>18</INSTANCE> </CONTEXT> <STAMP>glum+110531091209+6uNCUb</STAMP> <PARENT>0</PARENT> <SORTORDER>999</SORTORDER> <QUESTIONS> <QUESTION> <ID>20</ID> <PARENT>0</PARENT> <NAME>True false</NAME> <QUESTIONTEXT>Say true</QUESTIONTEXT> <QUESTIONTEXTFORMAT>1</QUESTIONTEXTFORMAT> <IMAGE></IMAGE> <GENERALFEEDBACK></GENERALFEEDBACK> <DEFAULTGRADE>1</DEFAULTGRADE> <PENALTY>1</PENALTY> <QTYPE>truefalse</QTYPE> <LENGTH>1</LENGTH> <STAMP>glum+110531085822+2NdCTO</STAMP> <VERSION>glum+110531085822+XaIMLY</VERSION> <HIDDEN>0</HIDDEN> <TIMECREATED>1306832302</TIMECREATED> <TIMEMODIFIED>1306832302</TIMEMODIFIED> <CREATEDBY>2</CREATEDBY> <MODIFIEDBY>2</MODIFIEDBY> <TRUEFALSE> <TRUEANSWER>23</TRUEANSWER> <FALSEANSWER>24</FALSEANSWER> </TRUEFALSE> <ANSWERS> <ANSWER> <ID>23</ID> <ANSWER_TEXT>True</ANSWER_TEXT> <FRACTION>1</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> <ANSWER> <ID>24</ID> <ANSWER_TEXT>False</ANSWER_TEXT> <FRACTION>0</FRACTION> <FEEDBACK></FEEDBACK> </ANSWER> </ANSWERS> </QUESTION> </QUESTIONS> </QUESTION_CATEGORY> <QUESTION_CATEGORY> <ID>15</ID> <NAME>Course level empty category</NAME> <INFO></INFO> <CONTEXT> <LEVEL>course</LEVEL> </CONTEXT> <STAMP>glum+110602145600+5BeIMW</STAMP> <PARENT>0</PARENT> <SORTORDER>999</SORTORDER> </QUESTION_CATEGORY> </QUESTION_CATEGORIES> <GRADEBOOK> <GRADE_CATEGORIES> <GRADE_CATEGORY> <ID>5</ID> <PARENT>$@NULL@$</PARENT> <FULLNAME>?</FULLNAME> <AGGREGATION>11</AGGREGATION> <KEEPHIGH>0</KEEPHIGH> <DROPLOW>0</DROPLOW> <AGGREGATEONLYGRADED>1</AGGREGATEONLYGRADED> <AGGREGATEOUTCOMES>0</AGGREGATEOUTCOMES> <AGGREGATESUBCATS>0</AGGREGATESUBCATS> <TIMECREATED>1306831013</TIMECREATED> <TIMEMODIFIED>1306831014</TIMEMODIFIED> </GRADE_CATEGORY> </GRADE_CATEGORIES> <GRADE_ITEMS> <GRADE_ITEM> <ID>8</ID> <CATEGORYID>$@NULL@$</CATEGORYID> <ITEMNAME>$@NULL@$</ITEMNAME> <ITEMTYPE>course</ITEMTYPE> <ITEMMODULE>$@NULL@$</ITEMMODULE> <ITEMINSTANCE>5</ITEMINSTANCE> <ITEMNUMBER>$@NULL@$</ITEMNUMBER> <ITEMINFO>$@NULL@$</ITEMINFO> <IDNUMBER>$@NULL@$</IDNUMBER> <CALCULATION>$@NULL@$</CALCULATION> <GRADETYPE>1</GRADETYPE> <GRADEMAX>100.00000</GRADEMAX> <GRADEMIN>0.00000</GRADEMIN> <SCALEID>$@NULL@$</SCALEID> <OUTCOMEID>$@NULL@$</OUTCOMEID> <GRADEPASS>0.00000</GRADEPASS> <MULTFACTOR>1.00000</MULTFACTOR> <PLUSFACTOR>0.00000</PLUSFACTOR> <AGGREGATIONCOEF>0.00000</AGGREGATIONCOEF> <DISPLAY>0</DISPLAY> <DECIMALS>$@NULL@$</DECIMALS> <HIDDEN>0</HIDDEN> <LOCKED>0</LOCKED> <LOCKTIME>0</LOCKTIME> <NEEDSUPDATE>0</NEEDSUPDATE> <TIMECREATED>1306831014</TIMECREATED> <TIMEMODIFIED>1306831014</TIMEMODIFIED> </GRADE_ITEM> <GRADE_ITEM> <ID>11</ID> <CATEGORYID>5</CATEGORYID> <ITEMNAME>Quiz 001</ITEMNAME> <ITEMTYPE>mod</ITEMTYPE> <ITEMMODULE>quiz</ITEMMODULE> <ITEMINSTANCE>5</ITEMINSTANCE> <ITEMNUMBER>0</ITEMNUMBER> <ITEMINFO>$@NULL@$</ITEMINFO> <IDNUMBER></IDNUMBER> <CALCULATION>$@NULL@$</CALCULATION> <GRADETYPE>1</GRADETYPE> <GRADEMAX>10.00000</GRADEMAX> <GRADEMIN>0.00000</GRADEMIN> <SCALEID>$@NULL@$</SCALEID> <OUTCOMEID>$@NULL@$</OUTCOMEID> <GRADEPASS>0.00000</GRADEPASS> <MULTFACTOR>1.00000</MULTFACTOR> <PLUSFACTOR>0.00000</PLUSFACTOR> <AGGREGATIONCOEF>0.00000</AGGREGATIONCOEF> <DISPLAY>0</DISPLAY> <DECIMALS>$@NULL@$</DECIMALS> <HIDDEN>0</HIDDEN> <LOCKED>0</LOCKED> <LOCKTIME>0</LOCKTIME> <NEEDSUPDATE>0</NEEDSUPDATE> <TIMECREATED>1306833129</TIMECREATED> <TIMEMODIFIED>1307122770</TIMEMODIFIED> </GRADE_ITEM> </GRADE_ITEMS> </GRADEBOOK> <MODULES> <MOD> <ID>5</ID> <MODTYPE>quiz</MODTYPE> <NAME>Quiz 001</NAME> <INTRO>This must be included in the backup so that it includes questions, too.<br /></INTRO> <TIMEOPEN>0</TIMEOPEN> <TIMECLOSE>0</TIMECLOSE> <OPTIONFLAGS>1</OPTIONFLAGS> <PENALTYSCHEME>1</PENALTYSCHEME> <ATTEMPTS_NUMBER>0</ATTEMPTS_NUMBER> <ATTEMPTONLAST>0</ATTEMPTONLAST> <GRADEMETHOD>1</GRADEMETHOD> <DECIMALPOINTS>2</DECIMALPOINTS> <REVIEW>4652015</REVIEW> <QUESTIONSPERPAGE>0</QUESTIONSPERPAGE> <SHUFFLEQUESTIONS>0</SHUFFLEQUESTIONS> <SHUFFLEANSWERS>1</SHUFFLEANSWERS> <QUESTIONS>17,11,9,10,7,8,19,18,20,0</QUESTIONS> <SUMGRADES>11</SUMGRADES> <GRADE>10</GRADE> <TIMECREATED>0</TIMECREATED> <TIMEMODIFIED>1307122770</TIMEMODIFIED> <TIMELIMIT>0</TIMELIMIT> <PASSWORD></PASSWORD> <SUBNET></SUBNET> <POPUP>0</POPUP> <DELAY1>0</DELAY1> <DELAY2>0</DELAY2> <QUESTION_INSTANCES> <QUESTION_INSTANCE> <ID>3</ID> <QUESTION>17</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>4</ID> <QUESTION>11</QUESTION> <GRADE>4</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>5</ID> <QUESTION>9</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>6</ID> <QUESTION>10</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>7</ID> <QUESTION>7</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>8</ID> <QUESTION>8</QUESTION> <GRADE>0</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>9</ID> <QUESTION>19</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>10</ID> <QUESTION>18</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> <QUESTION_INSTANCE> <ID>11</ID> <QUESTION>20</QUESTION> <GRADE>1</GRADE> </QUESTION_INSTANCE> </QUESTION_INSTANCES> <FEEDBACKS> <FEEDBACK> <ID>7</ID> <QUIZID>5</QUIZID> <FEEDBACKTEXT></FEEDBACKTEXT> <MINGRADE>0</MINGRADE> <MAXGRADE>11</MAXGRADE> </FEEDBACK> </FEEDBACKS> </MOD> </MODULES> <FORMATDATA> </FORMATDATA> </COURSE> </MOODLE_BACKUP> PK ��/[J���$G $G ! moodle1/tests/fixtures/moodle.xmlnu &1i� <?xml version="1.0" encoding="UTF-8"?> <MOODLE_BACKUP> <INFO> <NAME>restore.zip</NAME> <MOODLE_VERSION>2007101520</MOODLE_VERSION> <MOODLE_RELEASE>1.9.2 (Build: 20080711)</MOODLE_RELEASE> <BACKUP_VERSION>2008030300</BACKUP_VERSION> <BACKUP_RELEASE>1.9</BACKUP_RELEASE> <DATE>1299774017</DATE> <ORIGINAL_WWWROOT>http://elearning-testadam.uss.lsu.edu/moodle</ORIGINAL_WWWROOT> <ZIP_METHOD>internal</ZIP_METHOD> <DETAILS> <MOD> <NAME>forum</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> <INSTANCES> <INSTANCE> <ID>57</ID> <NAME>News forum</NAME> <INCLUDED>true</INCLUDED> <USERINFO>false</USERINFO> </INSTANCE> </INSTANCES> </MOD> <METACOURSE>false</METACOURSE> <USERS>course</USERS> <LOGS>false</LOGS> <USERFILES>true</USERFILES> <COURSEFILES>true</COURSEFILES> <SITEFILES>true</SITEFILES> <GRADEBOOKHISTORIES>false</GRADEBOOKHISTORIES> <MESSAGES>false</MESSAGES> <BLOGS>false</BLOGS> <BLOCKFORMAT>instances</BLOCKFORMAT> </DETAILS> </INFO> <ROLES> </ROLES> <COURSE> <HEADER> <ID>33</ID> <CATEGORY> <ID>1</ID> <NAME>Category 1</NAME> </CATEGORY> <PASSWORD></PASSWORD> <FULLNAME>Moodle 2.0 Test Restore</FULLNAME> <SHORTNAME>M2TR</SHORTNAME> <IDNUMBER></IDNUMBER> <SUMMARY>Cazzin'</SUMMARY> <FORMAT>weeks</FORMAT> <SHOWGRADES>1</SHOWGRADES> <NEWSITEMS>5</NEWSITEMS> <TEACHER>Teacher</TEACHER> <TEACHERS>Teachers</TEACHERS> <STUDENT>Student</STUDENT> <STUDENTS>Students</STUDENTS> <GUEST>0</GUEST> <STARTDATE>1299823200</STARTDATE> <NUMSECTIONS>10</NUMSECTIONS> <MAXBYTES>52428800</MAXBYTES> <SHOWREPORTS>0</SHOWREPORTS> <GROUPMODE>0</GROUPMODE> <GROUPMODEFORCE>0</GROUPMODEFORCE> <DEFAULTGROUPINGID>0</DEFAULTGROUPINGID> <LANG></LANG> <THEME></THEME> <COST></COST> <CURRENCY>USD</CURRENCY> <MARKER>0</MARKER> <VISIBLE>1</VISIBLE> <HIDDENSECTIONS>0</HIDDENSECTIONS> <TIMECREATED>1299773769</TIMECREATED> <TIMEMODIFIED>1299773769</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> </HEADER> <BLOCKS> <BLOCK> <ID>314</ID> <NAME>participants</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>0</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>315</ID> <NAME>activity_modules</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>1</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>316</ID> <NAME>search_forums</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>2</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>317</ID> <NAME>admin</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>3</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>318</ID> <NAME>course_list</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>l</POSITION> <WEIGHT>4</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>319</ID> <NAME>news_items</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>0</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>320</ID> <NAME>calendar_upcoming</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>1</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> <BLOCK> <ID>321</ID> <NAME>recent_activity</NAME> <PAGEID>33</PAGEID> <PAGETYPE>course-view</PAGETYPE> <POSITION>r</POSITION> <WEIGHT>2</WEIGHT> <VISIBLE>1</VISIBLE> <CONFIGDATA>Tjs=</CONFIGDATA> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </BLOCK> </BLOCKS> <SECTIONS> <SECTION> <ID>436</ID> <NUMBER>0</NUMBER> <SUMMARY>$@NULL@$</SUMMARY> <VISIBLE>1</VISIBLE> <MODS> <MOD> <ID>250</ID> <TYPE>forum</TYPE> <INSTANCE>57</INSTANCE> <ADDED>1299773780</ADDED> <SCORE>0</SCORE> <INDENT>0</INDENT> <VISIBLE>1</VISIBLE> <GROUPMODE>0</GROUPMODE> <GROUPINGID>0</GROUPINGID> <GROUPMEMBERSONLY>0</GROUPMEMBERSONLY> <IDNUMBER>$@NULL@$</IDNUMBER> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </MOD> </MODS> </SECTION> <SECTION> <ID>437</ID> <NUMBER>1</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>438</ID> <NUMBER>2</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>439</ID> <NUMBER>3</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>440</ID> <NUMBER>4</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>441</ID> <NUMBER>5</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>442</ID> <NUMBER>6</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>443</ID> <NUMBER>7</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>444</ID> <NUMBER>8</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>445</ID> <NUMBER>9</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> <SECTION> <ID>446</ID> <NUMBER>10</NUMBER> <SUMMARY></SUMMARY> <VISIBLE>1</VISIBLE> </SECTION> </SECTIONS> <USERS> <USER> <ID>2</ID> <AUTH>manual</AUTH> <CONFIRMED>1</CONFIRMED> <POLICYAGREED>0</POLICYAGREED> <DELETED>0</DELETED> <USERNAME>admin</USERNAME> <PASSWORD>ea557cf33bda866d97b07465f1ea3867</PASSWORD> <IDNUMBER>891220979</IDNUMBER> <FIRSTNAME>Admin</FIRSTNAME> <LASTNAME>User</LASTNAME> <EMAIL>adamzap@example.com</EMAIL> <EMAILSTOP>0</EMAILSTOP> <ICQ></ICQ> <SKYPE></SKYPE> <YAHOO></YAHOO> <AIM></AIM> <MSN></MSN> <PHONE1></PHONE1> <PHONE2></PHONE2> <INSTITUTION></INSTITUTION> <DEPARTMENT></DEPARTMENT> <ADDRESS></ADDRESS> <CITY>br</CITY> <COUNTRY>US</COUNTRY> <LANG>en_utf8</LANG> <THEME></THEME> <TIMEZONE>99</TIMEZONE> <FIRSTACCESS>0</FIRSTACCESS> <LASTACCESS>1265755409</LASTACCESS> <LASTLOGIN>1265658108</LASTLOGIN> <CURRENTLOGIN>1265741271</CURRENTLOGIN> <LASTIP>130.39.194.172</LASTIP> <SECRET>lknsJoSw0S6myJA</SECRET> <PICTURE>1</PICTURE> <URL></URL> <DESCRIPTION></DESCRIPTION> <MAILFORMAT>1</MAILFORMAT> <MAILDIGEST>0</MAILDIGEST> <MAILDISPLAY>1</MAILDISPLAY> <HTMLEDITOR>1</HTMLEDITOR> <AJAX>1</AJAX> <AUTOSUBSCRIBE>1</AUTOSUBSCRIBE> <TRACKFORUMS>0</TRACKFORUMS> <TIMEMODIFIED>1265216036</TIMEMODIFIED> <ROLES> <ROLE> <TYPE>needed</TYPE> </ROLE> </ROLES> <USER_PREFERENCES> <USER_PREFERENCE> <NAME>email_bounce_count</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>email_send_count</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>user_editadvanced_form_showadvanced</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> </USER_PREFERENCES> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </USER> <USER> <ID>3</ID> <AUTH>cas</AUTH> <CONFIRMED>1</CONFIRMED> <POLICYAGREED>0</POLICYAGREED> <DELETED>0</DELETED> <USERNAME>azaple1</USERNAME> <PASSWORD>fb005bdc5fb1b23ea95d238fb5ecb9e2</PASSWORD> <IDNUMBER>891111111</IDNUMBER> <FIRSTNAME>Adam</FIRSTNAME> <LASTNAME>Zapletal</LASTNAME> <EMAIL>azaple1@example.com</EMAIL> <EMAILSTOP>0</EMAILSTOP> <ICQ></ICQ> <SKYPE></SKYPE> <YAHOO></YAHOO> <AIM></AIM> <MSN></MSN> <PHONE1></PHONE1> <PHONE2></PHONE2> <INSTITUTION></INSTITUTION> <DEPARTMENT></DEPARTMENT> <ADDRESS></ADDRESS> <CITY>asdf</CITY> <COUNTRY>AL</COUNTRY> <LANG>en_utf8</LANG> <THEME></THEME> <TIMEZONE>99</TIMEZONE> <FIRSTACCESS>0</FIRSTACCESS> <LASTACCESS>1299774054</LASTACCESS> <LASTLOGIN>1297979000</LASTLOGIN> <CURRENTLOGIN>1299773727</CURRENTLOGIN> <LASTIP>173.253.129.223</LASTIP> <SECRET></SECRET> <PICTURE>1</PICTURE> <URL></URL> <DESCRIPTION></DESCRIPTION> <MAILFORMAT>1</MAILFORMAT> <MAILDIGEST>0</MAILDIGEST> <MAILDISPLAY>2</MAILDISPLAY> <HTMLEDITOR>1</HTMLEDITOR> <AJAX>1</AJAX> <AUTOSUBSCRIBE>1</AUTOSUBSCRIBE> <TRACKFORUMS>0</TRACKFORUMS> <TIMEMODIFIED>1288033285</TIMEMODIFIED> <ROLES> <ROLE> <TYPE>needed</TYPE> </ROLE> </ROLES> <USER_PREFERENCES> <USER_PREFERENCE> <NAME>assignment_mailinfo</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>assignment_perpage</NAME> <VALUE>10</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>assignment_quickgrade</NAME> <VALUE>0</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>auth_forcepasswordchange</NAME> <VALUE>0</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>forum_displaymode</NAME> <VALUE>3</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grader_report_preferences_form_showadvanced</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grade_report_simple_aggregationview51</NAME> <VALUE>0</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grade_report_simple_grader_collapsed_categories</NAME> <VALUE>a:2:{s:14:"aggregatesonly";a:4:{i:0;s:2:"70";i:1;s:2:"71";i:2;s:2:"72";i:3;s:2:"73";}s:10:"gradesonly";a:0:{}}</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grade_report_simple_meanselection</NAME> <VALUE>2</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grade_report_simple_showranges</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>grade_report_simple_showstickytab</NAME> <VALUE>0</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>lesson_view</NAME> <VALUE>collapsed</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>mod_resource_mod_form_showadvanced</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> <USER_PREFERENCE> <NAME>user_editadvanced_form_showadvanced</NAME> <VALUE>1</VALUE> </USER_PREFERENCE> </USER_PREFERENCES> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </USER> <USER> <ID>6</ID> <AUTH>cas</AUTH> <CONFIRMED>1</CONFIRMED> <POLICYAGREED>0</POLICYAGREED> <DELETED>0</DELETED> <USERNAME>pcali1</USERNAME> <PASSWORD>fb005bdc5fb1b23ea95d238fb5ecb9e2</PASSWORD> <IDNUMBER></IDNUMBER> <FIRSTNAME>Philip</FIRSTNAME> <LASTNAME>Cali</LASTNAME> <EMAIL>pcali1@example.com</EMAIL> <EMAILSTOP>0</EMAILSTOP> <ICQ></ICQ> <SKYPE></SKYPE> <YAHOO></YAHOO> <AIM></AIM> <MSN></MSN> <PHONE1></PHONE1> <PHONE2></PHONE2> <INSTITUTION></INSTITUTION> <DEPARTMENT></DEPARTMENT> <ADDRESS></ADDRESS> <CITY></CITY> <COUNTRY></COUNTRY> <LANG>en_utf8</LANG> <THEME></THEME> <TIMEZONE>99</TIMEZONE> <FIRSTACCESS>0</FIRSTACCESS> <LASTACCESS>1268662392</LASTACCESS> <LASTLOGIN>0</LASTLOGIN> <CURRENTLOGIN>1268662284</CURRENTLOGIN> <LASTIP>68.105.37.237</LASTIP> <SECRET></SECRET> <PICTURE>1</PICTURE> <URL></URL> <DESCRIPTION>$@NULL@$</DESCRIPTION> <MAILFORMAT>1</MAILFORMAT> <MAILDIGEST>0</MAILDIGEST> <MAILDISPLAY>2</MAILDISPLAY> <HTMLEDITOR>1</HTMLEDITOR> <AJAX>1</AJAX> <AUTOSUBSCRIBE>1</AUTOSUBSCRIBE> <TRACKFORUMS>0</TRACKFORUMS> <TIMEMODIFIED>1268662284</TIMEMODIFIED> <ROLES> <ROLE> <TYPE>needed</TYPE> </ROLE> </ROLES> <ROLES_OVERRIDES> </ROLES_OVERRIDES> <ROLES_ASSIGNMENTS> </ROLES_ASSIGNMENTS> </USER> </USERS> <GRADEBOOK> <GRADE_CATEGORIES> <GRADE_CATEGORY> <ID>90</ID> <PARENT>$@NULL@$</PARENT> <FULLNAME>?</FULLNAME> <AGGREGATION>11</AGGREGATION> <KEEPHIGH>0</KEEPHIGH> <DROPLOW>0</DROPLOW> <AGGREGATEONLYGRADED>1</AGGREGATEONLYGRADED> <AGGREGATEOUTCOMES>0</AGGREGATEOUTCOMES> <AGGREGATESUBCATS>0</AGGREGATESUBCATS> <TIMECREATED>1299774068</TIMECREATED> <TIMEMODIFIED>1299774068</TIMEMODIFIED> </GRADE_CATEGORY> </GRADE_CATEGORIES> <GRADE_ITEMS> <GRADE_ITEM> <ID>410</ID> <CATEGORYID>$@NULL@$</CATEGORYID> <ITEMNAME>$@NULL@$</ITEMNAME> <ITEMTYPE>course</ITEMTYPE> <ITEMMODULE>$@NULL@$</ITEMMODULE> <ITEMINSTANCE>90</ITEMINSTANCE> <ITEMNUMBER>$@NULL@$</ITEMNUMBER> <ITEMINFO>$@NULL@$</ITEMINFO> <IDNUMBER>$@NULL@$</IDNUMBER> <CALCULATION>$@NULL@$</CALCULATION> <GRADETYPE>1</GRADETYPE> <GRADEMAX>100.00000</GRADEMAX> <GRADEMIN>0.00000</GRADEMIN> <SCALEID>$@NULL@$</SCALEID> <OUTCOMEID>$@NULL@$</OUTCOMEID> <GRADEPASS>0.00000</GRADEPASS> <MULTFACTOR>1.00000</MULTFACTOR> <PLUSFACTOR>0.00000</PLUSFACTOR> <AGGREGATIONCOEF>1.00000</AGGREGATIONCOEF> <DISPLAY>0</DISPLAY> <DECIMALS>$@NULL@$</DECIMALS> <HIDDEN>0</HIDDEN> <LOCKED>0</LOCKED> <LOCKTIME>0</LOCKTIME> <NEEDSUPDATE>0</NEEDSUPDATE> <TIMECREATED>1299774068</TIMECREATED> <TIMEMODIFIED>1299774068</TIMEMODIFIED> </GRADE_ITEM> </GRADE_ITEMS> </GRADEBOOK> <MODULES> <MOD> <ID>57</ID> <MODTYPE>forum</MODTYPE> <TYPE>news</TYPE> <NAME>News forum</NAME> <INTRO>General news and announcements</INTRO> <ASSESSED>0</ASSESSED> <ASSESSTIMESTART>0</ASSESSTIMESTART> <ASSESSTIMEFINISH>0</ASSESSTIMEFINISH> <MAXBYTES>0</MAXBYTES> <SCALE>0</SCALE> <FORCESUBSCRIBE>1</FORCESUBSCRIBE> <TRACKINGTYPE>1</TRACKINGTYPE> <RSSTYPE>0</RSSTYPE> <RSSARTICLES>0</RSSARTICLES> <TIMEMODIFIED>1299773780</TIMEMODIFIED> <WARNAFTER>0</WARNAFTER> <BLOCKAFTER>0</BLOCKAFTER> <BLOCKPERIOD>0</BLOCKPERIOD> </MOD> </MODULES> <FORMATDATA> </FORMATDATA> </COURSE> </MOODLE_BACKUP> PK ��/[A����j �j ( moodle1/tests/moodle1_converter_test.phpnu &1i� <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more 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 convert_path; use convert_path_exception; use convert_factory; use convert_helper; use moodle1_converter; use moodle1_convert_empty_storage_exception; use moodle1_convert_exception; use moodle1_convert_storage_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/backup/converter/moodle1/lib.php'); /** * Unit tests for the moodle1 converter * * @package core_backup * @subpackage backup-convert * @category test * @copyright 2011 Mark Nielsen <mark@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class moodle1_converter_test extends \advanced_testcase { /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */ protected $tempdir; /** @var string the full name of the directory containing the unpacked Moodle 1.9 backup */ protected $tempdirpath; /** @var string saved hash of an icon file used during testing */ protected $iconhash; protected function setUp(): void { global $CFG; $this->tempdir = convert_helper::generate_id('unittest'); $this->tempdirpath = make_backup_temp_directory($this->tempdir); check_dir_exists("$this->tempdirpath/course_files/sub1"); check_dir_exists("$this->tempdirpath/moddata/unittest/4/7"); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/moodle.xml", "$this->tempdirpath/moodle.xml" ); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif", "$this->tempdirpath/course_files/file1.gif" ); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif", "$this->tempdirpath/course_files/sub1/file2.gif" ); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif", "$this->tempdirpath/moddata/unittest/4/file1.gif" ); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif", "$this->tempdirpath/moddata/unittest/4/icon.gif" ); $this->iconhash = \file_storage::hash_from_path($this->tempdirpath.'/moddata/unittest/4/icon.gif'); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif", "$this->tempdirpath/moddata/unittest/4/7/icon.gif" ); } protected function tearDown(): void { global $CFG; if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($this->tempdirpath); } } public function test_detect_format() { $detected = moodle1_converter::detect_format($this->tempdir); $this->assertEquals(backup::FORMAT_MOODLE1, $detected); } public function test_convert_factory() { $converter = convert_factory::get_converter('moodle1', $this->tempdir); $this->assertInstanceOf('moodle1_converter', $converter); } public function test_stash_storage_not_created() { $converter = convert_factory::get_converter('moodle1', $this->tempdir); $this->expectException(moodle1_convert_storage_exception::class); $converter->set_stash('tempinfo', 12); } public function test_stash_requiring_empty_stash() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); $converter->set_stash('tempinfo', 12); try { $converter->get_stash('anothertempinfo'); } catch (moodle1_convert_empty_storage_exception $e) { // we must drop the storage here so we are able to re-create it in the next test $this->expectException(moodle1_convert_empty_storage_exception::class); $converter->drop_stash_storage(); throw new moodle1_convert_empty_storage_exception('rethrowing'); } } public function test_stash_storage() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); // no implicit stashes $stashes = $converter->get_stash_names(); $this->assertEquals(gettype($stashes), 'array'); $this->assertTrue(empty($stashes)); // test stashes without itemid $converter->set_stash('tempinfo1', 12); $converter->set_stash('tempinfo2', array('a' => 2, 'b' => 3)); $stashes = $converter->get_stash_names(); $this->assertEquals('array', gettype($stashes)); $this->assertEquals(2, count($stashes)); $this->assertTrue(in_array('tempinfo1', $stashes)); $this->assertTrue(in_array('tempinfo2', $stashes)); $this->assertEquals(12, $converter->get_stash('tempinfo1')); $this->assertEquals(array('a' => 2, 'b' => 3), $converter->get_stash('tempinfo2')); // overwriting a stashed value is allowed $converter->set_stash('tempinfo1', '13'); $this->assertNotSame(13, $converter->get_stash('tempinfo1')); $this->assertSame('13', $converter->get_stash('tempinfo1')); // repeated reading is allowed $this->assertEquals('13', $converter->get_stash('tempinfo1')); // storing empty array $converter->set_stash('empty_array_stash', array()); $restored = $converter->get_stash('empty_array_stash'); //$this->assertEquals(gettype($restored), 'array'); // todo return null now, this needs MDL-27713 to be fixed, then uncomment $this->assertTrue(empty($restored)); // test stashes with itemid $converter->set_stash('tempinfo', 'Hello', 1); $converter->set_stash('tempinfo', 'World', 2); $this->assertSame('Hello', $converter->get_stash('tempinfo', 1)); $this->assertSame('World', $converter->get_stash('tempinfo', 2)); // test get_stash_itemids() $ids = $converter->get_stash_itemids('course_fileref'); $this->assertEquals(gettype($ids), 'array'); $this->assertTrue(empty($ids)); $converter->set_stash('course_fileref', null, 34); $converter->set_stash('course_fileref', null, 52); $ids = $converter->get_stash_itemids('course_fileref'); $this->assertEquals(2, count($ids)); $this->assertTrue(in_array(34, $ids)); $this->assertTrue(in_array(52, $ids)); $converter->drop_stash_storage(); } public function test_get_stash_or_default() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); $this->assertTrue(is_null($converter->get_stash_or_default('stashname'))); $this->assertTrue(is_null($converter->get_stash_or_default('stashname', 7))); $this->assertTrue('default' === $converter->get_stash_or_default('stashname', 0, 'default')); $this->assertTrue(array('foo', 'bar') === $converter->get_stash_or_default('stashname', 42, array('foo', 'bar'))); //$converter->set_stash('stashname', 0); //$this->assertFalse(is_null($converter->get_stash_or_default('stashname'))); // todo returns true now, this needs MDL-27713 to be fixed //$converter->set_stash('stashname', ''); //$this->assertFalse(is_null($converter->get_stash_or_default('stashname'))); // todo returns true now, this needs MDL-27713 to be fixed //$converter->set_stash('stashname', array()); //$this->assertFalse(is_null($converter->get_stash_or_default('stashname'))); // todo returns true now, this needs MDL-27713 to be fixed $converter->set_stash('stashname', 42); $this->assertTrue(42 === $converter->get_stash_or_default('stashname')); $this->assertTrue(is_null($converter->get_stash_or_default('stashname', 1))); $this->assertTrue(42 === $converter->get_stash_or_default('stashname', 0, 61)); $converter->set_stash('stashname', array(42 => (object)array('id' => 42)), 18); $stashed = $converter->get_stash_or_default('stashname', 18, 1984); $this->assertEquals(gettype($stashed), 'array'); $this->assertTrue(is_object($stashed[42])); $this->assertTrue($stashed[42]->id === 42); $converter->drop_stash_storage(); } public function test_get_contextid() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); // stash storage must be created in advance $converter->create_stash_storage(); // ids are generated on the first call $id1 = $converter->get_contextid(CONTEXT_BLOCK, 10); $id2 = $converter->get_contextid(CONTEXT_BLOCK, 11); $id3 = $converter->get_contextid(CONTEXT_MODULE, 10); $this->assertNotEquals($id1, $id2); $this->assertNotEquals($id1, $id3); $this->assertNotEquals($id2, $id3); // and then re-used if called with the same params $this->assertEquals($id1, $converter->get_contextid(CONTEXT_BLOCK, 10)); $this->assertEquals($id2, $converter->get_contextid(CONTEXT_BLOCK, 11)); $this->assertEquals($id3, $converter->get_contextid(CONTEXT_MODULE, 10)); // for system and course level, the instance is irrelevant // as we need only one system and one course $id1 = $converter->get_contextid(CONTEXT_COURSE); $id2 = $converter->get_contextid(CONTEXT_COURSE, 10); $id3 = $converter->get_contextid(CONTEXT_COURSE, 14); $this->assertEquals($id1, $id2); $this->assertEquals($id1, $id3); $id1 = $converter->get_contextid(CONTEXT_SYSTEM); $id2 = $converter->get_contextid(CONTEXT_SYSTEM, 11); $id3 = $converter->get_contextid(CONTEXT_SYSTEM, 15); $this->assertEquals($id1, $id2); $this->assertEquals($id1, $id3); $converter->drop_stash_storage(); } public function test_get_nextid() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $id1 = $converter->get_nextid(); $id2 = $converter->get_nextid(); $id3 = $converter->get_nextid(); $this->assertTrue(0 < $id1); $this->assertTrue($id1 < $id2); $this->assertTrue($id2 < $id3); } public function test_migrate_file() { $this->resetAfterTest(true); // set-up the file manager $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); $contextid = $converter->get_contextid(CONTEXT_MODULE, 32); $fileman = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea'); // this fileman has not converted anything yet $fileids = $fileman->get_fileids(); $this->assertEquals(gettype($fileids), 'array'); $this->assertEquals(0, count($fileids)); // try to migrate an invalid file $fileman->itemid = 1; $thrown = false; try { $fileman->migrate_file('/../../../../../../../../../../../../../../etc/passwd'); } catch (moodle1_convert_exception $e) { $thrown = true; } $this->assertTrue($thrown); // migrate a single file $fileman->itemid = 4; $fileman->migrate_file('moddata/unittest/4/icon.gif'); $subdir = substr($this->iconhash, 0, 2); $this->assertTrue(is_file($converter->get_workdir_path().'/files/'.$subdir.'/'.$this->iconhash)); // get the file id $fileids = $fileman->get_fileids(); $this->assertEquals(gettype($fileids), 'array'); $this->assertEquals(1, count($fileids)); // migrate another single file into another file area $fileman->filearea = 'anotherarea'; $fileman->itemid = 7; $fileman->migrate_file('moddata/unittest/4/7/icon.gif', '/', 'renamed.gif'); // get the file records $filerecordids = $converter->get_stash_itemids('files'); foreach ($filerecordids as $filerecordid) { $filerecord = $converter->get_stash('files', $filerecordid); $this->assertEquals($this->iconhash, $filerecord['contenthash']); $this->assertEquals($contextid, $filerecord['contextid']); $this->assertEquals('mod_unittest', $filerecord['component']); if ($filerecord['filearea'] === 'testarea') { $this->assertEquals(4, $filerecord['itemid']); $this->assertEquals('icon.gif', $filerecord['filename']); } } // explicitly clear the list of migrated files $this->assertTrue(count($fileman->get_fileids()) > 0); $fileman->reset_fileids(); $this->assertTrue(count($fileman->get_fileids()) == 0); $converter->drop_stash_storage(); } public function test_migrate_directory() { $this->resetAfterTest(true); // Set-up the file manager. $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); $contextid = $converter->get_contextid(CONTEXT_MODULE, 32); $fileman = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea'); // This fileman has not converted anything yet. $fileids = $fileman->get_fileids(); $this->assertEquals(gettype($fileids), 'array'); $this->assertEquals(0, count($fileids)); // Try to migrate a non-existing directory. $returned = $fileman->migrate_directory('not/existing/directory'); $this->assertEquals(gettype($returned), 'array'); $this->assertEquals(0, count($returned)); $fileids = $fileman->get_fileids(); $this->assertEquals(gettype($fileids), 'array'); $this->assertEquals(0, count($fileids)); // Try to migrate whole course_files. $returned = $fileman->migrate_directory('course_files'); $this->assertEquals(gettype($returned), 'array'); $this->assertEquals(4, count($returned)); // Two files, two directories. $fileids = $fileman->get_fileids(); $this->assertEquals(gettype($fileids), 'array'); $this->assertEquals(4, count($fileids)); $subdir = substr($this->iconhash, 0, 2); $this->assertTrue(is_file($converter->get_workdir_path().'/files/'.$subdir.'/'.$this->iconhash)); // Check the file records. $files = array(); $filerecordids = $converter->get_stash_itemids('files'); foreach ($filerecordids as $filerecordid) { $filerecord = $converter->get_stash('files', $filerecordid); $files[$filerecord['filepath'].$filerecord['filename']] = $filerecord; } $this->assertEquals('array', gettype($files['/.'])); $this->assertEquals('array', gettype($files['/file1.gif'])); $this->assertEquals('array', gettype($files['/sub1/.'])); $this->assertEquals('array', gettype($files['/sub1/file2.gif'])); $this->assertEquals(\file_storage::hash_from_string(''), $files['/.']['contenthash']); $this->assertEquals(\file_storage::hash_from_string(''), $files['/sub1/.']['contenthash']); $this->assertEquals($this->iconhash, $files['/file1.gif']['contenthash']); $this->assertEquals($this->iconhash, $files['/sub1/file2.gif']['contenthash']); $converter->drop_stash_storage(); } public function test_migrate_directory_with_trailing_slash() { $this->resetAfterTest(true); // Set-up the file manager. $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->create_stash_storage(); $contextid = $converter->get_contextid(CONTEXT_MODULE, 32); $fileman = $converter->get_file_manager($contextid, 'mod_unittest', 'testarea'); // Try to migrate a subdirectory passed with the trailing slash. $returned = $fileman->migrate_directory('course_files/sub1/'); // Debugging message must be thrown in this case. $this->assertDebuggingCalled(null, DEBUG_DEVELOPER); $this->assertEquals(gettype($returned), 'array'); $this->assertEquals(2, count($returned)); // One file, one directory. $converter->drop_stash_storage(); } public function test_convert_path() { $path = new convert_path('foo_bar', '/ROOT/THINGS/FOO/BAR'); $this->assertEquals('foo_bar', $path->get_name()); $this->assertEquals('/ROOT/THINGS/FOO/BAR', $path->get_path()); $this->assertEquals('process_foo_bar', $path->get_processing_method()); $this->assertEquals('on_foo_bar_start', $path->get_start_method()); $this->assertEquals('on_foo_bar_end', $path->get_end_method()); } public function test_convert_path_implicit_recipes() { $path = new convert_path('foo_bar', '/ROOT/THINGS/FOO/BAR'); $data = array( 'ID' => 76, 'ELOY' => 'stronk7', 'MARTIN' => 'moodler', 'EMPTY' => null, ); // apply default recipes (converting keys to lowercase) $data = $path->apply_recipes($data); $this->assertEquals(4, count($data)); $this->assertEquals(76, $data['id']); $this->assertEquals('stronk7', $data['eloy']); $this->assertEquals('moodler', $data['martin']); $this->assertSame(null, $data['empty']); } public function test_convert_path_explicit_recipes() { $path = new convert_path( 'foo_bar', '/ROOT/THINGS/FOO/BAR', array( 'newfields' => array( 'david' => 'mudrd8mz', 'petr' => 'skodak', ), 'renamefields' => array( 'empty' => 'nothing', ), 'dropfields' => array( 'id' ), ) ); $data = array( 'ID' => 76, 'ELOY' => 'stronk7', 'MARTIN' => 'moodler', 'EMPTY' => null, ); $data = $path->apply_recipes($data); $this->assertEquals(5, count($data)); $this->assertFalse(array_key_exists('id', $data)); $this->assertEquals('stronk7', $data['eloy']); $this->assertEquals('moodler', $data['martin']); $this->assertEquals('mudrd8mz', $data['david']); $this->assertEquals('skodak', $data['petr']); $this->assertSame(null, $data['nothing']); } public function test_grouped_data_on_nongrouped_convert_path() { // prepare some grouped data $data = array( 'ID' => 77, 'NAME' => 'Pale lagers', 'BEERS' => array( array( 'BEER' => array( 'ID' => 67, 'NAME' => 'Pilsner Urquell', ) ), array( 'BEER' => array( 'ID' => 34, 'NAME' => 'Heineken', ) ), ) ); // declare a non-grouped path $path = new convert_path('beer_style', '/ROOT/BEER_STYLES/BEER_STYLE'); // an attempt to apply recipes throws exception because we do not expect grouped data $this->expectException(convert_path_exception::class); $data = $path->apply_recipes($data); } public function test_grouped_convert_path_with_recipes() { // prepare some grouped data $data = array( 'ID' => 77, 'NAME' => 'Pale lagers', 'BEERS' => array( array( 'BEER' => array( 'ID' => 67, 'NAME' => 'Pilsner Urquell', ) ), array( 'BEER' => array( 'ID' => 34, 'NAME' => 'Heineken', ) ), ) ); // implict recipes work for grouped data if the path is declared as grouped $path = new convert_path('beer_style', '/ROOT/BEER_STYLES/BEER_STYLE', array(), true); $data = $path->apply_recipes($data); $this->assertEquals('Heineken', $data['beers'][1]['beer']['name']); // an attempt to provide explicit recipes on grouped elements throws exception $this->expectException(convert_path_exception::class); $path = new convert_path( 'beer_style', '/ROOT/BEER_STYLES/BEER_STYLE', array( 'renamefields' => array( 'name' => 'beername', // note this is confusing recipe because the 'name' is used for both // beer-style name ('Pale lagers') and beer name ('Pilsner Urquell') ) ), true); } public function test_referenced_course_files() { $text = 'This is a text containing links to file.php as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif" /><a href="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif$@FORCEDOWNLOAD@$">download image</a><br /> <div><a href=\'$@FILEPHP@$/../../../../../../../../../../../../../../../etc/passwd\'>download passwords</a></div> <div><a href=\'$@FILEPHP@$$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$etc$@SLASH@$shadow\'>download shadows</a></div> <br /><a href=\'$@FILEPHP@$$@SLASH@$MANUAL.DOC$@FORCEDOWNLOAD@$\'>download manual</a><br />'; $files = moodle1_converter::find_referenced_files($text); $this->assertEquals(gettype($files), 'array'); $this->assertEquals(2, count($files)); $this->assertTrue(in_array('/pics/news.gif', $files)); $this->assertTrue(in_array('/MANUAL.DOC', $files)); $text = moodle1_converter::rewrite_filephp_usage($text, array('/pics/news.gif', '/another/file/notused.txt')); $this->assertEquals($text, 'This is a text containing links to file.php as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="@@PLUGINFILE@@/pics/news.gif" /><a href="@@PLUGINFILE@@/pics/news.gif?forcedownload=1">download image</a><br /> <div><a href=\'$@FILEPHP@$/../../../../../../../../../../../../../../../etc/passwd\'>download passwords</a></div> <div><a href=\'$@FILEPHP@$$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$..$@SLASH@$etc$@SLASH@$shadow\'>download shadows</a></div> <br /><a href=\'$@FILEPHP@$$@SLASH@$MANUAL.DOC$@FORCEDOWNLOAD@$\'>download manual</a><br />'); } public function test_referenced_files_urlencoded() { $text = 'This is a text containing links to file.php as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif" /><a href="$@FILEPHP@$$@SLASH@$pics$@SLASH@$news.gif$@FORCEDOWNLOAD@$">no space</a><br /> <br /><a href=\'$@FILEPHP@$$@SLASH@$pics$@SLASH@$news%20with%20spaces.gif$@FORCEDOWNLOAD@$\'>with urlencoded spaces</a><br /> <a href="$@FILEPHP@$$@SLASH@$illegal%20pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (space and plus encoded)</a> <a href="$@FILEPHP@$$@SLASH@$illegal pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (none encoded)</a> <a href="$@FILEPHP@$$@SLASH@$illegal%20pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (only space encoded)</a> <a href="$@FILEPHP@$$@SLASH@$illegal pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (only plus)</a>'; $files = moodle1_converter::find_referenced_files($text); $this->assertEquals(gettype($files), 'array'); $this->assertEquals(3, count($files)); $this->assertTrue(in_array('/pics/news.gif', $files)); $this->assertTrue(in_array('/pics/news with spaces.gif', $files)); $this->assertTrue(in_array('/illegal pics+movies/romeo+juliet.avi', $files)); $text = moodle1_converter::rewrite_filephp_usage($text, $files); $this->assertEquals('This is a text containing links to file.php as it is parsed from the backup file. <br /><br /><img border="0" width="110" vspace="0" hspace="0" height="92" title="News" alt="News" src="@@PLUGINFILE@@/pics/news.gif" /><a href="@@PLUGINFILE@@/pics/news.gif?forcedownload=1">no space</a><br /> <br /><a href=\'@@PLUGINFILE@@/pics/news%20with%20spaces.gif?forcedownload=1\'>with urlencoded spaces</a><br /> <a href="@@PLUGINFILE@@/illegal%20pics%2Bmovies/romeo%2Bjuliet.avi">Download the full AVI for free! (space and plus encoded)</a> <a href="@@PLUGINFILE@@/illegal%20pics%2Bmovies/romeo%2Bjuliet.avi">Download the full AVI for free! (none encoded)</a> <a href="$@FILEPHP@$$@SLASH@$illegal%20pics+movies$@SLASH@$romeo+juliet.avi">Download the full AVI for free! (only space encoded)</a> <a href="$@FILEPHP@$$@SLASH@$illegal pics%2Bmovies$@SLASH@$romeo%2Bjuliet.avi">Download the full AVI for free! (only plus)</a>', $text); } public function test_question_bank_conversion() { global $CFG; $this->resetAfterTest(true); copy( "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/questions.xml", "$this->tempdirpath/moodle.xml" ); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->convert(); } public function test_convert_run_convert() { $this->resetAfterTest(true); $converter = convert_factory::get_converter('moodle1', $this->tempdir); $converter->convert(); } public function test_inforef_manager() { $converter = convert_factory::get_converter('moodle1', $this->tempdir); $inforef = $converter->get_inforef_manager('unittest'); $inforef->add_ref('file', 45); $inforef->add_refs('file', array(46, 47)); // todo test the write_refs() via some dummy xml_writer $this->expectException('coding_exception'); $inforef->add_ref('unknown_referenced_item_name', 76); } } PK ��/[��# �# convertlib.phpnu &1i� <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more 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 base converter 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'); /** * Base converter class * * All Moodle backup converters are supposed to extend this base class. * * @throws convert_exception */ abstract class base_converter implements loggable { /** @var string unique identifier of this converter instance */ protected $id; /** @var string the name of the directory containing the unpacked backup being converted */ protected $tempdir; /** @var string the name of the directory where the backup is converted to */ protected $workdir; /** @var null|base_logger logger to use during the conversion */ protected $logger = null; /** * Constructor * * @param string $tempdir the relative path to the directory containing the unpacked backup to convert * @param null|base_logger logger to use during the conversion */ public function __construct($tempdir, $logger = null) { $this->tempdir = $tempdir; $this->id = convert_helper::generate_id($tempdir); $this->workdir = $tempdir . '_' . $this->get_name() . '_' . $this->id; $this->set_logger($logger); $this->log('instantiating '.$this->get_name().' converter '.$this->get_id(), backup::LOG_DEBUG); $this->log('conversion source directory', backup::LOG_DEBUG, $this->tempdir); $this->log('conversion target directory', backup::LOG_DEBUG, $this->workdir); $this->init(); } /** * Sets the logger to use during the conversion * * @param null|base_logger $logger */ public function set_logger($logger) { if (is_null($logger) or ($logger instanceof base_logger)) { $this->logger = $logger; } } /** * If the logger was set for the converter, log the message * * If the $display is enabled, the spaces in the $message text are removed * and the text is used as a string identifier in the core_backup language file. * * @see backup_helper::log() * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { if ($this->logger instanceof base_logger) { backup_helper::log($message, $level, $a, $depth, $display, $this->logger); } } /** * Get instance identifier * * @return string the unique identifier of this converter instance */ public function get_id() { return $this->id; } /** * Get converter name * * @return string the system name of the converter */ public function get_name() { $parts = explode('_', get_class($this)); return array_shift($parts); } /** * Converts the backup directory */ public function convert() { try { $this->log('creating the target directory', backup::LOG_DEBUG); $this->create_workdir(); $this->log('executing the conversion', backup::LOG_DEBUG); $this->execute(); $this->log('replacing the source directory with the converted version', backup::LOG_DEBUG); $this->replace_tempdir(); } catch (Exception $e) { } // clean-up stuff if needed $this->destroy(); // eventually re-throw the execution exception if (isset($e) and ($e instanceof Exception)) { throw $e; } } /** * @return string the full path to the working directory */ public function get_workdir_path() { global $CFG; return make_backup_temp_directory($this->workdir); } /** * @return string the full path to the directory with the source backup */ public function get_tempdir_path() { global $CFG; return make_backup_temp_directory($this->tempdir); } /// public static methods ////////////////////////////////////////////////// /** * Makes sure that this converter is available at this site * * This is intended for eventual PHP extensions check, environment check etc. * All checks that do not depend on actual backup data should be done here. * * @return boolean true if this converter should be considered as available */ public static function is_available() { return true; } /** * Detects the format of the backup directory * * Moodle 2.x format is being detected by the core itself. The converters are * therefore supposed to detect the source format. Eventually, if the target * format os not {@link backup::FORMAT_MOODLE} then they should be able to * detect both source and target formats. * * @param string $tempdir the name of the backup directory * @return null|string null if not recognized, backup::FORMAT_xxx otherwise */ public static function detect_format($tempdir) { return null; } /** * Returns the basic information about the converter * * The returned array must contain the following keys: * 'from' - the supported source format, eg. backup::FORMAT_MOODLE1 * 'to' - the supported target format, eg. backup::FORMAT_MOODLE * 'cost' - the cost of the conversion, non-negative non-zero integer */ public static function description() { return array( 'from' => null, 'to' => null, 'cost' => null, ); } /// end of public API ////////////////////////////////////////////////////// /** * Initialize the instance if needed, called by the constructor */ protected function init() { } /** * Converts the contents of the tempdir into the target format in the workdir */ protected abstract function execute(); /** * Prepares a new empty working directory */ protected function create_workdir() { fulldelete($this->get_workdir_path()); if (!check_dir_exists($this->get_workdir_path())) { throw new convert_exception('failed_create_workdir'); } } /** * Replaces the source backup directory with the converted version * * If $CFG->keeptempdirectoriesonbackup is defined, the original source * source backup directory is kept for debugging purposes. */ protected function replace_tempdir() { global $CFG; $tempdir = $this->get_tempdir_path(); if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($tempdir); } else { if (!rename($tempdir, $tempdir . '_' . $this->get_name() . '_' . $this->id . '_source')) { throw new convert_exception('failed_rename_source_tempdir'); } } if (!rename($this->get_workdir_path(), $tempdir)) { throw new convert_exception('failed_move_converted_into_place'); } } /** * Cleans up stuff after the execution * * Note that we do not know if the execution was successful or not. * An exception might have been thrown. */ protected function destroy() { global $CFG; if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($this->get_workdir_path()); } } } /** * General convert-related exception * * @author David Mudrak <david@moodle.com> */ class convert_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); } } PK ��/[Q��� imscc11/backuplib.phpnu &1i� <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more 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 imscc11_export_converter} class * * @package core * @subpackage 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') || die(); require_once($CFG->dirroot . '/backup/converter/convertlib.php'); class imscc11_export_converter extends base_converter { static public function get_deps() { global $CFG; require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php'); return array( 'users' => setting_dependency::DISABLED_VALUE, 'filters' => setting_dependency::DISABLED_VALUE, 'blocks' => setting_dependency::DISABLED_VALUE ); } protected function execute() { } public static function description() { return array( 'from' => backup::FORMAT_MOODLE, 'to' => backup::FORMAT_IMSCC11, 'cost' => 10 ); } } class imscc11_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.imscc) $zipfile = $basepath . '/backup.imscc'; // Perform storage and return it (TODO: shouldn't be array but proper result object) // Let's send the file to file storage, everything already defined // 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($this->get_backupid()); // Extract useful information to decide $file = $sinfo['filename']->value; $filename = basename($file,'.'.pathinfo($file, PATHINFO_EXTENSION)).'.imscc'; // Backup filename $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 $ctxid = context_user::instance($userid)->id; $component = 'user'; $filearea = 'backup'; $itemid = 0; $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(); } return array('backup_destination' => $fs->create_file_from_pathname($fr, $zipfile)); } } class imscc11_zip_contents extends backup_execution_step { 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; } // Calculate the zip fullpath (in OS temp area it's always backup.mbz) $zipfile = $basepath . '/backup.imscc'; // Get the zip packer $zippacker = get_file_packer('application/zip'); // Zip files $zippacker->archive_to_pathname($files, $zipfile); } } class imscc11_backup_convert extends backup_execution_step { protected function define_execution() { global $CFG; // Get basepath $basepath = $this->get_basepath(); require_once($CFG->dirroot . '/backup/cc/cc_includes.php'); $tempdir = $CFG->backuptempdir . '/' . uniqid('', true); if (mkdir($tempdir, $CFG->directorypermissions, true)) { cc_convert_moodle2::convert($basepath, $tempdir); //Switch the directories if (empty($CFG->keeptempdirectoriesonbackup)) { fulldelete($basepath); } else { if (!rename($basepath, $basepath . '_moodle2_source')) { throw new backup_task_exception('failed_rename_source_tempdir'); } } if (!rename($tempdir, $basepath)) { throw new backup_task_exception('failed_move_converted_into_place'); } } } } PK ��/[����" " imscc11/lib.phpnu &1i� <?php /** * Provides Common Cartridge v1.1 converter class * * @package core * @subpackage 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/converter/convertlib.php'); require_once($CFG->dirroot.'/backup/cc/includes/constants.php'); require_once($CFG->dirroot.'/backup/cc/cc112moodle.php'); require_once($CFG->dirroot.'/backup/cc/validator.php'); class imscc11_converter extends base_converter { /** * Log a message * * @see parent::log() * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { parent::log('(imscc1) '.$message, $level, $a, $depth, $display); } /** * Detects the Common Cartridge 1.0 format of the backup directory * * @param string $tempdir the name of the backup directory * @return null|string backup::FORMAT_IMSCC11 if the Common Cartridge 1.1 is detected, null otherwise */ public static function detect_format($tempdir) { $filepath = make_backup_temp_directory($tempdir, false); if (!is_dir($filepath)) { throw new convert_helper_exception('tmp_backup_directory_not_found', $filepath); } $manifest = cc112moodle::get_manifest($filepath); if (file_exists($manifest)) { // Looks promising, lets load some information. $handle = fopen($manifest, 'r'); $xml_snippet = fread($handle, 1024); fclose($handle); // Check if it has the required strings. $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=http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1"; if (strpos($xml_snippet, $search_string) !== false) { return backup::FORMAT_IMSCC11; } } return null; } /** * Returns the basic information about the converter * * The returned array must contain the following keys: * 'from' - the supported source format, eg. backup::FORMAT_MOODLE1 * 'to' - the supported target format, eg. backup::FORMAT_MOODLE * 'cost' - the cost of the conversion, non-negative non-zero integer */ public static function description() { return array( 'from' => backup::FORMAT_IMSCC11, 'to' => backup::FORMAT_MOODLE1, 'cost' => 10 ); } protected function execute() { global $CFG; $manifest = cc112moodle::get_manifest($this->get_tempdir_path()); if (empty($manifest)) { throw new imscc11_convert_exception('No Manifest detected!'); } $this->log('validating manifest', backup::LOG_DEBUG, null, 1); $validator = new manifest_validator($CFG->dirroot . '/backup/cc/schemas11'); if (!$validator->validate($manifest)) { $this->log('validation error(s): '.PHP_EOL.error_messages::instance(), backup::LOG_DEBUG, null, 2); throw new imscc11_convert_exception(error_messages::instance()->to_string(true)); } $manifestdir = dirname($manifest); $cc112moodle = new cc112moodle($manifest); if ($cc112moodle->is_auth()) { throw new imscc11_convert_exception('protected_cc_not_supported'); } $status = $cc112moodle->generate_moodle_xml(); // Final cleanup. $xml_error = new libxml_errors_mgr(true); $mdoc = new DOMDocument(); $mdoc->preserveWhiteSpace = false; $mdoc->formatOutput = true; $mdoc->validateOnParse = false; $mdoc->strictErrorChecking = false; if ($mdoc->load($manifestdir.'/moodle.xml', LIBXML_NONET)) { $mdoc->save($this->get_workdir_path().'/moodle.xml', LIBXML_NOEMPTYTAG); } else { $xml_error->collect(); $this->log('validation error(s): '.PHP_EOL.error_messages::instance(), backup::LOG_DEBUG, null, 2); throw new imscc11_convert_exception(error_messages::instance()->to_string(true)); } // Move the files to the workdir. rename($manifestdir.'/course_files', $this->get_workdir_path().'/course_files'); } } /** * Exception thrown by this converter */ class imscc11_convert_exception extends convert_exception { } PK ��/[N��� � imscc1/backuplib.phpnu &1i� <?php require_once($CFG->dirroot . '/backup/converter/convertlib.php'); class imscc1_export_converter extends base_converter { public static function is_available() { return false; } protected function execute() { } }PK ��/[͙� imscc1/lib.phpnu &1i� <?php /** * Provides Common Cartridge v1 converter class * * @package core * @subpackage 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/converter/convertlib.php'); require_once($CFG->dirroot.'/backup/cc/includes/constants.php'); require_once($CFG->dirroot.'/backup/cc/cc2moodle.php'); require_once($CFG->dirroot.'/backup/cc/validator.php'); class imscc1_converter extends base_converter { /** * Log a message * * @see parent::log() * @param string $message message text * @param int $level message level {@example backup::LOG_WARNING} * @param null|mixed $a additional information * @param null|int $depth the message depth * @param bool $display whether the message should be sent to the output, too */ public function log($message, $level, $a = null, $depth = null, $display = false) { parent::log('(imscc1) '.$message, $level, $a, $depth, $display); } /** * Detects the Common Cartridge 1.0 format of the backup directory * * @param string $tempdir the name of the backup directory * @return null|string backup::FORMAT_IMSCC1 if the Common Cartridge 1.0 is detected, null otherwise */ public static function detect_format($tempdir) { $filepath = make_backup_temp_directory($tempdir, false); if (!is_dir($filepath)) { throw new convert_helper_exception('tmp_backup_directory_not_found', $filepath); } $manifest = cc2moodle::get_manifest($filepath); if (!empty($manifest)) { // Looks promising, lets load some information. $handle = fopen($manifest, 'r'); $xml_snippet = fread($handle, 1024); fclose($handle); // Check if it has the required strings. $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=http://www.imsglobal.org/xsd/imscc/imscp_v1p1"; if (strpos($xml_snippet, $search_string) !== false) { return backup::FORMAT_IMSCC1; } } return null; } /** * Returns the basic information about the converter * * The returned array must contain the following keys: * 'from' - the supported source format, eg. backup::FORMAT_MOODLE1 * 'to' - the supported target format, eg. backup::FORMAT_MOODLE * 'cost' - the cost of the conversion, non-negative non-zero integer */ public static function description() { return array( 'from' => backup::FORMAT_IMSCC1, 'to' => backup::FORMAT_MOODLE1, 'cost' => 10 ); } protected function execute() { global $CFG; $manifest = cc2moodle::get_manifest($this->get_tempdir_path()); if (empty($manifest)) { throw new imscc1_convert_exception('No Manifest detected!'); } $this->log('validating manifest', backup::LOG_DEBUG, null, 1); $validator = new manifest10_validator($CFG->dirroot . '/backup/cc/schemas'); if (!$validator->validate($manifest)) { $this->log('validation error(s): '.PHP_EOL.error_messages::instance(), backup::LOG_DEBUG, null, 2); throw new imscc1_convert_exception(error_messages::instance()->to_string(true)); } $manifestdir = dirname($manifest); $cc2moodle = new cc2moodle($manifest); if ($cc2moodle->is_auth()) { throw new imscc1_convert_exception('protected_cc_not_supported'); } $status = $cc2moodle->generate_moodle_xml(); // Final cleanup. $xml_error = new libxml_errors_mgr(true); $mdoc = new DOMDocument(); $mdoc->preserveWhiteSpace = false; $mdoc->formatOutput = true; $mdoc->validateOnParse = false; $mdoc->strictErrorChecking = false; if ($mdoc->load($manifestdir.'/moodle.xml', LIBXML_NONET)) { $mdoc->save($this->get_workdir_path().'/moodle.xml', LIBXML_NOEMPTYTAG); } else { $xml_error->collect(); $this->log('validation error(s): '.PHP_EOL.error_messages::instance(), backup::LOG_DEBUG, null, 2); throw new imscc1_convert_exception(error_messages::instance()->to_string(true)); } // Move the files to the workdir. rename($manifestdir.'/course_files', $this->get_workdir_path().'/course_files'); } } /** * Exception thrown by this converter */ class imscc1_convert_exception extends convert_exception { } PK ��/[V��� � moodle1/backuplib.phpnu &1i� PK ��/[;����� �� ; moodle1/lib.phpnu &1i� PK ��/[�� ]�C �C � moodle1/handlerlib.phpnu &1i� PK ��/[.�j~ moodle1/tests/fixtures/icon.gifnu &1i� PK ��/[���� �� $ � moodle1/tests/fixtures/questions.xmlnu &1i� PK ��/[J���$G $G ! �� moodle1/tests/fixtures/moodle.xmlnu &1i� PK ��/[A����j �j ( d moodle1/tests/moodle1_converter_test.phpnu &1i� PK ��/[��# �# �� convertlib.phpnu &1i� PK ��/[Q��� o� imscc11/backuplib.phpnu &1i� PK ��/[����" " �� imscc11/lib.phpnu &1i� PK ��/[N��� � ,� imscc1/backuplib.phpnu &1i� PK ��/[͙� e� imscc1/lib.phpnu &1i� PK ��
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка