Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
30
defined('MOODLE_INTERNAL') || die();
31
 
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');
35
 
36
/**
37
 * Handlers factory class
38
 */
39
abstract 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
 */
112
abstract 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
 */
149
abstract 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
 */
299
class 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
 */
560
class 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
 */
592
class 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
 */
661
class 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
 */
770
class 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
            // Have to hardcode - since quiz uses some hardcoded version numbers when restoring.
871
            // This is the lowest number used minus one.
872
            $data['version'] = 2011010099;
873
        } else {
874
            $data['version'] = null;
875
        }
876
 
877
        // stash the course module info in stashes like 'cminfo_forum' with
878
        // itemid set to the instance id. this is needed so that module handlers
879
        // can later obtain information about the course module and dump it into
880
        // the module.xml file
881
        $this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
882
    }
883
 
884
    /**
885
     * Writes sections/section_xxx/section.xml file and stashes it, too
886
     */
887
    public function on_course_section_end() {
888
 
889
        // migrate files embedded into the section summary field
890
        $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
891
        $fileman = $this->converter->get_file_manager($contextid, 'course', 'section', $this->currentsection['id']);
892
        $this->currentsection['summary'] = moodle1_converter::migrate_referenced_files($this->currentsection['summary'], $fileman);
893
 
894
        // write section's inforef.xml with the file references
895
        $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/inforef.xml');
896
        $this->xmlwriter->begin_tag('inforef');
897
        $this->xmlwriter->begin_tag('fileref');
898
        $fileids = $fileman->get_fileids();
899
        if (is_array($fileids)) {
900
            foreach ($fileids as $fileid) {
901
                $this->write_xml('file', array('id' => $fileid));
902
            }
903
        }
904
        $this->xmlwriter->end_tag('fileref');
905
        $this->xmlwriter->end_tag('inforef');
906
        $this->close_xml_writer();
907
 
908
        // stash the section info and write section.xml
909
        $this->converter->set_stash('sectioninfo', $this->currentsection, $this->currentsection['id']);
910
        $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml');
911
        $this->write_xml('section', $this->currentsection);
912
        $this->close_xml_writer();
913
        unset($this->currentsection);
914
    }
915
 
916
    /**
917
     * Stashes the course contents
918
     */
919
    public function on_course_sections_end() {
920
        $this->converter->set_stash('coursecontents', $this->coursecontents);
921
    }
922
 
923
    /**
924
     * Writes the information collected by mod handlers
925
     */
926
    public function on_course_modules_end() {
927
 
928
        foreach ($this->converter->get_stash('modnameslist') as $modname) {
929
            $modinfo = $this->converter->get_stash('modinfo_'.$modname);
930
            foreach ($modinfo['instances'] as $modinstanceid => $modinstance) {
931
                $cminfo    = $this->converter->get_stash('cminfo_'.$modname, $modinstanceid);
932
                $directory = 'activities/'.$modname.'_'.$cminfo['id'];
933
 
934
                // write module.xml
935
                $this->open_xml_writer($directory.'/module.xml');
936
                $this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
937
                $this->close_xml_writer();
938
 
939
                // write grades.xml
940
                $this->open_xml_writer($directory.'/grades.xml');
941
                $this->xmlwriter->begin_tag('activity_gradebook');
942
                $gradeitems = $this->converter->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array());
943
                if (!empty($gradeitems)) {
944
                    $this->xmlwriter->begin_tag('grade_items');
945
                    foreach ($gradeitems as $gradeitem) {
946
                        $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
947
                    }
948
                    $this->xmlwriter->end_tag('grade_items');
949
                }
950
                $this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9
951
                $this->xmlwriter->end_tag('activity_gradebook');
952
                $this->close_xml_writer();
953
 
954
                // todo: write proper roles.xml, for now we just make sure the file is present
955
                $this->make_sure_xml_exists($directory.'/roles.xml', 'roles');
956
            }
957
        }
958
    }
959
}
960
 
961
 
962
/**
963
 * Handles the conversion of the defined roles
964
 */
965
class moodle1_roles_definition_handler extends moodle1_xml_handler {
966
 
967
    /**
968
     * Where the roles are defined in the source moodle.xml
969
     */
970
    public function get_paths() {
971
        return array(
972
            new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
973
            new convert_path(
974
                'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
975
                array(
976
                    'newfields' => array(
977
                        'description'   => '',
978
                        'sortorder'     => 0,
979
                        'archetype'     => ''
980
                    )
981
                )
982
            )
983
        );
984
    }
985
 
986
    /**
987
     * If there are any roles defined in moodle.xml, convert them to roles.xml
988
     */
989
    public function process_roles_role($data) {
990
 
991
        if (!$this->has_xml_writer()) {
992
            $this->open_xml_writer('roles.xml');
993
            $this->xmlwriter->begin_tag('roles_definition');
994
        }
995
        if (!isset($data['nameincourse'])) {
996
            $data['nameincourse'] = null;
997
        }
998
        $this->write_xml('role', $data, array('role/id'));
999
    }
1000
 
1001
    /**
1002
     * Finishes writing roles.xml
1003
     */
1004
    public function on_roles_end() {
1005
 
1006
        if (!$this->has_xml_writer()) {
1007
            // no roles defined in moodle.xml so {link self::process_roles_role()}
1008
            // was never executed
1009
            $this->open_xml_writer('roles.xml');
1010
            $this->write_xml('roles_definition', array());
1011
 
1012
        } else {
1013
            // some roles were dumped into the file, let us close their wrapper now
1014
            $this->xmlwriter->end_tag('roles_definition');
1015
        }
1016
        $this->close_xml_writer();
1017
    }
1018
}
1019
 
1020
 
1021
/**
1022
 * Handles the conversion of the question bank included in the moodle.xml file
1023
 */
1024
class moodle1_question_bank_handler extends moodle1_xml_handler {
1025
 
1026
    /** @var array the current question category being parsed */
1027
    protected $currentcategory = null;
1028
 
1029
    /** @var array of the raw data for the current category */
1030
    protected $currentcategoryraw = null;
1031
 
1032
    /** @var moodle1_file_manager instance used to convert question images */
1033
    protected $fileman = null;
1034
 
1035
    /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
1036
    private $currentcategorywritten = false;
1037
 
1038
    /** @var bool was the <questions> tag already written (work around MDL-27693) */
1039
    private $questionswrapperwritten = false;
1040
 
1041
    /** @var array holds the instances of qtype specific conversion handlers */
1042
    private $qtypehandlers = null;
1043
 
1044
    /**
1045
     * Return the file manager instance used.
1046
     *
1047
     * @return moodle1_file_manager
1048
     */
1049
    public function get_file_manager() {
1050
        return $this->fileman;
1051
    }
1052
 
1053
    /**
1054
     * Returns the information about the question category context being currently parsed
1055
     *
1056
     * @return array with keys contextid, contextlevel and contextinstanceid
1057
     */
1058
    public function get_current_category_context() {
1059
        return $this->currentcategory;
1060
    }
1061
 
1062
    /**
1063
     * Registers path that are not qtype-specific
1064
     */
1065
    public function get_paths() {
1066
 
1067
        $paths = array(
1068
            new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
1069
            new convert_path(
1070
                'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
1071
                array(
1072
                    'newfields' => array(
1073
                        'infoformat' => 0
1074
                    )
1075
                )),
1076
            new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'),
1077
            new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'),
1078
            // the question element must be grouped so we can re-dispatch it to the qtype handler as a whole
1079
            new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true),
1080
        );
1081
 
1082
        // annotate all question subpaths required by the qtypes subplugins
1083
        $subpaths = array();
1084
        foreach ($this->get_qtype_handler('*') as $qtypehandler) {
1085
            foreach ($qtypehandler->get_question_subpaths() as $subpath) {
1086
                $subpaths[$subpath] = true;
1087
            }
1088
        }
1089
        foreach (array_keys($subpaths) as $subpath) {
1090
            $name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath));
1091
            $path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath;
1092
            $paths[] = new convert_path($name, $path);
1093
        }
1094
 
1095
        return $paths;
1096
    }
1097
 
1098
    /**
1099
     * Starts writing questions.xml and prepares the file manager instance
1100
     */
1101
    public function on_question_categories_start() {
1102
        $this->open_xml_writer('questions.xml');
1103
        $this->xmlwriter->begin_tag('question_categories');
1104
        if (is_null($this->fileman)) {
1105
            $this->fileman = $this->converter->get_file_manager();
1106
        }
1107
    }
1108
 
1109
    /**
1110
     * Initializes the current category cache
1111
     */
1112
    public function on_question_category_start() {
1113
        $this->currentcategory         = array();
1114
        $this->currentcategoryraw      = array();
1115
        $this->currentcategorywritten  = false;
1116
        $this->questionswrapperwritten = false;
1117
    }
1118
 
1119
    /**
1120
     * Populates the current question category data
1121
     *
1122
     * Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually
1123
     * called twice for both halves of the data. We merge them here into the currentcategory array.
1124
     */
1125
    public function process_question_category($data, $raw) {
1126
        $this->currentcategory    = array_merge($this->currentcategory, $data);
1127
        $this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw);
1128
    }
1129
 
1130
    /**
1131
     * Inject the context related information into the current category
1132
     */
1133
    public function process_question_category_context($data) {
1134
 
1135
        switch ($data['level']) {
1136
        case 'module':
1137
            $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']);
1138
            $this->currentcategory['contextlevel'] = CONTEXT_MODULE;
1139
            $this->currentcategory['contextinstanceid'] = $data['instance'];
1140
            break;
1141
        case 'course':
1142
            $originalcourseinfo = $this->converter->get_stash('original_course_info');
1143
            $originalcourseid   = $originalcourseinfo['original_course_id'];
1144
            $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSE);
1145
            $this->currentcategory['contextlevel'] = CONTEXT_COURSE;
1146
            $this->currentcategory['contextinstanceid'] = $originalcourseid;
1147
            break;
1148
        case 'coursecategory':
1149
            // this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance
1150
            // of the course category (1 = parent category, 2 = grand-parent category etc). We pretend
1151
            // that this level*10 is the id of that category and create an artifical contextid for it
1152
            $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSECAT, $data['coursecategorylevel'] * 10);
1153
            $this->currentcategory['contextlevel'] = CONTEXT_COURSECAT;
1154
            $this->currentcategory['contextinstanceid'] = $data['coursecategorylevel'] * 10;
1155
            break;
1156
        case 'system':
1157
            $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM);
1158
            $this->currentcategory['contextlevel'] = CONTEXT_SYSTEM;
1159
            $this->currentcategory['contextinstanceid'] = 0;
1160
            break;
1161
        }
1162
    }
1163
 
1164
    /**
1165
     * Writes the common <question> data and re-dispateches the whole grouped
1166
     * <QUESTION> data to the qtype for appending its qtype specific data processing
1167
     *
1168
     * @param array $data
1169
     * @param array $raw
1170
     * @return array
1171
     */
1172
    public function process_question(array $data, array $raw) {
1173
        global $CFG;
1174
 
1175
        // firstly make sure that the category data and the <questions> wrapper are written
1176
        // note that because of MDL-27693 we can't use {@link self::process_question_category()}
1177
        // and {@link self::on_questions_start()} to do so
1178
 
1179
        if (empty($this->currentcategorywritten)) {
1180
            $this->xmlwriter->begin_tag('question_category', array('id' => $this->currentcategory['id']));
1181
            foreach ($this->currentcategory as $name => $value) {
1182
                if ($name === 'id') {
1183
                    continue;
1184
                }
1185
                $this->xmlwriter->full_tag($name, $value);
1186
            }
1187
            $this->currentcategorywritten = true;
1188
        }
1189
 
1190
        if (empty($this->questionswrapperwritten)) {
1191
            $this->xmlwriter->begin_tag('questions');
1192
            $this->questionswrapperwritten = true;
1193
        }
1194
 
1195
        $qtype = $data['qtype'];
1196
 
1197
        // replay the upgrade step 2008050700 {@see question_fix_random_question_parents()}
1198
        if ($qtype == 'random' and $data['parent'] <> $data['id']) {
1199
            $data['parent'] = $data['id'];
1200
        }
1201
 
1202
        // replay the upgrade step 2010080900 and part of 2010080901
1203
        $data['generalfeedbackformat'] = $data['questiontextformat'];
1204
        $data['oldquestiontextformat'] = $data['questiontextformat'];
1205
 
1206
        if ($CFG->texteditors !== 'textarea') {
1207
            $data['questiontext'] = text_to_html($data['questiontext'], false, false, true);
1208
            $data['questiontextformat'] = FORMAT_HTML;
1209
            $data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true);
1210
            $data['generalfeedbackformat'] = FORMAT_HTML;
1211
        }
1212
 
1213
        // Migrate files in questiontext.
1214
        $this->fileman->contextid = $this->currentcategory['contextid'];
1215
        $this->fileman->component = 'question';
1216
        $this->fileman->filearea  = 'questiontext';
1217
        $this->fileman->itemid    = $data['id'];
1218
        $data['questiontext'] = moodle1_converter::migrate_referenced_files($data['questiontext'], $this->fileman);
1219
 
1220
        // Migrate files in generalfeedback.
1221
        $this->fileman->filearea  = 'generalfeedback';
1222
        $data['generalfeedback'] = moodle1_converter::migrate_referenced_files($data['generalfeedback'], $this->fileman);
1223
 
1224
        // replay the upgrade step 2010080901 - updating question image
1225
        if (!empty($data['image'])) {
1226
            if (core_text::substr(core_text::strtolower($data['image']), 0, 7) == 'http://') {
1227
                // it is a link, appending to existing question text
1228
                $data['questiontext'] .= ' <img src="' . $data['image'] . '" />';
1229
 
1230
            } else {
1231
                // it is a file in course_files
1232
                $filename = basename($data['image']);
1233
                $filepath = dirname($data['image']);
1234
                if (empty($filepath) or $filepath == '.' or $filepath == '/') {
1235
                    $filepath = '/';
1236
                } else {
1237
                    // append /
1238
                    $filepath = '/'.trim($filepath, './@#$ ').'/';
1239
                }
1240
 
1241
                if (file_exists($this->converter->get_tempdir_path().'/course_files'.$filepath.$filename)) {
1242
                    $this->fileman->contextid = $this->currentcategory['contextid'];
1243
                    $this->fileman->component = 'question';
1244
                    $this->fileman->filearea  = 'questiontext';
1245
                    $this->fileman->itemid    = $data['id'];
1246
                    $this->fileman->migrate_file('course_files'.$filepath.$filename, '/', $filename);
1247
                    // note this is slightly different from the upgrade code as we put the file into the
1248
                    // root folder here. this makes our life easier as we do not need to create all the
1249
                    // directories within the specified filearea/itemid
1250
                    $data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
1251
 
1252
                } else {
1253
                    $this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename));
1254
                }
1255
            }
1256
        }
1257
        unset($data['image']);
1258
 
1259
        // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
1260
        $data['defaultmark'] = $data['defaultgrade'];
1261
 
1262
        // write the common question data
1263
        $this->xmlwriter->begin_tag('question', array('id' => $data['id']));
1264
        foreach (array(
1265
            'parent', 'name', 'questiontext', 'questiontextformat',
1266
            'generalfeedback', 'generalfeedbackformat', 'defaultmark',
1267
            'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
1268
            'timecreated', 'timemodified', 'createdby', 'modifiedby'
1269
        ) as $fieldname) {
1270
            if (!array_key_exists($fieldname, $data)) {
1271
                throw new moodle1_convert_exception('missing_common_question_field', $fieldname);
1272
            }
1273
            $this->xmlwriter->full_tag($fieldname, $data[$fieldname]);
1274
        }
1275
        // unless we know that the given qtype does not append any own structures,
1276
        // give the handler a chance to do so now
1277
        if (!in_array($qtype, array('description', 'random'))) {
1278
            $handler = $this->get_qtype_handler($qtype);
1279
            if ($handler === false) {
1280
                $this->log('question type converter not found', backup::LOG_ERROR, $qtype);
1281
 
1282
            } else {
1283
                $this->xmlwriter->begin_tag('plugin_qtype_'.$qtype.'_question');
1284
                $handler->use_xml_writer($this->xmlwriter);
1285
                $handler->process_question($data, $raw);
1286
                $this->xmlwriter->end_tag('plugin_qtype_'.$qtype.'_question');
1287
            }
1288
        }
1289
 
1290
        $this->xmlwriter->end_tag('question');
1291
    }
1292
 
1293
    /**
1294
     * Closes the questions wrapper
1295
     */
1296
    public function on_questions_end() {
1297
        if ($this->questionswrapperwritten) {
1298
            $this->xmlwriter->end_tag('questions');
1299
        }
1300
    }
1301
 
1302
    /**
1303
     * Closes the question_category and annotates the category id
1304
     * so that it can be dumped into course/inforef.xml
1305
     */
1306
    public function on_question_category_end() {
1307
        // make sure that the category data were written by {@link self::process_question()}
1308
        // if not, write it now. this may happen when the current category does not contain any
1309
        // questions so the subpaths is missing completely
1310
        if (empty($this->currentcategorywritten)) {
1311
            $this->write_xml('question_category', $this->currentcategory, array('/question_category/id'));
1312
        } else {
1313
            $this->xmlwriter->end_tag('question_category');
1314
        }
1315
        $this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']);
1316
    }
1317
 
1318
    /**
1319
     * Stops writing questions.xml
1320
     */
1321
    public function on_question_categories_end() {
1322
        $this->xmlwriter->end_tag('question_categories');
1323
        $this->close_xml_writer();
1324
    }
1325
 
1326
    /**
1327
     * Provides access to the qtype handlers
1328
     *
1329
     * Returns either list of all qtype handler instances (if passed '*') or a particular handler
1330
     * for the given qtype or false if the qtype is not supported.
1331
     *
1332
     * @throws moodle1_convert_exception
1333
     * @param string $qtype the name of the question type or '*' for returning all
1334
     * @return array|moodle1_qtype_handler|bool
1335
     */
1336
    protected function get_qtype_handler($qtype) {
1337
 
1338
        if (is_null($this->qtypehandlers)) {
1339
            // initialize the list of qtype handler instances
1340
            $this->qtypehandlers = array();
1341
            foreach (core_component::get_plugin_list('qtype') as $qtypename => $qtypelocation) {
1342
                $filename = $qtypelocation.'/backup/moodle1/lib.php';
1343
                if (file_exists($filename)) {
1344
                    $classname = 'moodle1_qtype_'.$qtypename.'_handler';
1345
                    require_once($filename);
1346
                    if (!class_exists($classname)) {
1347
                        throw new moodle1_convert_exception('missing_handler_class', $classname);
1348
                    }
1349
                    $this->log('registering handler', backup::LOG_DEBUG, $classname, 2);
1350
                    $this->qtypehandlers[$qtypename] = new $classname($this, $qtypename);
1351
                }
1352
            }
1353
        }
1354
 
1355
        if ($qtype === '*') {
1356
            return $this->qtypehandlers;
1357
 
1358
        } else if (isset($this->qtypehandlers[$qtype])) {
1359
            return $this->qtypehandlers[$qtype];
1360
 
1361
        } else {
1362
            return false;
1363
        }
1364
    }
1365
}
1366
 
1367
 
1368
/**
1369
 * Handles the conversion of the scales included in the moodle.xml file
1370
 */
1371
class moodle1_scales_handler extends moodle1_handler {
1372
 
1373
    /** @var moodle1_file_manager instance used to convert question images */
1374
    protected $fileman = null;
1375
 
1376
    /**
1377
     * Registers paths
1378
     */
1379
    public function get_paths() {
1380
        return array(
1381
            new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
1382
            new convert_path(
1383
                'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
1384
                array(
1385
                    'renamefields' => array(
1386
                        'scaletext' => 'scale',
1387
                    ),
1388
                    'addfields' => array(
1389
                        'descriptionformat' => 0,
1390
                    )
1391
                )
1392
            ),
1393
        );
1394
    }
1395
 
1396
    /**
1397
     * Prepare the file manager for the files embedded in the scale description field
1398
     */
1399
    public function on_scales_start() {
1400
        $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1401
        $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'scale');
1402
    }
1403
 
1404
    /**
1405
     * This is executed every time we have one <SCALE> data available
1406
     *
1407
     * @param array $data
1408
     * @param array $raw
1409
     * @return array
1410
     */
1411
    public function process_scale(array $data, array $raw) {
1412
        global $CFG;
1413
 
1414
        // replay upgrade step 2009110400
1415
        if ($CFG->texteditors !== 'textarea') {
1416
            $data['description'] = text_to_html($data['description'], false, false, true);
1417
            $data['descriptionformat'] = FORMAT_HTML;
1418
        }
1419
 
1420
        // convert course files embedded into the scale description field
1421
        $this->fileman->itemid = $data['id'];
1422
        $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1423
 
1424
        // stash the scale
1425
        $this->converter->set_stash('scales', $data, $data['id']);
1426
    }
1427
}
1428
 
1429
 
1430
/**
1431
 * Handles the conversion of the outcomes
1432
 */
1433
class moodle1_outcomes_handler extends moodle1_xml_handler {
1434
 
1435
    /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
1436
    protected $fileman = null;
1437
 
1438
    /**
1439
     * Registers paths
1440
     */
1441
    public function get_paths() {
1442
        return array(
1443
            new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
1444
            new convert_path(
1445
                'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
1446
                array(
1447
                    'addfields' => array(
1448
                        'descriptionformat' => FORMAT_MOODLE,
1449
                    ),
1450
                )
1451
            ),
1452
        );
1453
    }
1454
 
1455
    /**
1456
     * Prepares the file manager and starts writing outcomes.xml
1457
     */
1458
    public function on_gradebook_grade_outcomes_start() {
1459
 
1460
        $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1461
        $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome');
1462
 
1463
        $this->open_xml_writer('outcomes.xml');
1464
        $this->xmlwriter->begin_tag('outcomes_definition');
1465
    }
1466
 
1467
    /**
1468
     * Processes GRADE_OUTCOME tags progressively
1469
     */
1470
    public function process_gradebook_grade_outcome(array $data, array $raw) {
1471
        global $CFG;
1472
 
1473
        // replay the upgrade step 2009110400
1474
        if ($CFG->texteditors !== 'textarea') {
1475
            $data['description']       = text_to_html($data['description'], false, false, true);
1476
            $data['descriptionformat'] = FORMAT_HTML;
1477
        }
1478
 
1479
        // convert course files embedded into the outcome description field
1480
        $this->fileman->itemid = $data['id'];
1481
        $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1482
 
1483
        // write the outcome data
1484
        $this->write_xml('outcome', $data, array('/outcome/id'));
1485
 
1486
        return $data;
1487
    }
1488
 
1489
    /**
1490
     * Closes outcomes.xml
1491
     */
1492
    public function on_gradebook_grade_outcomes_end() {
1493
        $this->xmlwriter->end_tag('outcomes_definition');
1494
        $this->close_xml_writer();
1495
    }
1496
}
1497
 
1498
 
1499
/**
1500
 * Handles the conversion of the gradebook structures in the moodle.xml file
1501
 */
1502
class moodle1_gradebook_handler extends moodle1_xml_handler {
1503
 
1504
    /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
1505
    protected $categoryparent = array();
1506
 
1507
    /**
1508
     * Registers paths
1509
     */
1510
    public function get_paths() {
1511
        return array(
1512
            new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
1513
            new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
1514
            new convert_path(
1515
                'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
1516
                array(
1517
                    'addfields' => array(
1518
                        'hidden' => 0,  // upgrade step 2010011200
1519
                    ),
1520
                )
1521
            ),
1522
            new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'),
1523
            new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'),
1524
        );
1525
    }
1526
 
1527
    /**
1528
     * Initializes the in-memory structures
1529
     *
1530
     * This should not be needed actually as the moodle.xml contains just one GRADEBOOK
1531
     * element. But who knows - maybe someone will want to write a mass conversion
1532
     * tool in the future (not me definitely ;-)
1533
     */
1534
    public function on_gradebook_start() {
1535
        $this->categoryparent = array();
1536
    }
1537
 
1538
    /**
1539
     * Processes one GRADE_LETTER data
1540
     *
1541
     * In Moodle 1.9, all grade_letters are from course context only. Therefore
1542
     * we put them here.
1543
     */
1544
    public function process_gradebook_grade_letter(array $data, array $raw) {
1545
        $this->converter->set_stash('gradebook_gradeletter', $data, $data['id']);
1546
    }
1547
 
1548
    /**
1549
     * Processes one GRADE_CATEGORY data
1550
     */
1551
    public function process_gradebook_grade_category(array $data, array $raw) {
1552
        $this->categoryparent[$data['id']] = $data['parent'];
1553
        $this->converter->set_stash('gradebook_gradecategory', $data, $data['id']);
1554
    }
1555
 
1556
    /**
1557
     * Processes one GRADE_ITEM data
1558
     */
1559
    public function process_gradebook_grade_item(array $data, array $raw) {
1560
 
1561
        // here we use get_nextid() to get a nondecreasing sequence
1562
        $data['sortorder'] = $this->converter->get_nextid();
1563
 
1564
        if ($data['itemtype'] === 'mod') {
1565
            return $this->process_mod_grade_item($data, $raw);
1566
 
1567
        } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
1568
            return $this->process_nonmod_grade_item($data, $raw);
1569
 
1570
        } else {
1571
            $this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']);
1572
        }
1573
    }
1574
 
1575
    /**
1576
     * Processes one GRADE_ITEM of the type 'mod'
1577
     */
1578
    protected function process_mod_grade_item(array $data, array $raw) {
1579
 
1580
        $stashname   = 'gradebook_modgradeitem_'.$data['itemmodule'];
1581
        $stashitemid = $data['iteminstance'];
1582
        $gradeitems  = $this->converter->get_stash_or_default($stashname, $stashitemid, array());
1583
 
1584
        // typically there will be single item with itemnumber 0
1585
        $gradeitems[$data['itemnumber']] = $data;
1586
 
1587
        $this->converter->set_stash($stashname, $gradeitems, $stashitemid);
1588
 
1589
        return $data;
1590
    }
1591
 
1592
    /**
1593
     * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
1594
     */
1595
    protected function process_nonmod_grade_item(array $data, array $raw) {
1596
 
1597
        $stashname   = 'gradebook_nonmodgradeitem';
1598
        $stashitemid = $data['id'];
1599
        $this->converter->set_stash($stashname, $data, $stashitemid);
1600
 
1601
        return $data;
1602
    }
1603
 
1604
    /**
1605
     * @todo
1606
     */
1607
    public function on_gradebook_grade_item_grades_start() {
1608
    }
1609
 
1610
    /**
1611
     * Writes the collected information into gradebook.xml
1612
     */
1613
    public function on_gradebook_end() {
1614
 
1615
        $this->open_xml_writer('gradebook.xml');
1616
        $this->xmlwriter->begin_tag('gradebook');
1617
        $this->write_grade_categories();
1618
        $this->write_grade_items();
1619
        $this->write_grade_letters();
1620
        $this->xmlwriter->end_tag('gradebook');
1621
        $this->close_xml_writer();
1622
    }
1623
 
1624
    /**
1625
     * Writes grade_categories
1626
     */
1627
    protected function write_grade_categories() {
1628
 
1629
        $this->xmlwriter->begin_tag('grade_categories');
1630
        foreach ($this->converter->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) {
1631
            $gradecategory = $this->converter->get_stash('gradebook_gradecategory', $gradecategoryid);
1632
            $path = $this->calculate_category_path($gradecategoryid);
1633
            $gradecategory['depth'] = count($path);
1634
            $gradecategory['path']  = '/'.implode('/', $path).'/';
1635
            $this->write_xml('grade_category', $gradecategory, array('/grade_category/id'));
1636
        }
1637
        $this->xmlwriter->end_tag('grade_categories');
1638
    }
1639
 
1640
    /**
1641
     * Calculates the path to the grade_category
1642
     *
1643
     * Moodle 1.9 backup does not store the grade_category's depth and path. This method is used
1644
     * to repopulate this information using the $this->categoryparent values.
1645
     *
1646
     * @param int $categoryid
1647
     * @return array of ids including the categoryid
1648
     */
1649
    protected function calculate_category_path($categoryid) {
1650
 
1651
        if (!array_key_exists($categoryid, $this->categoryparent)) {
1652
            throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
1653
        }
1654
 
1655
        $path = array($categoryid);
1656
        $parent = $this->categoryparent[$categoryid];
1657
        while (!is_null($parent)) {
1658
            array_unshift($path, $parent);
1659
            $parent = $this->categoryparent[$parent];
1660
            if (in_array($parent, $path)) {
1661
                throw new moodle1_convert_exception('circular_reference_in_categories_tree');
1662
            }
1663
        }
1664
 
1665
        return $path;
1666
    }
1667
 
1668
    /**
1669
     * Writes grade_items
1670
     */
1671
    protected function write_grade_items() {
1672
 
1673
        $this->xmlwriter->begin_tag('grade_items');
1674
        foreach ($this->converter->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) {
1675
            $gradeitem = $this->converter->get_stash('gradebook_nonmodgradeitem', $gradeitemid);
1676
            $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
1677
        }
1678
        $this->xmlwriter->end_tag('grade_items');
1679
    }
1680
 
1681
    /**
1682
     * Writes grade_letters
1683
     */
1684
    protected function write_grade_letters() {
1685
 
1686
        $this->xmlwriter->begin_tag('grade_letters');
1687
        foreach ($this->converter->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) {
1688
            $gradeletter = $this->converter->get_stash('gradebook_gradeletter', $gradeletterid);
1689
            $this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id'));
1690
        }
1691
        $this->xmlwriter->end_tag('grade_letters');
1692
    }
1693
}
1694
 
1695
 
1696
/**
1697
 * Shared base class for activity modules, blocks and qtype handlers
1698
 */
1699
abstract class moodle1_plugin_handler extends moodle1_xml_handler {
1700
 
1701
    /** @var string */
1702
    protected $plugintype;
1703
 
1704
    /** @var string */
1705
    protected $pluginname;
1706
 
1707
    /**
1708
     * @param moodle1_converter $converter the converter that requires us
1709
     * @param string $plugintype
1710
     * @param string $pluginname
1711
     */
1712
    public function __construct(moodle1_converter $converter, $plugintype, $pluginname) {
1713
 
1714
        parent::__construct($converter);
1715
        $this->plugintype = $plugintype;
1716
        $this->pluginname = $pluginname;
1717
    }
1718
 
1719
    /**
1720
     * Returns the normalized name of the plugin, eg mod_workshop
1721
     *
1722
     * @return string
1723
     */
1724
    public function get_component_name() {
1725
        return $this->plugintype.'_'.$this->pluginname;
1726
    }
1727
}
1728
 
1729
 
1730
/**
1731
 * Base class for all question type handlers
1732
 */
1733
abstract class moodle1_qtype_handler extends moodle1_plugin_handler {
1734
 
1735
    /** @var moodle1_question_bank_handler */
1736
    protected $qbankhandler;
1737
 
1738
    /**
1739
     * Returns the list of paths within one <QUESTION> that this qtype needs to have included
1740
     * in the grouped question structure
1741
     *
1742
     * @return array of strings
1743
     */
1744
    public function get_question_subpaths() {
1745
        return array();
1746
    }
1747
 
1748
    /**
1749
     * Gives the qtype handler a chance to write converted data into questions.xml
1750
     *
1751
     * @param array $data grouped question data
1752
     * @param array $raw grouped raw QUESTION data
1753
     */
1754
    public function process_question(array $data, array $raw) {
1755
    }
1756
 
1757
    /**
1758
     * Converts the answers and writes them into the questions.xml
1759
     *
1760
     * The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
1761
     *
1762
     * @param array $answers as parsed by the grouped parser in moodle.xml
1763
     * @param string $qtype containing the answers
1764
     */
1765
    protected function write_answers(array $answers, $qtype) {
1766
 
1767
        $this->xmlwriter->begin_tag('answers');
1768
        foreach ($answers as $elementname => $elements) {
1769
            foreach ($elements as $element) {
1770
                $answer = $this->convert_answer($element, $qtype);
1771
                // Migrate images in answertext.
1772
                if ($answer['answerformat'] == FORMAT_HTML) {
1773
                    $answer['answertext'] = $this->migrate_files($answer['answertext'], 'question', 'answer', $answer['id']);
1774
                }
1775
                // Migrate images in feedback.
1776
                if ($answer['feedbackformat'] == FORMAT_HTML) {
1777
                    $answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']);
1778
                }
1779
                $this->write_xml('answer', $answer, array('/answer/id'));
1780
            }
1781
        }
1782
        $this->xmlwriter->end_tag('answers');
1783
    }
1784
 
1785
    /**
1786
     * Migrate files belonging to one qtype plugin text field.
1787
     *
1788
     * @param array $text the html fragment containing references to files
1789
     * @param string $component the component for restored files
1790
     * @param string $filearea the file area for restored files
1791
     * @param int $itemid the itemid for restored files
1792
     *
1793
     * @return string the text for this field, after files references have been processed
1794
     */
1795
    protected function migrate_files($text, $component, $filearea, $itemid) {
1796
        $context = $this->qbankhandler->get_current_category_context();
1797
        $fileman = $this->qbankhandler->get_file_manager();
1798
        $fileman->contextid = $context['contextid'];
1799
        $fileman->component = $component;
1800
        $fileman->filearea  = $filearea;
1801
        $fileman->itemid    = $itemid;
1802
        $text = moodle1_converter::migrate_referenced_files($text, $fileman);
1803
        return $text;
1804
    }
1805
 
1806
    /**
1807
     * Writes the grouped numerical_units structure
1808
     *
1809
     * @param array $numericalunits
1810
     */
1811
    protected function write_numerical_units(array $numericalunits) {
1812
 
1813
        $this->xmlwriter->begin_tag('numerical_units');
1814
        foreach ($numericalunits as $elementname => $elements) {
1815
            foreach ($elements as $element) {
1816
                $element['id'] = $this->converter->get_nextid();
1817
                $this->write_xml('numerical_unit', $element, array('/numerical_unit/id'));
1818
            }
1819
        }
1820
        $this->xmlwriter->end_tag('numerical_units');
1821
    }
1822
 
1823
    /**
1824
     * Writes the numerical_options structure
1825
     *
1826
     * @see get_default_numerical_options()
1827
     * @param array $numericaloption
1828
     */
1829
    protected function write_numerical_options(array $numericaloption) {
1830
 
1831
        $this->xmlwriter->begin_tag('numerical_options');
1832
        if (!empty($numericaloption)) {
1833
            $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
1834
        }
1835
        $this->xmlwriter->end_tag('numerical_options');
1836
    }
1837
 
1838
    /**
1839
     * Returns default numerical_option structure
1840
     *
1841
     * This structure is not present in moodle.xml, we create a new artificial one here.
1842
     *
1843
     * @see write_numerical_options()
1844
     * @param int $oldquestiontextformat
1845
     * @return array
1846
     */
1847
    protected function get_default_numerical_options($oldquestiontextformat, $units) {
1848
        global $CFG;
1849
 
1850
        // replay the upgrade step 2009100100 - new table
1851
        $options = array(
1852
            'id'                 => $this->converter->get_nextid(),
1853
            'instructions'       => null,
1854
            'instructionsformat' => 0,
1855
            'showunits'          => 0,
1856
            'unitsleft'          => 0,
1857
            'unitgradingtype'    => 0,
1858
            'unitpenalty'        => 0.1
1859
        );
1860
 
1861
        // replay the upgrade step 2009100101
1862
        if ($CFG->texteditors !== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE) {
1863
            $options['instructionsformat'] = FORMAT_HTML;
1864
        } else {
1865
            $options['instructionsformat'] = $oldquestiontextformat;
1866
        }
1867
 
1868
        // Set a good default, depending on whether there are any units defined.
1869
        if (empty($units)) {
1870
            $options['showunits'] = 3;
1871
        }
1872
 
1873
        return $options;
1874
    }
1875
 
1876
    /**
1877
     * Writes the dataset_definitions structure
1878
     *
1879
     * @param array $datasetdefinitions array of dataset_definition structures
1880
     */
1881
    protected function write_dataset_definitions(array $datasetdefinitions) {
1882
 
1883
        $this->xmlwriter->begin_tag('dataset_definitions');
1884
        foreach ($datasetdefinitions as $datasetdefinition) {
1885
            $this->xmlwriter->begin_tag('dataset_definition', array('id' => $this->converter->get_nextid()));
1886
            foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) {
1887
                $this->xmlwriter->full_tag($element, $datasetdefinition[$element]);
1888
            }
1889
            $this->xmlwriter->begin_tag('dataset_items');
1890
            if (!empty($datasetdefinition['dataset_items']['dataset_item'])) {
1891
                foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) {
1892
                    $datasetitem['id'] = $this->converter->get_nextid();
1893
                    $this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id'));
1894
                }
1895
            }
1896
            $this->xmlwriter->end_tag('dataset_items');
1897
            $this->xmlwriter->end_tag('dataset_definition');
1898
        }
1899
        $this->xmlwriter->end_tag('dataset_definitions');
1900
    }
1901
 
1902
    /// implementation details follow //////////////////////////////////////////
1903
 
1904
    public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) {
1905
 
1906
        parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
1907
        $this->qbankhandler = $qbankhandler;
1908
    }
1909
 
1910
    /**
1911
     * @see self::get_question_subpaths()
1912
     */
1913
    final public function get_paths() {
1914
        throw new moodle1_convert_exception('qtype_handler_get_paths');
1915
    }
1916
 
1917
    /**
1918
     * Question type handlers cannot open the xml_writer
1919
     */
1920
    final protected function open_xml_writer($filename) {
1921
        throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1922
    }
1923
 
1924
    /**
1925
     * Question type handlers cannot close the xml_writer
1926
     */
1927
    final protected function close_xml_writer() {
1928
        throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1929
    }
1930
 
1931
    /**
1932
     * Provides a xml_writer instance to this qtype converter
1933
     *
1934
     * @param xml_writer $xmlwriter
1935
     */
1936
    public function use_xml_writer(xml_writer $xmlwriter) {
1937
        $this->xmlwriter = $xmlwriter;
1938
    }
1939
 
1940
    /**
1941
     * Converts <ANSWER> structure into the new <answer> one
1942
     *
1943
     * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
1944
     *
1945
     * @param array $old the parsed answer array in moodle.xml
1946
     * @param string $qtype the question type the answer is part of
1947
     * @return array
1948
     */
1949
    private function convert_answer(array $old, $qtype) {
1950
        global $CFG;
1951
 
1952
        $new                    = array();
1953
        $new['id']              = $old['id'];
1954
        $new['answertext']      = $old['answer_text'];
1955
        $new['answerformat']    = 0;   // upgrade step 2010080900
1956
        $new['fraction']        = $old['fraction'];
1957
        $new['feedback']        = $old['feedback'];
1958
        $new['feedbackformat']  = 0;   // upgrade step 2010080900
1959
 
1960
        // replay upgrade step 2010080901
1961
        if ($qtype !== 'multichoice') {
1962
            $new['answerformat'] = FORMAT_PLAIN;
1963
        } else {
1964
            $new['answertext'] = text_to_html($new['answertext'], false, false, true);
1965
            $new['answerformat'] = FORMAT_HTML;
1966
        }
1967
 
1968
        if ($CFG->texteditors !== 'textarea') {
1969
            if ($qtype == 'essay') {
1970
                $new['feedback'] = text_to_html($new['feedback'], false, false, true);
1971
            }
1972
            $new['feedbackformat'] = FORMAT_HTML;
1973
 
1974
        } else {
1975
            $new['feedbackformat'] = FORMAT_MOODLE;
1976
        }
1977
 
1978
        return $new;
1979
    }
1980
}
1981
 
1982
 
1983
/**
1984
 * Base class for activity module handlers
1985
 */
1986
abstract class moodle1_mod_handler extends moodle1_plugin_handler {
1987
 
1988
    /**
1989
     * Returns the name of the module, eg. 'forum'
1990
     *
1991
     * @return string
1992
     */
1993
    public function get_modname() {
1994
        return $this->pluginname;
1995
    }
1996
 
1997
    /**
1998
     * Returns course module information for the given instance id
1999
     *
2000
     * The information for this instance id has been stashed by
2001
     * {@link moodle1_course_outline_handler::process_course_module()}
2002
     *
2003
     * @param int $instance the module instance id
2004
     * @param string $modname the module type, defaults to $this->pluginname
2005
     * @return int
2006
     */
2007
    protected function get_cminfo($instance, $modname = null) {
2008
 
2009
        if (is_null($modname)) {
2010
            $modname = $this->pluginname;
2011
        }
2012
        return $this->converter->get_stash('cminfo_'.$modname, $instance);
2013
    }
2014
}
2015
 
2016
 
2017
/**
2018
 * Base class for all modules that are successors of the 1.9 resource module
2019
 */
2020
abstract class moodle1_resource_successor_handler extends moodle1_mod_handler {
2021
 
2022
    /**
2023
     * Resource successors do not attach to paths themselves, they are called explicitely
2024
     * by moodle1_mod_resource_handler
2025
     *
2026
     * @return array
2027
     */
2028
    final public function get_paths() {
2029
        return array();
2030
    }
2031
 
2032
    /**
2033
     * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
2034
     *
2035
     * Called by {@link moodle1_mod_resource_handler::process_resource()}
2036
     *
2037
     * @param array $data pre-cooked legacy resource data
2038
     * @param array $raw raw legacy resource data
2039
     */
2040
    public function process_legacy_resource(array $data, array $raw = null) {
2041
    }
2042
 
2043
    /**
2044
     * Called when the parses reaches the end </MOD> resource tag
2045
     *
2046
     * @param array $data the data returned by {@link self::process_resource} or just pre-cooked
2047
     */
2048
    public function on_legacy_resource_end(array $data) {
2049
    }
2050
}
2051
 
2052
/**
2053
 * Base class for block handlers
2054
 */
2055
abstract class moodle1_block_handler extends moodle1_plugin_handler {
2056
 
2057
    public function get_paths() {
2058
        $blockname = strtoupper($this->pluginname);
2059
        return array(
2060
            new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
2061
        );
2062
    }
2063
 
2064
    public function process_block(array $data) {
2065
        $newdata = $this->convert_common_block_data($data);
2066
 
2067
        $this->write_block_xml($newdata, $data);
2068
        $this->write_inforef_xml($newdata, $data);
2069
        $this->write_roles_xml($newdata, $data);
2070
 
2071
        return $data;
2072
    }
2073
 
2074
    protected function convert_common_block_data(array $olddata) {
2075
        $newdata = array();
2076
 
2077
        $newdata['blockname'] = $olddata['name'];
2078
        $newdata['parentcontextid'] = $this->converter->get_contextid(CONTEXT_COURSE, 0);
2079
        $newdata['showinsubcontexts'] = 0;
2080
        $newdata['pagetypepattern'] = $olddata['pagetype'].='-*';
2081
        $newdata['subpagepattern'] = null;
2082
        $newdata['defaultregion'] = ($olddata['position']=='l')?'side-pre':'side-post';
2083
        $newdata['defaultweight'] = $olddata['weight'];
2084
        $newdata['configdata'] = $this->convert_configdata($olddata);
2085
 
2086
        return $newdata;
2087
    }
2088
 
2089
    protected function convert_configdata(array $olddata) {
2090
        return $olddata['configdata'];
2091
    }
2092
 
2093
    protected function write_block_xml($newdata, $data) {
2094
        $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
2095
 
2096
        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/block.xml");
2097
        $this->xmlwriter->begin_tag('block', array('id' => $data['id'], 'contextid' => $contextid));
2098
 
2099
        foreach ($newdata as $field => $value) {
2100
            $this->xmlwriter->full_tag($field, $value);
2101
        }
2102
 
2103
        $this->xmlwriter->begin_tag('block_positions');
2104
        $this->xmlwriter->begin_tag('block_position', array('id' => 1));
2105
        $this->xmlwriter->full_tag('contextid', $newdata['parentcontextid']);
2106
        $this->xmlwriter->full_tag('pagetype', $data['pagetype']);
2107
        $this->xmlwriter->full_tag('subpage', '');
2108
        $this->xmlwriter->full_tag('visible', $data['visible']);
2109
        $this->xmlwriter->full_tag('region', $newdata['defaultregion']);
2110
        $this->xmlwriter->full_tag('weight', $newdata['defaultweight']);
2111
        $this->xmlwriter->end_tag('block_position');
2112
        $this->xmlwriter->end_tag('block_positions');
2113
        $this->xmlwriter->end_tag('block');
2114
        $this->close_xml_writer();
2115
    }
2116
 
2117
    protected function write_inforef_xml($newdata, $data) {
2118
        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
2119
        $this->xmlwriter->begin_tag('inforef');
2120
        // Subclasses may provide inforef contents if needed
2121
        $this->xmlwriter->end_tag('inforef');
2122
        $this->close_xml_writer();
2123
    }
2124
 
2125
    protected function write_roles_xml($newdata, $data) {
2126
        // This is an empty shell, as the moodle1 converter doesn't handle user data.
2127
        $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/roles.xml");
2128
        $this->xmlwriter->begin_tag('roles');
2129
        $this->xmlwriter->full_tag('role_overrides', '');
2130
        $this->xmlwriter->full_tag('role_assignments', '');
2131
        $this->xmlwriter->end_tag('roles');
2132
        $this->close_xml_writer();
2133
    }
2134
}
2135
 
2136
 
2137
/**
2138
 * Base class for block generic handler
2139
 */
2140
class moodle1_block_generic_handler extends moodle1_block_handler {
2141
 
2142
}
2143
 
2144
/**
2145
 * Base class for the activity modules' subplugins
2146
 */
2147
abstract class moodle1_submod_handler extends moodle1_plugin_handler {
2148
 
2149
    /** @var moodle1_mod_handler */
2150
    protected $parenthandler;
2151
 
2152
    /**
2153
     * @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of
2154
     * @param string $subplugintype the type of the subplugin
2155
     * @param string $subpluginname the name of the subplugin
2156
     */
2157
    public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) {
2158
        $this->parenthandler = $parenthandler;
2159
        parent::__construct($parenthandler->converter, $subplugintype, $subpluginname);
2160
    }
2161
 
2162
    /**
2163
     * Activity module subplugins can't declare any paths to handle
2164
     *
2165
     * The paths must be registered by the parent module and then re-dispatched to the
2166
     * relevant subplugins for eventual processing.
2167
     *
2168
     * @return array empty array
2169
     */
2170
    final public function get_paths() {
2171
        return array();
2172
    }
2173
}