1<?php
2
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/>.
17
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 */
29
30defined('MOODLE_INTERNAL') || die();
31
32require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
33require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
34require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');
35
36/**
37 * Handlers factory class
38 */
39abstract class moodle1_handlers_factory {
40
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) {
46
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        );
58
59        $handlers = array_merge($handlers, self::get_plugin_handlers('mod', $converter));
60        $handlers = array_merge($handlers, self::get_plugin_handlers('block', $converter));
61
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        }
68
69        return $handlers;
70    }
71
72    /// public API ends here ///////////////////////////////////////////////////
73
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;
85
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;
97            }
98
99            if (!class_exists($handlerclass)) {
100                throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
101            }
102            $handlers[] = new $handlerclass($converter, $type, $name);
103        }
104        return $handlers;
105    }
106}
107
108
109/**
110 * Base backup conversion handler
111 */
112abstract class moodle1_handler implements loggable {
113
114    /** @var moodle1_converter */
115    protected $converter;
116
117    /**
118     * @param moodle1_converter $converter the converter that requires us
119     */
120    public function __construct(moodle1_converter $converter) {
121        $this->converter = $converter;
122    }
123
124    /**
125     * @return moodle1_converter the converter that required this handler
126     */
127    public function get_converter() {
128        return $this->converter;
129    }
130
131    /**
132     * Log a message using the converter's logging mechanism
133     *
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
139     */
140    public function log($message, $level, $a = null, $depth = null, $display = false) {
141        $this->converter->log($message, $level, $a, $depth, $display);
142    }
143}
144
145
146/**
147 * Base backup conversion handler that generates an XML file
148 */
149abstract class moodle1_xml_handler extends moodle1_handler {
150
151    /** @var null|string the name of file we are writing to */
152    protected $xmlfilename;
153
154    /** @var null|xml_writer */
155    protected $xmlwriter;
156
157    /**
158     * Opens the XML writer - after calling, one is free to use $xmlwriter
159     *
160     * @param string $filename XML file name to write into
161     * @return void
162     */
163    protected function open_xml_writer($filename) {
164
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);
167        }
168
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);
173
174            if (!check_dir_exists($directory)) {
175                throw new moodle1_convert_exception('unable_create_target_directory', $directory);
176            }
177            $this->xmlwriter = new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer());
178            $this->xmlwriter->start();
179        }
180    }
181
182    /**
183     * Close the XML writer
184     *
185     * At the moment, the caller must close all tags before calling
186     *
187     * @return void
188     */
189    protected function close_xml_writer() {
190        if ($this->xmlwriter instanceof xml_writer) {
191            $this->xmlwriter->stop();
192        }
193        unset($this->xmlwriter);
194        $this->xmlwriter = null;
195        $this->xmlfilename = null;
196    }
197
198    /**
199     * Checks if the XML writer has been opened by {@link self::open_xml_writer()}
200     *
201     * @return bool
202     */
203    protected function has_xml_writer() {
204
205        if ($this->xmlwriter instanceof xml_writer) {
206            return true;
207        } else {
208            return false;
209        }
210    }
211
212    /**
213     * Writes the given XML tree data into the currently opened file
214     *
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
219     */
220    protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
221
222        if (!$this->has_xml_writer()) {
223            throw new moodle1_convert_exception('write_xml_without_writer');
224        }
225
226        $mypath    = $parent . $element;
227        $myattribs = array();
228
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]);
235                }
236            }
237        }
238
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;
247            }
248        }
249        $data = array_merge($leaves, $branches);
250
251        $this->xmlwriter->begin_tag($element, $myattribs);
252
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);
259            }
260        }
261
262        $this->xmlwriter->end_tag($element);
263    }
264
265    /**
266     * Makes sure that a new XML file exists, or creates it itself
267     *
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.
271     *
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
276     */
277    protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) {
278
279        $existed = file_exists($this->converter->get_workdir_path().'/'.$filename);
280
281        if ($existed) {
282            return true;
283        }
284
285        if ($rootelement !== false) {
286            $this->open_xml_writer($filename);
287            $this->write_xml($rootelement, $content);
288            $this->close_xml_writer();
289        }
290
291        return false;
292    }
293}
294
295
296/**
297 * Process the root element of the backup file
298 */
299class moodle1_root_handler extends moodle1_xml_handler {
300
301    public function get_paths() {
302        return array(new convert_path('root_element', '/MOODLE_BACKUP'));
303    }
304
305    /**
306     * Converts course_files and site_files
307     */
308    public function on_root_element_start() {
309
310        // convert course files
311        $fileshandler = new moodle1_files_handler($this->converter);
312        $fileshandler->process();
313    }
314
315    /**
316     * This is executed at the end of the moodle.xml parsing
317     */
318    public function on_root_element_end() {
319        global $CFG;
320
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');
324
325        ////////////////////////////////////////////////////////////////////////
326        // write moodle_backup.xml
327        ////////////////////////////////////////////////////////////////////////
328        $this->open_xml_writer('moodle_backup.xml');
329
330        $this->xmlwriter->begin_tag('moodle_backup');
331        $this->xmlwriter->begin_tag('information');
332
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);
346        }
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);
353        }
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));
363
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');
376
377        // moodle_backup/information/contents
378        $this->xmlwriter->begin_tag('contents');
379
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']
392            ));
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
404        }
405        $this->xmlwriter->end_tag('activities');
406
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));
426        }
427        $this->xmlwriter->end_tag('sections');
428
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);
435
436        $this->xmlwriter->end_tag('contents');
437
438        // moodle_backup/information/settings
439        $this->xmlwriter->begin_tag('settings');
440
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,
454        );
455        unset($backupinfo);
456        foreach ($rootsettings as $name => $value) {
457            $this->write_xml('setting', array(
458                'level' => 'root',
459                'name'  => $name,
460                'value' => $value));
461        }
462        unset($rootsettings);
463
464        // activity settings populated above
465        foreach ($activitysettings as $activitysetting) {
466            $this->write_xml('setting', $activitysetting);
467        }
468        unset($activitysettings);
469
470        // section settings populated above
471        foreach ($sectionsettings as $sectionsetting) {
472            $this->write_xml('setting', $sectionsetting);
473        }
474        unset($sectionsettings);
475
476        $this->xmlwriter->end_tag('settings');
477
478        $this->xmlwriter->end_tag('information');
479        $this->xmlwriter->end_tag('moodle_backup');
480
481        $this->close_xml_writer();
482
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'));
490        }
491        $this->xmlwriter->end_tag('files');
492        $this->close_xml_writer('files.xml');
493
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'));
501        }
502        $this->xmlwriter->end_tag('scales_definition');
503        $this->close_xml_writer('scales.xml');
504
505        ////////////////////////////////////////////////////////////////////////
506        // write course/inforef.xml
507        ////////////////////////////////////////////////////////////////////////
508        $this->open_xml_writer('course/inforef.xml');
509        $this->xmlwriter->begin_tag('inforef');
510
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));
517            }
518        }
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));
525            }
526        }
527        $this->xmlwriter->end_tag('fileref');
528
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));
532        }
533        $this->xmlwriter->end_tag('question_categoryref');
534
535        $this->xmlwriter->end_tag('inforef');
536        $this->close_xml_writer();
537
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()));
551    }
552}
553
554
555/**
556 * The class responsible for course and site files migration
557 *
558 * @todo migrate site_files
559 */
560class moodle1_files_handler extends moodle1_xml_handler {
561
562    /**
563     * Migrates course_files and site_files in the converter workdir
564     */
565    public function process() {
566        $this->migrate_course_files();
567        // todo $this->migrate_site_files();
568    }
569
570    /**
571     * Migrates course_files in the converter workdir
572     */
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);
580        }
581        $this->log('course files migrated', backup::LOG_INFO, count($ids));
582    }
583}
584
585
586/**
587 * Handles the conversion of /MOODLE_BACKUP/INFO paths
588 *
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.
591 */
592class moodle1_info_handler extends moodle1_handler {
593
594    /** @var array list of mod names included in info_details */
595    protected $modnames = array();
596
597    /** @var array the in-memory cache of the currently parsed info_details_mod element */
598    protected $currentmod;
599
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'),
606        );
607    }
608
609    /**
610     * Stashes the backup info for later processing by {@link moodle1_root_handler}
611     */
612    public function process_info($data) {
613        $this->converter->set_stash('backup_info', $data);
614    }
615
616    /**
617     * Initializes the in-memory cache for the current mod
618     */
619    public function process_info_details_mod($data) {
620        $this->currentmod = $data;
621        $this->currentmod['instances'] = array();
622    }
623
624    /**
625     * Appends the current instance data to the temporary in-memory cache
626     */
627    public function process_info_details_mod_instance($data) {
628        $this->currentmod['instances'][$data['id']] = $data;
629    }
630
631    /**
632     * Stashes the backup info for later processing by {@link moodle1_root_handler}
633     */
634    public function on_info_details_mod_end($data) {
635        global $CFG;
636
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);
644        }
645
646        $this->currentmod = array();
647    }
648
649    /**
650     * Stashes the list of activity module types for later processing by {@link moodle1_root_handler}
651     */
652    public function on_info_details_end() {
653        $this->converter->set_stash('modnameslist', $this->modnames);
654    }
655}
656
657
658/**
659 * Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths
660 */
661class moodle1_course_header_handler extends moodle1_xml_handler {
662
663    /** @var array we need to merge course information because it is dispatched twice */
664    protected $course = array();
665
666    /** @var array we need to merge course information because it is dispatched twice */
667    protected $courseraw = array();
668
669    /** @var array */
670    protected $category;
671
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(),
687                    ),
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',
709                    )
710                )
711            ),
712            new convert_path(
713                'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
714                array(
715                    'newfields' => array(
716                        'description' => null,
717                    )
718                )
719            ),
720        );
721    }
722
723    /**
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.
727     */
728    public function process_course_header($data, $raw) {
729       $this->course    = array_merge($this->course, $data);
730       $this->courseraw = array_merge($this->courseraw, $raw);
731    }
732
733    public function process_course_header_category($data) {
734        $this->category = $data;
735    }
736
737    public function on_course_header_end() {
738
739        $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
740
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
748        );
749        $this->converter->set_stash('original_course_info', $info);
750
751        $this->course['contextid'] = $contextid;
752        $this->course['category'] = $this->category;
753
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());
758
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();
763    }
764}
765
766
767/**
768 * Handles the conversion of course sections and course modules
769 */
770class moodle1_course_outline_handler extends moodle1_xml_handler {
771
772    /** @var array ordered list of the course contents */
773    protected $coursecontents = array();
774
775    /** @var array current section data */
776    protected $currentsection;
777
778    /**
779     * This handler is interested in course sections and course modules within them
780     */
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,
791                    ),
792                )
793            ),
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,
805                    ),
806                    'dropfields' => array(
807                        'instance',
808                        'roles_overrides',
809                        'roles_assignments',
810                    ),
811                    'renamefields' => array(
812                        'type' => 'modulename',
813                    ),
814                )
815            ),
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'),
819        );
820    }
821
822    public function process_course_section($data) {
823        $this->currentsection = $data;
824    }
825
826    /**
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
829     */
830    public function process_course_module($data, $raw) {
831        global $CFG;
832
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;
837        }
838
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
846        );
847
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'];
853        }
854
855        // add the sectionid and sectionnumber
856        $data['sectionid']      = $this->currentsection['id'];
857        $data['sectionnumber']  = $this->currentsection['number'];
858
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;
873        }
874
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']);
880    }
881
882    /**
883     * Writes sections/section_xxx/section.xml file and stashes it, too
884     */
885    public function on_course_section_end() {
886
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);
891
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));
900            }
901        }
902        $this->xmlwriter->end_tag('fileref');
903        $this->xmlwriter->end_tag('inforef');
904        $this->close_xml_writer();
905
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);
912    }
913
914    /**
915     * Stashes the course contents
916     */
917    public function on_course_sections_end() {
918        $this->converter->set_stash('coursecontents', $this->coursecontents);
919    }
920
921    /**
922     * Writes the information collected by mod handlers
923     */
924    public function on_course_modules_end() {
925
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'];
931
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();
936
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'));
945                    }
946                    $this->xmlwriter->end_tag('grade_items');
947                }
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();
951
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');
954            }
955        }
956    }
957}
958
959
960/**
961 * Handles the conversion of the defined roles
962 */
963class moodle1_roles_definition_handler extends moodle1_xml_handler {
964
965    /**
966     * Where the roles are defined in the source moodle.xml
967     */
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'     => ''
978                    )
979                )
980            )
981        );
982    }
983
984    /**
985     * If there are any roles defined in moodle.xml, convert them to roles.xml
986     */
987    public function process_roles_role($data) {
988
989        if (!$this->has_xml_writer()) {
990            $this->open_xml_writer('roles.xml');
991            $this->xmlwriter->begin_tag('roles_definition');
992        }
993        if (!isset($data['nameincourse'])) {
994            $data['nameincourse'] = null;
995        }
996        $this->write_xml('role', $data, array('role/id'));
997    }
998
999    /**
1000     * Finishes writing roles.xml
1001     */
1002    public function on_roles_end() {
1003
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());
1009
1010        } else {
1011            // some roles were dumped into the file, let us close their wrapper now
1012            $this->xmlwriter->end_tag('roles_definition');
1013        }
1014        $this->close_xml_writer();
1015    }
1016}
1017
1018
1019/**
1020 * Handles the conversion of the question bank included in the moodle.xml file
1021 */
1022class moodle1_question_bank_handler extends moodle1_xml_handler {
1023
1024    /** @var array the current question category being parsed */
1025    protected $currentcategory = null;
1026
1027    /** @var array of the raw data for the current category */
1028    protected $currentcategoryraw = null;
1029
1030    /** @var moodle1_file_manager instance used to convert question images */
1031    protected $fileman = null;
1032
1033    /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
1034    private $currentcategorywritten = false;
1035
1036    /** @var bool was the <questions> tag already written (work around MDL-27693) */
1037    private $questionswrapperwritten = false;
1038
1039    /** @var array holds the instances of qtype specific conversion handlers */
1040    private $qtypehandlers = null;
1041
1042    /**
1043     * Return the file manager instance used.
1044     *
1045     * @return moodle1_file_manager
1046     */
1047    public function get_file_manager() {
1048        return $this->fileman;
1049    }
1050
1051    /**
1052     * Returns the information about the question category context being currently parsed
1053     *
1054     * @return array with keys contextid, contextlevel and contextinstanceid
1055     */
1056    public function get_current_category_context() {
1057        return $this->currentcategory;
1058    }
1059
1060    /**
1061     * Registers path that are not qtype-specific
1062     */
1063    public function get_paths() {
1064
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
1072                    )
1073                )),
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),
1078        );
1079
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;
1085            }
1086        }
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);
1091        }
1092
1093        return $paths;
1094    }
1095
1096    /**
1097     * Starts writing questions.xml and prepares the file manager instance
1098     */
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();
1104        }
1105    }
1106
1107    /**
1108     * Initializes the current category cache
1109     */
1110    public function on_question_category_start() {
1111        $this->currentcategory         = array();
1112        $this->currentcategoryraw      = array();
1113        $this->currentcategorywritten  = false;
1114        $this->questionswrapperwritten = false;
1115    }
1116
1117    /**
1118     * Populates the current question category data
1119     *
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.
1122     */
1123    public function process_question_category($data, $raw) {
1124        $this->currentcategory    = array_merge($this->currentcategory, $data);
1125        $this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw);
1126    }
1127
1128    /**
1129     * Inject the context related information into the current category
1130     */
1131    public function process_question_category_context($data) {
1132
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;
1159        }
1160    }
1161
1162    /**
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
1165     *
1166     * @param array $data
1167     * @param array $raw
1168     * @return array
1169     */
1170    public function process_question(array $data, array $raw) {
1171        global $CFG;
1172
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
1176
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;
1182                }
1183                $this->xmlwriter->full_tag($name, $value);
1184            }
1185            $this->currentcategorywritten = true;
1186        }
1187
1188        if (empty($this->questionswrapperwritten)) {
1189            $this->xmlwriter->begin_tag('questions');
1190            $this->questionswrapperwritten = true;
1191        }
1192
1193        $qtype = $data['qtype'];
1194
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'];
1198        }
1199
1200        // replay the upgrade step 2010080900 and part of 2010080901
1201        $data['generalfeedbackformat'] = $data['questiontextformat'];
1202        $data['oldquestiontextformat'] = $data['questiontextformat'];
1203
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;
1209        }
1210
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);
1217
1218        // Migrate files in generalfeedback.
1219        $this->fileman->filearea  = 'generalfeedback';
1220        $data['generalfeedback'] = moodle1_converter::migrate_referenced_files($data['generalfeedback'], $this->fileman);
1221
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'] . '" />';
1227
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, './@#$ ').'/';
1237                }
1238
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 . '" />';
1249
1250                } else {
1251                    $this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename));
1252                }
1253            }
1254        }
1255        unset($data['image']);
1256
1257        // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
1258        $data['defaultmark'] = $data['defaultgrade'];
1259
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);
1270            }
1271            $this->xmlwriter->full_tag($fieldname, $data[$fieldname]);
1272        }
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);
1279
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');
1285            }
1286        }
1287
1288        $this->xmlwriter->end_tag('question');
1289    }
1290
1291    /**
1292     * Closes the questions wrapper
1293     */
1294    public function on_questions_end() {
1295        if ($this->questionswrapperwritten) {
1296            $this->xmlwriter->end_tag('questions');
1297        }
1298    }
1299
1300    /**
1301     * Closes the question_category and annotates the category id
1302     * so that it can be dumped into course/inforef.xml
1303     */
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');
1312        }
1313        $this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']);
1314    }
1315
1316    /**
1317     * Stops writing questions.xml
1318     */
1319    public function on_question_categories_end() {
1320        $this->xmlwriter->end_tag('question_categories');
1321        $this->close_xml_writer();
1322    }
1323
1324    /**
1325     * Provides access to the qtype handlers
1326     *
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.
1329     *
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
1333     */
1334    protected function get_qtype_handler($qtype) {
1335
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);
1346                    }
1347                    $this->log('registering handler', backup::LOG_DEBUG, $classname, 2);
1348                    $this->qtypehandlers[$qtypename] = new $classname($this, $qtypename);
1349                }
1350            }
1351        }
1352
1353        if ($qtype === '*') {
1354            return $this->qtypehandlers;
1355
1356        } else if (isset($this->qtypehandlers[$qtype])) {
1357            return $this->qtypehandlers[$qtype];
1358
1359        } else {
1360            return false;
1361        }
1362    }
1363}
1364
1365
1366/**
1367 * Handles the conversion of the scales included in the moodle.xml file
1368 */
1369class moodle1_scales_handler extends moodle1_handler {
1370
1371    /** @var moodle1_file_manager instance used to convert question images */
1372    protected $fileman = null;
1373
1374    /**
1375     * Registers paths
1376     */
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',
1385                    ),
1386                    'addfields' => array(
1387                        'descriptionformat' => 0,
1388                    )
1389                )
1390            ),
1391        );
1392    }
1393
1394    /**
1395     * Prepare the file manager for the files embedded in the scale description field
1396     */
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');
1400    }
1401
1402    /**
1403     * This is executed every time we have one <SCALE> data available
1404     *
1405     * @param array $data
1406     * @param array $raw
1407     * @return array
1408     */
1409    public function process_scale(array $data, array $raw) {
1410        global $CFG;
1411
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;
1416        }
1417
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);
1421
1422        // stash the scale
1423        $this->converter->set_stash('scales', $data, $data['id']);
1424    }
1425}
1426
1427
1428/**
1429 * Handles the conversion of the outcomes
1430 */
1431class moodle1_outcomes_handler extends moodle1_xml_handler {
1432
1433    /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
1434    protected $fileman = null;
1435
1436    /**
1437     * Registers paths
1438     */
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,
1447                    ),
1448                )
1449            ),
1450        );
1451    }
1452
1453    /**
1454     * Prepares the file manager and starts writing outcomes.xml
1455     */
1456    public function on_gradebook_grade_outcomes_start() {
1457
1458        $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1459        $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome');
1460
1461        $this->open_xml_writer('outcomes.xml');
1462        $this->xmlwriter->begin_tag('outcomes_definition');
1463    }
1464
1465    /**
1466     * Processes GRADE_OUTCOME tags progressively
1467     */
1468    public function process_gradebook_grade_outcome(array $data, array $raw) {
1469        global $CFG;
1470
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;
1475        }
1476
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);
1480
1481        // write the outcome data
1482        $this->write_xml('outcome', $data, array('/outcome/id'));
1483
1484        return $data;
1485    }
1486
1487    /**
1488     * Closes outcomes.xml
1489     */
1490    public function on_gradebook_grade_outcomes_end() {
1491        $this->xmlwriter->end_tag('outcomes_definition');
1492        $this->close_xml_writer();
1493    }
1494}
1495
1496
1497/**
1498 * Handles the conversion of the gradebook structures in the moodle.xml file
1499 */
1500class moodle1_gradebook_handler extends moodle1_xml_handler {
1501
1502    /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
1503    protected $categoryparent = array();
1504
1505    /**
1506     * Registers paths
1507     */
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
1517                    ),
1518                )
1519            ),
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'),
1522        );
1523    }
1524
1525    /**
1526     * Initializes the in-memory structures
1527     *
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 ;-)
1531     */
1532    public function on_gradebook_start() {
1533        $this->categoryparent = array();
1534    }
1535
1536    /**
1537     * Processes one GRADE_LETTER data
1538     *
1539     * In Moodle 1.9, all grade_letters are from course context only. Therefore
1540     * we put them here.
1541     */
1542    public function process_gradebook_grade_letter(array $data, array $raw) {
1543        $this->converter->set_stash('gradebook_gradeletter', $data, $data['id']);
1544    }
1545
1546    /**
1547     * Processes one GRADE_CATEGORY data
1548     */
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']);
1552    }
1553
1554    /**
1555     * Processes one GRADE_ITEM data
1556     */
1557    public function process_gradebook_grade_item(array $data, array $raw) {
1558
1559        // here we use get_nextid() to get a nondecreasing sequence
1560        $data['sortorder'] = $this->converter->get_nextid();
1561
1562        if ($data['itemtype'] === 'mod') {
1563            return $this->process_mod_grade_item($data, $raw);
1564
1565        } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
1566            return $this->process_nonmod_grade_item($data, $raw);
1567
1568        } else {
1569            $this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']);
1570        }
1571    }
1572
1573    /**
1574     * Processes one GRADE_ITEM of the type 'mod'
1575     */
1576    protected function process_mod_grade_item(array $data, array $raw) {
1577
1578        $stashname   = 'gradebook_modgradeitem_'.$data['itemmodule'];
1579        $stashitemid = $data['iteminstance'];
1580        $gradeitems  = $this->converter->get_stash_or_default($stashname, $stashitemid, array());
1581
1582        // typically there will be single item with itemnumber 0
1583        $gradeitems[$data['itemnumber']] = $data;
1584
1585        $this->converter->set_stash($stashname, $gradeitems, $stashitemid);
1586
1587        return $data;
1588    }
1589
1590    /**
1591     * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
1592     */
1593    protected function process_nonmod_grade_item(array $data, array $raw) {
1594
1595        $stashname   = 'gradebook_nonmodgradeitem';
1596        $stashitemid = $data['id'];
1597        $this->converter->set_stash($stashname, $data, $stashitemid);
1598
1599        return $data;
1600    }
1601
1602    /**
1603     * @todo
1604     */
1605    public function on_gradebook_grade_item_grades_start() {
1606    }
1607
1608    /**
1609     * Writes the collected information into gradebook.xml
1610     */
1611    public function on_gradebook_end() {
1612
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();
1620    }
1621
1622    /**
1623     * Writes grade_categories
1624     */
1625    protected function write_grade_categories() {
1626
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'));
1634        }
1635        $this->xmlwriter->end_tag('grade_categories');
1636    }
1637
1638    /**
1639     * Calculates the path to the grade_category
1640     *
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.
1643     *
1644     * @param int $categoryid
1645     * @return array of ids including the categoryid
1646     */
1647    protected function calculate_category_path($categoryid) {
1648
1649        if (!array_key_exists($categoryid, $this->categoryparent)) {
1650            throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
1651        }
1652
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');
1660            }
1661        }
1662
1663        return $path;
1664    }
1665
1666    /**
1667     * Writes grade_items
1668     */
1669    protected function write_grade_items() {
1670
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'));
1675        }
1676        $this->xmlwriter->end_tag('grade_items');
1677    }
1678
1679    /**
1680     * Writes grade_letters
1681     */
1682    protected function write_grade_letters() {
1683
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'));
1688        }
1689        $this->xmlwriter->end_tag('grade_letters');
1690    }
1691}
1692
1693
1694/**
1695 * Shared base class for activity modules, blocks and qtype handlers
1696 */
1697abstract class moodle1_plugin_handler extends moodle1_xml_handler {
1698
1699    /** @var string */
1700    protected $plugintype;
1701
1702    /** @var string */
1703    protected $pluginname;
1704
1705    /**
1706     * @param moodle1_converter $converter the converter that requires us
1707     * @param string $plugintype
1708     * @param string $pluginname
1709     */
1710    public function __construct(moodle1_converter $converter, $plugintype, $pluginname) {
1711
1712        parent::__construct($converter);
1713        $this->plugintype = $plugintype;
1714        $this->pluginname = $pluginname;
1715    }
1716
1717    /**
1718     * Returns the normalized name of the plugin, eg mod_workshop
1719     *
1720     * @return string
1721     */
1722    public function get_component_name() {
1723        return $this->plugintype.'_'.$this->pluginname;
1724    }
1725}
1726
1727
1728/**
1729 * Base class for all question type handlers
1730 */
1731abstract class moodle1_qtype_handler extends moodle1_plugin_handler {
1732
1733    /** @var moodle1_question_bank_handler */
1734    protected $qbankhandler;
1735
1736    /**
1737     * Returns the list of paths within one <QUESTION> that this qtype needs to have included
1738     * in the grouped question structure
1739     *
1740     * @return array of strings
1741     */
1742    public function get_question_subpaths() {
1743        return array();
1744    }
1745
1746    /**
1747     * Gives the qtype handler a chance to write converted data into questions.xml
1748     *
1749     * @param array $data grouped question data
1750     * @param array $raw grouped raw QUESTION data
1751     */
1752    public function process_question(array $data, array $raw) {
1753    }
1754
1755    /**
1756     * Converts the answers and writes them into the questions.xml
1757     *
1758     * The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
1759     *
1760     * @param array $answers as parsed by the grouped parser in moodle.xml
1761     * @param string $qtype containing the answers
1762     */
1763    protected function write_answers(array $answers, $qtype) {
1764
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']);
1772                }
1773                // Migrate images in feedback.
1774                if ($answer['feedbackformat'] == FORMAT_HTML) {
1775                    $answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']);
1776                }
1777                $this->write_xml('answer', $answer, array('/answer/id'));
1778            }
1779        }
1780        $this->xmlwriter->end_tag('answers');
1781    }
1782
1783    /**
1784     * Migrate files belonging to one qtype plugin text field.
1785     *
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
1790     *
1791     * @return string the text for this field, after files references have been processed
1792     */
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;
1802    }
1803
1804    /**
1805     * Writes the grouped numerical_units structure
1806     *
1807     * @param array $numericalunits
1808     */
1809    protected function write_numerical_units(array $numericalunits) {
1810
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'));
1816            }
1817        }
1818        $this->xmlwriter->end_tag('numerical_units');
1819    }
1820
1821    /**
1822     * Writes the numerical_options structure
1823     *
1824     * @see get_default_numerical_options()
1825     * @param array $numericaloption
1826     */
1827    protected function write_numerical_options(array $numericaloption) {
1828
1829        $this->xmlwriter->begin_tag('numerical_options');
1830        if (!empty($numericaloption)) {
1831            $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
1832        }
1833        $this->xmlwriter->end_tag('numerical_options');
1834    }
1835
1836    /**
1837     * Returns default numerical_option structure
1838     *
1839     * This structure is not present in moodle.xml, we create a new artificial one here.
1840     *
1841     * @see write_numerical_options()
1842     * @param int $oldquestiontextformat
1843     * @return array
1844     */
1845    protected function get_default_numerical_options($oldquestiontextformat, $units) {
1846        global $CFG;
1847
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
1857        );
1858
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;
1864        }
1865
1866        // Set a good default, depending on whether there are any units defined.
1867        if (empty($units)) {
1868            $options['showunits'] = 3;
1869        }
1870
1871        return $options;
1872    }
1873
1874    /**
1875     * Writes the dataset_definitions structure
1876     *
1877     * @param array $datasetdefinitions array of dataset_definition structures
1878     */
1879    protected function write_dataset_definitions(array $datasetdefinitions) {
1880
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]);
1886            }
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'));
1892                }
1893            }
1894            $this->xmlwriter->end_tag('dataset_items');
1895            $this->xmlwriter->end_tag('dataset_definition');
1896        }
1897        $this->xmlwriter->end_tag('dataset_definitions');
1898    }
1899
1900    /// implementation details follow //////////////////////////////////////////
1901
1902    public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) {
1903
1904        parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
1905        $this->qbankhandler = $qbankhandler;
1906    }
1907
1908    /**
1909     * @see self::get_question_subpaths()
1910     */
1911    final public function get_paths() {
1912        throw new moodle1_convert_exception('qtype_handler_get_paths');
1913    }
1914
1915    /**
1916     * Question type handlers cannot open the xml_writer
1917     */
1918    final protected function open_xml_writer($filename) {
1919        throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1920    }
1921
1922    /**
1923     * Question type handlers cannot close the xml_writer
1924     */
1925    final protected function close_xml_writer() {
1926        throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1927    }
1928
1929    /**
1930     * Provides a xml_writer instance to this qtype converter
1931     *
1932     * @param xml_writer $xmlwriter
1933     */
1934    public function use_xml_writer(xml_writer $xmlwriter) {
1935        $this->xmlwriter = $xmlwriter;
1936    }
1937
1938    /**
1939     * Converts <ANSWER> structure into the new <answer> one
1940     *
1941     * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
1942     *
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
1946     */
1947    private function convert_answer(array $old, $qtype) {
1948        global $CFG;
1949
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
1957
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;
1964        }
1965
1966        if ($CFG->texteditors !== 'textarea') {
1967            if ($qtype == 'essay') {
1968                $new['feedback'] = text_to_html($new['feedback'], false, false, true);
1969            }
1970            $new['feedbackformat'] = FORMAT_HTML;
1971
1972        } else {
1973            $new['feedbackformat'] = FORMAT_MOODLE;
1974        }
1975
1976        return $new;
1977    }
1978}
1979
1980
1981/**
1982 * Base class for activity module handlers
1983 */
1984abstract class moodle1_mod_handler extends moodle1_plugin_handler {
1985
1986    /**
1987     * Returns the name of the module, eg. 'forum'
1988     *
1989     * @return string
1990     */
1991    public function get_modname() {
1992        return $this->pluginname;
1993    }
1994
1995    /**
1996     * Returns course module information for the given instance id
1997     *
1998     * The information for this instance id has been stashed by
1999     * {@link moodle1_course_outline_handler::process_course_module()}
2000     *
2001     * @param int $instance the module instance id
2002     * @param string $modname the module type, defaults to $this->pluginname
2003     * @return int
2004     */
2005    protected function get_cminfo($instance, $modname = null) {
2006
2007        if (is_null($modname)) {
2008            $modname = $this->pluginname;
2009        }
2010        return $this->converter->get_stash('cminfo_'.$modname, $instance);
2011    }
2012}
2013
2014
2015/**
2016 * Base class for all modules that are successors of the 1.9 resource module
2017 */
2018abstract class moodle1_resource_successor_handler extends moodle1_mod_handler {
2019
2020    /**
2021     * Resource successors do not attach to paths themselves, they are called explicitely
2022     * by moodle1_mod_resource_handler
2023     *
2024     * @return array
2025     */
2026    final public function get_paths() {
2027        return array();
2028    }
2029
2030    /**
2031     * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
2032     *
2033     * Called by {@link moodle1_mod_resource_handler::process_resource()}
2034     *
2035     * @param array $data pre-cooked legacy resource data
2036     * @param array $raw raw legacy resource data
2037     */
2038    public function process_legacy_resource(array $data, array $raw = null) {
2039    }
2040
2041    /**
2042     * Called when the parses reaches the end </MOD> resource tag
2043     *
2044     * @param array $data the data returned by {@link self::process_resource} or just pre-cooked
2045     */
2046    public function on_legacy_resource_end(array $data) {
2047    }
2048}
2049
2050/**
2051 * Base class for block handlers
2052 */
2053abstract class moodle1_block_handler extends moodle1_plugin_handler {
2054
2055    public function get_paths() {
2056        $blockname = strtoupper($this->pluginname);
2057        return array(
2058            new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
2059        );
2060    }
2061
2062    public function process_block(array $data) {
2063        $newdata = $this->convert_common_block_data($data);
2064
2065        $this->write_block_xml($newdata, $data);
2066        $this->write_inforef_xml($newdata, $data);
2067        $this->write_roles_xml($newdata, $data);
2068
2069        return $data;
2070    }
2071
2072    protected function convert_common_block_data(array $olddata) {
2073        $newdata = array();
2074
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);
2083
2084        return $newdata;
2085    }
2086
2087    protected function convert_configdata(array $olddata) {
2088        return $olddata['configdata'];
2089    }
2090
2091    protected function write_block_xml($newdata, $data) {
2092        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
2093
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));
2096
2097        foreach ($newdata as $field => $value) {
2098            $this->xmlwriter->full_tag($field, $value);
2099        }
2100
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();
2113    }
2114
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();
2121    }
2122
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();
2131    }
2132}
2133
2134
2135/**
2136 * Base class for block generic handler
2137 */
2138class moodle1_block_generic_handler extends moodle1_block_handler {
2139
2140}
2141
2142/**
2143 * Base class for the activity modules' subplugins
2144 */
2145abstract class moodle1_submod_handler extends moodle1_plugin_handler {
2146
2147    /** @var moodle1_mod_handler */
2148    protected $parenthandler;
2149
2150    /**
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
2154     */
2155    public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) {
2156        $this->parenthandler = $parenthandler;
2157        parent::__construct($parenthandler->converter, $subplugintype, $subpluginname);
2158    }
2159
2160    /**
2161     * Activity module subplugins can't declare any paths to handle
2162     *
2163     * The paths must be registered by the parent module and then re-dispatched to the
2164     * relevant subplugins for eventual processing.
2165     *
2166     * @return array empty array
2167     */
2168    final public function get_paths() {
2169        return array();
2170    }
2171}
2172