Merge branch 'MDL-29058' of git://github.com/timhunt/moodle
[moodle.git] / backup / converter / moodle1 / handlerlib.php
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.
14 //
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
20  *
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.
23  *
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
28  */
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
38  */
39 abstract class moodle1_handlers_factory {
41     /**
42      * @param moodle1_converter the converter requesting the converters
43      * @return list of all available conversion handlers
44      */
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),
57         );
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));
66             }
67         }
69         return $handlers;
70     }
72     /// public API ends here ///////////////////////////////////////////////////
74     /**
75      * Runs through all plugins of a specific type and instantiates their handlers
76      *
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
82      */
83     protected static function get_plugin_handlers($type, moodle1_converter $converter) {
84         global $CFG;
86         $handlers = array();
87         $plugins = 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                 continue;
93             }
94             require_once($handlerfile);
96             if (!class_exists($handlerclass)) {
97                 throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
98             }
99             $handlers[] = new $handlerclass($converter, $type, $name);
100         }
101         return $handlers;
102     }
106 /**
107  * Base backup conversion handler
108  */
109 abstract class moodle1_handler implements loggable {
111     /** @var moodle1_converter */
112     protected $converter;
114     /**
115      * @param moodle1_converter $converter the converter that requires us
116      */
117     public function __construct(moodle1_converter $converter) {
118         $this->converter = $converter;
119     }
121     /**
122      * @return moodle1_converter the converter that required this handler
123      */
124     public function get_converter() {
125         return $this->converter;
126     }
128     /**
129      * Log a message using the converter's logging mechanism
130      *
131      * @param string $message message text
132      * @param int $level message level {@example backup::LOG_WARNING}
133      * @param null|mixed $a additional information
134      * @param null|int $depth the message depth
135      * @param bool $display whether the message should be sent to the output, too
136      */
137     public function log($message, $level, $a = null, $depth = null, $display = false) {
138         $this->converter->log($message, $level, $a, $depth, $display);
139     }
143 /**
144  * Base backup conversion handler that generates an XML file
145  */
146 abstract class moodle1_xml_handler extends moodle1_handler {
148     /** @var null|string the name of file we are writing to */
149     protected $xmlfilename;
151     /** @var null|xml_writer */
152     protected $xmlwriter;
154     /**
155      * Opens the XML writer - after calling, one is free to use $xmlwriter
156      *
157      * @param string $filename XML file name to write into
158      * @return void
159      */
160     protected function open_xml_writer($filename) {
162         if (!is_null($this->xmlfilename) and $filename !== $this->xmlfilename) {
163             throw new moodle1_convert_exception('xml_writer_already_opened_for_other_file', $this->xmlfilename);
164         }
166         if (!$this->xmlwriter instanceof xml_writer) {
167             $this->xmlfilename = $filename;
168             $fullpath  = $this->converter->get_workdir_path() . '/' . $this->xmlfilename;
169             $directory = pathinfo($fullpath, PATHINFO_DIRNAME);
171             if (!check_dir_exists($directory)) {
172                 throw new moodle1_convert_exception('unable_create_target_directory', $directory);
173             }
174             $this->xmlwriter = new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer());
175             $this->xmlwriter->start();
176         }
177     }
179     /**
180      * Close the XML writer
181      *
182      * At the moment, the caller must close all tags before calling
183      *
184      * @return void
185      */
186     protected function close_xml_writer() {
187         if ($this->xmlwriter instanceof xml_writer) {
188             $this->xmlwriter->stop();
189         }
190         unset($this->xmlwriter);
191         $this->xmlwriter = null;
192         $this->xmlfilename = null;
193     }
195     /**
196      * Checks if the XML writer has been opened by {@link self::open_xml_writer()}
197      *
198      * @return bool
199      */
200     protected function has_xml_writer() {
202         if ($this->xmlwriter instanceof xml_writer) {
203             return true;
204         } else {
205             return false;
206         }
207     }
209     /**
210      * Writes the given XML tree data into the currently opened file
211      *
212      * @param string $element the name of the root element of the tree
213      * @param array $data the associative array of data to write
214      * @param array $attribs list of additional fields written as attributes instead of nested elements
215      * @param string $parent used internally during the recursion, do not set yourself
216      */
217     protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
219         if (!$this->has_xml_writer()) {
220             throw new moodle1_convert_exception('write_xml_without_writer');
221         }
223         $mypath    = $parent . $element;
224         $myattribs = array();
226         // detect properties that should be rendered as element's attributes instead of children
227         foreach ($data as $name => $value) {
228             if (!is_array($value)) {
229                 if (in_array($mypath . '/' . $name, $attribs)) {
230                     $myattribs[$name] = $value;
231                     unset($data[$name]);
232                 }
233             }
234         }
236         // reorder the $data so that all sub-branches are at the end (needed by our parser)
237         $leaves   = array();
238         $branches = array();
239         foreach ($data as $name => $value) {
240             if (is_array($value)) {
241                 $branches[$name] = $value;
242             } else {
243                 $leaves[$name] = $value;
244             }
245         }
246         $data = array_merge($leaves, $branches);
248         $this->xmlwriter->begin_tag($element, $myattribs);
250         foreach ($data as $name => $value) {
251             if (is_array($value)) {
252                 // recursively call self
253                 $this->write_xml($name, $value, $attribs, $mypath.'/');
254             } else {
255                 $this->xmlwriter->full_tag($name, $value);
256             }
257         }
259         $this->xmlwriter->end_tag($element);
260     }
262     /**
263      * Makes sure that a new XML file exists, or creates it itself
264      *
265      * This is here so we can check that all XML files that the restore process relies on have
266      * been created by an executed handler. If the file is not found, this method can create it
267      * using the given $rootelement as an empty root container in the file.
268      *
269      * @param string $filename relative file name like 'course/course.xml'
270      * @param string|bool $rootelement root element to use, false to not create the file
271      * @param array $content content of the root element
272      * @return bool true is the file existed, false if it did not
273      */
274     protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) {
276         $existed = file_exists($this->converter->get_workdir_path().'/'.$filename);
278         if ($existed) {
279             return true;
280         }
282         if ($rootelement !== false) {
283             $this->open_xml_writer($filename);
284             $this->write_xml($rootelement, $content);
285             $this->close_xml_writer();
286         }
288         return false;
289     }
293 /**
294  * Process the root element of the backup file
295  */
296 class moodle1_root_handler extends moodle1_xml_handler {
298     public function get_paths() {
299         return array(new convert_path('root_element', '/MOODLE_BACKUP'));
300     }
302     /**
303      * Converts course_files and site_files
304      */
305     public function on_root_element_start() {
307         // convert course files
308         $fileshandler = new moodle1_files_handler($this->converter);
309         $fileshandler->process();
310     }
312     /**
313      * This is executed at the end of the moodle.xml parsing
314      */
315     public function on_root_element_end() {
316         global $CFG;
318         // restore the stashes prepared by other handlers for us
319         $backupinfo         = $this->converter->get_stash('backup_info');
320         $originalcourseinfo = $this->converter->get_stash('original_course_info');
322         ////////////////////////////////////////////////////////////////////////
323         // write moodle_backup.xml
324         ////////////////////////////////////////////////////////////////////////
325         $this->open_xml_writer('moodle_backup.xml');
327         $this->xmlwriter->begin_tag('moodle_backup');
328         $this->xmlwriter->begin_tag('information');
330         // moodle_backup/information
331         $this->xmlwriter->full_tag('name', $backupinfo['name']);
332         $this->xmlwriter->full_tag('moodle_version', $backupinfo['moodle_version']);
333         $this->xmlwriter->full_tag('moodle_release', $backupinfo['moodle_release']);
334         $this->xmlwriter->full_tag('backup_version', $CFG->backup_version); // {@see restore_prechecks_helper::execute_prechecks}
335         $this->xmlwriter->full_tag('backup_release', $CFG->backup_release);
336         $this->xmlwriter->full_tag('backup_date', $backupinfo['date']);
337         // see the commit c0543b - all backups created in 1.9 and later declare the
338         // information or it is considered as false
339         if (isset($backupinfo['mnet_remoteusers'])) {
340             $this->xmlwriter->full_tag('mnet_remoteusers', $backupinfo['mnet_remoteusers']);
341         } else {
342             $this->xmlwriter->full_tag('mnet_remoteusers', false);
343         }
344         $this->xmlwriter->full_tag('original_wwwroot', $backupinfo['original_wwwroot']);
345         // {@see backup_general_helper::backup_is_samesite()}
346         if (isset($backupinfo['original_site_identifier_hash'])) {
347             $this->xmlwriter->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']);
348         }
349         $this->xmlwriter->full_tag('original_course_id', $originalcourseinfo['original_course_id']);
350         $this->xmlwriter->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']);
351         $this->xmlwriter->full_tag('original_course_shortname', $originalcourseinfo['original_course_shortname']);
352         $this->xmlwriter->full_tag('original_course_startdate', $originalcourseinfo['original_course_startdate']);
353         $this->xmlwriter->full_tag('original_system_contextid', $this->converter->get_contextid(CONTEXT_SYSTEM));
354         // note that even though we have original_course_contextid available, we regenerate the
355         // original course contextid using our helper method to be sure that the data are consistent
356         // within the MBZ file
357         $this->xmlwriter->full_tag('original_course_contextid', $this->converter->get_contextid(CONTEXT_COURSE));
359         // moodle_backup/information/details
360         $this->xmlwriter->begin_tag('details');
361         $this->write_xml('detail', array(
362             'backup_id'     => $this->converter->get_id(),
363             'type'          => backup::TYPE_1COURSE,
364             'format'        => backup::FORMAT_MOODLE,
365             'interactive'   => backup::INTERACTIVE_YES,
366             'mode'          => backup::MODE_CONVERTED,
367             'execution'     => backup::EXECUTION_INMEDIATE,
368             'executiontime' => 0,
369         ), array('/detail/backup_id'));
370         $this->xmlwriter->end_tag('details');
372         // moodle_backup/information/contents
373         $this->xmlwriter->begin_tag('contents');
375         // moodle_backup/information/contents/activities
376         $this->xmlwriter->begin_tag('activities');
377         $activitysettings = array();
378         foreach ($this->converter->get_stash('coursecontents') as $activity) {
379             $modinfo = $this->converter->get_stash('modinfo_'.$activity['modulename']);
380             $modinstance = $modinfo['instances'][$activity['instanceid']];
381             $this->write_xml('activity', array(
382                 'moduleid'      => $activity['cmid'],
383                 'sectionid'     => $activity['sectionid'],
384                 'modulename'    => $activity['modulename'],
385                 'title'         => $modinstance['name'],
386                 'directory'     => 'activities/'.$activity['modulename'].'_'.$activity['cmid']
387             ));
388             $activitysettings[] = array(
389                 'level'     => 'activity',
390                 'activity'  => $activity['modulename'].'_'.$activity['cmid'],
391                 'name'      => $activity['modulename'].'_'.$activity['cmid'].'_included',
392                 'value'     => (($modinfo['included'] === 'true' and $modinstance['included'] === 'true') ? 1 : 0));
393             $activitysettings[] = array(
394                 'level'     => 'activity',
395                 'activity'  => $activity['modulename'].'_'.$activity['cmid'],
396                 'name'      => $activity['modulename'].'_'.$activity['cmid'].'_userinfo',
397                 //'value'     => (($modinfo['userinfo'] === 'true' and $modinstance['userinfo'] === 'true') ? 1 : 0));
398                 'value'     => 0); // todo hardcoded non-userinfo for now
399         }
400         $this->xmlwriter->end_tag('activities');
402         // moodle_backup/information/contents/sections
403         $this->xmlwriter->begin_tag('sections');
404         $sectionsettings = array();
405         foreach ($this->converter->get_stash_itemids('sectioninfo') as $sectionid) {
406             $sectioninfo = $this->converter->get_stash('sectioninfo', $sectionid);
407             $sectionsettings[] = array(
408                 'level'     => 'section',
409                 'section'   => 'section_'.$sectionid,
410                 'name'      => 'section_'.$sectionid.'_included',
411                 'value'     => 1);
412             $sectionsettings[] = array(
413                 'level'     => 'section',
414                 'section'   => 'section_'.$sectionid,
415                 'name'      => 'section_'.$sectionid.'_userinfo',
416                 'value'     => 0); // @todo how to detect this from moodle.xml?
417             $this->write_xml('section', array(
418                 'sectionid' => $sectionid,
419                 'title'     => $sectioninfo['number'], // because the title is not available
420                 'directory' => 'sections/section_'.$sectionid));
421         }
422         $this->xmlwriter->end_tag('sections');
424         // moodle_backup/information/contents/course
425         $this->write_xml('course', array(
426             'courseid'  => $originalcourseinfo['original_course_id'],
427             'title'     => $originalcourseinfo['original_course_shortname'],
428             'directory' => 'course'));
429         unset($originalcourseinfo);
431         $this->xmlwriter->end_tag('contents');
433         // moodle_backup/information/settings
434         $this->xmlwriter->begin_tag('settings');
436         // fake backup root seetings
437         $rootsettings = array(
438             'filename'         => $backupinfo['name'],
439             'users'            => 0, // @todo how to detect this from moodle.xml?
440             'anonymize'        => 0,
441             'role_assignments' => 0,
442             'user_files'       => 0,
443             'activities'       => 1,
444             'blocks'           => 1,
445             'filters'          => 0,
446             'comments'         => 0,
447             'userscompletion'  => 0,
448             'logs'             => 0,
449             'grade_histories'  => 0,
450         );
451         unset($backupinfo);
452         foreach ($rootsettings as $name => $value) {
453             $this->write_xml('setting', array(
454                 'level' => 'root',
455                 'name'  => $name,
456                 'value' => $value));
457         }
458         unset($rootsettings);
460         // activity settings populated above
461         foreach ($activitysettings as $activitysetting) {
462             $this->write_xml('setting', $activitysetting);
463         }
464         unset($activitysettings);
466         // section settings populated above
467         foreach ($sectionsettings as $sectionsetting) {
468             $this->write_xml('setting', $sectionsetting);
469         }
470         unset($sectionsettings);
472         $this->xmlwriter->end_tag('settings');
474         $this->xmlwriter->end_tag('information');
475         $this->xmlwriter->end_tag('moodle_backup');
477         $this->close_xml_writer();
479         ////////////////////////////////////////////////////////////////////////
480         // write files.xml
481         ////////////////////////////////////////////////////////////////////////
482         $this->open_xml_writer('files.xml');
483         $this->xmlwriter->begin_tag('files');
484         foreach ($this->converter->get_stash_itemids('files') as $fileid) {
485             $this->write_xml('file', $this->converter->get_stash('files', $fileid), array('/file/id'));
486         }
487         $this->xmlwriter->end_tag('files');
488         $this->close_xml_writer('files.xml');
490         ////////////////////////////////////////////////////////////////////////
491         // write scales.xml
492         ////////////////////////////////////////////////////////////////////////
493         $this->open_xml_writer('scales.xml');
494         $this->xmlwriter->begin_tag('scales_definition');
495         foreach ($this->converter->get_stash_itemids('scales') as $scaleid) {
496             $this->write_xml('scale', $this->converter->get_stash('scales', $scaleid), array('/scale/id'));
497         }
498         $this->xmlwriter->end_tag('scales_definition');
499         $this->close_xml_writer('scales.xml');
501         ////////////////////////////////////////////////////////////////////////
502         // write course/inforef.xml
503         ////////////////////////////////////////////////////////////////////////
504         $this->open_xml_writer('course/inforef.xml');
505         $this->xmlwriter->begin_tag('inforef');
507         $this->xmlwriter->begin_tag('fileref');
508         // legacy course files
509         $fileids = $this->converter->get_stash('course_files_ids');
510         if (is_array($fileids)) {
511             foreach ($fileids as $fileid) {
512                 $this->write_xml('file', array('id' => $fileid));
513             }
514         }
515         // todo site files
516         // course summary files
517         $fileids = $this->converter->get_stash('course_summary_files_ids');
518         if (is_array($fileids)) {
519             foreach ($fileids as $fileid) {
520                 $this->write_xml('file', array('id' => $fileid));
521             }
522         }
523         $this->xmlwriter->end_tag('fileref');
525         $this->xmlwriter->begin_tag('question_categoryref');
526         foreach ($this->converter->get_stash_itemids('question_categories') as $questioncategoryid) {
527             $this->write_xml('question_category', array('id' => $questioncategoryid));
528         }
529         $this->xmlwriter->end_tag('question_categoryref');
531         $this->xmlwriter->end_tag('inforef');
532         $this->close_xml_writer();
534         // make sure that the files required by the restore process have been generated.
535         // missing file may happen if the watched tag is not present in moodle.xml (for example
536         // QUESTION_CATEGORIES is optional in moodle.xml but questions.xml must exist in
537         // moodle2 format) or the handler has not been implemented yet.
538         // apparently this must be called after the handler had a chance to create the file.
539         $this->make_sure_xml_exists('questions.xml', 'question_categories');
540         $this->make_sure_xml_exists('groups.xml', 'groups');
541         $this->make_sure_xml_exists('outcomes.xml', 'outcomes_definition');
542         $this->make_sure_xml_exists('users.xml', 'users');
543         $this->make_sure_xml_exists('course/roles.xml', 'roles',
544             array('role_assignments' => array(), 'role_overrides' => array()));
545         $this->make_sure_xml_exists('course/enrolments.xml', 'enrolments',
546             array('enrols' => array()));
547     }
551 /**
552  * The class responsible for course and site files migration
553  *
554  * @todo migrate site_files
555  */
556 class moodle1_files_handler extends moodle1_xml_handler {
558     /**
559      * Migrates course_files and site_files in the converter workdir
560      */
561     public function process() {
562         $this->migrate_course_files();
563         // todo $this->migrate_site_files();
564     }
566     /**
567      * Migrates course_files in the converter workdir
568      */
569     protected function migrate_course_files() {
570         $ids  = array();
571         $fileman = $this->converter->get_file_manager($this->converter->get_contextid(CONTEXT_COURSE), 'course', 'legacy');
572         $this->converter->set_stash('course_files_ids', array());
573         if (file_exists($this->converter->get_tempdir_path().'/course_files')) {
574             $ids = $fileman->migrate_directory('course_files');
575             $this->converter->set_stash('course_files_ids', $ids);
576         }
577         $this->log('course files migrated', backup::LOG_INFO, count($ids));
578     }
582 /**
583  * Handles the conversion of /MOODLE_BACKUP/INFO paths
584  *
585  * We do not produce any XML file here, just storing the data in the temp
586  * table so thay can be used by a later handler.
587  */
588 class moodle1_info_handler extends moodle1_handler {
590     /** @var array list of mod names included in info_details */
591     protected $modnames = array();
593     /** @var array the in-memory cache of the currently parsed info_details_mod element */
594     protected $currentmod;
596     public function get_paths() {
597         return array(
598             new convert_path('info', '/MOODLE_BACKUP/INFO'),
599             new convert_path('info_details', '/MOODLE_BACKUP/INFO/DETAILS'),
600             new convert_path('info_details_mod', '/MOODLE_BACKUP/INFO/DETAILS/MOD'),
601             new convert_path('info_details_mod_instance', '/MOODLE_BACKUP/INFO/DETAILS/MOD/INSTANCES/INSTANCE'),
602         );
603     }
605     /**
606      * Stashes the backup info for later processing by {@link moodle1_root_handler}
607      */
608     public function process_info($data) {
609         $this->converter->set_stash('backup_info', $data);
610     }
612     /**
613      * Initializes the in-memory cache for the current mod
614      */
615     public function process_info_details_mod($data) {
616         $this->currentmod = $data;
617         $this->currentmod['instances'] = array();
618     }
620     /**
621      * Appends the current instance data to the temporary in-memory cache
622      */
623     public function process_info_details_mod_instance($data) {
624         $this->currentmod['instances'][$data['id']] = $data;
625     }
627     /**
628      * Stashes the backup info for later processing by {@link moodle1_root_handler}
629      */
630     public function on_info_details_mod_end($data) {
631         global $CFG;
633         // keep only such modules that seem to have the support for moodle1 implemented
634         $modname = $this->currentmod['name'];
635         if (file_exists($CFG->dirroot.'/mod/'.$modname.'/backup/moodle1/lib.php')) {
636             $this->converter->set_stash('modinfo_'.$modname, $this->currentmod);
637             $this->modnames[] = $modname;
638         } else {
639             $this->log('unsupported activity module', backup::LOG_WARNING, $modname);
640         }
642         $this->currentmod = array();
643     }
645     /**
646      * Stashes the list of activity module types for later processing by {@link moodle1_root_handler}
647      */
648     public function on_info_details_end() {
649         $this->converter->set_stash('modnameslist', $this->modnames);
650     }
654 /**
655  * Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths
656  */
657 class moodle1_course_header_handler extends moodle1_xml_handler {
659     /** @var array we need to merge course information because it is dispatched twice */
660     protected $course = array();
662     /** @var array we need to merge course information because it is dispatched twice */
663     protected $courseraw = array();
665     /** @var array */
666     protected $category;
668     public function get_paths() {
669         return array(
670             new convert_path(
671                 'course_header', '/MOODLE_BACKUP/COURSE/HEADER',
672                 array(
673                     'newfields' => array(
674                         'summaryformat'          => 1,
675                         'legacyfiles'            => 2,
676                         'requested'              => 0, // @todo not really new, but maybe never backed up?
677                         'restrictmodules'        => 0,
678                         'enablecompletion'       => 0,
679                         'completionstartonenrol' => 0,
680                         'completionnotify'       => 0,
681                         'tags'                   => array(),
682                         'allowed_modules'        => array(),
683                     ),
684                     'dropfields' => array(
685                         'roles_overrides',
686                         'roles_assignments',
687                         'cost',
688                         'currancy',
689                         'defaultrole',
690                         'enrol',
691                         'enrolenddate',
692                         'enrollable',
693                         'enrolperiod',
694                         'enrolstartdate',
695                         'expirynotify',
696                         'expirythreshold',
697                         'guest',
698                         'notifystudents',
699                         'password',
700                         'student',
701                         'students',
702                         'teacher',
703                         'teachers',
704                         'metacourse',
705                     )
706                 )
707             ),
708             new convert_path(
709                 'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
710                 array(
711                     'newfields' => array(
712                         'description' => null,
713                     )
714                 )
715             ),
716         );
717     }
719     /**
720      * Because there is the CATEGORY branch in the middle of the COURSE/HEADER
721      * branch, this is dispatched twice. We use $this->coursecooked to merge
722      * the result. Once the parser is fixed, it can be refactored.
723      */
724     public function process_course_header($data, $raw) {
725        $this->course    = array_merge($this->course, $data);
726        $this->courseraw = array_merge($this->courseraw, $raw);
727     }
729     public function process_course_header_category($data) {
730         $this->category = $data;
731     }
733     public function on_course_header_end() {
735         $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
737         // stash the information needed by other handlers
738         $info = array(
739             'original_course_id'        => $this->course['id'],
740             'original_course_fullname'  => $this->course['fullname'],
741             'original_course_shortname' => $this->course['shortname'],
742             'original_course_startdate' => $this->course['startdate'],
743             'original_course_contextid' => $contextid
744         );
745         $this->converter->set_stash('original_course_info', $info);
747         $this->course['contextid'] = $contextid;
748         $this->course['category'] = $this->category;
750         // migrate files embedded into the course summary and stash their ids
751         $fileman = $this->converter->get_file_manager($contextid, 'course', 'summary');
752         $this->course['summary'] = moodle1_converter::migrate_referenced_files($this->course['summary'], $fileman);
753         $this->converter->set_stash('course_summary_files_ids', $fileman->get_fileids());
755         // write course.xml
756         $this->open_xml_writer('course/course.xml');
757         $this->write_xml('course', $this->course, array('/course/id', '/course/contextid'));
758         $this->close_xml_writer();
759     }
763 /**
764  * Handles the conversion of course sections and course modules
765  */
766 class moodle1_course_outline_handler extends moodle1_xml_handler {
768     /** @var array ordered list of the course contents */
769     protected $coursecontents = array();
771     /** @var array current section data */
772     protected $currentsection;
774     /**
775      * This handler is interested in course sections and course modules within them
776      */
777     public function get_paths() {
778         return array(
779             new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'),
780             new convert_path(
781                 'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION',
782                 array(
783                     'newfields' => array(
784                         'name'          => null,
785                         'summaryformat' => 1,
786                         'sequence'      => null,
787                     ),
788                 )
789             ),
790             new convert_path(
791                 'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD',
792                 array(
793                     'newfields' => array(
794                         'completion'                => 0,
795                         'completiongradeitemnumber' => null,
796                         'completionview'            => 0,
797                         'completionexpected'        => 0,
798                         'availablefrom'             => 0,
799                         'availableuntil'            => 0,
800                         'showavailability'          => 0,
801                         'availability_info'         => array(),
802                         'visibleold'                => 1,
803                         'showdescription'           => 0,
804                     ),
805                     'dropfields' => array(
806                         'instance',
807                         'roles_overrides',
808                         'roles_assignments',
809                     ),
810                     'renamefields' => array(
811                         'type' => 'modulename',
812                     ),
813                 )
814             ),
815             new convert_path('course_modules', '/MOODLE_BACKUP/COURSE/MODULES'),
816             // todo new convert_path('course_module_roles_overrides', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'),
817             // todo new convert_path('course_module_roles_assignments', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_ASSIGNMENTS'),
818         );
819     }
821     public function process_course_section($data) {
822         $this->currentsection = $data;
823     }
825     /**
826      * Populates the section sequence field (order of course modules) and stashes the
827      * course module info so that is can be dumped to activities/xxxx_x/module.xml later
828      */
829     public function process_course_module($data, $raw) {
830         global $CFG;
832         // check that this type of module should be included in the mbz
833         $modinfo = $this->converter->get_stash_itemids('modinfo_'.$data['modulename']);
834         if (empty($modinfo)) {
835             return;
836         }
838         // add the course module into the course contents list
839         $this->coursecontents[$data['id']] = array(
840             'cmid'       => $data['id'],
841             'instanceid' => $raw['INSTANCE'],
842             'sectionid'  => $this->currentsection['id'],
843             'modulename' => $data['modulename'],
844             'title'      => null
845         );
847         // add the course module id into the section's sequence
848         if (is_null($this->currentsection['sequence'])) {
849             $this->currentsection['sequence'] = $data['id'];
850         } else {
851             $this->currentsection['sequence'] .= ',' . $data['id'];
852         }
854         // add the sectionid and sectionnumber
855         $data['sectionid']      = $this->currentsection['id'];
856         $data['sectionnumber']  = $this->currentsection['number'];
858         // generate the module version - this is a bit tricky as this information
859         // is not present in 1.9 backups. we will use the currently installed version
860         // whenever we can but that might not be accurate for some modules.
861         // also there might be problem with modules that are not present at the target
862         // host...
863         $versionfile = $CFG->dirroot.'/mod/'.$data['modulename'].'/version.php';
864         if (file_exists($versionfile)) {
865             include($versionfile);
866             $data['version'] = $module->version;
867         } else {
868             $data['version'] = null;
869         }
871         // stash the course module info in stashes like 'cminfo_forum' with
872         // itemid set to the instance id. this is needed so that module handlers
873         // can later obtain information about the course module and dump it into
874         // the module.xml file
875         $this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
876     }
878     /**
879      * Writes sections/section_xxx/section.xml file and stashes it, too
880      */
881     public function on_course_section_end() {
883         // migrate files embedded into the section summary field
884         $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
885         $fileman = $this->converter->get_file_manager($contextid, 'course', 'section', $this->currentsection['id']);
886         $this->currentsection['summary'] = moodle1_converter::migrate_referenced_files($this->currentsection['summary'], $fileman);
888         // write section's inforef.xml with the file references
889         $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/inforef.xml');
890         $this->xmlwriter->begin_tag('inforef');
891         $this->xmlwriter->begin_tag('fileref');
892         $fileids = $fileman->get_fileids();
893         if (is_array($fileids)) {
894             foreach ($fileids as $fileid) {
895                 $this->write_xml('file', array('id' => $fileid));
896             }
897         }
898         $this->xmlwriter->end_tag('fileref');
899         $this->xmlwriter->end_tag('inforef');
900         $this->close_xml_writer();
902         // stash the section info and write section.xml
903         $this->converter->set_stash('sectioninfo', $this->currentsection, $this->currentsection['id']);
904         $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml');
905         $this->write_xml('section', $this->currentsection);
906         $this->close_xml_writer();
907         unset($this->currentsection);
908     }
910     /**
911      * Stashes the course contents
912      */
913     public function on_course_sections_end() {
914         $this->converter->set_stash('coursecontents', $this->coursecontents);
915     }
917     /**
918      * Writes the information collected by mod handlers
919      */
920     public function on_course_modules_end() {
922         foreach ($this->converter->get_stash('modnameslist') as $modname) {
923             $modinfo = $this->converter->get_stash('modinfo_'.$modname);
924             foreach ($modinfo['instances'] as $modinstanceid => $modinstance) {
925                 $cminfo    = $this->converter->get_stash('cminfo_'.$modname, $modinstanceid);
926                 $directory = 'activities/'.$modname.'_'.$cminfo['id'];
928                 // write module.xml
929                 $this->open_xml_writer($directory.'/module.xml');
930                 $this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
931                 $this->close_xml_writer();
933                 // write grades.xml
934                 $this->open_xml_writer($directory.'/grades.xml');
935                 $this->xmlwriter->begin_tag('activity_gradebook');
936                 $gradeitems = $this->converter->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array());
937                 if (!empty($gradeitems)) {
938                     $this->xmlwriter->begin_tag('grade_items');
939                     foreach ($gradeitems as $gradeitem) {
940                         $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
941                     }
942                     $this->xmlwriter->end_tag('grade_items');
943                 }
944                 $this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9
945                 $this->xmlwriter->end_tag('activity_gradebook');
946                 $this->close_xml_writer();
948                 // todo: write proper roles.xml, for now we just make sure the file is present
949                 $this->make_sure_xml_exists($directory.'/roles.xml', 'roles');
950             }
951         }
952     }
956 /**
957  * Handles the conversion of the defined roles
958  */
959 class moodle1_roles_definition_handler extends moodle1_xml_handler {
961     /**
962      * Where the roles are defined in the source moodle.xml
963      */
964     public function get_paths() {
965         return array(
966             new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
967             new convert_path(
968                 'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
969                 array(
970                     'newfields' => array(
971                         'description'   => '',
972                         'sortorder'     => 0,
973                         'archetype'     => ''
974                     )
975                 )
976             )
977         );
978     }
980     /**
981      * If there are any roles defined in moodle.xml, convert them to roles.xml
982      */
983     public function process_roles_role($data) {
985         if (!$this->has_xml_writer()) {
986             $this->open_xml_writer('roles.xml');
987             $this->xmlwriter->begin_tag('roles_definition');
988         }
989         if (!isset($data['nameincourse'])) {
990             $data['nameincourse'] = null;
991         }
992         $this->write_xml('role', $data, array('role/id'));
993     }
995     /**
996      * Finishes writing roles.xml
997      */
998     public function on_roles_end() {
1000         if (!$this->has_xml_writer()) {
1001             // no roles defined in moodle.xml so {link self::process_roles_role()}
1002             // was never executed
1003             $this->open_xml_writer('roles.xml');
1004             $this->write_xml('roles_definition', array());
1006         } else {
1007             // some roles were dumped into the file, let us close their wrapper now
1008             $this->xmlwriter->end_tag('roles_definition');
1009         }
1010         $this->close_xml_writer();
1011     }
1015 /**
1016  * Handles the conversion of the question bank included in the moodle.xml file
1017  */
1018 class moodle1_question_bank_handler extends moodle1_xml_handler {
1020     /** @var array the current question category being parsed */
1021     protected $currentcategory = null;
1023     /** @var array of the raw data for the current category */
1024     protected $currentcategoryraw = null;
1026     /** @var moodle1_file_manager instance used to convert question images */
1027     protected $fileman = null;
1029     /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
1030     private $currentcategorywritten = false;
1032     /** @var bool was the <questions> tag already written (work around MDL-27693) */
1033     private $questionswrapperwritten = false;
1035     /** @var array holds the instances of qtype specific conversion handlers */
1036     private $qtypehandlers = null;
1038     /**
1039      * Registers path that are not qtype-specific
1040      */
1041     public function get_paths() {
1043         $paths = array(
1044             new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
1045             new convert_path(
1046                 'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
1047                 array(
1048                     'newfields' => array(
1049                         'infoformat' => 0
1050                     )
1051                 )),
1052             new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'),
1053             new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'),
1054             // the question element must be grouped so we can re-dispatch it to the qtype handler as a whole
1055             new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true),
1056         );
1058         // annotate all question subpaths required by the qtypes subplugins
1059         $subpaths = array();
1060         foreach ($this->get_qtype_handler('*') as $qtypehandler) {
1061             foreach ($qtypehandler->get_question_subpaths() as $subpath) {
1062                 $subpaths[$subpath] = true;
1063             }
1064         }
1065         foreach (array_keys($subpaths) as $subpath) {
1066             $name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath));
1067             $path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath;
1068             $paths[] = new convert_path($name, $path);
1069         }
1071         return $paths;
1072     }
1074     /**
1075      * Starts writing questions.xml and prepares the file manager instance
1076      */
1077     public function on_question_categories_start() {
1078         $this->open_xml_writer('questions.xml');
1079         $this->xmlwriter->begin_tag('question_categories');
1080         if (is_null($this->fileman)) {
1081             $this->fileman = $this->converter->get_file_manager();
1082         }
1083     }
1085     /**
1086      * Initializes the current category cache
1087      */
1088     public function on_question_category_start() {
1089         $this->currentcategory         = array();
1090         $this->currentcategoryraw      = array();
1091         $this->currentcategorywritten  = false;
1092         $this->questionswrapperwritten = false;
1093     }
1095     /**
1096      * Populates the current question category data
1097      *
1098      * Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually
1099      * called twice for both halves of the data. We merge them here into the currentcategory array.
1100      */
1101     public function process_question_category($data, $raw) {
1102         $this->currentcategory    = array_merge($this->currentcategory, $data);
1103         $this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw);
1104     }
1106     /**
1107      * Inject the context related information into the current category
1108      */
1109     public function process_question_category_context($data) {
1111         switch ($data['level']) {
1112         case 'module':
1113             $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']);
1114             $this->currentcategory['contextlevel'] = CONTEXT_MODULE;
1115             $this->currentcategory['contextinstanceid'] = $data['instance'];
1116             break;
1117         case 'course':
1118             $originalcourseinfo = $this->converter->get_stash('original_course_info');
1119             $originalcourseid   = $originalcourseinfo['original_course_id'];
1120             $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSE);
1121             $this->currentcategory['contextlevel'] = CONTEXT_COURSE;
1122             $this->currentcategory['contextinstanceid'] = $originalcourseid;
1123             break;
1124         case 'coursecategory':
1125             // this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance
1126             // of the course category (1 = parent category, 2 = grand-parent category etc). We pretend
1127             // that this level*10 is the id of that category and create an artifical contextid for it
1128             $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSECAT, $data['coursecategorylevel'] * 10);
1129             $this->currentcategory['contextlevel'] = CONTEXT_COURSECAT;
1130             $this->currentcategory['contextinstanceid'] = $data['coursecategorylevel'] * 10;
1131             break;
1132         case 'system':
1133             $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM);
1134             $this->currentcategory['contextlevel'] = CONTEXT_SYSTEM;
1135             $this->currentcategory['contextinstanceid'] = 0;
1136             break;
1137         }
1138     }
1140     /**
1141      * Writes the common <question> data and re-dispateches the whole grouped
1142      * <QUESTION> data to the qtype for appending its qtype specific data processing
1143      *
1144      * @param array $data
1145      * @param array $raw
1146      * @return array
1147      */
1148     public function process_question(array $data, array $raw) {
1149         global $CFG;
1151         // firstly make sure that the category data and the <questions> wrapper are written
1152         // note that because of MDL-27693 we can't use {@link self::process_question_category()}
1153         // and {@link self::on_questions_start()} to do so
1155         if (empty($this->currentcategorywritten)) {
1156             $this->xmlwriter->begin_tag('question_category', array('id' => $this->currentcategory['id']));
1157             foreach ($this->currentcategory as $name => $value) {
1158                 if ($name === 'id') {
1159                     continue;
1160                 }
1161                 $this->xmlwriter->full_tag($name, $value);
1162             }
1163             $this->currentcategorywritten = true;
1164         }
1166         if (empty($this->questionswrapperwritten)) {
1167             $this->xmlwriter->begin_tag('questions');
1168             $this->questionswrapperwritten = true;
1169         }
1171         $qtype = $data['qtype'];
1173         // replay the upgrade step 2008050700 {@see question_fix_random_question_parents()}
1174         if ($qtype == 'random' and $data['parent'] <> $data['id']) {
1175             $data['parent'] = $data['id'];
1176         }
1178         // replay the upgrade step 2010080900 and part of 2010080901
1179         $data['generalfeedbackformat'] = $data['questiontextformat'];
1180         $data['oldquestiontextformat'] = $data['questiontextformat'];
1182         if ($CFG->texteditors !== 'textarea') {
1183             $data['questiontext'] = text_to_html($data['questiontext'], false, false, true);
1184             $data['questiontextformat'] = FORMAT_HTML;
1185             $data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true);
1186             $data['generalfeedbackformat'] = FORMAT_HTML;
1187         }
1189         // replay the upgrade step 2010080901 - updating question image
1190         if (!empty($data['image'])) {
1191             $textlib = textlib_get_instance();
1192             if ($textlib->substr($textlib->strtolower($data['image']), 0, 7) == 'http://') {
1193                 // it is a link, appending to existing question text
1194                 $data['questiontext'] .= ' <img src="' . $data['image'] . '" />';
1196             } else {
1197                 // it is a file in course_files
1198                 $filename = basename($data['image']);
1199                 $filepath = dirname($data['image']);
1200                 if (empty($filepath) or $filepath == '.' or $filepath == '/') {
1201                     $filepath = '/';
1202                 } else {
1203                     // append /
1204                     $filepath = '/'.trim($filepath, './@#$ ').'/';
1205                 }
1207                 if (file_exists($this->converter->get_tempdir_path().'/course_files'.$filepath.$filename)) {
1208                     $this->fileman->contextid = $this->currentcategory['contextid'];
1209                     $this->fileman->component = 'question';
1210                     $this->fileman->filearea  = 'questiontext';
1211                     $this->fileman->itemid    = $data['id'];
1212                     $this->fileman->migrate_file('course_files'.$filepath.$filename, '/', $filename);
1213                     // note this is slightly different from the upgrade code as we put the file into the
1214                     // root folder here. this makes our life easier as we do not need to create all the
1215                     // directories within the specified filearea/itemid
1216                     $data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
1218                 } else {
1219                     $this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename));
1220                 }
1221             }
1222         }
1223         unset($data['image']);
1225         // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
1226         $data['defaultmark'] = $data['defaultgrade'];
1228         // write the common question data
1229         $this->xmlwriter->begin_tag('question', array('id' => $data['id']));
1230         foreach (array(
1231             'parent', 'name', 'questiontext', 'questiontextformat',
1232             'generalfeedback', 'generalfeedbackformat', 'defaultmark',
1233             'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
1234             'timecreated', 'timemodified', 'createdby', 'modifiedby'
1235         ) as $fieldname) {
1236             if (!array_key_exists($fieldname, $data)) {
1237                 throw new moodle1_convert_exception('missing_common_question_field', $fieldname);
1238             }
1239             $this->xmlwriter->full_tag($fieldname, $data[$fieldname]);
1240         }
1241         // unless we know that the given qtype does not append any own structures,
1242         // give the handler a chance to do so now
1243         if (!in_array($qtype, array('description', 'random'))) {
1244             $handler = $this->get_qtype_handler($qtype);
1245             if ($handler === false) {
1246                 $this->log('question type converter not found', backup::LOG_ERROR, $qtype);
1248             } else {
1249                 $this->xmlwriter->begin_tag('plugin_qtype_'.$qtype.'_question');
1250                 $handler->use_xml_writer($this->xmlwriter);
1251                 $handler->process_question($data, $raw);
1252                 $this->xmlwriter->end_tag('plugin_qtype_'.$qtype.'_question');
1253             }
1254         }
1256         $this->xmlwriter->end_tag('question');
1257     }
1259     /**
1260      * Closes the questions wrapper
1261      */
1262     public function on_questions_end() {
1263         $this->xmlwriter->end_tag('questions');
1264     }
1266     /**
1267      * Closes the question_category and annotates the category id
1268      * so that it can be dumped into course/inforef.xml
1269      */
1270     public function on_question_category_end() {
1271         // make sure that the category data were written by {@link self::process_question()}
1272         // if not, write it now. this may happen when the current category does not contain any
1273         // questions so the subpaths is missing completely
1274         if (empty($this->currentcategorywritten)) {
1275             $this->write_xml('question_category', $this->currentcategory, array('/question_category/id'));
1276         } else {
1277             $this->xmlwriter->end_tag('question_category');
1278         }
1279         $this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']);
1280     }
1282     /**
1283      * Stops writing questions.xml
1284      */
1285     public function on_question_categories_end() {
1286         $this->xmlwriter->end_tag('question_categories');
1287         $this->close_xml_writer();
1288     }
1290     /**
1291      * Provides access to the qtype handlers
1292      *
1293      * Returns either list of all qtype handler instances (if passed '*') or a particular handler
1294      * for the given qtype or false if the qtype is not supported.
1295      *
1296      * @throws moodle1_convert_exception
1297      * @param string $qtype the name of the question type or '*' for returning all
1298      * @return array|moodle1_qtype_handler|bool
1299      */
1300     protected function get_qtype_handler($qtype) {
1302         if (is_null($this->qtypehandlers)) {
1303             // initialize the list of qtype handler instances
1304             $this->qtypehandlers = array();
1305             foreach (get_plugin_list('qtype') as $qtypename => $qtypelocation) {
1306                 $filename = $qtypelocation.'/backup/moodle1/lib.php';
1307                 if (file_exists($filename)) {
1308                     $classname = 'moodle1_qtype_'.$qtypename.'_handler';
1309                     require_once($filename);
1310                     if (!class_exists($classname)) {
1311                         throw new moodle1_convert_exception('missing_handler_class', $classname);
1312                     }
1313                     $this->log('registering handler', backup::LOG_DEBUG, $classname, 2);
1314                     $this->qtypehandlers[$qtypename] = new $classname($this, $qtypename);
1315                 }
1316             }
1317         }
1319         if ($qtype === '*') {
1320             return $this->qtypehandlers;
1322         } else if (isset($this->qtypehandlers[$qtype])) {
1323             return $this->qtypehandlers[$qtype];
1325         } else {
1326             return false;
1327         }
1328     }
1332 /**
1333  * Handles the conversion of the scales included in the moodle.xml file
1334  */
1335 class moodle1_scales_handler extends moodle1_handler {
1337     /** @var moodle1_file_manager instance used to convert question images */
1338     protected $fileman = null;
1340     /**
1341      * Registers paths
1342      */
1343     public function get_paths() {
1344         return array(
1345             new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
1346             new convert_path(
1347                 'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
1348                 array(
1349                     'renamefields' => array(
1350                         'scaletext' => 'scale',
1351                     ),
1352                     'addfields' => array(
1353                         'descriptionformat' => 0,
1354                     )
1355                 )
1356             ),
1357         );
1358     }
1360     /**
1361      * Prepare the file manager for the files embedded in the scale description field
1362      */
1363     public function on_scales_start() {
1364         $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1365         $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'scale');
1366     }
1368     /**
1369      * This is executed every time we have one <SCALE> data available
1370      *
1371      * @param array $data
1372      * @param array $raw
1373      * @return array
1374      */
1375     public function process_scale(array $data, array $raw) {
1376         global $CFG;
1378         // replay upgrade step 2009110400
1379         if ($CFG->texteditors !== 'textarea') {
1380             $data['description'] = text_to_html($data['description'], false, false, true);
1381             $data['descriptionformat'] = FORMAT_HTML;
1382         }
1384         // convert course files embedded into the scale description field
1385         $this->fileman->itemid = $data['id'];
1386         $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1388         // stash the scale
1389         $this->converter->set_stash('scales', $data, $data['id']);
1390     }
1394 /**
1395  * Handles the conversion of the outcomes
1396  */
1397 class moodle1_outcomes_handler extends moodle1_xml_handler {
1399     /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
1400     protected $fileman = null;
1402     /**
1403      * Registers paths
1404      */
1405     public function get_paths() {
1406         return array(
1407             new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
1408             new convert_path(
1409                 'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
1410                 array(
1411                     'addfields' => array(
1412                         'descriptionformat' => FORMAT_MOODLE,
1413                     ),
1414                 )
1415             ),
1416         );
1417     }
1419     /**
1420      * Prepares the file manager and starts writing outcomes.xml
1421      */
1422     public function on_gradebook_grade_outcomes_start() {
1424         $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1425         $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome');
1427         $this->open_xml_writer('outcomes.xml');
1428         $this->xmlwriter->begin_tag('outcomes_definition');
1429     }
1431     /**
1432      * Processes GRADE_OUTCOME tags progressively
1433      */
1434     public function process_gradebook_grade_outcome(array $data, array $raw) {
1435         global $CFG;
1437         // replay the upgrade step 2009110400
1438         if ($CFG->texteditors !== 'textarea') {
1439             $data['description']       = text_to_html($data['description'], false, false, true);
1440             $data['descriptionformat'] = FORMAT_HTML;
1441         }
1443         // convert course files embedded into the outcome description field
1444         $this->fileman->itemid = $data['id'];
1445         $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1447         // write the outcome data
1448         $this->write_xml('outcome', $data, array('/outcome/id'));
1450         return $data;
1451     }
1453     /**
1454      * Closes outcomes.xml
1455      */
1456     public function on_gradebook_grade_outcomes_end() {
1457         $this->xmlwriter->end_tag('outcomes_definition');
1458         $this->close_xml_writer();
1459     }
1463 /**
1464  * Handles the conversion of the gradebook structures in the moodle.xml file
1465  */
1466 class moodle1_gradebook_handler extends moodle1_xml_handler {
1468     /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
1469     protected $categoryparent = array();
1471     /**
1472      * Registers paths
1473      */
1474     public function get_paths() {
1475         return array(
1476             new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
1477             new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
1478             new convert_path(
1479                 'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
1480                 array(
1481                     'addfields' => array(
1482                         'hidden' => 0,  // upgrade step 2010011200
1483                     ),
1484                 )
1485             ),
1486             new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'),
1487             new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'),
1488         );
1489     }
1491     /**
1492      * Initializes the in-memory structures
1493      *
1494      * This should not be needed actually as the moodle.xml contains just one GRADEBOOK
1495      * element. But who knows - maybe someone will want to write a mass conversion
1496      * tool in the future (not me definitely ;-)
1497      */
1498     public function on_gradebook_start() {
1499         $this->categoryparent = array();
1500     }
1502     /**
1503      * Processes one GRADE_LETTER data
1504      *
1505      * In Moodle 1.9, all grade_letters are from course context only. Therefore
1506      * we put them here.
1507      */
1508     public function process_gradebook_grade_letter(array $data, array $raw) {
1509         $this->converter->set_stash('gradebook_gradeletter', $data, $data['id']);
1510     }
1512     /**
1513      * Processes one GRADE_CATEGORY data
1514      */
1515     public function process_gradebook_grade_category(array $data, array $raw) {
1516         $this->categoryparent[$data['id']] = $data['parent'];
1517         $this->converter->set_stash('gradebook_gradecategory', $data, $data['id']);
1518     }
1520     /**
1521      * Processes one GRADE_ITEM data
1522      */
1523     public function process_gradebook_grade_item(array $data, array $raw) {
1525         // here we use get_nextid() to get a nondecreasing sequence
1526         $data['sortorder'] = $this->converter->get_nextid();
1528         if ($data['itemtype'] === 'mod') {
1529             return $this->process_mod_grade_item($data, $raw);
1531         } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
1532             return $this->process_nonmod_grade_item($data, $raw);
1534         } else {
1535             $this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']);
1536         }
1537     }
1539     /**
1540      * Processes one GRADE_ITEM of the type 'mod'
1541      */
1542     protected function process_mod_grade_item(array $data, array $raw) {
1544         $stashname   = 'gradebook_modgradeitem_'.$data['itemmodule'];
1545         $stashitemid = $data['iteminstance'];
1546         $gradeitems  = $this->converter->get_stash_or_default($stashname, $stashitemid, array());
1548         // typically there will be single item with itemnumber 0
1549         $gradeitems[$data['itemnumber']] = $data;
1551         $this->converter->set_stash($stashname, $gradeitems, $stashitemid);
1553         return $data;
1554     }
1556     /**
1557      * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
1558      */
1559     protected function process_nonmod_grade_item(array $data, array $raw) {
1561         $stashname   = 'gradebook_nonmodgradeitem';
1562         $stashitemid = $data['id'];
1563         $this->converter->set_stash($stashname, $data, $stashitemid);
1565         return $data;
1566     }
1568     /**
1569      * @todo
1570      */
1571     public function on_gradebook_grade_item_grades_start() {
1572     }
1574     /**
1575      * Writes the collected information into gradebook.xml
1576      */
1577     public function on_gradebook_end() {
1579         $this->open_xml_writer('gradebook.xml');
1580         $this->xmlwriter->begin_tag('gradebook');
1581         $this->write_grade_categories();
1582         $this->write_grade_items();
1583         $this->write_grade_letters();
1584         $this->xmlwriter->end_tag('gradebook');
1585         $this->close_xml_writer();
1586     }
1588     /**
1589      * Writes grade_categories
1590      */
1591     protected function write_grade_categories() {
1593         $this->xmlwriter->begin_tag('grade_categories');
1594         foreach ($this->converter->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) {
1595             $gradecategory = $this->converter->get_stash('gradebook_gradecategory', $gradecategoryid);
1596             $path = $this->calculate_category_path($gradecategoryid);
1597             $gradecategory['depth'] = count($path);
1598             $gradecategory['path']  = '/'.implode('/', $path).'/';
1599             $this->write_xml('grade_category', $gradecategory, array('/grade_category/id'));
1600         }
1601         $this->xmlwriter->end_tag('grade_categories');
1602     }
1604     /**
1605      * Calculates the path to the grade_category
1606      *
1607      * Moodle 1.9 backup does not store the grade_category's depth and path. This method is used
1608      * to repopulate this information using the $this->categoryparent values.
1609      *
1610      * @param int $categoryid
1611      * @return array of ids including the categoryid
1612      */
1613     protected function calculate_category_path($categoryid) {
1615         if (!array_key_exists($categoryid, $this->categoryparent)) {
1616             throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
1617         }
1619         $path = array($categoryid);
1620         $parent = $this->categoryparent[$categoryid];
1621         while (!is_null($parent)) {
1622             array_unshift($path, $parent);
1623             $parent = $this->categoryparent[$parent];
1624             if (in_array($parent, $path)) {
1625                 throw new moodle1_convert_exception('circular_reference_in_categories_tree');
1626             }
1627         }
1629         return $path;
1630     }
1632     /**
1633      * Writes grade_items
1634      */
1635     protected function write_grade_items() {
1637         $this->xmlwriter->begin_tag('grade_items');
1638         foreach ($this->converter->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) {
1639             $gradeitem = $this->converter->get_stash('gradebook_nonmodgradeitem', $gradeitemid);
1640             $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
1641         }
1642         $this->xmlwriter->end_tag('grade_items');
1643     }
1645     /**
1646      * Writes grade_letters
1647      */
1648     protected function write_grade_letters() {
1650         $this->xmlwriter->begin_tag('grade_letters');
1651         foreach ($this->converter->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) {
1652             $gradeletter = $this->converter->get_stash('gradebook_gradeletter', $gradeletterid);
1653             $this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id'));
1654         }
1655         $this->xmlwriter->end_tag('grade_letters');
1656     }
1660 /**
1661  * Shared base class for activity modules, blocks and qtype handlers
1662  */
1663 abstract class moodle1_plugin_handler extends moodle1_xml_handler {
1665     /** @var string */
1666     protected $plugintype;
1668     /** @var string */
1669     protected $pluginname;
1671     /**
1672      * @param moodle1_converter $converter the converter that requires us
1673      * @param string $plugintype
1674      * @param string $pluginname
1675      */
1676     public function __construct(moodle1_converter $converter, $plugintype, $pluginname) {
1678         parent::__construct($converter);
1679         $this->plugintype = $plugintype;
1680         $this->pluginname = $pluginname;
1681     }
1683     /**
1684      * Returns the normalized name of the plugin, eg mod_workshop
1685      *
1686      * @return string
1687      */
1688     public function get_component_name() {
1689         return $this->plugintype.'_'.$this->pluginname;
1690     }
1694 /**
1695  * Base class for all question type handlers
1696  */
1697 abstract class moodle1_qtype_handler extends moodle1_plugin_handler {
1699     /** @var moodle1_question_bank_handler */
1700     protected $qbankhandler;
1702     /**
1703      * Returns the list of paths within one <QUESTION> that this qtype needs to have included
1704      * in the grouped question structure
1705      *
1706      * @return array of strings
1707      */
1708     public function get_question_subpaths() {
1709         return array();
1710     }
1712     /**
1713      * Gives the qtype handler a chance to write converted data into questions.xml
1714      *
1715      * @param array $data grouped question data
1716      * @param array $raw grouped raw QUESTION data
1717      */
1718     public function process_question(array $data, array $raw) {
1719     }
1721     /**
1722      * Converts the answers and writes them into the questions.xml
1723      *
1724      * The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
1725      *
1726      * @param array $answers as parsed by the grouped parser in moodle.xml
1727      * @param string $qtype containing the answers
1728      */
1729     protected function write_answers(array $answers, $qtype) {
1731         $this->xmlwriter->begin_tag('answers');
1732         foreach ($answers as $elementname => $elements) {
1733             foreach ($elements as $element) {
1734                 $answer = $this->convert_answer($element, $qtype);
1735                 $this->write_xml('answer', $answer, array('/answer/id'));
1736             }
1737         }
1738         $this->xmlwriter->end_tag('answers');
1739     }
1741     /**
1742      * Writes the grouped numerical_units structure
1743      *
1744      * @param array $numericalunits
1745      */
1746     protected function write_numerical_units(array $numericalunits) {
1748         $this->xmlwriter->begin_tag('numerical_units');
1749         foreach ($numericalunits as $elementname => $elements) {
1750             foreach ($elements as $element) {
1751                 $element['id'] = $this->converter->get_nextid();
1752                 $this->write_xml('numerical_unit', $element, array('/numerical_unit/id'));
1753             }
1754         }
1755         $this->xmlwriter->end_tag('numerical_units');
1756     }
1758     /**
1759      * Writes the numerical_options structure
1760      *
1761      * @see get_default_numerical_options()
1762      * @param array $numericaloption
1763      */
1764     protected function write_numerical_options(array $numericaloption) {
1766         $this->xmlwriter->begin_tag('numerical_options');
1767         if (!empty($numericaloption)) {
1768             $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
1769         }
1770         $this->xmlwriter->end_tag('numerical_options');
1771     }
1773     /**
1774      * Returns default numerical_option structure
1775      *
1776      * This structure is not present in moodle.xml, we create a new artificial one here.
1777      *
1778      * @see write_numerical_options()
1779      * @param int $oldquestiontextformat
1780      * @return array
1781      */
1782     protected function get_default_numerical_options($oldquestiontextformat) {
1783         global $CFG;
1785         // replay the upgrade step 2009100100 - new table
1786         $options = array(
1787             'id'                 => $this->converter->get_nextid(),
1788             'instructions'       => null,
1789             'instructionsformat' => 0,
1790             'showunits'          => 0,
1791             'unitsleft'          => 0,
1792             'unitgradingtype'    => 0,
1793             'unitpenalty'        => 0.1
1794         );
1796         // replay the upgrade step 2009100101
1797         if ($CFG->texteditors !== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE) {
1798             $options['instructionsformat'] = FORMAT_HTML;
1799         } else {
1800             $options['instructionsformat'] = $oldquestiontextformat;
1801         }
1803         return $options;
1804     }
1806     /**
1807      * Writes the dataset_definitions structure
1808      *
1809      * @param array $datasetdefinitions array of dataset_definition structures
1810      */
1811     protected function write_dataset_definitions(array $datasetdefinitions) {
1813         $this->xmlwriter->begin_tag('dataset_definitions');
1814         foreach ($datasetdefinitions as $datasetdefinition) {
1815             $this->xmlwriter->begin_tag('dataset_definition', array('id' => $this->converter->get_nextid()));
1816             foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) {
1817                 $this->xmlwriter->full_tag($element, $datasetdefinition[$element]);
1818             }
1819             $this->xmlwriter->begin_tag('dataset_items');
1820             if (!empty($datasetdefinition['dataset_items']['dataset_item'])) {
1821                 foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) {
1822                     $datasetitem['id'] = $this->converter->get_nextid();
1823                     $this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id'));
1824                 }
1825             }
1826             $this->xmlwriter->end_tag('dataset_items');
1827             $this->xmlwriter->end_tag('dataset_definition');
1828         }
1829         $this->xmlwriter->end_tag('dataset_definitions');
1830     }
1832     /// implementation details follow //////////////////////////////////////////
1834     public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) {
1836         parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
1837         $this->qbankhandler = $qbankhandler;
1838     }
1840     /**
1841      * @see self::get_question_subpaths()
1842      */
1843     final public function get_paths() {
1844         throw new moodle1_convert_exception('qtype_handler_get_paths');
1845     }
1847     /**
1848      * Question type handlers cannot open the xml_writer
1849      */
1850     final protected function open_xml_writer() {
1851         throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1852     }
1854     /**
1855      * Question type handlers cannot close the xml_writer
1856      */
1857     final protected function close_xml_writer() {
1858         throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1859     }
1861     /**
1862      * Provides a xml_writer instance to this qtype converter
1863      *
1864      * @param xml_writer $xmlwriter
1865      */
1866     public function use_xml_writer(xml_writer $xmlwriter) {
1867         $this->xmlwriter = $xmlwriter;
1868     }
1870     /**
1871      * Converts <ANSWER> structure into the new <answer> one
1872      *
1873      * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
1874      *
1875      * @param array $old the parsed answer array in moodle.xml
1876      * @param string $qtype the question type the answer is part of
1877      * @return array
1878      */
1879     private function convert_answer(array $old, $qtype) {
1880         global $CFG;
1882         $new                    = array();
1883         $new['id']              = $old['id'];
1884         $new['answertext']      = $old['answer_text'];
1885         $new['answerformat']    = 0;   // upgrade step 2010080900
1886         $new['fraction']        = $old['fraction'];
1887         $new['feedback']        = $old['feedback'];
1888         $new['feedbackformat']  = 0;   // upgrade step 2010080900
1890         // replay upgrade step 2010080901
1891         if ($qtype !== 'multichoice') {
1892             $new['answerformat'] = FORMAT_PLAIN;
1893         } else {
1894             $new['answerformat'] = FORMAT_MOODLE;
1895         }
1897         if ($CFG->texteditors !== 'textarea') {
1898             if ($qtype == 'essay') {
1899                 $new['feedback'] = text_to_html($new['feedback'], false, false, true);
1900             }
1901             $new['feedbackformat'] = FORMAT_HTML;
1903         } else {
1904             $new['feedbackformat'] = FORMAT_MOODLE;
1905         }
1907         return $new;
1908     }
1912 /**
1913  * Base class for activity module handlers
1914  */
1915 abstract class moodle1_mod_handler extends moodle1_plugin_handler {
1917     /**
1918      * Returns the name of the module, eg. 'forum'
1919      *
1920      * @return string
1921      */
1922     public function get_modname() {
1923         return $this->pluginname;
1924     }
1926     /**
1927      * Returns course module information for the given instance id
1928      *
1929      * The information for this instance id has been stashed by
1930      * {@link moodle1_course_outline_handler::process_course_module()}
1931      *
1932      * @param int $instance the module instance id
1933      * @param string $modname the module type, defaults to $this->pluginname
1934      * @return int
1935      */
1936     protected function get_cminfo($instance, $modname = null) {
1938         if (is_null($modname)) {
1939             $modname = $this->pluginname;
1940         }
1941         return $this->converter->get_stash('cminfo_'.$modname, $instance);
1942     }
1946 /**
1947  * Base class for all modules that are successors of the 1.9 resource module
1948  */
1949 abstract class moodle1_resource_successor_handler extends moodle1_mod_handler {
1951     /**
1952      * Resource successors do not attach to paths themselves, they are called explicitely
1953      * by moodle1_mod_resource_handler
1954      *
1955      * @return array
1956      */
1957     final public function get_paths() {
1958         return array();
1959     }
1961     /**
1962      * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
1963      *
1964      * Called by {@link moodle1_mod_resource_handler::process_resource()}
1965      *
1966      * @param array $data pre-cooked legacy resource data
1967      * @param array $raw raw legacy resource data
1968      */
1969     public function process_legacy_resource(array $data, array $raw) {
1970     }
1972     /**
1973      * Called when the parses reaches the end </MOD> resource tag
1974      *
1975      * @param array $data the data returned by {@link self::process_resource} or just pre-cooked
1976      */
1977     public function on_legacy_resource_end(array $data) {
1978     }
1981 /**
1982  * Base class for block handlers
1983  */
1984 abstract class moodle1_block_handler extends moodle1_plugin_handler {
1989 /**
1990  * Base class for the activity modules' subplugins
1991  */
1992 abstract class moodle1_submod_handler extends moodle1_plugin_handler {
1994     /** @var moodle1_mod_handler */
1995     protected $parenthandler;
1997     /**
1998      * @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of
1999      * @param string $subplugintype the type of the subplugin
2000      * @param string $subpluginname the name of the subplugin
2001      */
2002     public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) {
2003         $this->parenthandler = $parenthandler;
2004         parent::__construct($parenthandler->converter, $subplugintype, $subpluginname);
2005     }
2007     /**
2008      * Activity module subplugins can't declare any paths to handle
2009      *
2010      * The paths must be registered by the parent module and then re-dispatched to the
2011      * relevant subplugins for eventual processing.
2012      *
2013      * @return array empty array
2014      */
2015     final public function get_paths() {
2016         return array();
2017     }