Merge branch 'MDL-63730-34-enfix' of git://github.com/mudrd8mz/moodle into MOODLE_34_...
[moodle.git] / backup / converter / moodle1 / handlerlib.php
blobbbdfd7f97fbce9f50c60f8bd2a8ff331686b62e3
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
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.
9 //
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/>.
18 /**
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
25 * @subpackage moodle1
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');
36 /**
37 * Handlers factory class
39 abstract class moodle1_handlers_factory {
41 /**
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) {
47 $handlers = array(
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));
69 return $handlers;
72 /// public API ends here ///////////////////////////////////////////////////
74 /**
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) {
84 global $CFG;
86 $handlers = array();
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";
95 } else {
96 continue;
99 if (!class_exists($handlerclass)) {
100 throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
102 $handlers[] = new $handlerclass($converter, $type, $name);
104 return $handlers;
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
161 * @return void
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
187 * @return void
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()}
201 * @return bool
203 protected function has_xml_writer() {
205 if ($this->xmlwriter instanceof xml_writer) {
206 return true;
207 } else {
208 return false;
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;
234 unset($data[$name]);
239 // reorder the $data so that all sub-branches are at the end (needed by our parser)
240 $leaves = array();
241 $branches = array();
242 foreach ($data as $name => $value) {
243 if (is_array($value)) {
244 $branches[$name] = $value;
245 } else {
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.'/');
257 } else {
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);
281 if ($existed) {
282 return true;
285 if ($rootelement !== false) {
286 $this->open_xml_writer($filename);
287 $this->write_xml($rootelement, $content);
288 $this->close_xml_writer();
291 return false;
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() {
319 global $CFG;
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']);
344 } else {
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']);
351 } else {
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',
416 'value' => 1);
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?
445 'anonymize' => 0,
446 'role_assignments' => 0,
447 'activities' => 1,
448 'blocks' => 1,
449 'filters' => 0,
450 'comments' => 0,
451 'userscompletion' => 0,
452 'logs' => 0,
453 'grade_histories' => 0,
455 unset($backupinfo);
456 foreach ($rootsettings as $name => $value) {
457 $this->write_xml('setting', array(
458 'level' => 'root',
459 'name' => $name,
460 'value' => $value));
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 ////////////////////////////////////////////////////////////////////////
484 // write files.xml
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 ////////////////////////////////////////////////////////////////////////
495 // write scales.xml
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));
519 // todo site files
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() {
574 $ids = array();
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() {
601 return array(
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) {
635 global $CFG;
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;
642 } else {
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();
669 /** @var array */
670 protected $category;
672 public function get_paths() {
673 return array(
674 new convert_path(
675 'course_header', '/MOODLE_BACKUP/COURSE/HEADER',
676 array(
677 'newfields' => array(
678 'summaryformat' => 1,
679 'legacyfiles' => 2,
680 'requested' => 0, // @todo not really new, but maybe never backed up?
681 'restrictmodules' => 0,
682 'enablecompletion' => 0,
683 'completionstartonenrol' => 0,
684 'completionnotify' => 0,
685 'tags' => array(),
686 'allowed_modules' => array(),
688 'dropfields' => array(
689 'roles_overrides',
690 'roles_assignments',
691 'cost',
692 'currancy',
693 'defaultrole',
694 'enrol',
695 'enrolenddate',
696 'enrollable',
697 'enrolperiod',
698 'enrolstartdate',
699 'expirynotify',
700 'expirythreshold',
701 'guest',
702 'notifystudents',
703 'password',
704 'student',
705 'students',
706 'teacher',
707 'teachers',
708 'metacourse',
712 new convert_path(
713 'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
714 array(
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
742 $info = array(
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());
759 // write course.xml
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() {
782 return array(
783 new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'),
784 new convert_path(
785 'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION',
786 array(
787 'newfields' => array(
788 'name' => null,
789 'summaryformat' => 1,
790 'sequence' => null,
794 new convert_path(
795 'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD',
796 array(
797 'newfields' => array(
798 'completion' => 0,
799 'completiongradeitemnumber' => null,
800 'completionview' => 0,
801 'completionexpected' => 0,
802 'availability' => null,
803 'visibleold' => 1,
804 'showdescription' => 0,
806 'dropfields' => array(
807 'instance',
808 'roles_overrides',
809 'roles_assignments',
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) {
831 global $CFG;
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)) {
836 return;
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'],
845 'title' => null
848 // add the course module id into the section's sequence
849 if (is_null($this->currentsection['sequence'])) {
850 $this->currentsection['sequence'] = $data['id'];
851 } else {
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
863 // host...
864 $versionfile = $CFG->dirroot.'/mod/'.$data['modulename'].'/version.php';
865 if (file_exists($versionfile)) {
866 $plugin = new stdClass();
867 $plugin->version = null;
868 $module = $plugin;
869 include($versionfile);
870 $data['version'] = $plugin->version;
871 } else {
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'];
932 // write module.xml
933 $this->open_xml_writer($directory.'/module.xml');
934 $this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
935 $this->close_xml_writer();
937 // write grades.xml
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() {
969 return array(
970 new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
971 new convert_path(
972 'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
973 array(
974 'newfields' => array(
975 'description' => '',
976 'sortorder' => 0,
977 'archetype' => ''
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());
1010 } else {
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() {
1065 $paths = array(
1066 new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
1067 new convert_path(
1068 'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
1069 array(
1070 'newfields' => array(
1071 'infoformat' => 0
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);
1093 return $paths;
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']) {
1134 case 'module':
1135 $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']);
1136 $this->currentcategory['contextlevel'] = CONTEXT_MODULE;
1137 $this->currentcategory['contextinstanceid'] = $data['instance'];
1138 break;
1139 case 'course':
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;
1145 break;
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;
1153 break;
1154 case 'system':
1155 $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM);
1156 $this->currentcategory['contextlevel'] = CONTEXT_SYSTEM;
1157 $this->currentcategory['contextinstanceid'] = 0;
1158 break;
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
1167 * @param array $raw
1168 * @return array
1170 public function process_question(array $data, array $raw) {
1171 global $CFG;
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') {
1181 continue;
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'] . '" />';
1228 } else {
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 == '/') {
1233 $filepath = '/';
1234 } else {
1235 // append /
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 . '" />';
1250 } else {
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']));
1262 foreach (array(
1263 'parent', 'name', 'questiontext', 'questiontextformat',
1264 'generalfeedback', 'generalfeedbackformat', 'defaultmark',
1265 'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
1266 'timecreated', 'timemodified', 'createdby', 'modifiedby'
1267 ) as $fieldname) {
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);
1280 } else {
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'));
1310 } else {
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];
1359 } else {
1360 return false;
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;
1375 * Registers paths
1377 public function get_paths() {
1378 return array(
1379 new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
1380 new convert_path(
1381 'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
1382 array(
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
1406 * @param array $raw
1407 * @return array
1409 public function process_scale(array $data, array $raw) {
1410 global $CFG;
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);
1422 // stash the scale
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;
1437 * Registers paths
1439 public function get_paths() {
1440 return array(
1441 new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
1442 new convert_path(
1443 'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
1444 array(
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) {
1469 global $CFG;
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'));
1484 return $data;
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();
1506 * Registers paths
1508 public function get_paths() {
1509 return array(
1510 new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
1511 new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
1512 new convert_path(
1513 'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
1514 array(
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
1540 * we put them here.
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);
1568 } else {
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);
1587 return $data;
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);
1599 return $data;
1603 * @todo
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');
1663 return $path;
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 {
1699 /** @var string */
1700 protected $plugintype;
1702 /** @var string */
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
1720 * @return string
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() {
1743 return array();
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);
1801 return $text;
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
1843 * @return array
1845 protected function get_default_numerical_options($oldquestiontextformat, $units) {
1846 global $CFG;
1848 // replay the upgrade step 2009100100 - new table
1849 $options = array(
1850 'id' => $this->converter->get_nextid(),
1851 'instructions' => null,
1852 'instructionsformat' => 0,
1853 'showunits' => 0,
1854 'unitsleft' => 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;
1862 } else {
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;
1871 return $options;
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
1945 * @return array
1947 private function convert_answer(array $old, $qtype) {
1948 global $CFG;
1950 $new = array();
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;
1961 } else {
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;
1972 } else {
1973 $new['feedbackformat'] = FORMAT_MOODLE;
1976 return $new;
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'
1989 * @return string
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
2003 * @return int
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
2024 * @return array
2026 final public function get_paths() {
2027 return array();
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);
2057 return array(
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);
2069 return $data;
2072 protected function convert_common_block_data(array $olddata) {
2073 $newdata = array();
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);
2084 return $newdata;
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() {
2169 return array();