3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Defines Moodle 1.9 backup conversion handlers
21 * Handlers are classes responsible for the actual conversion work. Their logic
22 * is similar to the functionality provided by steps in plan based restore process.
24 * @package backup-convert
26 * @copyright 2011 David Mudrak <david@moodle.com>
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 defined('MOODLE_INTERNAL') ||
die();
32 require_once($CFG->dirroot
. '/backup/util/xml/xml_writer.class.php');
33 require_once($CFG->dirroot
. '/backup/util/xml/output/xml_output.class.php');
34 require_once($CFG->dirroot
. '/backup/util/xml/output/file_xml_output.class.php');
37 * Handlers factory class
39 abstract class moodle1_handlers_factory
{
42 * @param moodle1_converter the converter requesting the converters
43 * @return list of all available conversion handlers
45 public static function get_handlers(moodle1_converter
$converter) {
48 new moodle1_root_handler($converter),
49 new moodle1_info_handler($converter),
50 new moodle1_course_header_handler($converter),
51 new moodle1_course_outline_handler($converter),
52 new moodle1_roles_definition_handler($converter),
53 new moodle1_question_bank_handler($converter),
54 new moodle1_scales_handler($converter),
55 new moodle1_outcomes_handler($converter),
56 new moodle1_gradebook_handler($converter),
59 $handlers = array_merge($handlers, self
::get_plugin_handlers('mod', $converter));
60 $handlers = array_merge($handlers, self
::get_plugin_handlers('block', $converter));
62 // make sure that all handlers have expected class
63 foreach ($handlers as $handler) {
64 if (!$handler instanceof moodle1_handler
) {
65 throw new moodle1_convert_exception('wrong_handler_class', get_class($handler));
72 /// public API ends here ///////////////////////////////////////////////////
75 * Runs through all plugins of a specific type and instantiates their handlers
77 * @todo ask mod's subplugins
78 * @param string $type the plugin type
79 * @param moodle1_converter $converter the converter requesting the handler
80 * @throws moodle1_convert_exception
81 * @return array of {@link moodle1_handler} instances
83 protected static function get_plugin_handlers($type, moodle1_converter
$converter) {
87 $plugins = core_component
::get_plugin_list($type);
88 foreach ($plugins as $name => $dir) {
89 $handlerfile = $dir . '/backup/moodle1/lib.php';
90 $handlerclass = "moodle1_{$type}_{$name}_handler";
91 if (file_exists($handlerfile)) {
92 require_once($handlerfile);
93 } elseif ($type == 'block') {
94 $handlerclass = "moodle1_block_generic_handler";
99 if (!class_exists($handlerclass)) {
100 throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
102 $handlers[] = new $handlerclass($converter, $type, $name);
110 * Base backup conversion handler
112 abstract class moodle1_handler
implements loggable
{
114 /** @var moodle1_converter */
115 protected $converter;
118 * @param moodle1_converter $converter the converter that requires us
120 public function __construct(moodle1_converter
$converter) {
121 $this->converter
= $converter;
125 * @return moodle1_converter the converter that required this handler
127 public function get_converter() {
128 return $this->converter
;
132 * Log a message using the converter's logging mechanism
134 * @param string $message message text
135 * @param int $level message level {@example backup::LOG_WARNING}
136 * @param null|mixed $a additional information
137 * @param null|int $depth the message depth
138 * @param bool $display whether the message should be sent to the output, too
140 public function log($message, $level, $a = null, $depth = null, $display = false) {
141 $this->converter
->log($message, $level, $a, $depth, $display);
147 * Base backup conversion handler that generates an XML file
149 abstract class moodle1_xml_handler
extends moodle1_handler
{
151 /** @var null|string the name of file we are writing to */
152 protected $xmlfilename;
154 /** @var null|xml_writer */
155 protected $xmlwriter;
158 * Opens the XML writer - after calling, one is free to use $xmlwriter
160 * @param string $filename XML file name to write into
163 protected function open_xml_writer($filename) {
165 if (!is_null($this->xmlfilename
) and $filename !== $this->xmlfilename
) {
166 throw new moodle1_convert_exception('xml_writer_already_opened_for_other_file', $this->xmlfilename
);
169 if (!$this->xmlwriter
instanceof xml_writer
) {
170 $this->xmlfilename
= $filename;
171 $fullpath = $this->converter
->get_workdir_path() . '/' . $this->xmlfilename
;
172 $directory = pathinfo($fullpath, PATHINFO_DIRNAME
);
174 if (!check_dir_exists($directory)) {
175 throw new moodle1_convert_exception('unable_create_target_directory', $directory);
177 $this->xmlwriter
= new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer());
178 $this->xmlwriter
->start();
183 * Close the XML writer
185 * At the moment, the caller must close all tags before calling
189 protected function close_xml_writer() {
190 if ($this->xmlwriter
instanceof xml_writer
) {
191 $this->xmlwriter
->stop();
193 unset($this->xmlwriter
);
194 $this->xmlwriter
= null;
195 $this->xmlfilename
= null;
199 * Checks if the XML writer has been opened by {@link self::open_xml_writer()}
203 protected function has_xml_writer() {
205 if ($this->xmlwriter
instanceof xml_writer
) {
213 * Writes the given XML tree data into the currently opened file
215 * @param string $element the name of the root element of the tree
216 * @param array $data the associative array of data to write
217 * @param array $attribs list of additional fields written as attributes instead of nested elements
218 * @param string $parent used internally during the recursion, do not set yourself
220 protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
222 if (!$this->has_xml_writer()) {
223 throw new moodle1_convert_exception('write_xml_without_writer');
226 $mypath = $parent . $element;
227 $myattribs = array();
229 // detect properties that should be rendered as element's attributes instead of children
230 foreach ($data as $name => $value) {
231 if (!is_array($value)) {
232 if (in_array($mypath . '/' . $name, $attribs)) {
233 $myattribs[$name] = $value;
239 // reorder the $data so that all sub-branches are at the end (needed by our parser)
242 foreach ($data as $name => $value) {
243 if (is_array($value)) {
244 $branches[$name] = $value;
246 $leaves[$name] = $value;
249 $data = array_merge($leaves, $branches);
251 $this->xmlwriter
->begin_tag($element, $myattribs);
253 foreach ($data as $name => $value) {
254 if (is_array($value)) {
255 // recursively call self
256 $this->write_xml($name, $value, $attribs, $mypath.'/');
258 $this->xmlwriter
->full_tag($name, $value);
262 $this->xmlwriter
->end_tag($element);
266 * Makes sure that a new XML file exists, or creates it itself
268 * This is here so we can check that all XML files that the restore process relies on have
269 * been created by an executed handler. If the file is not found, this method can create it
270 * using the given $rootelement as an empty root container in the file.
272 * @param string $filename relative file name like 'course/course.xml'
273 * @param string|bool $rootelement root element to use, false to not create the file
274 * @param array $content content of the root element
275 * @return bool true is the file existed, false if it did not
277 protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) {
279 $existed = file_exists($this->converter
->get_workdir_path().'/'.$filename);
285 if ($rootelement !== false) {
286 $this->open_xml_writer($filename);
287 $this->write_xml($rootelement, $content);
288 $this->close_xml_writer();
297 * Process the root element of the backup file
299 class moodle1_root_handler
extends moodle1_xml_handler
{
301 public function get_paths() {
302 return array(new convert_path('root_element', '/MOODLE_BACKUP'));
306 * Converts course_files and site_files
308 public function on_root_element_start() {
310 // convert course files
311 $fileshandler = new moodle1_files_handler($this->converter
);
312 $fileshandler->process();
316 * This is executed at the end of the moodle.xml parsing
318 public function on_root_element_end() {
321 // restore the stashes prepared by other handlers for us
322 $backupinfo = $this->converter
->get_stash('backup_info');
323 $originalcourseinfo = $this->converter
->get_stash('original_course_info');
325 ////////////////////////////////////////////////////////////////////////
326 // write moodle_backup.xml
327 ////////////////////////////////////////////////////////////////////////
328 $this->open_xml_writer('moodle_backup.xml');
330 $this->xmlwriter
->begin_tag('moodle_backup');
331 $this->xmlwriter
->begin_tag('information');
333 // moodle_backup/information
334 $this->xmlwriter
->full_tag('name', $backupinfo['name']);
335 $this->xmlwriter
->full_tag('moodle_version', $backupinfo['moodle_version']);
336 $this->xmlwriter
->full_tag('moodle_release', $backupinfo['moodle_release']);
337 $this->xmlwriter
->full_tag('backup_version', $CFG->backup_version
); // {@see restore_prechecks_helper::execute_prechecks}
338 $this->xmlwriter
->full_tag('backup_release', $CFG->backup_release
);
339 $this->xmlwriter
->full_tag('backup_date', $backupinfo['date']);
340 // see the commit c0543b - all backups created in 1.9 and later declare the
341 // information or it is considered as false
342 if (isset($backupinfo['mnet_remoteusers'])) {
343 $this->xmlwriter
->full_tag('mnet_remoteusers', $backupinfo['mnet_remoteusers']);
345 $this->xmlwriter
->full_tag('mnet_remoteusers', false);
347 $this->xmlwriter
->full_tag('original_wwwroot', $backupinfo['original_wwwroot']);
348 // {@see backup_general_helper::backup_is_samesite()}
349 if (isset($backupinfo['original_site_identifier_hash'])) {
350 $this->xmlwriter
->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']);
352 $this->xmlwriter
->full_tag('original_site_identifier_hash', null);
354 $this->xmlwriter
->full_tag('original_course_id', $originalcourseinfo['original_course_id']);
355 $this->xmlwriter
->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']);
356 $this->xmlwriter
->full_tag('original_course_shortname', $originalcourseinfo['original_course_shortname']);
357 $this->xmlwriter
->full_tag('original_course_startdate', $originalcourseinfo['original_course_startdate']);
358 $this->xmlwriter
->full_tag('original_system_contextid', $this->converter
->get_contextid(CONTEXT_SYSTEM
));
359 // note that even though we have original_course_contextid available, we regenerate the
360 // original course contextid using our helper method to be sure that the data are consistent
361 // within the MBZ file
362 $this->xmlwriter
->full_tag('original_course_contextid', $this->converter
->get_contextid(CONTEXT_COURSE
));
364 // moodle_backup/information/details
365 $this->xmlwriter
->begin_tag('details');
366 $this->write_xml('detail', array(
367 'backup_id' => $this->converter
->get_id(),
368 'type' => backup
::TYPE_1COURSE
,
369 'format' => backup
::FORMAT_MOODLE
,
370 'interactive' => backup
::INTERACTIVE_YES
,
371 'mode' => backup
::MODE_CONVERTED
,
372 'execution' => backup
::EXECUTION_INMEDIATE
,
373 'executiontime' => 0,
374 ), array('/detail/backup_id'));
375 $this->xmlwriter
->end_tag('details');
377 // moodle_backup/information/contents
378 $this->xmlwriter
->begin_tag('contents');
380 // moodle_backup/information/contents/activities
381 $this->xmlwriter
->begin_tag('activities');
382 $activitysettings = array();
383 foreach ($this->converter
->get_stash('coursecontents') as $activity) {
384 $modinfo = $this->converter
->get_stash('modinfo_'.$activity['modulename']);
385 $modinstance = $modinfo['instances'][$activity['instanceid']];
386 $this->write_xml('activity', array(
387 'moduleid' => $activity['cmid'],
388 'sectionid' => $activity['sectionid'],
389 'modulename' => $activity['modulename'],
390 'title' => $modinstance['name'],
391 'directory' => 'activities/'.$activity['modulename'].'_'.$activity['cmid']
393 $activitysettings[] = array(
394 'level' => 'activity',
395 'activity' => $activity['modulename'].'_'.$activity['cmid'],
396 'name' => $activity['modulename'].'_'.$activity['cmid'].'_included',
397 'value' => (($modinfo['included'] === 'true' and $modinstance['included'] === 'true') ?
1 : 0));
398 $activitysettings[] = array(
399 'level' => 'activity',
400 'activity' => $activity['modulename'].'_'.$activity['cmid'],
401 'name' => $activity['modulename'].'_'.$activity['cmid'].'_userinfo',
402 //'value' => (($modinfo['userinfo'] === 'true' and $modinstance['userinfo'] === 'true') ? 1 : 0));
403 'value' => 0); // todo hardcoded non-userinfo for now
405 $this->xmlwriter
->end_tag('activities');
407 // moodle_backup/information/contents/sections
408 $this->xmlwriter
->begin_tag('sections');
409 $sectionsettings = array();
410 foreach ($this->converter
->get_stash_itemids('sectioninfo') as $sectionid) {
411 $sectioninfo = $this->converter
->get_stash('sectioninfo', $sectionid);
412 $sectionsettings[] = array(
413 'level' => 'section',
414 'section' => 'section_'.$sectionid,
415 'name' => 'section_'.$sectionid.'_included',
417 $sectionsettings[] = array(
418 'level' => 'section',
419 'section' => 'section_'.$sectionid,
420 'name' => 'section_'.$sectionid.'_userinfo',
421 'value' => 0); // @todo how to detect this from moodle.xml?
422 $this->write_xml('section', array(
423 'sectionid' => $sectionid,
424 'title' => $sectioninfo['number'], // because the title is not available
425 'directory' => 'sections/section_'.$sectionid));
427 $this->xmlwriter
->end_tag('sections');
429 // moodle_backup/information/contents/course
430 $this->write_xml('course', array(
431 'courseid' => $originalcourseinfo['original_course_id'],
432 'title' => $originalcourseinfo['original_course_shortname'],
433 'directory' => 'course'));
434 unset($originalcourseinfo);
436 $this->xmlwriter
->end_tag('contents');
438 // moodle_backup/information/settings
439 $this->xmlwriter
->begin_tag('settings');
441 // fake backup root seetings
442 $rootsettings = array(
443 'filename' => $backupinfo['name'],
444 'users' => 0, // @todo how to detect this from moodle.xml?
446 'role_assignments' => 0,
451 'userscompletion' => 0,
453 'grade_histories' => 0,
456 foreach ($rootsettings as $name => $value) {
457 $this->write_xml('setting', array(
462 unset($rootsettings);
464 // activity settings populated above
465 foreach ($activitysettings as $activitysetting) {
466 $this->write_xml('setting', $activitysetting);
468 unset($activitysettings);
470 // section settings populated above
471 foreach ($sectionsettings as $sectionsetting) {
472 $this->write_xml('setting', $sectionsetting);
474 unset($sectionsettings);
476 $this->xmlwriter
->end_tag('settings');
478 $this->xmlwriter
->end_tag('information');
479 $this->xmlwriter
->end_tag('moodle_backup');
481 $this->close_xml_writer();
483 ////////////////////////////////////////////////////////////////////////
485 ////////////////////////////////////////////////////////////////////////
486 $this->open_xml_writer('files.xml');
487 $this->xmlwriter
->begin_tag('files');
488 foreach ($this->converter
->get_stash_itemids('files') as $fileid) {
489 $this->write_xml('file', $this->converter
->get_stash('files', $fileid), array('/file/id'));
491 $this->xmlwriter
->end_tag('files');
492 $this->close_xml_writer('files.xml');
494 ////////////////////////////////////////////////////////////////////////
496 ////////////////////////////////////////////////////////////////////////
497 $this->open_xml_writer('scales.xml');
498 $this->xmlwriter
->begin_tag('scales_definition');
499 foreach ($this->converter
->get_stash_itemids('scales') as $scaleid) {
500 $this->write_xml('scale', $this->converter
->get_stash('scales', $scaleid), array('/scale/id'));
502 $this->xmlwriter
->end_tag('scales_definition');
503 $this->close_xml_writer('scales.xml');
505 ////////////////////////////////////////////////////////////////////////
506 // write course/inforef.xml
507 ////////////////////////////////////////////////////////////////////////
508 $this->open_xml_writer('course/inforef.xml');
509 $this->xmlwriter
->begin_tag('inforef');
511 $this->xmlwriter
->begin_tag('fileref');
512 // legacy course files
513 $fileids = $this->converter
->get_stash('course_files_ids');
514 if (is_array($fileids)) {
515 foreach ($fileids as $fileid) {
516 $this->write_xml('file', array('id' => $fileid));
520 // course summary files
521 $fileids = $this->converter
->get_stash('course_summary_files_ids');
522 if (is_array($fileids)) {
523 foreach ($fileids as $fileid) {
524 $this->write_xml('file', array('id' => $fileid));
527 $this->xmlwriter
->end_tag('fileref');
529 $this->xmlwriter
->begin_tag('question_categoryref');
530 foreach ($this->converter
->get_stash_itemids('question_categories') as $questioncategoryid) {
531 $this->write_xml('question_category', array('id' => $questioncategoryid));
533 $this->xmlwriter
->end_tag('question_categoryref');
535 $this->xmlwriter
->end_tag('inforef');
536 $this->close_xml_writer();
538 // make sure that the files required by the restore process have been generated.
539 // missing file may happen if the watched tag is not present in moodle.xml (for example
540 // QUESTION_CATEGORIES is optional in moodle.xml but questions.xml must exist in
541 // moodle2 format) or the handler has not been implemented yet.
542 // apparently this must be called after the handler had a chance to create the file.
543 $this->make_sure_xml_exists('questions.xml', 'question_categories');
544 $this->make_sure_xml_exists('groups.xml', 'groups');
545 $this->make_sure_xml_exists('outcomes.xml', 'outcomes_definition');
546 $this->make_sure_xml_exists('users.xml', 'users');
547 $this->make_sure_xml_exists('course/roles.xml', 'roles',
548 array('role_assignments' => array(), 'role_overrides' => array()));
549 $this->make_sure_xml_exists('course/enrolments.xml', 'enrolments',
550 array('enrols' => array()));
556 * The class responsible for course and site files migration
558 * @todo migrate site_files
560 class moodle1_files_handler
extends moodle1_xml_handler
{
563 * Migrates course_files and site_files in the converter workdir
565 public function process() {
566 $this->migrate_course_files();
567 // todo $this->migrate_site_files();
571 * Migrates course_files in the converter workdir
573 protected function migrate_course_files() {
575 $fileman = $this->converter
->get_file_manager($this->converter
->get_contextid(CONTEXT_COURSE
), 'course', 'legacy');
576 $this->converter
->set_stash('course_files_ids', array());
577 if (file_exists($this->converter
->get_tempdir_path().'/course_files')) {
578 $ids = $fileman->migrate_directory('course_files');
579 $this->converter
->set_stash('course_files_ids', $ids);
581 $this->log('course files migrated', backup
::LOG_INFO
, count($ids));
587 * Handles the conversion of /MOODLE_BACKUP/INFO paths
589 * We do not produce any XML file here, just storing the data in the temp
590 * table so thay can be used by a later handler.
592 class moodle1_info_handler
extends moodle1_handler
{
594 /** @var array list of mod names included in info_details */
595 protected $modnames = array();
597 /** @var array the in-memory cache of the currently parsed info_details_mod element */
598 protected $currentmod;
600 public function get_paths() {
602 new convert_path('info', '/MOODLE_BACKUP/INFO'),
603 new convert_path('info_details', '/MOODLE_BACKUP/INFO/DETAILS'),
604 new convert_path('info_details_mod', '/MOODLE_BACKUP/INFO/DETAILS/MOD'),
605 new convert_path('info_details_mod_instance', '/MOODLE_BACKUP/INFO/DETAILS/MOD/INSTANCES/INSTANCE'),
610 * Stashes the backup info for later processing by {@link moodle1_root_handler}
612 public function process_info($data) {
613 $this->converter
->set_stash('backup_info', $data);
617 * Initializes the in-memory cache for the current mod
619 public function process_info_details_mod($data) {
620 $this->currentmod
= $data;
621 $this->currentmod
['instances'] = array();
625 * Appends the current instance data to the temporary in-memory cache
627 public function process_info_details_mod_instance($data) {
628 $this->currentmod
['instances'][$data['id']] = $data;
632 * Stashes the backup info for later processing by {@link moodle1_root_handler}
634 public function on_info_details_mod_end($data) {
637 // keep only such modules that seem to have the support for moodle1 implemented
638 $modname = $this->currentmod
['name'];
639 if (file_exists($CFG->dirroot
.'/mod/'.$modname.'/backup/moodle1/lib.php')) {
640 $this->converter
->set_stash('modinfo_'.$modname, $this->currentmod
);
641 $this->modnames
[] = $modname;
643 $this->log('unsupported activity module', backup
::LOG_WARNING
, $modname);
646 $this->currentmod
= array();
650 * Stashes the list of activity module types for later processing by {@link moodle1_root_handler}
652 public function on_info_details_end() {
653 $this->converter
->set_stash('modnameslist', $this->modnames
);
659 * Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths
661 class moodle1_course_header_handler
extends moodle1_xml_handler
{
663 /** @var array we need to merge course information because it is dispatched twice */
664 protected $course = array();
666 /** @var array we need to merge course information because it is dispatched twice */
667 protected $courseraw = array();
672 public function get_paths() {
675 'course_header', '/MOODLE_BACKUP/COURSE/HEADER',
677 'newfields' => array(
678 'summaryformat' => 1,
680 'requested' => 0, // @todo not really new, but maybe never backed up?
681 'restrictmodules' => 0,
682 'enablecompletion' => 0,
683 'completionstartonenrol' => 0,
684 'completionnotify' => 0,
686 'allowed_modules' => array(),
688 'dropfields' => array(
713 'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
715 'newfields' => array(
716 'description' => null,
724 * Because there is the CATEGORY branch in the middle of the COURSE/HEADER
725 * branch, this is dispatched twice. We use $this->coursecooked to merge
726 * the result. Once the parser is fixed, it can be refactored.
728 public function process_course_header($data, $raw) {
729 $this->course
= array_merge($this->course
, $data);
730 $this->courseraw
= array_merge($this->courseraw
, $raw);
733 public function process_course_header_category($data) {
734 $this->category
= $data;
737 public function on_course_header_end() {
739 $contextid = $this->converter
->get_contextid(CONTEXT_COURSE
);
741 // stash the information needed by other handlers
743 'original_course_id' => $this->course
['id'],
744 'original_course_fullname' => $this->course
['fullname'],
745 'original_course_shortname' => $this->course
['shortname'],
746 'original_course_startdate' => $this->course
['startdate'],
747 'original_course_contextid' => $contextid
749 $this->converter
->set_stash('original_course_info', $info);
751 $this->course
['contextid'] = $contextid;
752 $this->course
['category'] = $this->category
;
754 // migrate files embedded into the course summary and stash their ids
755 $fileman = $this->converter
->get_file_manager($contextid, 'course', 'summary');
756 $this->course
['summary'] = moodle1_converter
::migrate_referenced_files($this->course
['summary'], $fileman);
757 $this->converter
->set_stash('course_summary_files_ids', $fileman->get_fileids());
760 $this->open_xml_writer('course/course.xml');
761 $this->write_xml('course', $this->course
, array('/course/id', '/course/contextid'));
762 $this->close_xml_writer();
768 * Handles the conversion of course sections and course modules
770 class moodle1_course_outline_handler
extends moodle1_xml_handler
{
772 /** @var array ordered list of the course contents */
773 protected $coursecontents = array();
775 /** @var array current section data */
776 protected $currentsection;
779 * This handler is interested in course sections and course modules within them
781 public function get_paths() {
783 new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'),
785 'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION',
787 'newfields' => array(
789 'summaryformat' => 1,
795 'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD',
797 'newfields' => array(
799 'completiongradeitemnumber' => null,
800 'completionview' => 0,
801 'completionexpected' => 0,
802 'availability' => null,
804 'showdescription' => 0,
806 'dropfields' => array(
811 'renamefields' => array(
812 'type' => 'modulename',
816 new convert_path('course_modules', '/MOODLE_BACKUP/COURSE/MODULES'),
817 // todo new convert_path('course_module_roles_overrides', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'),
818 // todo new convert_path('course_module_roles_assignments', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_ASSIGNMENTS'),
822 public function process_course_section($data) {
823 $this->currentsection
= $data;
827 * Populates the section sequence field (order of course modules) and stashes the
828 * course module info so that is can be dumped to activities/xxxx_x/module.xml later
830 public function process_course_module($data, $raw) {
833 // check that this type of module should be included in the mbz
834 $modinfo = $this->converter
->get_stash_itemids('modinfo_'.$data['modulename']);
835 if (empty($modinfo)) {
839 // add the course module into the course contents list
840 $this->coursecontents
[$data['id']] = array(
841 'cmid' => $data['id'],
842 'instanceid' => $raw['INSTANCE'],
843 'sectionid' => $this->currentsection
['id'],
844 'modulename' => $data['modulename'],
848 // add the course module id into the section's sequence
849 if (is_null($this->currentsection
['sequence'])) {
850 $this->currentsection
['sequence'] = $data['id'];
852 $this->currentsection
['sequence'] .= ',' . $data['id'];
855 // add the sectionid and sectionnumber
856 $data['sectionid'] = $this->currentsection
['id'];
857 $data['sectionnumber'] = $this->currentsection
['number'];
859 // generate the module version - this is a bit tricky as this information
860 // is not present in 1.9 backups. we will use the currently installed version
861 // whenever we can but that might not be accurate for some modules.
862 // also there might be problem with modules that are not present at the target
864 $versionfile = $CFG->dirroot
.'/mod/'.$data['modulename'].'/version.php';
865 if (file_exists($versionfile)) {
866 $plugin = new stdClass();
867 $plugin->version
= null;
869 include($versionfile);
870 $data['version'] = $plugin->version
;
872 $data['version'] = null;
875 // stash the course module info in stashes like 'cminfo_forum' with
876 // itemid set to the instance id. this is needed so that module handlers
877 // can later obtain information about the course module and dump it into
878 // the module.xml file
879 $this->converter
->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
883 * Writes sections/section_xxx/section.xml file and stashes it, too
885 public function on_course_section_end() {
887 // migrate files embedded into the section summary field
888 $contextid = $this->converter
->get_contextid(CONTEXT_COURSE
);
889 $fileman = $this->converter
->get_file_manager($contextid, 'course', 'section', $this->currentsection
['id']);
890 $this->currentsection
['summary'] = moodle1_converter
::migrate_referenced_files($this->currentsection
['summary'], $fileman);
892 // write section's inforef.xml with the file references
893 $this->open_xml_writer('sections/section_' . $this->currentsection
['id'] . '/inforef.xml');
894 $this->xmlwriter
->begin_tag('inforef');
895 $this->xmlwriter
->begin_tag('fileref');
896 $fileids = $fileman->get_fileids();
897 if (is_array($fileids)) {
898 foreach ($fileids as $fileid) {
899 $this->write_xml('file', array('id' => $fileid));
902 $this->xmlwriter
->end_tag('fileref');
903 $this->xmlwriter
->end_tag('inforef');
904 $this->close_xml_writer();
906 // stash the section info and write section.xml
907 $this->converter
->set_stash('sectioninfo', $this->currentsection
, $this->currentsection
['id']);
908 $this->open_xml_writer('sections/section_' . $this->currentsection
['id'] . '/section.xml');
909 $this->write_xml('section', $this->currentsection
);
910 $this->close_xml_writer();
911 unset($this->currentsection
);
915 * Stashes the course contents
917 public function on_course_sections_end() {
918 $this->converter
->set_stash('coursecontents', $this->coursecontents
);
922 * Writes the information collected by mod handlers
924 public function on_course_modules_end() {
926 foreach ($this->converter
->get_stash('modnameslist') as $modname) {
927 $modinfo = $this->converter
->get_stash('modinfo_'.$modname);
928 foreach ($modinfo['instances'] as $modinstanceid => $modinstance) {
929 $cminfo = $this->converter
->get_stash('cminfo_'.$modname, $modinstanceid);
930 $directory = 'activities/'.$modname.'_'.$cminfo['id'];
933 $this->open_xml_writer($directory.'/module.xml');
934 $this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
935 $this->close_xml_writer();
938 $this->open_xml_writer($directory.'/grades.xml');
939 $this->xmlwriter
->begin_tag('activity_gradebook');
940 $gradeitems = $this->converter
->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array());
941 if (!empty($gradeitems)) {
942 $this->xmlwriter
->begin_tag('grade_items');
943 foreach ($gradeitems as $gradeitem) {
944 $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
946 $this->xmlwriter
->end_tag('grade_items');
948 $this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9
949 $this->xmlwriter
->end_tag('activity_gradebook');
950 $this->close_xml_writer();
952 // todo: write proper roles.xml, for now we just make sure the file is present
953 $this->make_sure_xml_exists($directory.'/roles.xml', 'roles');
961 * Handles the conversion of the defined roles
963 class moodle1_roles_definition_handler
extends moodle1_xml_handler
{
966 * Where the roles are defined in the source moodle.xml
968 public function get_paths() {
970 new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
972 'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
974 'newfields' => array(
985 * If there are any roles defined in moodle.xml, convert them to roles.xml
987 public function process_roles_role($data) {
989 if (!$this->has_xml_writer()) {
990 $this->open_xml_writer('roles.xml');
991 $this->xmlwriter
->begin_tag('roles_definition');
993 if (!isset($data['nameincourse'])) {
994 $data['nameincourse'] = null;
996 $this->write_xml('role', $data, array('role/id'));
1000 * Finishes writing roles.xml
1002 public function on_roles_end() {
1004 if (!$this->has_xml_writer()) {
1005 // no roles defined in moodle.xml so {link self::process_roles_role()}
1006 // was never executed
1007 $this->open_xml_writer('roles.xml');
1008 $this->write_xml('roles_definition', array());
1011 // some roles were dumped into the file, let us close their wrapper now
1012 $this->xmlwriter
->end_tag('roles_definition');
1014 $this->close_xml_writer();
1020 * Handles the conversion of the question bank included in the moodle.xml file
1022 class moodle1_question_bank_handler
extends moodle1_xml_handler
{
1024 /** @var array the current question category being parsed */
1025 protected $currentcategory = null;
1027 /** @var array of the raw data for the current category */
1028 protected $currentcategoryraw = null;
1030 /** @var moodle1_file_manager instance used to convert question images */
1031 protected $fileman = null;
1033 /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
1034 private $currentcategorywritten = false;
1036 /** @var bool was the <questions> tag already written (work around MDL-27693) */
1037 private $questionswrapperwritten = false;
1039 /** @var array holds the instances of qtype specific conversion handlers */
1040 private $qtypehandlers = null;
1043 * Return the file manager instance used.
1045 * @return moodle1_file_manager
1047 public function get_file_manager() {
1048 return $this->fileman
;
1052 * Returns the information about the question category context being currently parsed
1054 * @return array with keys contextid, contextlevel and contextinstanceid
1056 public function get_current_category_context() {
1057 return $this->currentcategory
;
1061 * Registers path that are not qtype-specific
1063 public function get_paths() {
1066 new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
1068 'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
1070 'newfields' => array(
1074 new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'),
1075 new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'),
1076 // the question element must be grouped so we can re-dispatch it to the qtype handler as a whole
1077 new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true),
1080 // annotate all question subpaths required by the qtypes subplugins
1081 $subpaths = array();
1082 foreach ($this->get_qtype_handler('*') as $qtypehandler) {
1083 foreach ($qtypehandler->get_question_subpaths() as $subpath) {
1084 $subpaths[$subpath] = true;
1087 foreach (array_keys($subpaths) as $subpath) {
1088 $name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath));
1089 $path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath;
1090 $paths[] = new convert_path($name, $path);
1097 * Starts writing questions.xml and prepares the file manager instance
1099 public function on_question_categories_start() {
1100 $this->open_xml_writer('questions.xml');
1101 $this->xmlwriter
->begin_tag('question_categories');
1102 if (is_null($this->fileman
)) {
1103 $this->fileman
= $this->converter
->get_file_manager();
1108 * Initializes the current category cache
1110 public function on_question_category_start() {
1111 $this->currentcategory
= array();
1112 $this->currentcategoryraw
= array();
1113 $this->currentcategorywritten
= false;
1114 $this->questionswrapperwritten
= false;
1118 * Populates the current question category data
1120 * Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually
1121 * called twice for both halves of the data. We merge them here into the currentcategory array.
1123 public function process_question_category($data, $raw) {
1124 $this->currentcategory
= array_merge($this->currentcategory
, $data);
1125 $this->currentcategoryraw
= array_merge($this->currentcategoryraw
, $raw);
1129 * Inject the context related information into the current category
1131 public function process_question_category_context($data) {
1133 switch ($data['level']) {
1135 $this->currentcategory
['contextid'] = $this->converter
->get_contextid(CONTEXT_MODULE
, $data['instance']);
1136 $this->currentcategory
['contextlevel'] = CONTEXT_MODULE
;
1137 $this->currentcategory
['contextinstanceid'] = $data['instance'];
1140 $originalcourseinfo = $this->converter
->get_stash('original_course_info');
1141 $originalcourseid = $originalcourseinfo['original_course_id'];
1142 $this->currentcategory
['contextid'] = $this->converter
->get_contextid(CONTEXT_COURSE
);
1143 $this->currentcategory
['contextlevel'] = CONTEXT_COURSE
;
1144 $this->currentcategory
['contextinstanceid'] = $originalcourseid;
1146 case 'coursecategory':
1147 // this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance
1148 // of the course category (1 = parent category, 2 = grand-parent category etc). We pretend
1149 // that this level*10 is the id of that category and create an artifical contextid for it
1150 $this->currentcategory
['contextid'] = $this->converter
->get_contextid(CONTEXT_COURSECAT
, $data['coursecategorylevel'] * 10);
1151 $this->currentcategory
['contextlevel'] = CONTEXT_COURSECAT
;
1152 $this->currentcategory
['contextinstanceid'] = $data['coursecategorylevel'] * 10;
1155 $this->currentcategory
['contextid'] = $this->converter
->get_contextid(CONTEXT_SYSTEM
);
1156 $this->currentcategory
['contextlevel'] = CONTEXT_SYSTEM
;
1157 $this->currentcategory
['contextinstanceid'] = 0;
1163 * Writes the common <question> data and re-dispateches the whole grouped
1164 * <QUESTION> data to the qtype for appending its qtype specific data processing
1166 * @param array $data
1170 public function process_question(array $data, array $raw) {
1173 // firstly make sure that the category data and the <questions> wrapper are written
1174 // note that because of MDL-27693 we can't use {@link self::process_question_category()}
1175 // and {@link self::on_questions_start()} to do so
1177 if (empty($this->currentcategorywritten
)) {
1178 $this->xmlwriter
->begin_tag('question_category', array('id' => $this->currentcategory
['id']));
1179 foreach ($this->currentcategory
as $name => $value) {
1180 if ($name === 'id') {
1183 $this->xmlwriter
->full_tag($name, $value);
1185 $this->currentcategorywritten
= true;
1188 if (empty($this->questionswrapperwritten
)) {
1189 $this->xmlwriter
->begin_tag('questions');
1190 $this->questionswrapperwritten
= true;
1193 $qtype = $data['qtype'];
1195 // replay the upgrade step 2008050700 {@see question_fix_random_question_parents()}
1196 if ($qtype == 'random' and $data['parent'] <> $data['id']) {
1197 $data['parent'] = $data['id'];
1200 // replay the upgrade step 2010080900 and part of 2010080901
1201 $data['generalfeedbackformat'] = $data['questiontextformat'];
1202 $data['oldquestiontextformat'] = $data['questiontextformat'];
1204 if ($CFG->texteditors
!== 'textarea') {
1205 $data['questiontext'] = text_to_html($data['questiontext'], false, false, true);
1206 $data['questiontextformat'] = FORMAT_HTML
;
1207 $data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true);
1208 $data['generalfeedbackformat'] = FORMAT_HTML
;
1211 // Migrate files in questiontext.
1212 $this->fileman
->contextid
= $this->currentcategory
['contextid'];
1213 $this->fileman
->component
= 'question';
1214 $this->fileman
->filearea
= 'questiontext';
1215 $this->fileman
->itemid
= $data['id'];
1216 $data['questiontext'] = moodle1_converter
::migrate_referenced_files($data['questiontext'], $this->fileman
);
1218 // Migrate files in generalfeedback.
1219 $this->fileman
->filearea
= 'generalfeedback';
1220 $data['generalfeedback'] = moodle1_converter
::migrate_referenced_files($data['generalfeedback'], $this->fileman
);
1222 // replay the upgrade step 2010080901 - updating question image
1223 if (!empty($data['image'])) {
1224 if (core_text
::substr(core_text
::strtolower($data['image']), 0, 7) == 'http://') {
1225 // it is a link, appending to existing question text
1226 $data['questiontext'] .= ' <img src="' . $data['image'] . '" />';
1229 // it is a file in course_files
1230 $filename = basename($data['image']);
1231 $filepath = dirname($data['image']);
1232 if (empty($filepath) or $filepath == '.' or $filepath == '/') {
1236 $filepath = '/'.trim($filepath, './@#$ ').'/';
1239 if (file_exists($this->converter
->get_tempdir_path().'/course_files'.$filepath.$filename)) {
1240 $this->fileman
->contextid
= $this->currentcategory
['contextid'];
1241 $this->fileman
->component
= 'question';
1242 $this->fileman
->filearea
= 'questiontext';
1243 $this->fileman
->itemid
= $data['id'];
1244 $this->fileman
->migrate_file('course_files'.$filepath.$filename, '/', $filename);
1245 // note this is slightly different from the upgrade code as we put the file into the
1246 // root folder here. this makes our life easier as we do not need to create all the
1247 // directories within the specified filearea/itemid
1248 $data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
1251 $this->log('question file not found', backup
::LOG_WARNING
, array($data['id'], $filepath.$filename));
1255 unset($data['image']);
1257 // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
1258 $data['defaultmark'] = $data['defaultgrade'];
1260 // write the common question data
1261 $this->xmlwriter
->begin_tag('question', array('id' => $data['id']));
1263 'parent', 'name', 'questiontext', 'questiontextformat',
1264 'generalfeedback', 'generalfeedbackformat', 'defaultmark',
1265 'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
1266 'timecreated', 'timemodified', 'createdby', 'modifiedby'
1268 if (!array_key_exists($fieldname, $data)) {
1269 throw new moodle1_convert_exception('missing_common_question_field', $fieldname);
1271 $this->xmlwriter
->full_tag($fieldname, $data[$fieldname]);
1273 // unless we know that the given qtype does not append any own structures,
1274 // give the handler a chance to do so now
1275 if (!in_array($qtype, array('description', 'random'))) {
1276 $handler = $this->get_qtype_handler($qtype);
1277 if ($handler === false) {
1278 $this->log('question type converter not found', backup
::LOG_ERROR
, $qtype);
1281 $this->xmlwriter
->begin_tag('plugin_qtype_'.$qtype.'_question');
1282 $handler->use_xml_writer($this->xmlwriter
);
1283 $handler->process_question($data, $raw);
1284 $this->xmlwriter
->end_tag('plugin_qtype_'.$qtype.'_question');
1288 $this->xmlwriter
->end_tag('question');
1292 * Closes the questions wrapper
1294 public function on_questions_end() {
1295 if ($this->questionswrapperwritten
) {
1296 $this->xmlwriter
->end_tag('questions');
1301 * Closes the question_category and annotates the category id
1302 * so that it can be dumped into course/inforef.xml
1304 public function on_question_category_end() {
1305 // make sure that the category data were written by {@link self::process_question()}
1306 // if not, write it now. this may happen when the current category does not contain any
1307 // questions so the subpaths is missing completely
1308 if (empty($this->currentcategorywritten
)) {
1309 $this->write_xml('question_category', $this->currentcategory
, array('/question_category/id'));
1311 $this->xmlwriter
->end_tag('question_category');
1313 $this->converter
->set_stash('question_categories', $this->currentcategory
, $this->currentcategory
['id']);
1317 * Stops writing questions.xml
1319 public function on_question_categories_end() {
1320 $this->xmlwriter
->end_tag('question_categories');
1321 $this->close_xml_writer();
1325 * Provides access to the qtype handlers
1327 * Returns either list of all qtype handler instances (if passed '*') or a particular handler
1328 * for the given qtype or false if the qtype is not supported.
1330 * @throws moodle1_convert_exception
1331 * @param string $qtype the name of the question type or '*' for returning all
1332 * @return array|moodle1_qtype_handler|bool
1334 protected function get_qtype_handler($qtype) {
1336 if (is_null($this->qtypehandlers
)) {
1337 // initialize the list of qtype handler instances
1338 $this->qtypehandlers
= array();
1339 foreach (core_component
::get_plugin_list('qtype') as $qtypename => $qtypelocation) {
1340 $filename = $qtypelocation.'/backup/moodle1/lib.php';
1341 if (file_exists($filename)) {
1342 $classname = 'moodle1_qtype_'.$qtypename.'_handler';
1343 require_once($filename);
1344 if (!class_exists($classname)) {
1345 throw new moodle1_convert_exception('missing_handler_class', $classname);
1347 $this->log('registering handler', backup
::LOG_DEBUG
, $classname, 2);
1348 $this->qtypehandlers
[$qtypename] = new $classname($this, $qtypename);
1353 if ($qtype === '*') {
1354 return $this->qtypehandlers
;
1356 } else if (isset($this->qtypehandlers
[$qtype])) {
1357 return $this->qtypehandlers
[$qtype];
1367 * Handles the conversion of the scales included in the moodle.xml file
1369 class moodle1_scales_handler
extends moodle1_handler
{
1371 /** @var moodle1_file_manager instance used to convert question images */
1372 protected $fileman = null;
1377 public function get_paths() {
1379 new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
1381 'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
1383 'renamefields' => array(
1384 'scaletext' => 'scale',
1386 'addfields' => array(
1387 'descriptionformat' => 0,
1395 * Prepare the file manager for the files embedded in the scale description field
1397 public function on_scales_start() {
1398 $syscontextid = $this->converter
->get_contextid(CONTEXT_SYSTEM
);
1399 $this->fileman
= $this->converter
->get_file_manager($syscontextid, 'grade', 'scale');
1403 * This is executed every time we have one <SCALE> data available
1405 * @param array $data
1409 public function process_scale(array $data, array $raw) {
1412 // replay upgrade step 2009110400
1413 if ($CFG->texteditors
!== 'textarea') {
1414 $data['description'] = text_to_html($data['description'], false, false, true);
1415 $data['descriptionformat'] = FORMAT_HTML
;
1418 // convert course files embedded into the scale description field
1419 $this->fileman
->itemid
= $data['id'];
1420 $data['description'] = moodle1_converter
::migrate_referenced_files($data['description'], $this->fileman
);
1423 $this->converter
->set_stash('scales', $data, $data['id']);
1429 * Handles the conversion of the outcomes
1431 class moodle1_outcomes_handler
extends moodle1_xml_handler
{
1433 /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
1434 protected $fileman = null;
1439 public function get_paths() {
1441 new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
1443 'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
1445 'addfields' => array(
1446 'descriptionformat' => FORMAT_MOODLE
,
1454 * Prepares the file manager and starts writing outcomes.xml
1456 public function on_gradebook_grade_outcomes_start() {
1458 $syscontextid = $this->converter
->get_contextid(CONTEXT_SYSTEM
);
1459 $this->fileman
= $this->converter
->get_file_manager($syscontextid, 'grade', 'outcome');
1461 $this->open_xml_writer('outcomes.xml');
1462 $this->xmlwriter
->begin_tag('outcomes_definition');
1466 * Processes GRADE_OUTCOME tags progressively
1468 public function process_gradebook_grade_outcome(array $data, array $raw) {
1471 // replay the upgrade step 2009110400
1472 if ($CFG->texteditors
!== 'textarea') {
1473 $data['description'] = text_to_html($data['description'], false, false, true);
1474 $data['descriptionformat'] = FORMAT_HTML
;
1477 // convert course files embedded into the outcome description field
1478 $this->fileman
->itemid
= $data['id'];
1479 $data['description'] = moodle1_converter
::migrate_referenced_files($data['description'], $this->fileman
);
1481 // write the outcome data
1482 $this->write_xml('outcome', $data, array('/outcome/id'));
1488 * Closes outcomes.xml
1490 public function on_gradebook_grade_outcomes_end() {
1491 $this->xmlwriter
->end_tag('outcomes_definition');
1492 $this->close_xml_writer();
1498 * Handles the conversion of the gradebook structures in the moodle.xml file
1500 class moodle1_gradebook_handler
extends moodle1_xml_handler
{
1502 /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
1503 protected $categoryparent = array();
1508 public function get_paths() {
1510 new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
1511 new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
1513 'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
1515 'addfields' => array(
1516 'hidden' => 0, // upgrade step 2010011200
1520 new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'),
1521 new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'),
1526 * Initializes the in-memory structures
1528 * This should not be needed actually as the moodle.xml contains just one GRADEBOOK
1529 * element. But who knows - maybe someone will want to write a mass conversion
1530 * tool in the future (not me definitely ;-)
1532 public function on_gradebook_start() {
1533 $this->categoryparent
= array();
1537 * Processes one GRADE_LETTER data
1539 * In Moodle 1.9, all grade_letters are from course context only. Therefore
1542 public function process_gradebook_grade_letter(array $data, array $raw) {
1543 $this->converter
->set_stash('gradebook_gradeletter', $data, $data['id']);
1547 * Processes one GRADE_CATEGORY data
1549 public function process_gradebook_grade_category(array $data, array $raw) {
1550 $this->categoryparent
[$data['id']] = $data['parent'];
1551 $this->converter
->set_stash('gradebook_gradecategory', $data, $data['id']);
1555 * Processes one GRADE_ITEM data
1557 public function process_gradebook_grade_item(array $data, array $raw) {
1559 // here we use get_nextid() to get a nondecreasing sequence
1560 $data['sortorder'] = $this->converter
->get_nextid();
1562 if ($data['itemtype'] === 'mod') {
1563 return $this->process_mod_grade_item($data, $raw);
1565 } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
1566 return $this->process_nonmod_grade_item($data, $raw);
1569 $this->log('unsupported grade_item type', backup
::LOG_ERROR
, $data['itemtype']);
1574 * Processes one GRADE_ITEM of the type 'mod'
1576 protected function process_mod_grade_item(array $data, array $raw) {
1578 $stashname = 'gradebook_modgradeitem_'.$data['itemmodule'];
1579 $stashitemid = $data['iteminstance'];
1580 $gradeitems = $this->converter
->get_stash_or_default($stashname, $stashitemid, array());
1582 // typically there will be single item with itemnumber 0
1583 $gradeitems[$data['itemnumber']] = $data;
1585 $this->converter
->set_stash($stashname, $gradeitems, $stashitemid);
1591 * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
1593 protected function process_nonmod_grade_item(array $data, array $raw) {
1595 $stashname = 'gradebook_nonmodgradeitem';
1596 $stashitemid = $data['id'];
1597 $this->converter
->set_stash($stashname, $data, $stashitemid);
1605 public function on_gradebook_grade_item_grades_start() {
1609 * Writes the collected information into gradebook.xml
1611 public function on_gradebook_end() {
1613 $this->open_xml_writer('gradebook.xml');
1614 $this->xmlwriter
->begin_tag('gradebook');
1615 $this->write_grade_categories();
1616 $this->write_grade_items();
1617 $this->write_grade_letters();
1618 $this->xmlwriter
->end_tag('gradebook');
1619 $this->close_xml_writer();
1623 * Writes grade_categories
1625 protected function write_grade_categories() {
1627 $this->xmlwriter
->begin_tag('grade_categories');
1628 foreach ($this->converter
->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) {
1629 $gradecategory = $this->converter
->get_stash('gradebook_gradecategory', $gradecategoryid);
1630 $path = $this->calculate_category_path($gradecategoryid);
1631 $gradecategory['depth'] = count($path);
1632 $gradecategory['path'] = '/'.implode('/', $path).'/';
1633 $this->write_xml('grade_category', $gradecategory, array('/grade_category/id'));
1635 $this->xmlwriter
->end_tag('grade_categories');
1639 * Calculates the path to the grade_category
1641 * Moodle 1.9 backup does not store the grade_category's depth and path. This method is used
1642 * to repopulate this information using the $this->categoryparent values.
1644 * @param int $categoryid
1645 * @return array of ids including the categoryid
1647 protected function calculate_category_path($categoryid) {
1649 if (!array_key_exists($categoryid, $this->categoryparent
)) {
1650 throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
1653 $path = array($categoryid);
1654 $parent = $this->categoryparent
[$categoryid];
1655 while (!is_null($parent)) {
1656 array_unshift($path, $parent);
1657 $parent = $this->categoryparent
[$parent];
1658 if (in_array($parent, $path)) {
1659 throw new moodle1_convert_exception('circular_reference_in_categories_tree');
1667 * Writes grade_items
1669 protected function write_grade_items() {
1671 $this->xmlwriter
->begin_tag('grade_items');
1672 foreach ($this->converter
->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) {
1673 $gradeitem = $this->converter
->get_stash('gradebook_nonmodgradeitem', $gradeitemid);
1674 $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
1676 $this->xmlwriter
->end_tag('grade_items');
1680 * Writes grade_letters
1682 protected function write_grade_letters() {
1684 $this->xmlwriter
->begin_tag('grade_letters');
1685 foreach ($this->converter
->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) {
1686 $gradeletter = $this->converter
->get_stash('gradebook_gradeletter', $gradeletterid);
1687 $this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id'));
1689 $this->xmlwriter
->end_tag('grade_letters');
1695 * Shared base class for activity modules, blocks and qtype handlers
1697 abstract class moodle1_plugin_handler
extends moodle1_xml_handler
{
1700 protected $plugintype;
1703 protected $pluginname;
1706 * @param moodle1_converter $converter the converter that requires us
1707 * @param string $plugintype
1708 * @param string $pluginname
1710 public function __construct(moodle1_converter
$converter, $plugintype, $pluginname) {
1712 parent
::__construct($converter);
1713 $this->plugintype
= $plugintype;
1714 $this->pluginname
= $pluginname;
1718 * Returns the normalized name of the plugin, eg mod_workshop
1722 public function get_component_name() {
1723 return $this->plugintype
.'_'.$this->pluginname
;
1729 * Base class for all question type handlers
1731 abstract class moodle1_qtype_handler
extends moodle1_plugin_handler
{
1733 /** @var moodle1_question_bank_handler */
1734 protected $qbankhandler;
1737 * Returns the list of paths within one <QUESTION> that this qtype needs to have included
1738 * in the grouped question structure
1740 * @return array of strings
1742 public function get_question_subpaths() {
1747 * Gives the qtype handler a chance to write converted data into questions.xml
1749 * @param array $data grouped question data
1750 * @param array $raw grouped raw QUESTION data
1752 public function process_question(array $data, array $raw) {
1756 * Converts the answers and writes them into the questions.xml
1758 * The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
1760 * @param array $answers as parsed by the grouped parser in moodle.xml
1761 * @param string $qtype containing the answers
1763 protected function write_answers(array $answers, $qtype) {
1765 $this->xmlwriter
->begin_tag('answers');
1766 foreach ($answers as $elementname => $elements) {
1767 foreach ($elements as $element) {
1768 $answer = $this->convert_answer($element, $qtype);
1769 // Migrate images in answertext.
1770 if ($answer['answerformat'] == FORMAT_HTML
) {
1771 $answer['answertext'] = $this->migrate_files($answer['answertext'], 'question', 'answer', $answer['id']);
1773 // Migrate images in feedback.
1774 if ($answer['feedbackformat'] == FORMAT_HTML
) {
1775 $answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']);
1777 $this->write_xml('answer', $answer, array('/answer/id'));
1780 $this->xmlwriter
->end_tag('answers');
1784 * Migrate files belonging to one qtype plugin text field.
1786 * @param array $text the html fragment containing references to files
1787 * @param string $component the component for restored files
1788 * @param string $filearea the file area for restored files
1789 * @param int $itemid the itemid for restored files
1791 * @return string the text for this field, after files references have been processed
1793 protected function migrate_files($text, $component, $filearea, $itemid) {
1794 $context = $this->qbankhandler
->get_current_category_context();
1795 $fileman = $this->qbankhandler
->get_file_manager();
1796 $fileman->contextid
= $context['contextid'];
1797 $fileman->component
= $component;
1798 $fileman->filearea
= $filearea;
1799 $fileman->itemid
= $itemid;
1800 $text = moodle1_converter
::migrate_referenced_files($text, $fileman);
1805 * Writes the grouped numerical_units structure
1807 * @param array $numericalunits
1809 protected function write_numerical_units(array $numericalunits) {
1811 $this->xmlwriter
->begin_tag('numerical_units');
1812 foreach ($numericalunits as $elementname => $elements) {
1813 foreach ($elements as $element) {
1814 $element['id'] = $this->converter
->get_nextid();
1815 $this->write_xml('numerical_unit', $element, array('/numerical_unit/id'));
1818 $this->xmlwriter
->end_tag('numerical_units');
1822 * Writes the numerical_options structure
1824 * @see get_default_numerical_options()
1825 * @param array $numericaloption
1827 protected function write_numerical_options(array $numericaloption) {
1829 $this->xmlwriter
->begin_tag('numerical_options');
1830 if (!empty($numericaloption)) {
1831 $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
1833 $this->xmlwriter
->end_tag('numerical_options');
1837 * Returns default numerical_option structure
1839 * This structure is not present in moodle.xml, we create a new artificial one here.
1841 * @see write_numerical_options()
1842 * @param int $oldquestiontextformat
1845 protected function get_default_numerical_options($oldquestiontextformat, $units) {
1848 // replay the upgrade step 2009100100 - new table
1850 'id' => $this->converter
->get_nextid(),
1851 'instructions' => null,
1852 'instructionsformat' => 0,
1855 'unitgradingtype' => 0,
1856 'unitpenalty' => 0.1
1859 // replay the upgrade step 2009100101
1860 if ($CFG->texteditors
!== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE
) {
1861 $options['instructionsformat'] = FORMAT_HTML
;
1863 $options['instructionsformat'] = $oldquestiontextformat;
1866 // Set a good default, depending on whether there are any units defined.
1867 if (empty($units)) {
1868 $options['showunits'] = 3;
1875 * Writes the dataset_definitions structure
1877 * @param array $datasetdefinitions array of dataset_definition structures
1879 protected function write_dataset_definitions(array $datasetdefinitions) {
1881 $this->xmlwriter
->begin_tag('dataset_definitions');
1882 foreach ($datasetdefinitions as $datasetdefinition) {
1883 $this->xmlwriter
->begin_tag('dataset_definition', array('id' => $this->converter
->get_nextid()));
1884 foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) {
1885 $this->xmlwriter
->full_tag($element, $datasetdefinition[$element]);
1887 $this->xmlwriter
->begin_tag('dataset_items');
1888 if (!empty($datasetdefinition['dataset_items']['dataset_item'])) {
1889 foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) {
1890 $datasetitem['id'] = $this->converter
->get_nextid();
1891 $this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id'));
1894 $this->xmlwriter
->end_tag('dataset_items');
1895 $this->xmlwriter
->end_tag('dataset_definition');
1897 $this->xmlwriter
->end_tag('dataset_definitions');
1900 /// implementation details follow //////////////////////////////////////////
1902 public function __construct(moodle1_question_bank_handler
$qbankhandler, $qtype) {
1904 parent
::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
1905 $this->qbankhandler
= $qbankhandler;
1909 * @see self::get_question_subpaths()
1911 final public function get_paths() {
1912 throw new moodle1_convert_exception('qtype_handler_get_paths');
1916 * Question type handlers cannot open the xml_writer
1918 final protected function open_xml_writer($filename) {
1919 throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1923 * Question type handlers cannot close the xml_writer
1925 final protected function close_xml_writer() {
1926 throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1930 * Provides a xml_writer instance to this qtype converter
1932 * @param xml_writer $xmlwriter
1934 public function use_xml_writer(xml_writer
$xmlwriter) {
1935 $this->xmlwriter
= $xmlwriter;
1939 * Converts <ANSWER> structure into the new <answer> one
1941 * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
1943 * @param array $old the parsed answer array in moodle.xml
1944 * @param string $qtype the question type the answer is part of
1947 private function convert_answer(array $old, $qtype) {
1951 $new['id'] = $old['id'];
1952 $new['answertext'] = $old['answer_text'];
1953 $new['answerformat'] = 0; // upgrade step 2010080900
1954 $new['fraction'] = $old['fraction'];
1955 $new['feedback'] = $old['feedback'];
1956 $new['feedbackformat'] = 0; // upgrade step 2010080900
1958 // replay upgrade step 2010080901
1959 if ($qtype !== 'multichoice') {
1960 $new['answerformat'] = FORMAT_PLAIN
;
1962 $new['answertext'] = text_to_html($new['answertext'], false, false, true);
1963 $new['answerformat'] = FORMAT_HTML
;
1966 if ($CFG->texteditors
!== 'textarea') {
1967 if ($qtype == 'essay') {
1968 $new['feedback'] = text_to_html($new['feedback'], false, false, true);
1970 $new['feedbackformat'] = FORMAT_HTML
;
1973 $new['feedbackformat'] = FORMAT_MOODLE
;
1982 * Base class for activity module handlers
1984 abstract class moodle1_mod_handler
extends moodle1_plugin_handler
{
1987 * Returns the name of the module, eg. 'forum'
1991 public function get_modname() {
1992 return $this->pluginname
;
1996 * Returns course module information for the given instance id
1998 * The information for this instance id has been stashed by
1999 * {@link moodle1_course_outline_handler::process_course_module()}
2001 * @param int $instance the module instance id
2002 * @param string $modname the module type, defaults to $this->pluginname
2005 protected function get_cminfo($instance, $modname = null) {
2007 if (is_null($modname)) {
2008 $modname = $this->pluginname
;
2010 return $this->converter
->get_stash('cminfo_'.$modname, $instance);
2016 * Base class for all modules that are successors of the 1.9 resource module
2018 abstract class moodle1_resource_successor_handler
extends moodle1_mod_handler
{
2021 * Resource successors do not attach to paths themselves, they are called explicitely
2022 * by moodle1_mod_resource_handler
2026 final public function get_paths() {
2031 * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
2033 * Called by {@link moodle1_mod_resource_handler::process_resource()}
2035 * @param array $data pre-cooked legacy resource data
2036 * @param array $raw raw legacy resource data
2038 public function process_legacy_resource(array $data, array $raw = null) {
2042 * Called when the parses reaches the end </MOD> resource tag
2044 * @param array $data the data returned by {@link self::process_resource} or just pre-cooked
2046 public function on_legacy_resource_end(array $data) {
2051 * Base class for block handlers
2053 abstract class moodle1_block_handler
extends moodle1_plugin_handler
{
2055 public function get_paths() {
2056 $blockname = strtoupper($this->pluginname
);
2058 new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
2062 public function process_block(array $data) {
2063 $newdata = $this->convert_common_block_data($data);
2065 $this->write_block_xml($newdata, $data);
2066 $this->write_inforef_xml($newdata, $data);
2067 $this->write_roles_xml($newdata, $data);
2072 protected function convert_common_block_data(array $olddata) {
2075 $newdata['blockname'] = $olddata['name'];
2076 $newdata['parentcontextid'] = $this->converter
->get_contextid(CONTEXT_COURSE
, 0);
2077 $newdata['showinsubcontexts'] = 0;
2078 $newdata['pagetypepattern'] = $olddata['pagetype'].='-*';
2079 $newdata['subpagepattern'] = null;
2080 $newdata['defaultregion'] = ($olddata['position']=='l')?
'side-pre':'side-post';
2081 $newdata['defaultweight'] = $olddata['weight'];
2082 $newdata['configdata'] = $this->convert_configdata($olddata);
2087 protected function convert_configdata(array $olddata) {
2088 return $olddata['configdata'];
2091 protected function write_block_xml($newdata, $data) {
2092 $contextid = $this->converter
->get_contextid(CONTEXT_BLOCK
, $data['id']);
2094 $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/block.xml");
2095 $this->xmlwriter
->begin_tag('block', array('id' => $data['id'], 'contextid' => $contextid));
2097 foreach ($newdata as $field => $value) {
2098 $this->xmlwriter
->full_tag($field, $value);
2101 $this->xmlwriter
->begin_tag('block_positions');
2102 $this->xmlwriter
->begin_tag('block_position', array('id' => 1));
2103 $this->xmlwriter
->full_tag('contextid', $newdata['parentcontextid']);
2104 $this->xmlwriter
->full_tag('pagetype', $data['pagetype']);
2105 $this->xmlwriter
->full_tag('subpage', '');
2106 $this->xmlwriter
->full_tag('visible', $data['visible']);
2107 $this->xmlwriter
->full_tag('region', $newdata['defaultregion']);
2108 $this->xmlwriter
->full_tag('weight', $newdata['defaultweight']);
2109 $this->xmlwriter
->end_tag('block_position');
2110 $this->xmlwriter
->end_tag('block_positions');
2111 $this->xmlwriter
->end_tag('block');
2112 $this->close_xml_writer();
2115 protected function write_inforef_xml($newdata, $data) {
2116 $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
2117 $this->xmlwriter
->begin_tag('inforef');
2118 // Subclasses may provide inforef contents if needed
2119 $this->xmlwriter
->end_tag('inforef');
2120 $this->close_xml_writer();
2123 protected function write_roles_xml($newdata, $data) {
2124 // This is an empty shell, as the moodle1 converter doesn't handle user data.
2125 $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/roles.xml");
2126 $this->xmlwriter
->begin_tag('roles');
2127 $this->xmlwriter
->full_tag('role_overrides', '');
2128 $this->xmlwriter
->full_tag('role_assignments', '');
2129 $this->xmlwriter
->end_tag('roles');
2130 $this->close_xml_writer();
2136 * Base class for block generic handler
2138 class moodle1_block_generic_handler
extends moodle1_block_handler
{
2143 * Base class for the activity modules' subplugins
2145 abstract class moodle1_submod_handler
extends moodle1_plugin_handler
{
2147 /** @var moodle1_mod_handler */
2148 protected $parenthandler;
2151 * @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of
2152 * @param string $subplugintype the type of the subplugin
2153 * @param string $subpluginname the name of the subplugin
2155 public function __construct(moodle1_mod_handler
$parenthandler, $subplugintype, $subpluginname) {
2156 $this->parenthandler
= $parenthandler;
2157 parent
::__construct($parenthandler->converter
, $subplugintype, $subpluginname);
2161 * Activity module subplugins can't declare any paths to handle
2163 * The paths must be registered by the parent module and then re-dispatched to the
2164 * relevant subplugins for eventual processing.
2166 * @return array empty array
2168 final public function get_paths() {