You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2171 lines
81 KiB
2171 lines
81 KiB
<?php
|
|
|
|
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Defines Moodle 1.9 backup conversion handlers
|
|
*
|
|
* Handlers are classes responsible for the actual conversion work. Their logic
|
|
* is similar to the functionality provided by steps in plan based restore process.
|
|
*
|
|
* @package backup-convert
|
|
* @subpackage moodle1
|
|
* @copyright 2011 David Mudrak <david@moodle.com>
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
|
|
require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
|
|
require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');
|
|
|
|
/**
|
|
* Handlers factory class
|
|
*/
|
|
abstract class moodle1_handlers_factory {
|
|
|
|
/**
|
|
* @param moodle1_converter the converter requesting the converters
|
|
* @return list of all available conversion handlers
|
|
*/
|
|
public static function get_handlers(moodle1_converter $converter) {
|
|
|
|
$handlers = array(
|
|
new moodle1_root_handler($converter),
|
|
new moodle1_info_handler($converter),
|
|
new moodle1_course_header_handler($converter),
|
|
new moodle1_course_outline_handler($converter),
|
|
new moodle1_roles_definition_handler($converter),
|
|
new moodle1_question_bank_handler($converter),
|
|
new moodle1_scales_handler($converter),
|
|
new moodle1_outcomes_handler($converter),
|
|
new moodle1_gradebook_handler($converter),
|
|
);
|
|
|
|
$handlers = array_merge($handlers, self::get_plugin_handlers('mod', $converter));
|
|
$handlers = array_merge($handlers, self::get_plugin_handlers('block', $converter));
|
|
|
|
// make sure that all handlers have expected class
|
|
foreach ($handlers as $handler) {
|
|
if (!$handler instanceof moodle1_handler) {
|
|
throw new moodle1_convert_exception('wrong_handler_class', get_class($handler));
|
|
}
|
|
}
|
|
|
|
return $handlers;
|
|
}
|
|
|
|
/// public API ends here ///////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Runs through all plugins of a specific type and instantiates their handlers
|
|
*
|
|
* @todo ask mod's subplugins
|
|
* @param string $type the plugin type
|
|
* @param moodle1_converter $converter the converter requesting the handler
|
|
* @throws moodle1_convert_exception
|
|
* @return array of {@link moodle1_handler} instances
|
|
*/
|
|
protected static function get_plugin_handlers($type, moodle1_converter $converter) {
|
|
global $CFG;
|
|
|
|
$handlers = array();
|
|
$plugins = core_component::get_plugin_list($type);
|
|
foreach ($plugins as $name => $dir) {
|
|
$handlerfile = $dir . '/backup/moodle1/lib.php';
|
|
$handlerclass = "moodle1_{$type}_{$name}_handler";
|
|
if (file_exists($handlerfile)) {
|
|
require_once($handlerfile);
|
|
} elseif ($type == 'block') {
|
|
$handlerclass = "moodle1_block_generic_handler";
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
if (!class_exists($handlerclass)) {
|
|
throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
|
|
}
|
|
$handlers[] = new $handlerclass($converter, $type, $name);
|
|
}
|
|
return $handlers;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base backup conversion handler
|
|
*/
|
|
abstract class moodle1_handler implements loggable {
|
|
|
|
/** @var moodle1_converter */
|
|
protected $converter;
|
|
|
|
/**
|
|
* @param moodle1_converter $converter the converter that requires us
|
|
*/
|
|
public function __construct(moodle1_converter $converter) {
|
|
$this->converter = $converter;
|
|
}
|
|
|
|
/**
|
|
* @return moodle1_converter the converter that required this handler
|
|
*/
|
|
public function get_converter() {
|
|
return $this->converter;
|
|
}
|
|
|
|
/**
|
|
* Log a message using the converter's logging mechanism
|
|
*
|
|
* @param string $message message text
|
|
* @param int $level message level {@example backup::LOG_WARNING}
|
|
* @param null|mixed $a additional information
|
|
* @param null|int $depth the message depth
|
|
* @param bool $display whether the message should be sent to the output, too
|
|
*/
|
|
public function log($message, $level, $a = null, $depth = null, $display = false) {
|
|
$this->converter->log($message, $level, $a, $depth, $display);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base backup conversion handler that generates an XML file
|
|
*/
|
|
abstract class moodle1_xml_handler extends moodle1_handler {
|
|
|
|
/** @var null|string the name of file we are writing to */
|
|
protected $xmlfilename;
|
|
|
|
/** @var null|xml_writer */
|
|
protected $xmlwriter;
|
|
|
|
/**
|
|
* Opens the XML writer - after calling, one is free to use $xmlwriter
|
|
*
|
|
* @param string $filename XML file name to write into
|
|
* @return void
|
|
*/
|
|
protected function open_xml_writer($filename) {
|
|
|
|
if (!is_null($this->xmlfilename) and $filename !== $this->xmlfilename) {
|
|
throw new moodle1_convert_exception('xml_writer_already_opened_for_other_file', $this->xmlfilename);
|
|
}
|
|
|
|
if (!$this->xmlwriter instanceof xml_writer) {
|
|
$this->xmlfilename = $filename;
|
|
$fullpath = $this->converter->get_workdir_path() . '/' . $this->xmlfilename;
|
|
$directory = pathinfo($fullpath, PATHINFO_DIRNAME);
|
|
|
|
if (!check_dir_exists($directory)) {
|
|
throw new moodle1_convert_exception('unable_create_target_directory', $directory);
|
|
}
|
|
$this->xmlwriter = new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer());
|
|
$this->xmlwriter->start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the XML writer
|
|
*
|
|
* At the moment, the caller must close all tags before calling
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function close_xml_writer() {
|
|
if ($this->xmlwriter instanceof xml_writer) {
|
|
$this->xmlwriter->stop();
|
|
}
|
|
unset($this->xmlwriter);
|
|
$this->xmlwriter = null;
|
|
$this->xmlfilename = null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the XML writer has been opened by {@link self::open_xml_writer()}
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function has_xml_writer() {
|
|
|
|
if ($this->xmlwriter instanceof xml_writer) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the given XML tree data into the currently opened file
|
|
*
|
|
* @param string $element the name of the root element of the tree
|
|
* @param array $data the associative array of data to write
|
|
* @param array $attribs list of additional fields written as attributes instead of nested elements
|
|
* @param string $parent used internally during the recursion, do not set yourself
|
|
*/
|
|
protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
|
|
|
|
if (!$this->has_xml_writer()) {
|
|
throw new moodle1_convert_exception('write_xml_without_writer');
|
|
}
|
|
|
|
$mypath = $parent . $element;
|
|
$myattribs = array();
|
|
|
|
// detect properties that should be rendered as element's attributes instead of children
|
|
foreach ($data as $name => $value) {
|
|
if (!is_array($value)) {
|
|
if (in_array($mypath . '/' . $name, $attribs)) {
|
|
$myattribs[$name] = $value;
|
|
unset($data[$name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// reorder the $data so that all sub-branches are at the end (needed by our parser)
|
|
$leaves = array();
|
|
$branches = array();
|
|
foreach ($data as $name => $value) {
|
|
if (is_array($value)) {
|
|
$branches[$name] = $value;
|
|
} else {
|
|
$leaves[$name] = $value;
|
|
}
|
|
}
|
|
$data = array_merge($leaves, $branches);
|
|
|
|
$this->xmlwriter->begin_tag($element, $myattribs);
|
|
|
|
foreach ($data as $name => $value) {
|
|
if (is_array($value)) {
|
|
// recursively call self
|
|
$this->write_xml($name, $value, $attribs, $mypath.'/');
|
|
} else {
|
|
$this->xmlwriter->full_tag($name, $value);
|
|
}
|
|
}
|
|
|
|
$this->xmlwriter->end_tag($element);
|
|
}
|
|
|
|
/**
|
|
* Makes sure that a new XML file exists, or creates it itself
|
|
*
|
|
* This is here so we can check that all XML files that the restore process relies on have
|
|
* been created by an executed handler. If the file is not found, this method can create it
|
|
* using the given $rootelement as an empty root container in the file.
|
|
*
|
|
* @param string $filename relative file name like 'course/course.xml'
|
|
* @param string|bool $rootelement root element to use, false to not create the file
|
|
* @param array $content content of the root element
|
|
* @return bool true is the file existed, false if it did not
|
|
*/
|
|
protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) {
|
|
|
|
$existed = file_exists($this->converter->get_workdir_path().'/'.$filename);
|
|
|
|
if ($existed) {
|
|
return true;
|
|
}
|
|
|
|
if ($rootelement !== false) {
|
|
$this->open_xml_writer($filename);
|
|
$this->write_xml($rootelement, $content);
|
|
$this->close_xml_writer();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Process the root element of the backup file
|
|
*/
|
|
class moodle1_root_handler extends moodle1_xml_handler {
|
|
|
|
public function get_paths() {
|
|
return array(new convert_path('root_element', '/MOODLE_BACKUP'));
|
|
}
|
|
|
|
/**
|
|
* Converts course_files and site_files
|
|
*/
|
|
public function on_root_element_start() {
|
|
|
|
// convert course files
|
|
$fileshandler = new moodle1_files_handler($this->converter);
|
|
$fileshandler->process();
|
|
}
|
|
|
|
/**
|
|
* This is executed at the end of the moodle.xml parsing
|
|
*/
|
|
public function on_root_element_end() {
|
|
global $CFG;
|
|
|
|
// restore the stashes prepared by other handlers for us
|
|
$backupinfo = $this->converter->get_stash('backup_info');
|
|
$originalcourseinfo = $this->converter->get_stash('original_course_info');
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// write moodle_backup.xml
|
|
////////////////////////////////////////////////////////////////////////
|
|
$this->open_xml_writer('moodle_backup.xml');
|
|
|
|
$this->xmlwriter->begin_tag('moodle_backup');
|
|
$this->xmlwriter->begin_tag('information');
|
|
|
|
// moodle_backup/information
|
|
$this->xmlwriter->full_tag('name', $backupinfo['name']);
|
|
$this->xmlwriter->full_tag('moodle_version', $backupinfo['moodle_version']);
|
|
$this->xmlwriter->full_tag('moodle_release', $backupinfo['moodle_release']);
|
|
$this->xmlwriter->full_tag('backup_version', $CFG->backup_version); // {@see restore_prechecks_helper::execute_prechecks}
|
|
$this->xmlwriter->full_tag('backup_release', $CFG->backup_release);
|
|
$this->xmlwriter->full_tag('backup_date', $backupinfo['date']);
|
|
// see the commit c0543b - all backups created in 1.9 and later declare the
|
|
// information or it is considered as false
|
|
if (isset($backupinfo['mnet_remoteusers'])) {
|
|
$this->xmlwriter->full_tag('mnet_remoteusers', $backupinfo['mnet_remoteusers']);
|
|
} else {
|
|
$this->xmlwriter->full_tag('mnet_remoteusers', false);
|
|
}
|
|
$this->xmlwriter->full_tag('original_wwwroot', $backupinfo['original_wwwroot']);
|
|
// {@see backup_general_helper::backup_is_samesite()}
|
|
if (isset($backupinfo['original_site_identifier_hash'])) {
|
|
$this->xmlwriter->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']);
|
|
} else {
|
|
$this->xmlwriter->full_tag('original_site_identifier_hash', null);
|
|
}
|
|
$this->xmlwriter->full_tag('original_course_id', $originalcourseinfo['original_course_id']);
|
|
$this->xmlwriter->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']);
|
|
$this->xmlwriter->full_tag('original_course_shortname', $originalcourseinfo['original_course_shortname']);
|
|
$this->xmlwriter->full_tag('original_course_startdate', $originalcourseinfo['original_course_startdate']);
|
|
$this->xmlwriter->full_tag('original_system_contextid', $this->converter->get_contextid(CONTEXT_SYSTEM));
|
|
// note that even though we have original_course_contextid available, we regenerate the
|
|
// original course contextid using our helper method to be sure that the data are consistent
|
|
// within the MBZ file
|
|
$this->xmlwriter->full_tag('original_course_contextid', $this->converter->get_contextid(CONTEXT_COURSE));
|
|
|
|
// moodle_backup/information/details
|
|
$this->xmlwriter->begin_tag('details');
|
|
$this->write_xml('detail', array(
|
|
'backup_id' => $this->converter->get_id(),
|
|
'type' => backup::TYPE_1COURSE,
|
|
'format' => backup::FORMAT_MOODLE,
|
|
'interactive' => backup::INTERACTIVE_YES,
|
|
'mode' => backup::MODE_CONVERTED,
|
|
'execution' => backup::EXECUTION_INMEDIATE,
|
|
'executiontime' => 0,
|
|
), array('/detail/backup_id'));
|
|
$this->xmlwriter->end_tag('details');
|
|
|
|
// moodle_backup/information/contents
|
|
$this->xmlwriter->begin_tag('contents');
|
|
|
|
// moodle_backup/information/contents/activities
|
|
$this->xmlwriter->begin_tag('activities');
|
|
$activitysettings = array();
|
|
foreach ($this->converter->get_stash('coursecontents') as $activity) {
|
|
$modinfo = $this->converter->get_stash('modinfo_'.$activity['modulename']);
|
|
$modinstance = $modinfo['instances'][$activity['instanceid']];
|
|
$this->write_xml('activity', array(
|
|
'moduleid' => $activity['cmid'],
|
|
'sectionid' => $activity['sectionid'],
|
|
'modulename' => $activity['modulename'],
|
|
'title' => $modinstance['name'],
|
|
'directory' => 'activities/'.$activity['modulename'].'_'.$activity['cmid']
|
|
));
|
|
$activitysettings[] = array(
|
|
'level' => 'activity',
|
|
'activity' => $activity['modulename'].'_'.$activity['cmid'],
|
|
'name' => $activity['modulename'].'_'.$activity['cmid'].'_included',
|
|
'value' => (($modinfo['included'] === 'true' and $modinstance['included'] === 'true') ? 1 : 0));
|
|
$activitysettings[] = array(
|
|
'level' => 'activity',
|
|
'activity' => $activity['modulename'].'_'.$activity['cmid'],
|
|
'name' => $activity['modulename'].'_'.$activity['cmid'].'_userinfo',
|
|
//'value' => (($modinfo['userinfo'] === 'true' and $modinstance['userinfo'] === 'true') ? 1 : 0));
|
|
'value' => 0); // todo hardcoded non-userinfo for now
|
|
}
|
|
$this->xmlwriter->end_tag('activities');
|
|
|
|
// moodle_backup/information/contents/sections
|
|
$this->xmlwriter->begin_tag('sections');
|
|
$sectionsettings = array();
|
|
foreach ($this->converter->get_stash_itemids('sectioninfo') as $sectionid) {
|
|
$sectioninfo = $this->converter->get_stash('sectioninfo', $sectionid);
|
|
$sectionsettings[] = array(
|
|
'level' => 'section',
|
|
'section' => 'section_'.$sectionid,
|
|
'name' => 'section_'.$sectionid.'_included',
|
|
'value' => 1);
|
|
$sectionsettings[] = array(
|
|
'level' => 'section',
|
|
'section' => 'section_'.$sectionid,
|
|
'name' => 'section_'.$sectionid.'_userinfo',
|
|
'value' => 0); // @todo how to detect this from moodle.xml?
|
|
$this->write_xml('section', array(
|
|
'sectionid' => $sectionid,
|
|
'title' => $sectioninfo['number'], // because the title is not available
|
|
'directory' => 'sections/section_'.$sectionid));
|
|
}
|
|
$this->xmlwriter->end_tag('sections');
|
|
|
|
// moodle_backup/information/contents/course
|
|
$this->write_xml('course', array(
|
|
'courseid' => $originalcourseinfo['original_course_id'],
|
|
'title' => $originalcourseinfo['original_course_shortname'],
|
|
'directory' => 'course'));
|
|
unset($originalcourseinfo);
|
|
|
|
$this->xmlwriter->end_tag('contents');
|
|
|
|
// moodle_backup/information/settings
|
|
$this->xmlwriter->begin_tag('settings');
|
|
|
|
// fake backup root seetings
|
|
$rootsettings = array(
|
|
'filename' => $backupinfo['name'],
|
|
'users' => 0, // @todo how to detect this from moodle.xml?
|
|
'anonymize' => 0,
|
|
'role_assignments' => 0,
|
|
'activities' => 1,
|
|
'blocks' => 1,
|
|
'filters' => 0,
|
|
'comments' => 0,
|
|
'userscompletion' => 0,
|
|
'logs' => 0,
|
|
'grade_histories' => 0,
|
|
);
|
|
unset($backupinfo);
|
|
foreach ($rootsettings as $name => $value) {
|
|
$this->write_xml('setting', array(
|
|
'level' => 'root',
|
|
'name' => $name,
|
|
'value' => $value));
|
|
}
|
|
unset($rootsettings);
|
|
|
|
// activity settings populated above
|
|
foreach ($activitysettings as $activitysetting) {
|
|
$this->write_xml('setting', $activitysetting);
|
|
}
|
|
unset($activitysettings);
|
|
|
|
// section settings populated above
|
|
foreach ($sectionsettings as $sectionsetting) {
|
|
$this->write_xml('setting', $sectionsetting);
|
|
}
|
|
unset($sectionsettings);
|
|
|
|
$this->xmlwriter->end_tag('settings');
|
|
|
|
$this->xmlwriter->end_tag('information');
|
|
$this->xmlwriter->end_tag('moodle_backup');
|
|
|
|
$this->close_xml_writer();
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// write files.xml
|
|
////////////////////////////////////////////////////////////////////////
|
|
$this->open_xml_writer('files.xml');
|
|
$this->xmlwriter->begin_tag('files');
|
|
foreach ($this->converter->get_stash_itemids('files') as $fileid) {
|
|
$this->write_xml('file', $this->converter->get_stash('files', $fileid), array('/file/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('files');
|
|
$this->close_xml_writer('files.xml');
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// write scales.xml
|
|
////////////////////////////////////////////////////////////////////////
|
|
$this->open_xml_writer('scales.xml');
|
|
$this->xmlwriter->begin_tag('scales_definition');
|
|
foreach ($this->converter->get_stash_itemids('scales') as $scaleid) {
|
|
$this->write_xml('scale', $this->converter->get_stash('scales', $scaleid), array('/scale/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('scales_definition');
|
|
$this->close_xml_writer('scales.xml');
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// write course/inforef.xml
|
|
////////////////////////////////////////////////////////////////////////
|
|
$this->open_xml_writer('course/inforef.xml');
|
|
$this->xmlwriter->begin_tag('inforef');
|
|
|
|
$this->xmlwriter->begin_tag('fileref');
|
|
// legacy course files
|
|
$fileids = $this->converter->get_stash('course_files_ids');
|
|
if (is_array($fileids)) {
|
|
foreach ($fileids as $fileid) {
|
|
$this->write_xml('file', array('id' => $fileid));
|
|
}
|
|
}
|
|
// todo site files
|
|
// course summary files
|
|
$fileids = $this->converter->get_stash('course_summary_files_ids');
|
|
if (is_array($fileids)) {
|
|
foreach ($fileids as $fileid) {
|
|
$this->write_xml('file', array('id' => $fileid));
|
|
}
|
|
}
|
|
$this->xmlwriter->end_tag('fileref');
|
|
|
|
$this->xmlwriter->begin_tag('question_categoryref');
|
|
foreach ($this->converter->get_stash_itemids('question_categories') as $questioncategoryid) {
|
|
$this->write_xml('question_category', array('id' => $questioncategoryid));
|
|
}
|
|
$this->xmlwriter->end_tag('question_categoryref');
|
|
|
|
$this->xmlwriter->end_tag('inforef');
|
|
$this->close_xml_writer();
|
|
|
|
// make sure that the files required by the restore process have been generated.
|
|
// missing file may happen if the watched tag is not present in moodle.xml (for example
|
|
// QUESTION_CATEGORIES is optional in moodle.xml but questions.xml must exist in
|
|
// moodle2 format) or the handler has not been implemented yet.
|
|
// apparently this must be called after the handler had a chance to create the file.
|
|
$this->make_sure_xml_exists('questions.xml', 'question_categories');
|
|
$this->make_sure_xml_exists('groups.xml', 'groups');
|
|
$this->make_sure_xml_exists('outcomes.xml', 'outcomes_definition');
|
|
$this->make_sure_xml_exists('users.xml', 'users');
|
|
$this->make_sure_xml_exists('course/roles.xml', 'roles',
|
|
array('role_assignments' => array(), 'role_overrides' => array()));
|
|
$this->make_sure_xml_exists('course/enrolments.xml', 'enrolments',
|
|
array('enrols' => array()));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* The class responsible for course and site files migration
|
|
*
|
|
* @todo migrate site_files
|
|
*/
|
|
class moodle1_files_handler extends moodle1_xml_handler {
|
|
|
|
/**
|
|
* Migrates course_files and site_files in the converter workdir
|
|
*/
|
|
public function process() {
|
|
$this->migrate_course_files();
|
|
// todo $this->migrate_site_files();
|
|
}
|
|
|
|
/**
|
|
* Migrates course_files in the converter workdir
|
|
*/
|
|
protected function migrate_course_files() {
|
|
$ids = array();
|
|
$fileman = $this->converter->get_file_manager($this->converter->get_contextid(CONTEXT_COURSE), 'course', 'legacy');
|
|
$this->converter->set_stash('course_files_ids', array());
|
|
if (file_exists($this->converter->get_tempdir_path().'/course_files')) {
|
|
$ids = $fileman->migrate_directory('course_files');
|
|
$this->converter->set_stash('course_files_ids', $ids);
|
|
}
|
|
$this->log('course files migrated', backup::LOG_INFO, count($ids));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of /MOODLE_BACKUP/INFO paths
|
|
*
|
|
* We do not produce any XML file here, just storing the data in the temp
|
|
* table so thay can be used by a later handler.
|
|
*/
|
|
class moodle1_info_handler extends moodle1_handler {
|
|
|
|
/** @var array list of mod names included in info_details */
|
|
protected $modnames = array();
|
|
|
|
/** @var array the in-memory cache of the currently parsed info_details_mod element */
|
|
protected $currentmod;
|
|
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('info', '/MOODLE_BACKUP/INFO'),
|
|
new convert_path('info_details', '/MOODLE_BACKUP/INFO/DETAILS'),
|
|
new convert_path('info_details_mod', '/MOODLE_BACKUP/INFO/DETAILS/MOD'),
|
|
new convert_path('info_details_mod_instance', '/MOODLE_BACKUP/INFO/DETAILS/MOD/INSTANCES/INSTANCE'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stashes the backup info for later processing by {@link moodle1_root_handler}
|
|
*/
|
|
public function process_info($data) {
|
|
$this->converter->set_stash('backup_info', $data);
|
|
}
|
|
|
|
/**
|
|
* Initializes the in-memory cache for the current mod
|
|
*/
|
|
public function process_info_details_mod($data) {
|
|
$this->currentmod = $data;
|
|
$this->currentmod['instances'] = array();
|
|
}
|
|
|
|
/**
|
|
* Appends the current instance data to the temporary in-memory cache
|
|
*/
|
|
public function process_info_details_mod_instance($data) {
|
|
$this->currentmod['instances'][$data['id']] = $data;
|
|
}
|
|
|
|
/**
|
|
* Stashes the backup info for later processing by {@link moodle1_root_handler}
|
|
*/
|
|
public function on_info_details_mod_end($data) {
|
|
global $CFG;
|
|
|
|
// keep only such modules that seem to have the support for moodle1 implemented
|
|
$modname = $this->currentmod['name'];
|
|
if (file_exists($CFG->dirroot.'/mod/'.$modname.'/backup/moodle1/lib.php')) {
|
|
$this->converter->set_stash('modinfo_'.$modname, $this->currentmod);
|
|
$this->modnames[] = $modname;
|
|
} else {
|
|
$this->log('unsupported activity module', backup::LOG_WARNING, $modname);
|
|
}
|
|
|
|
$this->currentmod = array();
|
|
}
|
|
|
|
/**
|
|
* Stashes the list of activity module types for later processing by {@link moodle1_root_handler}
|
|
*/
|
|
public function on_info_details_end() {
|
|
$this->converter->set_stash('modnameslist', $this->modnames);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths
|
|
*/
|
|
class moodle1_course_header_handler extends moodle1_xml_handler {
|
|
|
|
/** @var array we need to merge course information because it is dispatched twice */
|
|
protected $course = array();
|
|
|
|
/** @var array we need to merge course information because it is dispatched twice */
|
|
protected $courseraw = array();
|
|
|
|
/** @var array */
|
|
protected $category;
|
|
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path(
|
|
'course_header', '/MOODLE_BACKUP/COURSE/HEADER',
|
|
array(
|
|
'newfields' => array(
|
|
'summaryformat' => 1,
|
|
'legacyfiles' => 2,
|
|
'requested' => 0, // @todo not really new, but maybe never backed up?
|
|
'restrictmodules' => 0,
|
|
'enablecompletion' => 0,
|
|
'completionstartonenrol' => 0,
|
|
'completionnotify' => 0,
|
|
'tags' => array(),
|
|
'allowed_modules' => array(),
|
|
),
|
|
'dropfields' => array(
|
|
'roles_overrides',
|
|
'roles_assignments',
|
|
'cost',
|
|
'currancy',
|
|
'defaultrole',
|
|
'enrol',
|
|
'enrolenddate',
|
|
'enrollable',
|
|
'enrolperiod',
|
|
'enrolstartdate',
|
|
'expirynotify',
|
|
'expirythreshold',
|
|
'guest',
|
|
'notifystudents',
|
|
'password',
|
|
'student',
|
|
'students',
|
|
'teacher',
|
|
'teachers',
|
|
'metacourse',
|
|
)
|
|
)
|
|
),
|
|
new convert_path(
|
|
'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
|
|
array(
|
|
'newfields' => array(
|
|
'description' => null,
|
|
)
|
|
)
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Because there is the CATEGORY branch in the middle of the COURSE/HEADER
|
|
* branch, this is dispatched twice. We use $this->coursecooked to merge
|
|
* the result. Once the parser is fixed, it can be refactored.
|
|
*/
|
|
public function process_course_header($data, $raw) {
|
|
$this->course = array_merge($this->course, $data);
|
|
$this->courseraw = array_merge($this->courseraw, $raw);
|
|
}
|
|
|
|
public function process_course_header_category($data) {
|
|
$this->category = $data;
|
|
}
|
|
|
|
public function on_course_header_end() {
|
|
|
|
$contextid = $this->converter->get_contextid(CONTEXT_COURSE);
|
|
|
|
// stash the information needed by other handlers
|
|
$info = array(
|
|
'original_course_id' => $this->course['id'],
|
|
'original_course_fullname' => $this->course['fullname'],
|
|
'original_course_shortname' => $this->course['shortname'],
|
|
'original_course_startdate' => $this->course['startdate'],
|
|
'original_course_contextid' => $contextid
|
|
);
|
|
$this->converter->set_stash('original_course_info', $info);
|
|
|
|
$this->course['contextid'] = $contextid;
|
|
$this->course['category'] = $this->category;
|
|
|
|
// migrate files embedded into the course summary and stash their ids
|
|
$fileman = $this->converter->get_file_manager($contextid, 'course', 'summary');
|
|
$this->course['summary'] = moodle1_converter::migrate_referenced_files($this->course['summary'], $fileman);
|
|
$this->converter->set_stash('course_summary_files_ids', $fileman->get_fileids());
|
|
|
|
// write course.xml
|
|
$this->open_xml_writer('course/course.xml');
|
|
$this->write_xml('course', $this->course, array('/course/id', '/course/contextid'));
|
|
$this->close_xml_writer();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of course sections and course modules
|
|
*/
|
|
class moodle1_course_outline_handler extends moodle1_xml_handler {
|
|
|
|
/** @var array ordered list of the course contents */
|
|
protected $coursecontents = array();
|
|
|
|
/** @var array current section data */
|
|
protected $currentsection;
|
|
|
|
/**
|
|
* This handler is interested in course sections and course modules within them
|
|
*/
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'),
|
|
new convert_path(
|
|
'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION',
|
|
array(
|
|
'newfields' => array(
|
|
'name' => null,
|
|
'summaryformat' => 1,
|
|
'sequence' => null,
|
|
),
|
|
)
|
|
),
|
|
new convert_path(
|
|
'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD',
|
|
array(
|
|
'newfields' => array(
|
|
'completion' => 0,
|
|
'completiongradeitemnumber' => null,
|
|
'completionview' => 0,
|
|
'completionexpected' => 0,
|
|
'availability' => null,
|
|
'visibleold' => 1,
|
|
'showdescription' => 0,
|
|
),
|
|
'dropfields' => array(
|
|
'instance',
|
|
'roles_overrides',
|
|
'roles_assignments',
|
|
),
|
|
'renamefields' => array(
|
|
'type' => 'modulename',
|
|
),
|
|
)
|
|
),
|
|
new convert_path('course_modules', '/MOODLE_BACKUP/COURSE/MODULES'),
|
|
// todo new convert_path('course_module_roles_overrides', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'),
|
|
// todo new convert_path('course_module_roles_assignments', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_ASSIGNMENTS'),
|
|
);
|
|
}
|
|
|
|
public function process_course_section($data) {
|
|
$this->currentsection = $data;
|
|
}
|
|
|
|
/**
|
|
* Populates the section sequence field (order of course modules) and stashes the
|
|
* course module info so that is can be dumped to activities/xxxx_x/module.xml later
|
|
*/
|
|
public function process_course_module($data, $raw) {
|
|
global $CFG;
|
|
|
|
// check that this type of module should be included in the mbz
|
|
$modinfo = $this->converter->get_stash_itemids('modinfo_'.$data['modulename']);
|
|
if (empty($modinfo)) {
|
|
return;
|
|
}
|
|
|
|
// add the course module into the course contents list
|
|
$this->coursecontents[$data['id']] = array(
|
|
'cmid' => $data['id'],
|
|
'instanceid' => $raw['INSTANCE'],
|
|
'sectionid' => $this->currentsection['id'],
|
|
'modulename' => $data['modulename'],
|
|
'title' => null
|
|
);
|
|
|
|
// add the course module id into the section's sequence
|
|
if (is_null($this->currentsection['sequence'])) {
|
|
$this->currentsection['sequence'] = $data['id'];
|
|
} else {
|
|
$this->currentsection['sequence'] .= ',' . $data['id'];
|
|
}
|
|
|
|
// add the sectionid and sectionnumber
|
|
$data['sectionid'] = $this->currentsection['id'];
|
|
$data['sectionnumber'] = $this->currentsection['number'];
|
|
|
|
// generate the module version - this is a bit tricky as this information
|
|
// is not present in 1.9 backups. we will use the currently installed version
|
|
// whenever we can but that might not be accurate for some modules.
|
|
// also there might be problem with modules that are not present at the target
|
|
// host...
|
|
$versionfile = $CFG->dirroot.'/mod/'.$data['modulename'].'/version.php';
|
|
if (file_exists($versionfile)) {
|
|
$plugin = new stdClass();
|
|
$plugin->version = null;
|
|
$module = $plugin;
|
|
include($versionfile);
|
|
$data['version'] = $plugin->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 and dump it into
|
|
// the module.xml file
|
|
$this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
|
|
}
|
|
|
|
/**
|
|
* Writes sections/section_xxx/section.xml file and stashes it, too
|
|
*/
|
|
public function on_course_section_end() {
|
|
|
|
// migrate files embedded into the section summary field
|
|
$contextid = $this->converter->get_contextid(CONTEXT_COURSE);
|
|
$fileman = $this->converter->get_file_manager($contextid, 'course', 'section', $this->currentsection['id']);
|
|
$this->currentsection['summary'] = moodle1_converter::migrate_referenced_files($this->currentsection['summary'], $fileman);
|
|
|
|
// write section's inforef.xml with the file references
|
|
$this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/inforef.xml');
|
|
$this->xmlwriter->begin_tag('inforef');
|
|
$this->xmlwriter->begin_tag('fileref');
|
|
$fileids = $fileman->get_fileids();
|
|
if (is_array($fileids)) {
|
|
foreach ($fileids as $fileid) {
|
|
$this->write_xml('file', array('id' => $fileid));
|
|
}
|
|
}
|
|
$this->xmlwriter->end_tag('fileref');
|
|
$this->xmlwriter->end_tag('inforef');
|
|
$this->close_xml_writer();
|
|
|
|
// stash the section info and write section.xml
|
|
$this->converter->set_stash('sectioninfo', $this->currentsection, $this->currentsection['id']);
|
|
$this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml');
|
|
$this->write_xml('section', $this->currentsection);
|
|
$this->close_xml_writer();
|
|
unset($this->currentsection);
|
|
}
|
|
|
|
/**
|
|
* Stashes the course contents
|
|
*/
|
|
public function on_course_sections_end() {
|
|
$this->converter->set_stash('coursecontents', $this->coursecontents);
|
|
}
|
|
|
|
/**
|
|
* Writes the information collected by mod handlers
|
|
*/
|
|
public function on_course_modules_end() {
|
|
|
|
foreach ($this->converter->get_stash('modnameslist') as $modname) {
|
|
$modinfo = $this->converter->get_stash('modinfo_'.$modname);
|
|
foreach ($modinfo['instances'] as $modinstanceid => $modinstance) {
|
|
$cminfo = $this->converter->get_stash('cminfo_'.$modname, $modinstanceid);
|
|
$directory = 'activities/'.$modname.'_'.$cminfo['id'];
|
|
|
|
// write module.xml
|
|
$this->open_xml_writer($directory.'/module.xml');
|
|
$this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
|
|
$this->close_xml_writer();
|
|
|
|
// write grades.xml
|
|
$this->open_xml_writer($directory.'/grades.xml');
|
|
$this->xmlwriter->begin_tag('activity_gradebook');
|
|
$gradeitems = $this->converter->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array());
|
|
if (!empty($gradeitems)) {
|
|
$this->xmlwriter->begin_tag('grade_items');
|
|
foreach ($gradeitems as $gradeitem) {
|
|
$this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('grade_items');
|
|
}
|
|
$this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9
|
|
$this->xmlwriter->end_tag('activity_gradebook');
|
|
$this->close_xml_writer();
|
|
|
|
// todo: write proper roles.xml, for now we just make sure the file is present
|
|
$this->make_sure_xml_exists($directory.'/roles.xml', 'roles');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of the defined roles
|
|
*/
|
|
class moodle1_roles_definition_handler extends moodle1_xml_handler {
|
|
|
|
/**
|
|
* Where the roles are defined in the source moodle.xml
|
|
*/
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
|
|
new convert_path(
|
|
'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
|
|
array(
|
|
'newfields' => array(
|
|
'description' => '',
|
|
'sortorder' => 0,
|
|
'archetype' => ''
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* If there are any roles defined in moodle.xml, convert them to roles.xml
|
|
*/
|
|
public function process_roles_role($data) {
|
|
|
|
if (!$this->has_xml_writer()) {
|
|
$this->open_xml_writer('roles.xml');
|
|
$this->xmlwriter->begin_tag('roles_definition');
|
|
}
|
|
if (!isset($data['nameincourse'])) {
|
|
$data['nameincourse'] = null;
|
|
}
|
|
$this->write_xml('role', $data, array('role/id'));
|
|
}
|
|
|
|
/**
|
|
* Finishes writing roles.xml
|
|
*/
|
|
public function on_roles_end() {
|
|
|
|
if (!$this->has_xml_writer()) {
|
|
// no roles defined in moodle.xml so {link self::process_roles_role()}
|
|
// was never executed
|
|
$this->open_xml_writer('roles.xml');
|
|
$this->write_xml('roles_definition', array());
|
|
|
|
} else {
|
|
// some roles were dumped into the file, let us close their wrapper now
|
|
$this->xmlwriter->end_tag('roles_definition');
|
|
}
|
|
$this->close_xml_writer();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of the question bank included in the moodle.xml file
|
|
*/
|
|
class moodle1_question_bank_handler extends moodle1_xml_handler {
|
|
|
|
/** @var array the current question category being parsed */
|
|
protected $currentcategory = null;
|
|
|
|
/** @var array of the raw data for the current category */
|
|
protected $currentcategoryraw = null;
|
|
|
|
/** @var moodle1_file_manager instance used to convert question images */
|
|
protected $fileman = null;
|
|
|
|
/** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
|
|
private $currentcategorywritten = false;
|
|
|
|
/** @var bool was the <questions> tag already written (work around MDL-27693) */
|
|
private $questionswrapperwritten = false;
|
|
|
|
/** @var array holds the instances of qtype specific conversion handlers */
|
|
private $qtypehandlers = null;
|
|
|
|
/**
|
|
* Return the file manager instance used.
|
|
*
|
|
* @return moodle1_file_manager
|
|
*/
|
|
public function get_file_manager() {
|
|
return $this->fileman;
|
|
}
|
|
|
|
/**
|
|
* Returns the information about the question category context being currently parsed
|
|
*
|
|
* @return array with keys contextid, contextlevel and contextinstanceid
|
|
*/
|
|
public function get_current_category_context() {
|
|
return $this->currentcategory;
|
|
}
|
|
|
|
/**
|
|
* Registers path that are not qtype-specific
|
|
*/
|
|
public function get_paths() {
|
|
|
|
$paths = array(
|
|
new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
|
|
new convert_path(
|
|
'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
|
|
array(
|
|
'newfields' => array(
|
|
'infoformat' => 0
|
|
)
|
|
)),
|
|
new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'),
|
|
new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'),
|
|
// the question element must be grouped so we can re-dispatch it to the qtype handler as a whole
|
|
new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true),
|
|
);
|
|
|
|
// annotate all question subpaths required by the qtypes subplugins
|
|
$subpaths = array();
|
|
foreach ($this->get_qtype_handler('*') as $qtypehandler) {
|
|
foreach ($qtypehandler->get_question_subpaths() as $subpath) {
|
|
$subpaths[$subpath] = true;
|
|
}
|
|
}
|
|
foreach (array_keys($subpaths) as $subpath) {
|
|
$name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath));
|
|
$path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath;
|
|
$paths[] = new convert_path($name, $path);
|
|
}
|
|
|
|
return $paths;
|
|
}
|
|
|
|
/**
|
|
* Starts writing questions.xml and prepares the file manager instance
|
|
*/
|
|
public function on_question_categories_start() {
|
|
$this->open_xml_writer('questions.xml');
|
|
$this->xmlwriter->begin_tag('question_categories');
|
|
if (is_null($this->fileman)) {
|
|
$this->fileman = $this->converter->get_file_manager();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the current category cache
|
|
*/
|
|
public function on_question_category_start() {
|
|
$this->currentcategory = array();
|
|
$this->currentcategoryraw = array();
|
|
$this->currentcategorywritten = false;
|
|
$this->questionswrapperwritten = false;
|
|
}
|
|
|
|
/**
|
|
* Populates the current question category data
|
|
*
|
|
* Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually
|
|
* called twice for both halves of the data. We merge them here into the currentcategory array.
|
|
*/
|
|
public function process_question_category($data, $raw) {
|
|
$this->currentcategory = array_merge($this->currentcategory, $data);
|
|
$this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw);
|
|
}
|
|
|
|
/**
|
|
* Inject the context related information into the current category
|
|
*/
|
|
public function process_question_category_context($data) {
|
|
|
|
switch ($data['level']) {
|
|
case 'module':
|
|
$this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']);
|
|
$this->currentcategory['contextlevel'] = CONTEXT_MODULE;
|
|
$this->currentcategory['contextinstanceid'] = $data['instance'];
|
|
break;
|
|
case 'course':
|
|
$originalcourseinfo = $this->converter->get_stash('original_course_info');
|
|
$originalcourseid = $originalcourseinfo['original_course_id'];
|
|
$this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSE);
|
|
$this->currentcategory['contextlevel'] = CONTEXT_COURSE;
|
|
$this->currentcategory['contextinstanceid'] = $originalcourseid;
|
|
break;
|
|
case 'coursecategory':
|
|
// this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance
|
|
// of the course category (1 = parent category, 2 = grand-parent category etc). We pretend
|
|
// that this level*10 is the id of that category and create an artifical contextid for it
|
|
$this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSECAT, $data['coursecategorylevel'] * 10);
|
|
$this->currentcategory['contextlevel'] = CONTEXT_COURSECAT;
|
|
$this->currentcategory['contextinstanceid'] = $data['coursecategorylevel'] * 10;
|
|
break;
|
|
case 'system':
|
|
$this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM);
|
|
$this->currentcategory['contextlevel'] = CONTEXT_SYSTEM;
|
|
$this->currentcategory['contextinstanceid'] = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the common <question> data and re-dispateches the whole grouped
|
|
* <QUESTION> data to the qtype for appending its qtype specific data processing
|
|
*
|
|
* @param array $data
|
|
* @param array $raw
|
|
* @return array
|
|
*/
|
|
public function process_question(array $data, array $raw) {
|
|
global $CFG;
|
|
|
|
// firstly make sure that the category data and the <questions> wrapper are written
|
|
// note that because of MDL-27693 we can't use {@link self::process_question_category()}
|
|
// and {@link self::on_questions_start()} to do so
|
|
|
|
if (empty($this->currentcategorywritten)) {
|
|
$this->xmlwriter->begin_tag('question_category', array('id' => $this->currentcategory['id']));
|
|
foreach ($this->currentcategory as $name => $value) {
|
|
if ($name === 'id') {
|
|
continue;
|
|
}
|
|
$this->xmlwriter->full_tag($name, $value);
|
|
}
|
|
$this->currentcategorywritten = true;
|
|
}
|
|
|
|
if (empty($this->questionswrapperwritten)) {
|
|
$this->xmlwriter->begin_tag('questions');
|
|
$this->questionswrapperwritten = true;
|
|
}
|
|
|
|
$qtype = $data['qtype'];
|
|
|
|
// replay the upgrade step 2008050700 {@see question_fix_random_question_parents()}
|
|
if ($qtype == 'random' and $data['parent'] <> $data['id']) {
|
|
$data['parent'] = $data['id'];
|
|
}
|
|
|
|
// replay the upgrade step 2010080900 and part of 2010080901
|
|
$data['generalfeedbackformat'] = $data['questiontextformat'];
|
|
$data['oldquestiontextformat'] = $data['questiontextformat'];
|
|
|
|
if ($CFG->texteditors !== 'textarea') {
|
|
$data['questiontext'] = text_to_html($data['questiontext'], false, false, true);
|
|
$data['questiontextformat'] = FORMAT_HTML;
|
|
$data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true);
|
|
$data['generalfeedbackformat'] = FORMAT_HTML;
|
|
}
|
|
|
|
// Migrate files in questiontext.
|
|
$this->fileman->contextid = $this->currentcategory['contextid'];
|
|
$this->fileman->component = 'question';
|
|
$this->fileman->filearea = 'questiontext';
|
|
$this->fileman->itemid = $data['id'];
|
|
$data['questiontext'] = moodle1_converter::migrate_referenced_files($data['questiontext'], $this->fileman);
|
|
|
|
// Migrate files in generalfeedback.
|
|
$this->fileman->filearea = 'generalfeedback';
|
|
$data['generalfeedback'] = moodle1_converter::migrate_referenced_files($data['generalfeedback'], $this->fileman);
|
|
|
|
// replay the upgrade step 2010080901 - updating question image
|
|
if (!empty($data['image'])) {
|
|
if (core_text::substr(core_text::strtolower($data['image']), 0, 7) == 'http://') {
|
|
// it is a link, appending to existing question text
|
|
$data['questiontext'] .= ' <img src="' . $data['image'] . '" />';
|
|
|
|
} else {
|
|
// it is a file in course_files
|
|
$filename = basename($data['image']);
|
|
$filepath = dirname($data['image']);
|
|
if (empty($filepath) or $filepath == '.' or $filepath == '/') {
|
|
$filepath = '/';
|
|
} else {
|
|
// append /
|
|
$filepath = '/'.trim($filepath, './@#$ ').'/';
|
|
}
|
|
|
|
if (file_exists($this->converter->get_tempdir_path().'/course_files'.$filepath.$filename)) {
|
|
$this->fileman->contextid = $this->currentcategory['contextid'];
|
|
$this->fileman->component = 'question';
|
|
$this->fileman->filearea = 'questiontext';
|
|
$this->fileman->itemid = $data['id'];
|
|
$this->fileman->migrate_file('course_files'.$filepath.$filename, '/', $filename);
|
|
// note this is slightly different from the upgrade code as we put the file into the
|
|
// root folder here. this makes our life easier as we do not need to create all the
|
|
// directories within the specified filearea/itemid
|
|
$data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
|
|
|
|
} else {
|
|
$this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename));
|
|
}
|
|
}
|
|
}
|
|
unset($data['image']);
|
|
|
|
// replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
|
|
$data['defaultmark'] = $data['defaultgrade'];
|
|
|
|
// write the common question data
|
|
$this->xmlwriter->begin_tag('question', array('id' => $data['id']));
|
|
foreach (array(
|
|
'parent', 'name', 'questiontext', 'questiontextformat',
|
|
'generalfeedback', 'generalfeedbackformat', 'defaultmark',
|
|
'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
|
|
'timecreated', 'timemodified', 'createdby', 'modifiedby'
|
|
) as $fieldname) {
|
|
if (!array_key_exists($fieldname, $data)) {
|
|
throw new moodle1_convert_exception('missing_common_question_field', $fieldname);
|
|
}
|
|
$this->xmlwriter->full_tag($fieldname, $data[$fieldname]);
|
|
}
|
|
// unless we know that the given qtype does not append any own structures,
|
|
// give the handler a chance to do so now
|
|
if (!in_array($qtype, array('description', 'random'))) {
|
|
$handler = $this->get_qtype_handler($qtype);
|
|
if ($handler === false) {
|
|
$this->log('question type converter not found', backup::LOG_ERROR, $qtype);
|
|
|
|
} else {
|
|
$this->xmlwriter->begin_tag('plugin_qtype_'.$qtype.'_question');
|
|
$handler->use_xml_writer($this->xmlwriter);
|
|
$handler->process_question($data, $raw);
|
|
$this->xmlwriter->end_tag('plugin_qtype_'.$qtype.'_question');
|
|
}
|
|
}
|
|
|
|
$this->xmlwriter->end_tag('question');
|
|
}
|
|
|
|
/**
|
|
* Closes the questions wrapper
|
|
*/
|
|
public function on_questions_end() {
|
|
if ($this->questionswrapperwritten) {
|
|
$this->xmlwriter->end_tag('questions');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the question_category and annotates the category id
|
|
* so that it can be dumped into course/inforef.xml
|
|
*/
|
|
public function on_question_category_end() {
|
|
// make sure that the category data were written by {@link self::process_question()}
|
|
// if not, write it now. this may happen when the current category does not contain any
|
|
// questions so the subpaths is missing completely
|
|
if (empty($this->currentcategorywritten)) {
|
|
$this->write_xml('question_category', $this->currentcategory, array('/question_category/id'));
|
|
} else {
|
|
$this->xmlwriter->end_tag('question_category');
|
|
}
|
|
$this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']);
|
|
}
|
|
|
|
/**
|
|
* Stops writing questions.xml
|
|
*/
|
|
public function on_question_categories_end() {
|
|
$this->xmlwriter->end_tag('question_categories');
|
|
$this->close_xml_writer();
|
|
}
|
|
|
|
/**
|
|
* Provides access to the qtype handlers
|
|
*
|
|
* Returns either list of all qtype handler instances (if passed '*') or a particular handler
|
|
* for the given qtype or false if the qtype is not supported.
|
|
*
|
|
* @throws moodle1_convert_exception
|
|
* @param string $qtype the name of the question type or '*' for returning all
|
|
* @return array|moodle1_qtype_handler|bool
|
|
*/
|
|
protected function get_qtype_handler($qtype) {
|
|
|
|
if (is_null($this->qtypehandlers)) {
|
|
// initialize the list of qtype handler instances
|
|
$this->qtypehandlers = array();
|
|
foreach (core_component::get_plugin_list('qtype') as $qtypename => $qtypelocation) {
|
|
$filename = $qtypelocation.'/backup/moodle1/lib.php';
|
|
if (file_exists($filename)) {
|
|
$classname = 'moodle1_qtype_'.$qtypename.'_handler';
|
|
require_once($filename);
|
|
if (!class_exists($classname)) {
|
|
throw new moodle1_convert_exception('missing_handler_class', $classname);
|
|
}
|
|
$this->log('registering handler', backup::LOG_DEBUG, $classname, 2);
|
|
$this->qtypehandlers[$qtypename] = new $classname($this, $qtypename);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($qtype === '*') {
|
|
return $this->qtypehandlers;
|
|
|
|
} else if (isset($this->qtypehandlers[$qtype])) {
|
|
return $this->qtypehandlers[$qtype];
|
|
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of the scales included in the moodle.xml file
|
|
*/
|
|
class moodle1_scales_handler extends moodle1_handler {
|
|
|
|
/** @var moodle1_file_manager instance used to convert question images */
|
|
protected $fileman = null;
|
|
|
|
/**
|
|
* Registers paths
|
|
*/
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
|
|
new convert_path(
|
|
'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
|
|
array(
|
|
'renamefields' => array(
|
|
'scaletext' => 'scale',
|
|
),
|
|
'addfields' => array(
|
|
'descriptionformat' => 0,
|
|
)
|
|
)
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepare the file manager for the files embedded in the scale description field
|
|
*/
|
|
public function on_scales_start() {
|
|
$syscontextid = $this->converter->get_contextid(CONTEXT_SYSTEM);
|
|
$this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'scale');
|
|
}
|
|
|
|
/**
|
|
* This is executed every time we have one <SCALE> data available
|
|
*
|
|
* @param array $data
|
|
* @param array $raw
|
|
* @return array
|
|
*/
|
|
public function process_scale(array $data, array $raw) {
|
|
global $CFG;
|
|
|
|
// replay upgrade step 2009110400
|
|
if ($CFG->texteditors !== 'textarea') {
|
|
$data['description'] = text_to_html($data['description'], false, false, true);
|
|
$data['descriptionformat'] = FORMAT_HTML;
|
|
}
|
|
|
|
// convert course files embedded into the scale description field
|
|
$this->fileman->itemid = $data['id'];
|
|
$data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
|
|
|
|
// stash the scale
|
|
$this->converter->set_stash('scales', $data, $data['id']);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of the outcomes
|
|
*/
|
|
class moodle1_outcomes_handler extends moodle1_xml_handler {
|
|
|
|
/** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
|
|
protected $fileman = null;
|
|
|
|
/**
|
|
* Registers paths
|
|
*/
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
|
|
new convert_path(
|
|
'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
|
|
array(
|
|
'addfields' => array(
|
|
'descriptionformat' => FORMAT_MOODLE,
|
|
),
|
|
)
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepares the file manager and starts writing outcomes.xml
|
|
*/
|
|
public function on_gradebook_grade_outcomes_start() {
|
|
|
|
$syscontextid = $this->converter->get_contextid(CONTEXT_SYSTEM);
|
|
$this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome');
|
|
|
|
$this->open_xml_writer('outcomes.xml');
|
|
$this->xmlwriter->begin_tag('outcomes_definition');
|
|
}
|
|
|
|
/**
|
|
* Processes GRADE_OUTCOME tags progressively
|
|
*/
|
|
public function process_gradebook_grade_outcome(array $data, array $raw) {
|
|
global $CFG;
|
|
|
|
// replay the upgrade step 2009110400
|
|
if ($CFG->texteditors !== 'textarea') {
|
|
$data['description'] = text_to_html($data['description'], false, false, true);
|
|
$data['descriptionformat'] = FORMAT_HTML;
|
|
}
|
|
|
|
// convert course files embedded into the outcome description field
|
|
$this->fileman->itemid = $data['id'];
|
|
$data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
|
|
|
|
// write the outcome data
|
|
$this->write_xml('outcome', $data, array('/outcome/id'));
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Closes outcomes.xml
|
|
*/
|
|
public function on_gradebook_grade_outcomes_end() {
|
|
$this->xmlwriter->end_tag('outcomes_definition');
|
|
$this->close_xml_writer();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the conversion of the gradebook structures in the moodle.xml file
|
|
*/
|
|
class moodle1_gradebook_handler extends moodle1_xml_handler {
|
|
|
|
/** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
|
|
protected $categoryparent = array();
|
|
|
|
/**
|
|
* Registers paths
|
|
*/
|
|
public function get_paths() {
|
|
return array(
|
|
new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
|
|
new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
|
|
new convert_path(
|
|
'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
|
|
array(
|
|
'addfields' => array(
|
|
'hidden' => 0, // upgrade step 2010011200
|
|
),
|
|
)
|
|
),
|
|
new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'),
|
|
new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initializes the in-memory structures
|
|
*
|
|
* This should not be needed actually as the moodle.xml contains just one GRADEBOOK
|
|
* element. But who knows - maybe someone will want to write a mass conversion
|
|
* tool in the future (not me definitely ;-)
|
|
*/
|
|
public function on_gradebook_start() {
|
|
$this->categoryparent = array();
|
|
}
|
|
|
|
/**
|
|
* Processes one GRADE_LETTER data
|
|
*
|
|
* In Moodle 1.9, all grade_letters are from course context only. Therefore
|
|
* we put them here.
|
|
*/
|
|
public function process_gradebook_grade_letter(array $data, array $raw) {
|
|
$this->converter->set_stash('gradebook_gradeletter', $data, $data['id']);
|
|
}
|
|
|
|
/**
|
|
* Processes one GRADE_CATEGORY data
|
|
*/
|
|
public function process_gradebook_grade_category(array $data, array $raw) {
|
|
$this->categoryparent[$data['id']] = $data['parent'];
|
|
$this->converter->set_stash('gradebook_gradecategory', $data, $data['id']);
|
|
}
|
|
|
|
/**
|
|
* Processes one GRADE_ITEM data
|
|
*/
|
|
public function process_gradebook_grade_item(array $data, array $raw) {
|
|
|
|
// here we use get_nextid() to get a nondecreasing sequence
|
|
$data['sortorder'] = $this->converter->get_nextid();
|
|
|
|
if ($data['itemtype'] === 'mod') {
|
|
return $this->process_mod_grade_item($data, $raw);
|
|
|
|
} else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
|
|
return $this->process_nonmod_grade_item($data, $raw);
|
|
|
|
} else {
|
|
$this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes one GRADE_ITEM of the type 'mod'
|
|
*/
|
|
protected function process_mod_grade_item(array $data, array $raw) {
|
|
|
|
$stashname = 'gradebook_modgradeitem_'.$data['itemmodule'];
|
|
$stashitemid = $data['iteminstance'];
|
|
$gradeitems = $this->converter->get_stash_or_default($stashname, $stashitemid, array());
|
|
|
|
// typically there will be single item with itemnumber 0
|
|
$gradeitems[$data['itemnumber']] = $data;
|
|
|
|
$this->converter->set_stash($stashname, $gradeitems, $stashitemid);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
|
|
*/
|
|
protected function process_nonmod_grade_item(array $data, array $raw) {
|
|
|
|
$stashname = 'gradebook_nonmodgradeitem';
|
|
$stashitemid = $data['id'];
|
|
$this->converter->set_stash($stashname, $data, $stashitemid);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @todo
|
|
*/
|
|
public function on_gradebook_grade_item_grades_start() {
|
|
}
|
|
|
|
/**
|
|
* Writes the collected information into gradebook.xml
|
|
*/
|
|
public function on_gradebook_end() {
|
|
|
|
$this->open_xml_writer('gradebook.xml');
|
|
$this->xmlwriter->begin_tag('gradebook');
|
|
$this->write_grade_categories();
|
|
$this->write_grade_items();
|
|
$this->write_grade_letters();
|
|
$this->xmlwriter->end_tag('gradebook');
|
|
$this->close_xml_writer();
|
|
}
|
|
|
|
/**
|
|
* Writes grade_categories
|
|
*/
|
|
protected function write_grade_categories() {
|
|
|
|
$this->xmlwriter->begin_tag('grade_categories');
|
|
foreach ($this->converter->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) {
|
|
$gradecategory = $this->converter->get_stash('gradebook_gradecategory', $gradecategoryid);
|
|
$path = $this->calculate_category_path($gradecategoryid);
|
|
$gradecategory['depth'] = count($path);
|
|
$gradecategory['path'] = '/'.implode('/', $path).'/';
|
|
$this->write_xml('grade_category', $gradecategory, array('/grade_category/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('grade_categories');
|
|
}
|
|
|
|
/**
|
|
* Calculates the path to the grade_category
|
|
*
|
|
* Moodle 1.9 backup does not store the grade_category's depth and path. This method is used
|
|
* to repopulate this information using the $this->categoryparent values.
|
|
*
|
|
* @param int $categoryid
|
|
* @return array of ids including the categoryid
|
|
*/
|
|
protected function calculate_category_path($categoryid) {
|
|
|
|
if (!array_key_exists($categoryid, $this->categoryparent)) {
|
|
throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
|
|
}
|
|
|
|
$path = array($categoryid);
|
|
$parent = $this->categoryparent[$categoryid];
|
|
while (!is_null($parent)) {
|
|
array_unshift($path, $parent);
|
|
$parent = $this->categoryparent[$parent];
|
|
if (in_array($parent, $path)) {
|
|
throw new moodle1_convert_exception('circular_reference_in_categories_tree');
|
|
}
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Writes grade_items
|
|
*/
|
|
protected function write_grade_items() {
|
|
|
|
$this->xmlwriter->begin_tag('grade_items');
|
|
foreach ($this->converter->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) {
|
|
$gradeitem = $this->converter->get_stash('gradebook_nonmodgradeitem', $gradeitemid);
|
|
$this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('grade_items');
|
|
}
|
|
|
|
/**
|
|
* Writes grade_letters
|
|
*/
|
|
protected function write_grade_letters() {
|
|
|
|
$this->xmlwriter->begin_tag('grade_letters');
|
|
foreach ($this->converter->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) {
|
|
$gradeletter = $this->converter->get_stash('gradebook_gradeletter', $gradeletterid);
|
|
$this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('grade_letters');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Shared base class for activity modules, blocks and qtype handlers
|
|
*/
|
|
abstract class moodle1_plugin_handler extends moodle1_xml_handler {
|
|
|
|
/** @var string */
|
|
protected $plugintype;
|
|
|
|
/** @var string */
|
|
protected $pluginname;
|
|
|
|
/**
|
|
* @param moodle1_converter $converter the converter that requires us
|
|
* @param string $plugintype
|
|
* @param string $pluginname
|
|
*/
|
|
public function __construct(moodle1_converter $converter, $plugintype, $pluginname) {
|
|
|
|
parent::__construct($converter);
|
|
$this->plugintype = $plugintype;
|
|
$this->pluginname = $pluginname;
|
|
}
|
|
|
|
/**
|
|
* Returns the normalized name of the plugin, eg mod_workshop
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_component_name() {
|
|
return $this->plugintype.'_'.$this->pluginname;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base class for all question type handlers
|
|
*/
|
|
abstract class moodle1_qtype_handler extends moodle1_plugin_handler {
|
|
|
|
/** @var moodle1_question_bank_handler */
|
|
protected $qbankhandler;
|
|
|
|
/**
|
|
* Returns the list of paths within one <QUESTION> that this qtype needs to have included
|
|
* in the grouped question structure
|
|
*
|
|
* @return array of strings
|
|
*/
|
|
public function get_question_subpaths() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Gives the qtype handler a chance to write converted data into questions.xml
|
|
*
|
|
* @param array $data grouped question data
|
|
* @param array $raw grouped raw QUESTION data
|
|
*/
|
|
public function process_question(array $data, array $raw) {
|
|
}
|
|
|
|
/**
|
|
* Converts the answers and writes them into the questions.xml
|
|
*
|
|
* The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
|
|
*
|
|
* @param array $answers as parsed by the grouped parser in moodle.xml
|
|
* @param string $qtype containing the answers
|
|
*/
|
|
protected function write_answers(array $answers, $qtype) {
|
|
|
|
$this->xmlwriter->begin_tag('answers');
|
|
foreach ($answers as $elementname => $elements) {
|
|
foreach ($elements as $element) {
|
|
$answer = $this->convert_answer($element, $qtype);
|
|
// Migrate images in answertext.
|
|
if ($answer['answerformat'] == FORMAT_HTML) {
|
|
$answer['answertext'] = $this->migrate_files($answer['answertext'], 'question', 'answer', $answer['id']);
|
|
}
|
|
// Migrate images in feedback.
|
|
if ($answer['feedbackformat'] == FORMAT_HTML) {
|
|
$answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']);
|
|
}
|
|
$this->write_xml('answer', $answer, array('/answer/id'));
|
|
}
|
|
}
|
|
$this->xmlwriter->end_tag('answers');
|
|
}
|
|
|
|
/**
|
|
* Migrate files belonging to one qtype plugin text field.
|
|
*
|
|
* @param array $text the html fragment containing references to files
|
|
* @param string $component the component for restored files
|
|
* @param string $filearea the file area for restored files
|
|
* @param int $itemid the itemid for restored files
|
|
*
|
|
* @return string the text for this field, after files references have been processed
|
|
*/
|
|
protected function migrate_files($text, $component, $filearea, $itemid) {
|
|
$context = $this->qbankhandler->get_current_category_context();
|
|
$fileman = $this->qbankhandler->get_file_manager();
|
|
$fileman->contextid = $context['contextid'];
|
|
$fileman->component = $component;
|
|
$fileman->filearea = $filearea;
|
|
$fileman->itemid = $itemid;
|
|
$text = moodle1_converter::migrate_referenced_files($text, $fileman);
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Writes the grouped numerical_units structure
|
|
*
|
|
* @param array $numericalunits
|
|
*/
|
|
protected function write_numerical_units(array $numericalunits) {
|
|
|
|
$this->xmlwriter->begin_tag('numerical_units');
|
|
foreach ($numericalunits as $elementname => $elements) {
|
|
foreach ($elements as $element) {
|
|
$element['id'] = $this->converter->get_nextid();
|
|
$this->write_xml('numerical_unit', $element, array('/numerical_unit/id'));
|
|
}
|
|
}
|
|
$this->xmlwriter->end_tag('numerical_units');
|
|
}
|
|
|
|
/**
|
|
* Writes the numerical_options structure
|
|
*
|
|
* @see get_default_numerical_options()
|
|
* @param array $numericaloption
|
|
*/
|
|
protected function write_numerical_options(array $numericaloption) {
|
|
|
|
$this->xmlwriter->begin_tag('numerical_options');
|
|
if (!empty($numericaloption)) {
|
|
$this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
|
|
}
|
|
$this->xmlwriter->end_tag('numerical_options');
|
|
}
|
|
|
|
/**
|
|
* Returns default numerical_option structure
|
|
*
|
|
* This structure is not present in moodle.xml, we create a new artificial one here.
|
|
*
|
|
* @see write_numerical_options()
|
|
* @param int $oldquestiontextformat
|
|
* @return array
|
|
*/
|
|
protected function get_default_numerical_options($oldquestiontextformat, $units) {
|
|
global $CFG;
|
|
|
|
// replay the upgrade step 2009100100 - new table
|
|
$options = array(
|
|
'id' => $this->converter->get_nextid(),
|
|
'instructions' => null,
|
|
'instructionsformat' => 0,
|
|
'showunits' => 0,
|
|
'unitsleft' => 0,
|
|
'unitgradingtype' => 0,
|
|
'unitpenalty' => 0.1
|
|
);
|
|
|
|
// replay the upgrade step 2009100101
|
|
if ($CFG->texteditors !== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE) {
|
|
$options['instructionsformat'] = FORMAT_HTML;
|
|
} else {
|
|
$options['instructionsformat'] = $oldquestiontextformat;
|
|
}
|
|
|
|
// Set a good default, depending on whether there are any units defined.
|
|
if (empty($units)) {
|
|
$options['showunits'] = 3;
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
/**
|
|
* Writes the dataset_definitions structure
|
|
*
|
|
* @param array $datasetdefinitions array of dataset_definition structures
|
|
*/
|
|
protected function write_dataset_definitions(array $datasetdefinitions) {
|
|
|
|
$this->xmlwriter->begin_tag('dataset_definitions');
|
|
foreach ($datasetdefinitions as $datasetdefinition) {
|
|
$this->xmlwriter->begin_tag('dataset_definition', array('id' => $this->converter->get_nextid()));
|
|
foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) {
|
|
$this->xmlwriter->full_tag($element, $datasetdefinition[$element]);
|
|
}
|
|
$this->xmlwriter->begin_tag('dataset_items');
|
|
if (!empty($datasetdefinition['dataset_items']['dataset_item'])) {
|
|
foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) {
|
|
$datasetitem['id'] = $this->converter->get_nextid();
|
|
$this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id'));
|
|
}
|
|
}
|
|
$this->xmlwriter->end_tag('dataset_items');
|
|
$this->xmlwriter->end_tag('dataset_definition');
|
|
}
|
|
$this->xmlwriter->end_tag('dataset_definitions');
|
|
}
|
|
|
|
/// implementation details follow //////////////////////////////////////////
|
|
|
|
public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) {
|
|
|
|
parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
|
|
$this->qbankhandler = $qbankhandler;
|
|
}
|
|
|
|
/**
|
|
* @see self::get_question_subpaths()
|
|
*/
|
|
final public function get_paths() {
|
|
throw new moodle1_convert_exception('qtype_handler_get_paths');
|
|
}
|
|
|
|
/**
|
|
* Question type handlers cannot open the xml_writer
|
|
*/
|
|
final protected function open_xml_writer($filename) {
|
|
throw new moodle1_convert_exception('opening_xml_writer_forbidden');
|
|
}
|
|
|
|
/**
|
|
* Question type handlers cannot close the xml_writer
|
|
*/
|
|
final protected function close_xml_writer() {
|
|
throw new moodle1_convert_exception('opening_xml_writer_forbidden');
|
|
}
|
|
|
|
/**
|
|
* Provides a xml_writer instance to this qtype converter
|
|
*
|
|
* @param xml_writer $xmlwriter
|
|
*/
|
|
public function use_xml_writer(xml_writer $xmlwriter) {
|
|
$this->xmlwriter = $xmlwriter;
|
|
}
|
|
|
|
/**
|
|
* Converts <ANSWER> structure into the new <answer> one
|
|
*
|
|
* See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
|
|
*
|
|
* @param array $old the parsed answer array in moodle.xml
|
|
* @param string $qtype the question type the answer is part of
|
|
* @return array
|
|
*/
|
|
private function convert_answer(array $old, $qtype) {
|
|
global $CFG;
|
|
|
|
$new = array();
|
|
$new['id'] = $old['id'];
|
|
$new['answertext'] = $old['answer_text'];
|
|
$new['answerformat'] = 0; // upgrade step 2010080900
|
|
$new['fraction'] = $old['fraction'];
|
|
$new['feedback'] = $old['feedback'];
|
|
$new['feedbackformat'] = 0; // upgrade step 2010080900
|
|
|
|
// replay upgrade step 2010080901
|
|
if ($qtype !== 'multichoice') {
|
|
$new['answerformat'] = FORMAT_PLAIN;
|
|
} else {
|
|
$new['answertext'] = text_to_html($new['answertext'], false, false, true);
|
|
$new['answerformat'] = FORMAT_HTML;
|
|
}
|
|
|
|
if ($CFG->texteditors !== 'textarea') {
|
|
if ($qtype == 'essay') {
|
|
$new['feedback'] = text_to_html($new['feedback'], false, false, true);
|
|
}
|
|
$new['feedbackformat'] = FORMAT_HTML;
|
|
|
|
} else {
|
|
$new['feedbackformat'] = FORMAT_MOODLE;
|
|
}
|
|
|
|
return $new;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base class for activity module handlers
|
|
*/
|
|
abstract class moodle1_mod_handler extends moodle1_plugin_handler {
|
|
|
|
/**
|
|
* Returns the name of the module, eg. 'forum'
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_modname() {
|
|
return $this->pluginname;
|
|
}
|
|
|
|
/**
|
|
* Returns course module information for the given instance id
|
|
*
|
|
* The information for this instance id has been stashed by
|
|
* {@link moodle1_course_outline_handler::process_course_module()}
|
|
*
|
|
* @param int $instance the module instance id
|
|
* @param string $modname the module type, defaults to $this->pluginname
|
|
* @return int
|
|
*/
|
|
protected function get_cminfo($instance, $modname = null) {
|
|
|
|
if (is_null($modname)) {
|
|
$modname = $this->pluginname;
|
|
}
|
|
return $this->converter->get_stash('cminfo_'.$modname, $instance);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base class for all modules that are successors of the 1.9 resource module
|
|
*/
|
|
abstract class moodle1_resource_successor_handler extends moodle1_mod_handler {
|
|
|
|
/**
|
|
* Resource successors do not attach to paths themselves, they are called explicitely
|
|
* by moodle1_mod_resource_handler
|
|
*
|
|
* @return array
|
|
*/
|
|
final public function get_paths() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
|
|
*
|
|
* Called by {@link moodle1_mod_resource_handler::process_resource()}
|
|
*
|
|
* @param array $data pre-cooked legacy resource data
|
|
* @param array $raw raw legacy resource data
|
|
*/
|
|
public function process_legacy_resource(array $data, array $raw = null) {
|
|
}
|
|
|
|
/**
|
|
* Called when the parses reaches the end </MOD> resource tag
|
|
*
|
|
* @param array $data the data returned by {@link self::process_resource} or just pre-cooked
|
|
*/
|
|
public function on_legacy_resource_end(array $data) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for block handlers
|
|
*/
|
|
abstract class moodle1_block_handler extends moodle1_plugin_handler {
|
|
|
|
public function get_paths() {
|
|
$blockname = strtoupper($this->pluginname);
|
|
return array(
|
|
new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
|
|
);
|
|
}
|
|
|
|
public function process_block(array $data) {
|
|
$newdata = $this->convert_common_block_data($data);
|
|
|
|
$this->write_block_xml($newdata, $data);
|
|
$this->write_inforef_xml($newdata, $data);
|
|
$this->write_roles_xml($newdata, $data);
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function convert_common_block_data(array $olddata) {
|
|
$newdata = array();
|
|
|
|
$newdata['blockname'] = $olddata['name'];
|
|
$newdata['parentcontextid'] = $this->converter->get_contextid(CONTEXT_COURSE, 0);
|
|
$newdata['showinsubcontexts'] = 0;
|
|
$newdata['pagetypepattern'] = $olddata['pagetype'].='-*';
|
|
$newdata['subpagepattern'] = null;
|
|
$newdata['defaultregion'] = ($olddata['position']=='l')?'side-pre':'side-post';
|
|
$newdata['defaultweight'] = $olddata['weight'];
|
|
$newdata['configdata'] = $this->convert_configdata($olddata);
|
|
|
|
return $newdata;
|
|
}
|
|
|
|
protected function convert_configdata(array $olddata) {
|
|
return $olddata['configdata'];
|
|
}
|
|
|
|
protected function write_block_xml($newdata, $data) {
|
|
$contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
|
|
|
|
$this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/block.xml");
|
|
$this->xmlwriter->begin_tag('block', array('id' => $data['id'], 'contextid' => $contextid));
|
|
|
|
foreach ($newdata as $field => $value) {
|
|
$this->xmlwriter->full_tag($field, $value);
|
|
}
|
|
|
|
$this->xmlwriter->begin_tag('block_positions');
|
|
$this->xmlwriter->begin_tag('block_position', array('id' => 1));
|
|
$this->xmlwriter->full_tag('contextid', $newdata['parentcontextid']);
|
|
$this->xmlwriter->full_tag('pagetype', $data['pagetype']);
|
|
$this->xmlwriter->full_tag('subpage', '');
|
|
$this->xmlwriter->full_tag('visible', $data['visible']);
|
|
$this->xmlwriter->full_tag('region', $newdata['defaultregion']);
|
|
$this->xmlwriter->full_tag('weight', $newdata['defaultweight']);
|
|
$this->xmlwriter->end_tag('block_position');
|
|
$this->xmlwriter->end_tag('block_positions');
|
|
$this->xmlwriter->end_tag('block');
|
|
$this->close_xml_writer();
|
|
}
|
|
|
|
protected function write_inforef_xml($newdata, $data) {
|
|
$this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
|
|
$this->xmlwriter->begin_tag('inforef');
|
|
// Subclasses may provide inforef contents if needed
|
|
$this->xmlwriter->end_tag('inforef');
|
|
$this->close_xml_writer();
|
|
}
|
|
|
|
protected function write_roles_xml($newdata, $data) {
|
|
// This is an empty shell, as the moodle1 converter doesn't handle user data.
|
|
$this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/roles.xml");
|
|
$this->xmlwriter->begin_tag('roles');
|
|
$this->xmlwriter->full_tag('role_overrides', '');
|
|
$this->xmlwriter->full_tag('role_assignments', '');
|
|
$this->xmlwriter->end_tag('roles');
|
|
$this->close_xml_writer();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Base class for block generic handler
|
|
*/
|
|
class moodle1_block_generic_handler extends moodle1_block_handler {
|
|
|
|
}
|
|
|
|
/**
|
|
* Base class for the activity modules' subplugins
|
|
*/
|
|
abstract class moodle1_submod_handler extends moodle1_plugin_handler {
|
|
|
|
/** @var moodle1_mod_handler */
|
|
protected $parenthandler;
|
|
|
|
/**
|
|
* @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of
|
|
* @param string $subplugintype the type of the subplugin
|
|
* @param string $subpluginname the name of the subplugin
|
|
*/
|
|
public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) {
|
|
$this->parenthandler = $parenthandler;
|
|
parent::__construct($parenthandler->converter, $subplugintype, $subpluginname);
|
|
}
|
|
|
|
/**
|
|
* Activity module subplugins can't declare any paths to handle
|
|
*
|
|
* The paths must be registered by the parent module and then re-dispatched to the
|
|
* relevant subplugins for eventual processing.
|
|
*
|
|
* @return array empty array
|
|
*/
|
|
final public function get_paths() {
|
|
return array();
|
|
}
|
|
}
|
|
|