Merge branch 'master' into backup-convert
authorDavid Mudrak <david@moodle.com>
Fri, 13 May 2011 01:16:13 +0000 (03:16 +0200)
committerDavid Mudrak <david@moodle.com>
Fri, 13 May 2011 01:16:13 +0000 (03:16 +0200)
13 files changed:
backup/controller/restore_controller.class.php
backup/converter/convertlib.php [new file with mode: 0644]
backup/converter/moodle1/handlerlib.php [new file with mode: 0644]
backup/converter/moodle1/lib.php [new file with mode: 0644]
backup/converter/moodle1/simpletest/files/moodle.xml [new file with mode: 0755]
backup/converter/moodle1/simpletest/testlib.php [new file with mode: 0644]
backup/util/factories/convert_factory.class.php [new file with mode: 0644]
backup/util/helper/backup_general_helper.class.php
backup/util/helper/convert_helper.class.php [new file with mode: 0644]
backup/util/helper/simpletest/testconverthelper.php [new file with mode: 0644]
backup/util/includes/convert_includes.php [new file with mode: 0644]
mod/choice/backup/moodle1/lib.php [new file with mode: 0644]
mod/forum/backup/moodle1/lib.php [new file with mode: 0644]

index f08b09b..c68433c 100644 (file)
@@ -377,38 +377,36 @@ class restore_controller extends backup implements loggable {
     }
 
     /**
-     * convert from current format to backup::MOODLE format
+     * Converts from current format to backup::MOODLE format
      */
     public function convert() {
+        global $CFG;
+        require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php');
+
         if ($this->status != backup::STATUS_REQUIRE_CONV) {
             throw new restore_controller_exception('cannot_convert_not_required_status');
         }
-        if ($this->format == backup::FORMAT_UNKNOWN) {
-            throw new restore_controller_exception('cannot_convert_from_unknown_format');
-        }
-        if ($this->format == backup::FORMAT_MOODLE1) {
-            // TODO: Implement moodle1 => moodle2 conversion
-            throw new restore_controller_exception('cannot_convert_yet_from_moodle1_format');
+
+        // Run conversion to the proper format
+        if (!convert_helper::to_moodle2_format($this->get_tempdir(), $this->format)) {
+            // todo - unable to find the conversion path, what to do now?
+            // throwing the exception as a temporary solution
+            throw new restore_controller_exception('unable_to_find_conversion_path');
         }
 
-        // Once conversions have finished, we check again the format
-        $newformat = backup_general_helper::detect_backup_format($tempdir);
+        // If no exceptions were thrown, then we are in the proper format
+        $this->format = backup::FORMAT_MOODLE;
 
-        // If format is moodle2, load plan, apply security and set status based on interactivity
-        if ($newformat === backup::FORMAT_MOODLE) {
-            // Load plan
-            $this->load_plan();
+        // Load plan, apply security and set status based on interactivity
+        $this->load_plan();
 
-            // Perform all initial security checks and apply (2nd param) them to settings automatically
-            restore_check::check_security($this, true);
+        // Perform all initial security checks and apply (2nd param) them to settings automatically
+        restore_check::check_security($this, true);
 
-            if ($this->interactive == backup::INTERACTIVE_YES) {
-                $this->set_status(backup::STATUS_SETTING_UI);
-            } else {
-                $this->set_status(backup::STATUS_NEED_PRECHECK);
-            }
+        if ($this->interactive == backup::INTERACTIVE_YES) {
+            $this->set_status(backup::STATUS_SETTING_UI);
         } else {
-            throw new restore_controller_exception('conversion_ended_with_wrong_format', $newformat);
+            $this->set_status(backup::STATUS_NEED_PRECHECK);
         }
     }
 
diff --git a/backup/converter/convertlib.php b/backup/converter/convertlib.php
new file mode 100644 (file)
index 0000000..73341e4
--- /dev/null
@@ -0,0 +1,241 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more 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 {
+
+    /** @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;
+
+    /**
+     * Constructor
+     *
+     * @param string $tempdir the relative path to the directory containing the unpacked backup to convert
+     */
+    public function __construct($tempdir) {
+
+        $this->tempdir  = $tempdir;
+        $this->id       = convert_helper::generate_id($this->workdir);
+        $this->workdir  = $tempdir . '_' . $this->get_name() . '_' . $this->id;
+        $this->init();
+    }
+
+    /**
+     * 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() {
+        return array_shift(explode('_', get_class($this)));
+    }
+
+    /**
+     * Converts the backup directory
+     */
+    public function convert() {
+
+        try {
+            $this->create_workdir();
+            $this->execute();
+            $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;
+        }
+    }
+
+    /// 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,
+        );
+    }
+
+    /**
+     * @return string the full path to the working directory
+     */
+    public function get_workdir_path() {
+        global $CFG;
+
+        return "$CFG->dataroot/temp/backup/$this->workdir";
+    }
+
+    /**
+     * @return string the full path to the directory with the source backup
+     */
+    public function get_tempdir_path() {
+        global $CFG;
+
+        return "$CFG->dataroot/temp/backup/$this->tempdir";
+    }
+
+    /// 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;
+
+        if (empty($CFG->keeptempdirectoriesonbackup)) {
+            fulldelete($this->get_tempdir_path());
+        } else {
+            if (!rename($this->get_tempdir_path(), $this->get_tempdir_path()  . '_' . $this->get_name() . '_' . $this->id . '_source')) {
+                throw new convert_exception('failed_rename_source_tempdir');
+            }
+        }
+
+        if (!rename($this->get_workdir_path(), $this->get_tempdir_path())) {
+            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);
+    }
+}
diff --git a/backup/converter/moodle1/handlerlib.php b/backup/converter/moodle1/handlerlib.php
new file mode 100644 (file)
index 0000000..970557d
--- /dev/null
@@ -0,0 +1,549 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more 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),
+        );
+
+        $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 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 convert_exception
+     * @return array of {@link moodle1_handler} instances
+     */
+    protected static function get_plugin_handlers($type, moodle1_converter $converter) {
+        global $CFG;
+
+        $handlers = array();
+        $plugins = get_plugin_list($type);
+        foreach ($plugins as $name => $dir) {
+            $handlerfile  = $dir . '/backup/moodle1/lib.php';
+            $handlerclass = "moodle1_{$type}_{$name}_handler";
+            if (!file_exists($handlerfile)) {
+                continue;
+            }
+            require_once($handlerfile);
+
+            if (!class_exists($handlerclass)) {
+                throw new convert_exception('missing_handler_class', $handlerclass);
+            }
+            $handlers[] = new $handlerclass($converter, $type, $name);
+        }
+        return $handlers;
+    }
+}
+
+
+/**
+ * Base backup conversion handler
+ */
+abstract class moodle1_handler {
+
+    /** @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;
+    }
+}
+
+
+/**
+ * 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
+     */
+    public function open_xml_writer($filename) {
+
+        if (!is_null($this->xmlfilename) and $filename !== $this->xmlfilename) {
+            throw new 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 convert_exception('unable_create_target_directory', $directory);
+            }
+            $this->xmlwriter = new xml_writer(new file_xml_output($fullpath));
+            $this->xmlwriter->start();
+        }
+    }
+
+    /**
+     * Close the XML writer
+     *
+     * At the moment, the caller must close all tags before calling
+     *
+     * @return void
+     */
+    public function close_xml_writer() {
+        if ($this->xmlwriter instanceof xml_writer) {
+            $this->xmlwriter->stop();
+        }
+        unset($this->xmlwriter);
+        $this->xmlwriter = null;
+        $this->xmlfilename = null;
+    }
+
+    /**
+     * 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 (all 'id' are there automatically)
+     * @param string $parent used internally during the recursion, do not set yourself
+     */
+    public function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
+
+        $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 ($name === 'id' or 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);
+    }
+}
+
+
+/**
+ * Process the root element of the backup file
+ */
+class moodle1_root_handler extends moodle1_handler {
+
+    public function get_paths() {
+        return array(new convert_path('root_element', '/MOODLE_BACKUP'));
+    }
+
+    public function process_root_element($data) {
+    }
+
+    /**
+     * This is executed at the very start of the moodle.xml parsing
+     */
+    public function on_root_element_start() {
+    }
+
+    /**
+     * This is executed at the end of the moodle.xml parsing
+     */
+    public function on_root_element_end() {
+    }
+}
+
+
+/**
+ * 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 {
+
+    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'),
+        );
+    }
+
+    public function process_info($data) {
+    }
+
+    public function process_info_details($data) {
+    }
+
+    public function process_info_details_mod($data) {
+    }
+
+    public function process_info_details_mod_instance($data) {
+    }
+}
+
+
+/**
+ * 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'            => 1, // @todo is this correct?
+                        '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, $this->course['id']);
+
+        // 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;
+
+        $this->open_xml_writer('course/course.xml');
+        $this->write_xml('course', $this->course, array('/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 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_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,
+                        'availablefrom'             => 0,
+                        'availableuntil'            => 0,
+                        'showavailability'          => 0,
+                        'availability_info'         => array(),
+                        'visibleold'                => 1,
+                    ),
+                    'dropfields' => array(
+                        'instance',
+                        'roles_overrides',
+                        'roles_assignments',
+                    ),
+                    'renamefields' => array(
+                        'type' => 'modulename',
+                    ),
+                )
+            )
+        );
+    }
+
+    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;
+
+        // 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)) {
+            include($versionfile);
+            $data['version'] = $module->version;
+        } 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.
+        $this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
+
+        // write the module.xml file
+        $this->open_xml_writer('activities/'.$data['modulename'].'_'.$data['id'].'/module.xml');
+        $this->write_xml('module', $data, array('/module/version'));
+        $this->close_xml_writer();
+    }
+
+    /**
+     * Writes sections/section_xxx/section.xml file
+     */
+    public function on_course_section_end() {
+
+        $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml');
+        $this->write_xml('section', $this->currentsection);
+        $this->close_xml_writer();
+        unset($this->currentsection);
+    }
+}
+
+
+/**
+ * Shared base class for activity modules and blocks 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;
+    }
+}
+
+
+/**
+ * Base class for activity module handlers
+ */
+abstract class moodle1_mod_handler extends moodle1_plugin_handler {
+
+    /**
+     * Returns course module id for the given instance id
+     *
+     * The mapping from instance id to course module id has been stashed by
+     * {@link moodle1_course_outline_handler::process_course_module()}
+     *
+     * @param int $instance the module instance id
+     * @return int
+     */
+    protected function get_moduleid($instance) {
+
+        $stashed = $this->converter->get_stash('cminfo_'.$this->pluginname, $instance);
+        return $stashed['id'];
+    }
+}
+
+
+/**
+ * Base class for activity module handlers
+ */
+abstract class moodle1_block_handler extends moodle1_plugin_handler {
+
+}
diff --git a/backup/converter/moodle1/lib.php b/backup/converter/moodle1/lib.php
new file mode 100644 (file)
index 0000000..1d298a2
--- /dev/null
@@ -0,0 +1,822 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more 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(dirname(__FILE__) . '/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 string the current module being processed */
+    protected $currentmod = '';
+
+    /** @var string the current block being processed */
+    protected $currentblock = '';
+
+    /** @var string path currently locking processing of children */
+    protected $pathlock;
+
+    /**
+     * Instructs the dispatcher to ignore all children below path processor returning it
+     */
+    const SKIP_ALL_CHILDREN = -991399;
+
+    /**
+     * 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;
+
+        $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/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();
+
+        // good boy, prepare XML parser and processor
+        $this->xmlparser = new progressive_parser();
+        $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
+        $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->register_handler($handler, $handler->get_paths());
+        }
+    }
+
+    /**
+     * Converts the contents of the tempdir into the target format in the workdir
+     */
+    protected function execute() {
+        $this->create_stash_storage();
+        $this->xmlparser->process();
+        $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->currentmod, $path);
+        }
+
+        if ($path !== $data['path']) {
+            if (!array_key_exists($path, $this->pathelements)) {
+                // no handler registered for the transformed MOD or BLOCK path
+                // todo add this event to the convert log instead of debugging
+                //debugging('No handler registered for the path ' . $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', $object);
+        }
+
+        // release the lock if we aren't anymore within children of it
+        if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
+            $this->pathlock = null;
+        }
+
+        // if 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']);
+            $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_data($returned);
+
+        // use just the cooked parsed data otherwise
+        } else {
+            $element->set_data($data);
+        }
+    }
+
+    /**
+     * Executes operations required at the start of a watched path
+     *
+     * Note that this is called before the MOD and BLOCK paths are expanded
+     * so the current plugin is not known yet. Also note that this is
+     * triggered before the previous path is actually dispatched.
+     *
+     * @param string $path in the original file
+     */
+    public function path_start_reached($path) {
+
+        if (empty($this->pathelements[$path])) {
+            return;
+        }
+
+        $element = $this->pathelements[$path];
+        $pobject = $element->get_processing_object();
+        $method  = 'on_' . $element->get_name() . '_start';
+
+        if (method_exists($pobject, $method)) {
+            $pobject->$method();
+        }
+    }
+
+    /**
+     * 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->currentmod, $path);
+        }
+
+        if (empty($this->pathelements[$path])) {
+            return;
+        }
+
+        $element = $this->pathelements[$path];
+        $pobject = $element->get_processing_object();
+        $method  = 'on_' . $element->get_name() . '_end';
+
+        if (method_exists($pobject, $method)) {
+            $pobject->$method();
+        }
+    }
+
+    /**
+     * 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');
+        } else {
+            return $record->info;
+        }
+    }
+
+    /**
+     * 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.
+     *
+     * @see get_context_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) {
+        static $autoincrement = 0;
+
+        $stashname = 'context' . $level;
+
+        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
+            $this->set_stash($stashname, ++$autoincrement, $instance);
+            return $autoincrement;
+        }
+    }
+}
+
+
+/**
+ * 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
+ */
+class moodle1_parser_processor extends grouped_parser_processor {
+
+    /** @var moodle1_converter */
+    protected $converter;
+
+    public function __construct(moodle1_converter $converter) {
+        $this->converter = $converter;
+        parent::__construct();
+    }
+
+    /**
+     * Provide NULL and legacy file.php uses decoding
+     */
+    public function process_cdata($cdata) {
+        global $CFG;
+
+        if ($cdata === '$@NULL@$') {  // Some cases we know we can skip complete processing
+            return null;
+        } else if ($cdata === '') {
+            return '';
+        } else if (is_numeric($cdata)) {
+            return $cdata;
+        } else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
+            return $cdata;                // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
+        } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
+            return $cdata;
+        }
+        // Decode file.php calls
+        $search = array ("$@FILEPHP@$");
+        $replace = array(get_file_url($this->courseid));
+        $result = str_replace($search, $replace, $cdata);
+        // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
+        $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
+        if ($CFG->slasharguments) {
+            $replace = array('/', '?forcedownload=1');
+        } else {
+            $replace = array('%2F', '&amp;forcedownload=1');
+        }
+        return str_replace($search, $replace, $result);
+    }
+
+    /**
+     * Override this method so we'll be able to skip
+     * dispatching some well-known chunks, like the
+     * ones being 100% part of subplugins stuff. Useful
+     * for allowing development without having all the
+     * possible restore subplugins defined
+     */
+    protected function postprocess_chunk($data) {
+
+        // Iterate over all the data tags, if any of them is
+        // not 'subplugin_XXXX' or has value, then it's a valid chunk,
+        // pass it to standard (parent) processing of chunks.
+        foreach ($data['tags'] as $key => $value) {
+            if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) {
+                parent::postprocess_chunk($data);
+                return;
+            }
+        }
+        // Arrived here, all the tags correspond to sublplugins and are empty,
+        // skip the chunk, and debug_developer notice
+        $this->chunks--; // not counted
+        debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) .
+                  ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER);
+    }
+
+    /**
+     * 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);
+    }
+}
+
+
+/**
+ * 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 mixed last data read for this element or returned data by processing method */
+    protected $data = 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
+     *
+     * @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 processing method name
+        $this->set_processing_method('process_' . $name);
+
+        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 element data
+     *
+     * @param mixed
+     */
+    public function set_data($data) {
+        $this->data = $data;
+    }
+
+    /**
+     * 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);
+
+            // 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 mixed the element data
+     */
+    public function get_data() {
+        return $this->data;
+    }
+
+
+    /// 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 the element's processing 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', $pobject);
+        }
+        if (!method_exists($pobject, $this->get_processing_method())) {
+            throw new convert_path_exception('convert_path_missingmethod', $this->get_processing_method());
+        }
+    }
+}
+
+
+/**
+ * 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);
+    }
+}
diff --git a/backup/converter/moodle1/simpletest/files/moodle.xml b/backup/converter/moodle1/simpletest/files/moodle.xml
new file mode 100755 (executable)
index 0000000..5440c5e
--- /dev/null
@@ -0,0 +1,602 @@
+<?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>Miscellaneous</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@gmail.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@lsu.edu</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:&quot;aggregatesonly&quot;;a:4:{i:0;s:2:&quot;70&quot;;i:1;s:2:&quot;71&quot;;i:2;s:2:&quot;72&quot;;i:3;s:2:&quot;73&quot;;}s:10:&quot;gradesonly&quot;;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@lsu.edu</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>
diff --git a/backup/converter/moodle1/simpletest/testlib.php b/backup/converter/moodle1/simpletest/testlib.php
new file mode 100644 (file)
index 0000000..c72076b
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the moodle1 converter
+ *
+ * @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/converter/moodle1/lib.php');
+
+class moodle1_converter_test extends UnitTestCase {
+
+    public static $includecoverage = array();
+
+    /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */
+    protected $tempdir;
+
+    public function setUp() {
+        global $CFG;
+
+        $this->tempdir = convert_helper::generate_id('simpletest');
+        check_dir_exists("$CFG->dataroot/temp/backup/$this->tempdir");
+        copy(
+            "$CFG->dirroot/backup/converter/moodle1/simpletest/files/moodle.xml",
+            "$CFG->dataroot/temp/backup/$this->tempdir/moodle.xml"
+        );
+    }
+
+    public function tearDown() {
+        global $CFG;
+        if (empty($CFG->keeptempdirectoriesonbackup)) {
+            fulldelete("$CFG->dataroot/temp/backup/$this->tempdir");
+        }
+    }
+
+    public function test_detect_format() {
+        $detected = moodle1_converter::detect_format($this->tempdir);
+        $this->assertEqual(backup::FORMAT_MOODLE1, $detected);
+    }
+
+    public function test_convert_factory() {
+        $converter = convert_factory::converter('moodle1', $this->tempdir);
+        $this->assertIsA($converter, 'moodle1_converter');
+    }
+
+    public function test_stash_storage_not_created() {
+        $converter = convert_factory::converter('moodle1', $this->tempdir);
+        $this->expectException('moodle1_convert_storage_exception');
+        $converter->set_stash('tempinfo', 12);
+    }
+
+    public function test_stash_requiring_empty_stash() {
+        $converter = convert_factory::converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+        $converter->set_stash('tempinfo', 12);
+        $this->expectException('moodle1_convert_empty_storage_exception');
+        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
+            $converter->drop_stash_storage();
+            throw new moodle1_convert_empty_storage_exception('rethrowing');
+        }
+    }
+
+    public function test_stash_storage() {
+        $converter = convert_factory::converter('moodle1', $this->tempdir);
+        $converter->create_stash_storage();
+
+        // test stashes without itemid
+        $converter->set_stash('tempinfo1', 12);
+        $converter->set_stash('tempinfo2', array('a' => 2, 'b' => 3));
+        $this->assertIdentical(12, $converter->get_stash('tempinfo1'));
+        $this->assertIdentical(array('a' => 2, 'b' => 3), $converter->get_stash('tempinfo2'));
+
+        // overwriting a stashed value is allowed
+        $converter->set_stash('tempinfo1', '13');
+        $this->assertNotIdentical(13, $converter->get_stash('tempinfo1'));
+        $this->assertIdentical('13', $converter->get_stash('tempinfo1'));
+
+        // repeated reading is allowed
+        $this->assertIdentical('13', $converter->get_stash('tempinfo1'));
+
+        // test stashes with itemid
+        $converter->set_stash('tempinfo', 'Hello', 1);
+        $converter->set_stash('tempinfo', 'World', 2);
+        $this->assertIdentical('Hello', $converter->get_stash('tempinfo', 1));
+        $this->assertIdentical('World', $converter->get_stash('tempinfo', 2));
+
+        $converter->drop_stash_storage();
+    }
+
+    public function test_get_contextid() {
+        $converter = convert_factory::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_COURSE, 10);
+        $id2 = $converter->get_contextid(CONTEXT_COURSE, 11);
+        $id3 = $converter->get_contextid(CONTEXT_MODULE, 10);
+
+        $this->assertNotEqual($id1, $id2);
+        $this->assertNotEqual($id1, $id3);
+        $this->assertNotEqual($id2, $id3);
+
+        // and then re-used if called with the same params
+        $this->assertEqual($id1, $converter->get_contextid(CONTEXT_COURSE, 10));
+        $this->assertEqual($id2, $converter->get_contextid(CONTEXT_COURSE, 11));
+        $this->assertEqual($id3, $converter->get_contextid(CONTEXT_MODULE, 10));
+
+        $converter->drop_stash_storage();
+    }
+
+    public function test_convert_run_convert() {
+        $converter = convert_factory::converter('moodle1', $this->tempdir);
+        $converter->convert();
+    }
+}
diff --git a/backup/util/factories/convert_factory.class.php b/backup/util/factories/convert_factory.class.php
new file mode 100644 (file)
index 0000000..c02825b
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * @package    core
+ * @subpackage backup-convert
+ * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Factory class to create new instances of backup converters
+ */
+abstract class convert_factory {
+
+    /**
+     * Instantinates the given converter operating on a given directory
+     *
+     * @throws coding_exception
+     * @param  $name The converter name
+     * @param  $tempdir The temp directory to operate on
+     * @return base_converter
+     */
+    public static function converter($name, $tempdir) {
+        global $CFG;
+
+        $name = clean_param($name, PARAM_SAFEDIR);
+
+        $classfile = "$CFG->dirroot/backup/converter/$name/lib.php";
+        $classname = "{$name}_converter";
+
+        if (!file_exists($classfile)) {
+            throw new coding_exception("Converter factory error: class file not found $classfile");
+        }
+        require_once($classfile);
+
+        if (!class_exists($classname)) {
+            throw new coding_exception("Converter factory error: class not found $classname");
+        }
+        return new $classname($tempdir);
+    }
+}
index 99db287..f0891f6 100644 (file)
@@ -228,50 +228,35 @@ abstract class backup_general_helper extends backup_helper {
     }
 
     /**
-     * Given one temp/backup/xxx dir, detect its format
+     * Detects the format of the given unpacked backup directory
      *
-     * TODO: Move harcoded detection here to delegated classes under backup/format (moodle1, imscc..)
-     *       conversion code will be there too.
+     * @param string $tempdir the name of the backup directory
+     * @return string one of backup::FORMAT_xxx constants
      */
     public static function detect_backup_format($tempdir) {
         global $CFG;
-        $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle_backup.xml';
+        require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php');
+        require_once($CFG->dirroot . '/backup/util/factories/convert_factory.class.php');
 
-        // Does tempdir exist and is dir
-        if (!is_dir(dirname($filepath))) {
-            throw new backup_helper_exception('tmp_backup_directory_not_found', dirname($filepath));
+        if (convert_helper::detect_moodle2_format($tempdir)) {
+            return backup::FORMAT_MOODLE;
         }
 
-        // First look for MOODLE (moodle2) format
-        if (file_exists($filepath)) { // Looks promising, lets load some information
-            $handle = fopen ($filepath, "r");
-            $first_chars = fread($handle,200);
-            $status = fclose ($handle);
-            // Check if it has the required strings
-            if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false &&
-                strpos($first_chars,'<moodle_backup>') !== false &&
-                strpos($first_chars,'<information>') !== false) {
-                    return backup::FORMAT_MOODLE;
+        // see if a converter can identify the format
+        $converters = convert_factory::available_converters();
+        foreach ($converters as $name) {
+            $classname = "{$name}_converter";
+            if (!class_exists($classname)) {
+                throw new coding_exception("available_converters() is supposed to load
+                    converter classes but class $classname not found");
             }
-        }
 
-        // Then look for MOODLE1 (moodle1) format
-        $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle.xml';
-        if (file_exists($filepath)) { // Looks promising, lets load some information
-            $handle = fopen ($filepath, "r");
-            $first_chars = fread($handle,200);
-            $status = fclose ($handle);
-            // Check if it has the required strings
-            if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false &&
-                strpos($first_chars,'<MOODLE_BACKUP>') !== false &&
-                strpos($first_chars,'<INFO>') !== false) {
-                    return backup::FORMAT_MOODLE1;
+            $detected = call_user_func($classname .'::detect_format', $tempdir);
+            if (!empty($detected)) {
+                return $detected;
             }
         }
 
-        // Other formats
-
-        // Arrived here, unknown format
         return backup::FORMAT_UNKNOWN;
     }
 }
diff --git a/backup/util/helper/convert_helper.class.php b/backup/util/helper/convert_helper.class.php
new file mode 100644 (file)
index 0000000..51488ae
--- /dev/null
@@ -0,0 +1,337 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Provides {@link convert_helper} and {@link convert_helper_exception} classes
+ *
+ * @package    core
+ * @subpackage backup-convert
+ * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');
+
+/**
+ * Provides various functionality via its static methods
+ */
+abstract class convert_helper {
+
+    /**
+     * @param string $entropy
+     * @return string random identifier
+     */
+    public static function generate_id($entropy) {
+        return md5(time() . '-' . $entropy . '-' . random_string(20));
+    }
+
+    /**
+     * Returns the list of all available converters and loads their classes
+     *
+     * Converter must be installed as a directory in backup/converter/ and its
+     * method is_available() must return true to get to the list.
+     *
+     * @see base_converter::is_available()
+     * @return array of strings
+     */
+    public static function available_converters() {
+        global $CFG;
+
+        $converters = array();
+        $plugins    = get_list_of_plugins('backup/converter');
+        foreach ($plugins as $name) {
+            $classfile = "$CFG->dirroot/backup/converter/$name/lib.php";
+            $classname = "{$name}_converter";
+
+            if (!file_exists($classfile)) {
+                throw new convert_helper_exception('converter_classfile_not_found', $classfile);
+            }
+            require_once($classfile);
+
+            if (!class_exists($classname)) {
+                throw new convert_helper_exception('converter_classname_not_found', $classname);
+            }
+
+            if (call_user_func($classname .'::is_available')) {
+                $converters[] = $name;
+            }
+        }
+
+        return $converters;
+    }
+
+    /**
+     * Detects if the given folder contains an unpacked moodle2 backup
+     *
+     * @param string $tempdir the name of the backup directory
+     * @return boolean true if moodle2 format detected, false otherwise
+     */
+    public static function detect_moodle2_format($tempdir) {
+        global $CFG;
+
+        $dirpath    = $CFG->dataroot . '/temp/backup/' . $tempdir;
+        $filepath   = $dirpath . '/moodle_backup.xml';
+
+        if (!is_dir($dirpath)) {
+            throw new converter_helper_exception('tmp_backup_directory_not_found', $dirpath);
+        }
+
+        if (!file_exists($filepath)) {
+            return false;
+        }
+
+        $handle     = fopen($filepath, 'r');
+        $firstchars = fread($handle, 200);
+        $status     = fclose($handle);
+
+        if (strpos($firstchars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
+            strpos($firstchars,'<moodle_backup>') !== false and
+            strpos($firstchars,'<information>') !== false) {
+                return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Converts the given directory with the backup into moodle2 format
+     *
+     * @param string $tempdir The directory to convert
+     * @param string $format The current format, if already detected
+     * @throws convert_helper_exception
+     * @return bool false if unable to find the conversion path, true otherwise
+     */
+    public static function to_moodle2_format($tempdir, $format = null) {
+
+        if (is_null($format)) {
+            $format = backup_general_helper::detect_backup_format($tempdir);
+        }
+
+        // get the supported conversion paths from all available converters
+        $converters   = convert_factory::available_converters();
+        $descriptions = array();
+        foreach ($converters as $name) {
+            $classname = "{$name}_converter";
+            if (!class_exists($classname)) {
+                throw new convert_helper_exception('class_not_loaded', $classname);
+            }
+            $descriptions[$name] = call_user_func($classname .'::description');
+        }
+
+        // choose the best conversion path for the given format
+        $path = self::choose_conversion_path($format, $descriptions);
+
+        if (empty($path)) {
+            // unable to convert
+            return false;
+        }
+
+        foreach ($path as $name) {
+            $converter = convert_factory::converter($name, $tempdir);
+            $converter->convert();
+        }
+
+        // make sure we ended with moodle2 format
+        if (!self::detect_moodle2_format($tempdir)) {
+            throw new convert_helper_exception('conversion_failed');
+        }
+
+        return true;
+    }
+
+   /**
+    * Inserts an inforef into the conversion temp table
+    */
+    public static function set_inforef($contextid) {
+        global $DB;
+    }
+
+    public static function get_inforef($contextid) {
+    }
+
+    /**
+     * Converts a plain old php object (popo?) into a string...
+     * Useful for debuging failed db inserts, or anything like that
+     */
+    public static function obj_to_readable($obj) {
+        $mapper = function($field, $value) { return "$field=$value"; };
+        $fields = get_object_vars($obj);
+
+        return implode(", ", array_map($mapper, array_keys($fields), array_values($fields)));
+    }
+
+    /// end of public API //////////////////////////////////////////////////////
+
+    /**
+     * Choose the best conversion path for the given format
+     *
+     * Given the source format and the list of available converters and their properties,
+     * this methods picks the most effective way how to convert the source format into
+     * the target moodle2 format. The method returns a list of converters that should be
+     * called, in order.
+     *
+     * This implementation uses Dijkstra's algorithm to find the shortest way through
+     * the oriented graph.
+     *
+     * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm
+     * @author David Mudrak <david@moodle.com>
+     * @param string $format the source backup format, one of backup::FORMAT_xxx
+     * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name
+     * @return array ordered list of converter names to call (may be empty if not reachable)
+     */
+    protected static function choose_conversion_path($format, array $descriptions) {
+
+        // construct an oriented graph of conversion paths. backup formats are nodes
+        // and the the converters are edges of the graph.
+        $paths = array();   // [fromnode][tonode] => converter
+        foreach ($descriptions as $converter => $description) {
+            $from   = $description['from'];
+            $to     = $description['to'];
+            $cost   = $description['cost'];
+
+            if (is_null($from) or $from === backup::FORMAT_UNKNOWN or
+                is_null($to) or $to === backup::FORMAT_UNKNOWN or
+                is_null($cost) or $cost <= 0) {
+                    throw new convert_helper_exception('invalid_converter_description', $converter);
+            }
+
+            if (!isset($paths[$from][$to])) {
+                $paths[$from][$to] = $converter;
+            } else {
+                // if there are two converters available for the same conversion
+                // path, choose the one with the lowest cost. if there are more
+                // available converters with the same cost, the chosen one is
+                // undefined (depends on the order of processing)
+                if ($descriptions[$paths[$from][$to]]['cost'] > $cost) {
+                    $paths[$from][$to] = $converter;
+                }
+            }
+        }
+
+        if (empty($paths)) {
+            // no conversion paths available
+            return array();
+        }
+
+        // now use Dijkstra's algorithm and find the shortest conversion path
+
+        $dist = array(); // list of nodes and their distances from the source format
+        $prev = array(); // list of previous nodes in optimal path from the source format
+        foreach ($paths as $fromnode => $tonodes) {
+            $dist[$fromnode] = null; // infinitive distance, can't be reached
+            $prev[$fromnode] = null; // unknown
+            foreach ($tonodes as $tonode => $converter) {
+                $dist[$tonode] = null; // infinitive distance, can't be reached
+                $prev[$tonode] = null; // unknown
+            }
+        }
+
+        if (!array_key_exists($format, $dist)) {
+            return array();
+        } else {
+            $dist[$format] = 0;
+        }
+
+        $queue = array_flip(array_keys($dist));
+        while (!empty($queue)) {
+            // find the node with the smallest distance from the source in the queue
+            // in the first iteration, this will find the original format node itself
+            $closest = null;
+            foreach ($queue as $node => $undefined) {
+                if (is_null($dist[$node])) {
+                    continue;
+                }
+                if (is_null($closest) or ($dist[$node] < $dist[$closest])) {
+                    $closest = $node;
+                }
+            }
+
+            if (is_null($closest) or is_null($dist[$closest])) {
+                // all remaining nodes are inaccessible from source
+                break;
+            }
+
+            if ($closest === backup::FORMAT_MOODLE) {
+                // bingo we can break now
+                break;
+            }
+
+            unset($queue[$closest]);
+
+            // visit all neighbors and update distances to them eventually
+
+            if (!isset($paths[$closest])) {
+                continue;
+            }
+            $neighbors = array_keys($paths[$closest]);
+            // keep just neighbors that are in the queue yet
+            foreach ($neighbors as $ix => $neighbor) {
+                if (!array_key_exists($neighbor, $queue)) {
+                    unset($neighbors[$ix]);
+                }
+            }
+
+            foreach ($neighbors as $neighbor) {
+                // the alternative distance to the neighbor if we went thru the
+                // current $closest node
+                $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost'];
+
+                if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) {
+                    // we found a shorter way to the $neighbor, remember it
+                    $dist[$neighbor] = $alt;
+                    $prev[$neighbor] = $closest;
+                }
+            }
+        }
+
+        if (is_null($dist[backup::FORMAT_MOODLE])) {
+            // unable to find a conversion path, the target format not reachable
+            return array();
+        }
+
+        // reconstruct the optimal path from the source format to the target one
+        $conversionpath = array();
+        $target         = backup::FORMAT_MOODLE;
+        while (isset($prev[$target])) {
+            array_unshift($conversionpath, $paths[$prev[$target]][$target]);
+            $target = $prev[$target];
+        }
+
+        return $conversionpath;
+    }
+}
+
+/**
+ * General convert_helper related exception
+ *
+ * @author David Mudrak <david@moodle.com>
+ */
+class convert_helper_exception extends moodle_exception {
+
+    /**
+     * Constructor
+     *
+     * @param string $errorcode key for the corresponding error string
+     * @param object $a extra words and phrases that might be required in the error string
+     * @param string $debuginfo optional debugging information
+     */
+    public function __construct($errorcode, $a = null, $debuginfo = null) {
+        parent::__construct($errorcode, '', '', $a, $debuginfo);
+    }
+}
diff --git a/backup/util/helper/simpletest/testconverthelper.php b/backup/util/helper/simpletest/testconverthelper.php
new file mode 100644 (file)
index 0000000..c198239
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for backup/util/helper/convert_helper.class.php
+ *
+ * @package    core
+ * @subpackage backup-convert
+ * @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/helper/convert_helper.class.php');
+
+/**
+ * Provides access to the protected methods we need to test
+ */
+class testable_convert_helper extends convert_helper {
+
+    public static function choose_conversion_path($format, array $descriptions) {
+        return parent::choose_conversion_path($format, $descriptions);
+    }
+}
+
+/**
+ * Defines the test methods
+ */
+class convert_helper_test extends UnitTestCase {
+
+    public static $includecoverage = array();
+
+    public function test_choose_conversion_path() {
+
+        // no converters available
+        $descriptions = array();
+        $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+        $this->assertEqual($path, array());
+
+        // missing source and/or targets
+        $descriptions = array(
+            // some custom converter
+            'exporter' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => 'some_custom_format',
+                'cost'  => 10,
+            ),
+            // another custom converter
+            'converter' => array(
+                'from'  => 'yet_another_crazy_custom_format',
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 10,
+            ),
+        );
+        $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+        $this->assertEqual($path, array());
+
+        $path = testable_convert_helper::choose_conversion_path('some_other_custom_format', $descriptions);
+        $this->assertEqual($path, array());
+
+        // single step conversion
+        $path = testable_convert_helper::choose_conversion_path('yet_another_crazy_custom_format', $descriptions);
+        $this->assertEqual($path, array('converter'));
+
+        // no conversion needed - this is supposed to be detected by the caller
+        $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE, $descriptions);
+        $this->assertEqual($path, array());
+
+        // two alternatives
+        $descriptions = array(
+            // standard moodle 1.9 -> 2.x converter
+            'moodle1' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 10,
+            ),
+            // alternative moodle 1.9 -> 2.x converter
+            'alternative' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 8,
+            )
+        );
+        $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+        $this->assertEqual($path, array('alternative'));
+
+        // complex case
+        $descriptions = array(
+            // standard moodle 1.9 -> 2.x converter
+            'moodle1' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 10,
+            ),
+            // alternative moodle 1.9 -> 2.x converter
+            'alternative' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 8,
+            ),
+            // custom converter from 1.9 -> custom 'CFv1' format
+            'cc1' => array(
+                'from'  => backup::FORMAT_MOODLE1,
+                'to'    => 'CFv1',
+                'cost'  => 2,
+            ),
+            // custom converter from custom 'CFv1' format -> moodle 2.0 format
+            'cc2' => array(
+                'from'  => 'CFv1',
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 5,
+            ),
+            // custom converter from CFv1 -> CFv2 format
+            'cc3' => array(
+                'from'  => 'CFv1',
+                'to'    => 'CFv2',
+                'cost'  => 2,
+            ),
+            // custom converter from CFv2 -> moodle 2.0 format
+            'cc4' => array(
+                'from'  => 'CFv2',
+                'to'    => backup::FORMAT_MOODLE,
+                'cost'  => 2,
+            ),
+        );
+
+        // ask the helper to find the most effective way
+        $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+        $this->assertEqual($path, array('cc1', 'cc3', 'cc4'));
+    }
+}
diff --git a/backup/util/includes/convert_includes.php b/backup/util/includes/convert_includes.php
new file mode 100644 (file)
index 0000000..5bc430f
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Makes sure that all general code needed by backup-convert code is included
+ *
+ * @package    core
+ * @subpackage backup-convert
+ * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); // req by backup.class.php
+require_once($CFG->dirroot . '/backup/backup.class.php'); // provides backup::FORMAT_xxx constants
+require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php');
+require_once($CFG->dirroot . '/backup/util/factories/convert_factory.class.php');
+require_once($CFG->libdir . '/filelib.php');
diff --git a/mod/choice/backup/moodle1/lib.php b/mod/choice/backup/moodle1/lib.php
new file mode 100644 (file)
index 0000000..f3dbed0
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more 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 support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    mod
+ * @subpackage choice
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Choice conversion handler
+ */
+class moodle1_mod_choice_handler extends moodle1_mod_handler {
+
+    /**
+     * Declare the paths in moodle.xml we are able to convert
+     *
+     * The method returns list of {@link convert_path} instances.
+     * For each path returned, the corresponding conversion method must be
+     * defined.
+     *
+     * Note that the path /MOODLE_BACKUP/COURSE/MODULES/MOD/CHOICE does not
+     * actually exist in the file. The last element with the module name was
+     * appended by the moodle1_converter class.
+     *
+     * @return array of {@link convert_path} instances
+     */
+    public function get_paths() {
+        return array(
+            new convert_path(
+                'choice', '/MOODLE_BACKUP/COURSE/MODULES/MOD/CHOICE',
+                array(
+                    'renamefields' => array(
+                        'text' => 'intro',
+                        'format' => 'introformat',
+                    ),
+                    'newfields' => array(
+                        'completionsubmit' => 0,
+                    ),
+                    'dropfields' => array(
+                        'modtype'
+                    ),
+                )
+            ),
+            new convert_path('choice_option', '/MOODLE_BACKUP/COURSE/MODULES/MOD/CHOICE/OPTIONS/OPTION'),
+        );
+    }
+
+    /**
+     * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/CHOICE
+     * data available
+     */
+    public function process_choice($data) {
+
+        // get the course module id and context id
+        $instanceid = $data['id'];
+        $moduleid   = $this->get_moduleid($instanceid);
+        $contextid  = $this->converter->get_contextid(CONTEXT_MODULE, $moduleid);
+
+        // we now have all information needed to start writing into the file
+        $this->open_xml_writer("activities/choice_{$moduleid}/choice.xml");
+        $this->xmlwriter->begin_tag('activity', array('id' => $instanceid, 'moduleid' => $moduleid,
+            'modulename' => 'choice', 'contextid' => $contextid));
+        $this->xmlwriter->begin_tag('choice', array('id' => $instanceid));
+
+        unset($data['id']); // we already write it as attribute, do not repeat it as child element
+        foreach ($data as $field => $value) {
+            $this->xmlwriter->full_tag($field, $value);
+        }
+
+        $this->xmlwriter->begin_tag('options');
+    }
+
+    /**
+     * This is executed every time we have one /MOODLE_BACKUP/COURSE/MODULES/MOD/CHOICE/OPTIONS/OPTION
+     * data available
+     */
+    public function process_choice_option($data) {
+        $this->write_xml('option', $data, array('/option/id'));
+    }
+
+    /**
+     * This is executed when we reach the closing </MOD> tag of our 'choice' path
+     */
+    public function on_choice_end() {
+
+        $this->xmlwriter->end_tag('options');
+        $this->xmlwriter->end_tag('choice');
+        $this->xmlwriter->end_tag('activity');
+        $this->close_xml_writer();
+    }
+}
diff --git a/mod/forum/backup/moodle1/lib.php b/mod/forum/backup/moodle1/lib.php
new file mode 100644 (file)
index 0000000..ddd4036
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more 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 support for the conversion of moodle1 backup to the moodle2 format
+ *
+ * @package    mod
+ * @subpackage forum
+ * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Forum conversion handler
+ */
+class moodle1_mod_forum_handler extends moodle1_mod_handler {
+
+    /**
+     * Declare the paths in moodle.xml we are able to convert
+     *
+     * The method returns list of {@link convert_path} instances.
+     * For each path returned, the corresponding conversion method must be
+     * defined.
+     *
+     * Note that the paths /MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM do not
+     * actually exist in the file. The last element with the module name was
+     * appended by the moodle1_converter class.
+     *
+     * @return array of {@link convert_path} instances
+     */
+    public function get_paths() {
+        return array(
+            new convert_path('forum', '/MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM'),
+        );
+    }
+
+    /**
+     * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/FORUM data
+     */
+    public function process_forum($data) {
+    }
+}