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
 * Provides classes used by the moodle1 converter
20
 *
21
 * @package    backup-convert
22
 * @subpackage moodle1
23
 * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
require_once($CFG->dirroot . '/backup/converter/convertlib.php');
30
require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php');
31
require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
32
require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php');
33
require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php');
34
require_once($CFG->dirroot . '/backup/util/dbops/restore_dbops.class.php');
35
require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php');
36
require_once(__DIR__ . '/handlerlib.php');
37
 
38
/**
39
 * Converter of Moodle 1.9 backup into Moodle 2.x format
40
 */
41
class moodle1_converter extends base_converter {
42
 
43
    /** @var progressive_parser moodle.xml file parser */
44
    protected $xmlparser;
45
 
46
    /** @var moodle1_parser_processor */
47
    protected $xmlprocessor;
48
 
49
    /** @var array of {@link convert_path} to process */
50
    protected $pathelements = array();
51
 
52
    /** @var null|string the current module being processed - used to expand the MOD paths */
53
    protected $currentmod = null;
54
 
55
    /** @var null|string the current block being processed - used to expand the BLOCK paths */
56
    protected $currentblock = null;
57
 
58
    /** @var string path currently locking processing of children */
59
    protected $pathlock;
60
 
61
    /** @var int used by the serial number {@link get_nextid()} */
62
    private $nextid = 1;
63
 
64
    /**
65
     * Instructs the dispatcher to ignore all children below path processor returning it
66
     */
67
    const SKIP_ALL_CHILDREN = -991399;
68
 
69
    /**
70
     * Log a message
71
     *
72
     * @see parent::log()
73
     * @param string $message message text
74
     * @param int $level message level {@example backup::LOG_WARNING}
75
     * @param null|mixed $a additional information
76
     * @param null|int $depth the message depth
77
     * @param bool $display whether the message should be sent to the output, too
78
     */
79
    public function log($message, $level, $a = null, $depth = null, $display = false) {
80
        parent::log('(moodle1) '.$message, $level, $a, $depth, $display);
81
    }
82
 
83
    /**
84
     * Detects the Moodle 1.9 format of the backup directory
85
     *
86
     * @param string $tempdir the name of the backup directory
87
     * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise
88
     */
89
    public static function detect_format($tempdir) {
90
        global $CFG;
91
 
92
        $tempdirpath = make_backup_temp_directory($tempdir, false);
93
        $filepath = $tempdirpath . '/moodle.xml';
94
        if (file_exists($filepath)) {
95
            // looks promising, lets load some information
96
            $handle = fopen($filepath, 'r');
97
            $first_chars = fread($handle, 200);
98
            fclose($handle);
99
 
100
            // check if it has the required strings
101
            if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
102
                strpos($first_chars,'<MOODLE_BACKUP>') !== false and
103
                strpos($first_chars,'<INFO>') !== false) {
104
 
105
                return backup::FORMAT_MOODLE1;
106
            }
107
        }
108
 
109
        return null;
110
    }
111
 
112
    /**
113
     * Initialize the instance if needed, called by the constructor
114
     *
115
     * Here we create objects we need before the execution.
116
     */
117
    protected function init() {
118
 
119
        // ask your mother first before going out playing with toys
120
        parent::init();
121
 
122
        $this->log('initializing '.$this->get_name().' converter', backup::LOG_INFO);
123
 
124
        // good boy, prepare XML parser and processor
125
        $this->log('setting xml parser', backup::LOG_DEBUG, null, 1);
126
        $this->xmlparser = new progressive_parser();
127
        $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
128
        $this->log('setting xml processor', backup::LOG_DEBUG, null, 1);
129
        $this->xmlprocessor = new moodle1_parser_processor($this);
130
        $this->xmlparser->set_processor($this->xmlprocessor);
131
 
132
        // make sure that MOD and BLOCK paths are visited
133
        $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/MODULES/MOD');
134
        $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK');
135
 
136
        // register the conversion handlers
137
        foreach (moodle1_handlers_factory::get_handlers($this) as $handler) {
138
            $this->log('registering handler', backup::LOG_DEBUG, get_class($handler), 1);
139
            $this->register_handler($handler, $handler->get_paths());
140
        }
141
    }
142
 
143
    /**
144
     * Converts the contents of the tempdir into the target format in the workdir
145
     */
146
    protected function execute() {
147
        $this->log('creating the stash storage', backup::LOG_DEBUG);
148
        $this->create_stash_storage();
149
 
150
        $this->log('parsing moodle.xml starts', backup::LOG_DEBUG);
151
        $this->xmlparser->process();
152
        $this->log('parsing moodle.xml done', backup::LOG_DEBUG);
153
 
154
        $this->log('dropping the stash storage', backup::LOG_DEBUG);
155
        $this->drop_stash_storage();
156
    }
157
 
158
    /**
159
     * Register a handler for the given path elements
160
     */
161
    protected function register_handler(moodle1_handler $handler, array $elements) {
162
 
163
        // first iteration, push them to new array, indexed by name
164
        // to detect duplicates in names or paths
165
        $names = array();
166
        $paths = array();
167
        foreach($elements as $element) {
168
            if (!$element instanceof convert_path) {
169
                throw new convert_exception('path_element_wrong_class', get_class($element));
170
            }
171
            if (array_key_exists($element->get_name(), $names)) {
172
                throw new convert_exception('path_element_name_alreadyexists', $element->get_name());
173
            }
174
            if (array_key_exists($element->get_path(), $paths)) {
175
                throw new convert_exception('path_element_path_alreadyexists', $element->get_path());
176
            }
177
            $names[$element->get_name()] = true;
178
            $paths[$element->get_path()] = $element;
179
        }
180
 
181
        // now, for each element not having a processing object yet, assign the handler
182
        // if the element is not a memeber of a group
183
        foreach($paths as $key => $element) {
184
            if (is_null($element->get_processing_object()) and !$this->grouped_parent_exists($element, $paths)) {
185
                $paths[$key]->set_processing_object($handler);
186
            }
187
            // add the element path to the processor
188
            $this->xmlprocessor->add_path($element->get_path(), $element->is_grouped());
189
        }
190
 
191
        // done, store the paths (duplicates by path are discarded)
192
        $this->pathelements = array_merge($this->pathelements, $paths);
193
 
194
        // remove the injected plugin name element from the MOD and BLOCK paths
195
        // and register such collapsed path, too
196
        foreach ($elements as $element) {
197
            $path = $element->get_path();
198
            $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/MODULES\/MOD\/(\w+)\//', '/MOODLE_BACKUP/COURSE/MODULES/MOD/', $path);
199
            $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/BLOCKS\/BLOCK\/(\w+)\//', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/', $path);
200
            if (!empty($path) and $path != $element->get_path()) {
201
                $this->xmlprocessor->add_path($path, false);
202
            }
203
        }
204
    }
205
 
206
    /**
207
     * Helper method used by {@link self::register_handler()}
208
     *
209
     * @param convert_path $pelement path element
210
     * @param array of convert_path instances
211
     * @return bool true if grouped parent was found, false otherwise
212
     */
213
    protected function grouped_parent_exists($pelement, $elements) {
214
 
215
        foreach ($elements as $element) {
216
            if ($pelement->get_path() == $element->get_path()) {
217
                // don't compare against itself
218
                continue;
219
            }
220
            // if the element is grouped and it is a parent of pelement, return true
221
            if ($element->is_grouped() and strpos($pelement->get_path() .  '/', $element->get_path()) === 0) {
222
                return true;
223
            }
224
        }
225
 
226
        // no grouped parent found
227
        return false;
228
    }
229
 
230
    /**
231
     * Process the data obtained from the XML parser processor
232
     *
233
     * This methods receives one chunk of information from the XML parser
234
     * processor and dispatches it, following the naming rules.
235
     * We are expanding the modules and blocks paths here to include the plugin's name.
236
     *
237
     * @param array $data
238
     */
239
    public function process_chunk($data) {
240
 
241
        $path = $data['path'];
242
 
243
        // expand the MOD paths so that they contain the module name
244
        if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
245
            $this->currentmod = strtoupper($data['tags']['MODTYPE']);
246
            $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
247
 
248
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
249
            $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
250
        }
251
 
252
        // expand the BLOCK paths so that they contain the module name
253
        if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
254
            $this->currentblock = strtoupper($data['tags']['NAME']);
255
            $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
256
 
257
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
258
            $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
259
        }
260
 
261
        if ($path !== $data['path']) {
262
            if (!array_key_exists($path, $this->pathelements)) {
263
                // no handler registered for the transformed MOD or BLOCK path
264
                $this->log('no handler attached', backup::LOG_WARNING, $path);
265
                return;
266
 
267
            } else {
268
                // pretend as if the original $data contained the tranformed path
269
                $data['path'] = $path;
270
            }
271
        }
272
 
273
        if (!array_key_exists($data['path'], $this->pathelements)) {
274
            // path added to the processor without the handler
275
            throw new convert_exception('missing_path_handler', $data['path']);
276
        }
277
 
278
        $element  = $this->pathelements[$data['path']];
279
        $object   = $element->get_processing_object();
280
        $method   = $element->get_processing_method();
281
        $returned = null; // data returned by the processing method, if any
282
 
283
        if (empty($object)) {
284
            throw new convert_exception('missing_processing_object', null, $data['path']);
285
        }
286
 
287
        // release the lock if we aren't anymore within children of it
288
        if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
289
            $this->pathlock = null;
290
        }
291
 
292
        // if the path is not locked, apply the element's recipes and dispatch
293
        // the cooked tags to the processing method
294
        if (is_null($this->pathlock)) {
295
            $rawdatatags  = $data['tags'];
296
            $data['tags'] = $element->apply_recipes($data['tags']);
297
 
298
            // if the processing method exists, give it a chance to modify data
299
            if (method_exists($object, $method)) {
300
                $returned = $object->$method($data['tags'], $rawdatatags);
301
            }
302
        }
303
 
304
        // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path
305
        // and lock it so that its children are not dispatched
306
        if ($returned === self::SKIP_ALL_CHILDREN) {
307
            // check we haven't any previous lock
308
            if (!is_null($this->pathlock)) {
309
                throw new convert_exception('already_locked_path', $data['path']);
310
            }
311
            // set the lock - nothing below the current path will be dispatched
312
            $this->pathlock = $data['path'] . '/';
313
 
314
        // if the method has returned any info, set element data to it
315
        } else if (!is_null($returned)) {
316
            $element->set_tags($returned);
317
 
318
        // use just the cooked parsed data otherwise
319
        } else {
320
            $element->set_tags($data['tags']);
321
        }
322
    }
323
 
324
    /**
325
     * Executes operations required at the start of a watched path
326
     *
327
     * For MOD and BLOCK paths, this is supported only for the sub-paths, not the root
328
     * module/block element. For the illustration:
329
     *
330
     * You CAN'T attach on_xxx_start() listener to a path like
331
     * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP because the <MOD> must
332
     * be processed first in {@link self::process_chunk()} where $this->currentmod
333
     * is set.
334
     *
335
     * You CAN attach some on_xxx_start() listener to a path like
336
     * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP/SUBMISSIONS because it is
337
     * a sub-path under <MOD> and we have $this->currentmod already set when the
338
     * <SUBMISSIONS> is reached.
339
     *
340
     * @param string $path in the original file
341
     */
342
    public function path_start_reached($path) {
343
 
344
        if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
345
            $this->currentmod = null;
346
            $forbidden = true;
347
 
348
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
349
            // expand the MOD paths so that they contain the module name
350
            $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
351
        }
352
 
353
        if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
354
            $this->currentblock = null;
355
            $forbidden = true;
356
 
357
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
358
            // expand the BLOCK paths so that they contain the module name
359
            $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
360
        }
361
 
362
        if (empty($this->pathelements[$path])) {
363
            return;
364
        }
365
 
366
        $element = $this->pathelements[$path];
367
        $pobject = $element->get_processing_object();
368
        $method  = $element->get_start_method();
369
 
370
        if (method_exists($pobject, $method)) {
371
            if (empty($forbidden)) {
372
                $pobject->$method();
373
 
374
            } else {
375
                // this path is not supported because we do not know the module/block yet
376
                throw new coding_exception('Attaching the on-start event listener to the root MOD or BLOCK element is forbidden.');
377
            }
378
        }
379
    }
380
 
381
    /**
382
     * Executes operations required at the end of a watched path
383
     *
384
     * @param string $path in the original file
385
     */
386
    public function path_end_reached($path) {
387
 
388
        // expand the MOD paths so that they contain the current module name
389
        if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
390
            $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
391
 
392
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
393
            $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
394
        }
395
 
396
        // expand the BLOCK paths so that they contain the module name
397
        if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
398
            $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
399
 
400
        } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
401
            $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
402
        }
403
 
404
        if (empty($this->pathelements[$path])) {
405
            return;
406
        }
407
 
408
        $element = $this->pathelements[$path];
409
        $pobject = $element->get_processing_object();
410
        $method  = $element->get_end_method();
411
        $tags    = $element->get_tags();
412
 
413
        if (method_exists($pobject, $method)) {
414
            $pobject->$method($tags);
415
        }
416
    }
417
 
418
    /**
419
     * Creates the temporary storage for stashed data
420
     *
421
     * This implementation uses backup_ids_temp table.
422
     */
423
    public function create_stash_storage() {
424
        backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
425
    }
426
 
427
    /**
428
     * Drops the temporary storage of stashed data
429
     *
430
     * This implementation uses backup_ids_temp table.
431
     */
432
    public function drop_stash_storage() {
433
        backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
434
    }
435
 
436
    /**
437
     * Stores some information for later processing
438
     *
439
     * This implementation uses backup_ids_temp table to store data. Make
440
     * sure that the $stashname + $itemid combo is unique.
441
     *
442
     * @param string $stashname name of the stash
443
     * @param mixed $info information to stash
444
     * @param int $itemid optional id for multiple infos within the same stashname
445
     */
446
    public function set_stash($stashname, $info, $itemid = 0) {
447
        try {
448
            restore_dbops::set_backup_ids_record($this->get_id(), $stashname, $itemid, 0, null, $info);
449
 
450
        } catch (dml_exception $e) {
451
            throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage());
452
        }
453
    }
454
 
455
    /**
456
     * Restores a given stash stored previously by {@link self::set_stash()}
457
     *
458
     * @param string $stashname name of the stash
459
     * @param int $itemid optional id for multiple infos within the same stashname
460
     * @throws moodle1_convert_empty_storage_exception if the info has not been stashed previously
461
     * @return mixed stashed data
462
     */
463
    public function get_stash($stashname, $itemid = 0) {
464
 
465
        $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid);
466
 
467
        if (empty($record)) {
468
            throw new moodle1_convert_empty_storage_exception('required_not_stashed_data', array($stashname, $itemid));
469
        } else {
470
            if (empty($record->info)) {
471
                return array();
472
            }
473
            return $record->info;
474
        }
475
    }
476
 
477
    /**
478
     * Restores a given stash or returns the given default if there is no such stash
479
     *
480
     * @param string $stashname name of the stash
481
     * @param int $itemid optional id for multiple infos within the same stashname
482
     * @param mixed $default information to return if the info has not been stashed previously
483
     * @return mixed stashed data or the default value
484
     */
485
    public function get_stash_or_default($stashname, $itemid = 0, $default = null) {
486
        try {
487
            return $this->get_stash($stashname, $itemid);
488
        } catch (moodle1_convert_empty_storage_exception $e) {
489
            return $default;
490
        }
491
    }
492
 
493
    /**
494
     * Returns the list of existing stashes
495
     *
496
     * @return array
497
     */
498
    public function get_stash_names() {
499
        global $DB;
500
 
501
        $search = array(
502
            'backupid' => $this->get_id(),
503
        );
504
 
505
        return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemname'));
506
    }
507
 
508
    /**
509
     * Returns the list of stashed $itemids in the given stash
510
     *
511
     * @param string $stashname
512
     * @return array
513
     */
514
    public function get_stash_itemids($stashname) {
515
        global $DB;
516
 
517
        $search = array(
518
            'backupid' => $this->get_id(),
519
            'itemname' => $stashname
520
        );
521
 
522
        return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemid'));
523
    }
524
 
525
    /**
526
     * Generates an artificial context id
527
     *
528
     * Moodle 1.9 backups do not contain any context information. But we need them
529
     * in Moodle 2.x format so here we generate fictive context id for every given
530
     * context level + instance combo.
531
     *
532
     * CONTEXT_SYSTEM and CONTEXT_COURSE ignore the $instance as they represent a
533
     * single system or the course being restored.
534
     *
535
     * @see context_system::instance()
536
     * @see context_course::instance()
537
     * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE
538
     * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules
539
     * @return int the context id
540
     */
541
    public function get_contextid($level, $instance = 0) {
542
 
543
        $stashname = 'context' . $level;
544
 
545
        if ($level == CONTEXT_SYSTEM or $level == CONTEXT_COURSE) {
546
            $instance = 0;
547
        }
548
 
549
        try {
550
            // try the previously stashed id
551
            return $this->get_stash($stashname, $instance);
552
 
553
        } catch (moodle1_convert_empty_storage_exception $e) {
554
            // this context level + instance is required for the first time
555
            $newid = $this->get_nextid();
556
            $this->set_stash($stashname, $newid, $instance);
557
            return $newid;
558
        }
559
    }
560
 
561
    /**
562
     * Simple autoincrement generator
563
     *
564
     * @return int the next number in a row of numbers
565
     */
566
    public function get_nextid() {
567
        return $this->nextid++;
568
    }
569
 
570
    /**
571
     * Creates and returns new instance of the file manager
572
     *
573
     * @param int $contextid the default context id of the files being migrated
574
     * @param string $component the default component name of the files being migrated
575
     * @param string $filearea the default file area of the files being migrated
576
     * @param int $itemid the default item id of the files being migrated
577
     * @param int $userid initial user id of the files being migrated
578
     * @return moodle1_file_manager
579
     */
580
    public function get_file_manager($contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
581
        return new moodle1_file_manager($this, $contextid, $component, $filearea, $itemid, $userid);
582
    }
583
 
584
    /**
585
     * Creates and returns new instance of the inforef manager
586
     *
587
     * @param string $name the name of the annotator (like course, section, activity, block)
588
     * @param int $id the id of the annotator if required
589
     * @return moodle1_inforef_manager
590
     */
591
    public function get_inforef_manager($name, $id = 0) {
592
        return new moodle1_inforef_manager($this, $name, $id);
593
    }
594
 
595
 
596
    /**
597
     * Migrates all course files referenced from the hypertext using the given filemanager
598
     *
599
     * This is typically used to convert images embedded into the intro fields.
600
     *
601
     * @param string $text hypertext containing $@FILEPHP@$ referenced
602
     * @param moodle1_file_manager $fileman file manager to use for the file migration
603
     * @return string the original $text with $@FILEPHP@$ references replaced with the new @@PLUGINFILE@@
604
     */
605
    public static function migrate_referenced_files($text, moodle1_file_manager $fileman) {
606
 
607
        $files = self::find_referenced_files($text);
608
        if (!empty($files)) {
609
            foreach ($files as $file) {
610
                try {
611
                    $fileman->migrate_file('course_files'.$file, dirname($file));
612
                } catch (moodle1_convert_exception $e) {
613
                    // file probably does not exist
614
                    $fileman->log('error migrating file', backup::LOG_WARNING, 'course_files'.$file);
615
                }
616
            }
617
            $text = self::rewrite_filephp_usage($text, $files);
618
        }
619
 
620
        return $text;
621
    }
622
 
623
    /**
624
     * Detects all links to file.php encoded via $@FILEPHP@$ and returns the files to migrate
625
     *
626
     * @see self::migrate_referenced_files()
627
     * @param string $text
628
     * @return array
629
     */
630
    public static function find_referenced_files($text) {
631
 
632
        $files = array();
633
 
634
        if (empty($text) or is_numeric($text)) {
635
            return $files;
636
        }
637
 
638
        $matches = array();
639
        $pattern = '|(["\'])(\$@FILEPHP@\$.+?)\1|';
640
        $result = preg_match_all($pattern, $text, $matches);
641
        if ($result === false) {
642
            throw new moodle1_convert_exception('error_while_searching_for_referenced_files');
643
        }
644
        if ($result == 0) {
645
            return $files;
646
        }
647
        foreach ($matches[2] as $match) {
648
            $file = str_replace(array('$@FILEPHP@$', '$@SLASH@$', '$@FORCEDOWNLOAD@$'), array('', '/', ''), $match);
649
            if ($file === clean_param($file, PARAM_PATH)) {
650
                $files[] = rawurldecode($file);
651
            }
652
        }
653
 
654
        return array_unique($files);
655
    }
656
 
657
    /**
658
     * Given the list of migrated files, rewrites references to them from $@FILEPHP@$ form to the @@PLUGINFILE@@ one
659
     *
660
     * @see self::migrate_referenced_files()
661
     * @param string $text
662
     * @param array $files
663
     * @return string
664
     */
665
    public static function rewrite_filephp_usage($text, array $files) {
666
 
667
        foreach ($files as $file) {
668
            // Expect URLs properly encoded by default.
669
            $parts   = explode('/', $file);
670
            $encoded = implode('/', array_map('rawurlencode', $parts));
671
            $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $encoded);
672
            $text    = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
673
            $text    = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
674
            // Add support for URLs without any encoding.
675
            $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $file);
676
            $text    = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
677
            $text    = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
678
        }
679
 
680
        return $text;
681
    }
682
 
683
    /**
684
     * @see parent::description()
685
     */
686
    public static function description() {
687
 
688
        return array(
689
            'from'  => backup::FORMAT_MOODLE1,
690
            'to'    => backup::FORMAT_MOODLE,
691
            'cost'  => 10,
692
        );
693
    }
694
}
695
 
696
 
697
/**
698
 * Exception thrown by this converter
699
 */
700
class moodle1_convert_exception extends convert_exception {
701
}
702
 
703
 
704
/**
705
 * Exception thrown by the temporary storage subsystem of moodle1_converter
706
 */
707
class moodle1_convert_storage_exception extends moodle1_convert_exception {
708
}
709
 
710
 
711
/**
712
 * Exception thrown by the temporary storage subsystem of moodle1_converter
713
 */
714
class moodle1_convert_empty_storage_exception extends moodle1_convert_exception {
715
}
716
 
717
 
718
/**
719
 * XML parser processor used for processing parsed moodle.xml
720
 */
721
class moodle1_parser_processor extends grouped_parser_processor {
722
 
723
    /** @var moodle1_converter */
724
    protected $converter;
725
 
726
    public function __construct(moodle1_converter $converter) {
727
        $this->converter = $converter;
728
        parent::__construct();
729
    }
730
 
731
    /**
732
     * Provides NULL decoding
733
     *
734
     * Note that we do not decode $@FILEPHP@$ and friends here as we are going to write them
735
     * back immediately into another XML file.
736
     */
737
    public function process_cdata($cdata) {
738
 
739
        if ($cdata === '$@NULL@$') {
740
            return null;
741
        }
742
 
743
        return $cdata;
744
    }
745
 
746
    /**
747
     * Dispatches the data chunk to the converter class
748
     *
749
     * @param array $data the chunk of parsed data
750
     */
751
    protected function dispatch_chunk($data) {
752
        $this->converter->process_chunk($data);
753
    }
754
 
755
    /**
756
     * Informs the converter at the start of a watched path
757
     *
758
     * @param string $path
759
     */
760
    protected function notify_path_start($path) {
761
        $this->converter->path_start_reached($path);
762
    }
763
 
764
    /**
765
     * Informs the converter at the end of a watched path
766
     *
767
     * @param string $path
768
     */
769
    protected function notify_path_end($path) {
770
        $this->converter->path_end_reached($path);
771
    }
772
}
773
 
774
 
775
/**
776
 * XML transformer that modifies the content of the files being written during the conversion
777
 *
778
 * @see backup_xml_transformer
779
 */
780
class moodle1_xml_transformer extends xml_contenttransformer {
781
 
782
    /**
783
     * Modify the content before it is writter to a file
784
     *
785
     * @param string|mixed $content
786
     */
787
    public function process($content) {
788
 
789
        // the content should be a string. If array or object is given, try our best recursively
790
        // but inform the developer
791
        if (is_array($content)) {
792
            debugging('Moodle1 XML transformer should not process arrays but plain content always', DEBUG_DEVELOPER);
793
            foreach($content as $key => $plaincontent) {
794
                $content[$key] = $this->process($plaincontent);
795
            }
796
            return $content;
797
 
798
        } else if (is_object($content)) {
799
            debugging('Moodle1 XML transformer should not process objects but plain content always', DEBUG_DEVELOPER);
800
            foreach((array)$content as $key => $plaincontent) {
801
                $content[$key] = $this->process($plaincontent);
802
            }
803
            return (object)$content;
804
        }
805
 
806
        // try to deal with some trivial cases first
807
        if (is_null($content)) {
808
            return '$@NULL@$';
809
 
810
        } else if ($content === '') {
811
            return '';
812
 
813
        } else if (is_numeric($content)) {
814
            return $content;
815
 
816
        } else if (strlen($content) < 32) {
817
            return $content;
818
        }
819
 
820
        return $content;
821
    }
822
}
823
 
824
 
825
/**
826
 * Class representing a path to be converted from XML file
827
 *
828
 * This was created as a copy of {@link restore_path_element} and should be refactored
829
 * probably.
830
 */
831
class convert_path {
832
 
833
    /** @var string name of the element */
834
    protected $name;
835
 
836
    /** @var string path within the XML file this element will handle */
837
    protected $path;
838
 
839
    /** @var bool flag to define if this element will get child ones grouped or no */
840
    protected $grouped;
841
 
842
    /** @var object object instance in charge of processing this element. */
843
    protected $pobject = null;
844
 
845
    /** @var string the name of the processing method */
846
    protected $pmethod = null;
847
 
848
    /** @var string the name of the path start event handler */
849
    protected $smethod = null;
850
 
851
    /** @var string the name of the path end event handler */
852
    protected $emethod = null;
853
 
854
    /** @var mixed last data read for this element or returned data by processing method */
855
    protected $tags = null;
856
 
857
    /** @var array of deprecated fields that are dropped */
858
    protected $dropfields = array();
859
 
860
    /** @var array of fields renaming */
861
    protected $renamefields = array();
862
 
863
    /** @var array of new fields to add and their initial values */
864
    protected $newfields = array();
865
 
866
    /**
867
     * Constructor
868
     *
869
     * The optional recipe array can have three keys, and for each key, the value is another array.
870
     * - newfields    => array fieldname => defaultvalue indicates fields that have been added to the table,
871
     *                                                   and so should be added to the XML.
872
     * - dropfields   => array fieldname                 indicates fieldsthat have been dropped from the table,
873
     *                                                   and so can be dropped from the XML.
874
     * - renamefields => array oldname => newname        indicates fieldsthat have been renamed in the table,
875
     *                                                   and so should be renamed in the XML.
876
     * {@line moodle1_course_outline_handler} is a good example that uses all of these.
877
     *
878
     * @param string $name name of the element
879
     * @param string $path path of the element
880
     * @param array $recipe basic description of the structure conversion
881
     * @param bool $grouped to gather information in grouped mode or no
882
     */
883
    public function __construct($name, $path, array $recipe = array(), $grouped = false) {
884
 
885
        $this->validate_name($name);
886
 
887
        $this->name     = $name;
888
        $this->path     = $path;
889
        $this->grouped  = $grouped;
890
 
891
        // set the default method names
892
        $this->set_processing_method('process_' . $name);
893
        $this->set_start_method('on_'.$name.'_start');
894
        $this->set_end_method('on_'.$name.'_end');
895
 
896
        if ($grouped and !empty($recipe)) {
897
            throw new convert_path_exception('recipes_not_supported_for_grouped_elements');
898
        }
899
 
900
        if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
901
            $this->set_dropped_fields($recipe['dropfields']);
902
        }
903
        if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
904
            $this->set_renamed_fields($recipe['renamefields']);
905
        }
906
        if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
907
            $this->set_new_fields($recipe['newfields']);
908
        }
909
    }
910
 
911
    /**
912
     * Validates and sets the given processing object
913
     *
914
     * @param object $pobject processing object, must provide a method to be called
915
     */
916
    public function set_processing_object($pobject) {
917
        $this->validate_pobject($pobject);
918
        $this->pobject = $pobject;
919
    }
920
 
921
    /**
922
     * Sets the name of the processing method
923
     *
924
     * @param string $pmethod
925
     */
926
    public function set_processing_method($pmethod) {
927
        $this->pmethod = $pmethod;
928
    }
929
 
930
    /**
931
     * Sets the name of the path start event listener
932
     *
933
     * @param string $smethod
934
     */
935
    public function set_start_method($smethod) {
936
        $this->smethod = $smethod;
937
    }
938
 
939
    /**
940
     * Sets the name of the path end event listener
941
     *
942
     * @param string $emethod
943
     */
944
    public function set_end_method($emethod) {
945
        $this->emethod = $emethod;
946
    }
947
 
948
    /**
949
     * Sets the element tags
950
     *
951
     * @param array $tags
952
     */
953
    public function set_tags($tags) {
954
        $this->tags = $tags;
955
    }
956
 
957
    /**
958
     * Sets the list of deprecated fields to drop
959
     *
960
     * @param array $fields
961
     */
962
    public function set_dropped_fields(array $fields) {
963
        $this->dropfields = $fields;
964
    }
965
 
966
    /**
967
     * Sets the required new names of the current fields
968
     *
969
     * @param array $fields (string)$currentname => (string)$newname
970
     */
971
    public function set_renamed_fields(array $fields) {
972
        $this->renamefields = $fields;
973
    }
974
 
975
    /**
976
     * Sets the new fields and their values
977
     *
978
     * @param array $fields (string)$field => (mixed)value
979
     */
980
    public function set_new_fields(array $fields) {
981
        $this->newfields = $fields;
982
    }
983
 
984
    /**
985
     * Cooks the parsed tags data by applying known recipes
986
     *
987
     * Recipes are used for common trivial operations like adding new fields
988
     * or renaming fields. The handler's processing method receives cooked
989
     * data.
990
     *
991
     * @param array $data the contents of the element
992
     * @return array
993
     */
994
    public function apply_recipes(array $data) {
995
 
996
        $cooked = array();
997
 
998
        foreach ($data as $name => $value) {
999
            // lower case rocks!
1000
            $name = strtolower($name);
1001
 
1002
            if (is_array($value)) {
1003
                if ($this->is_grouped()) {
1004
                    $value = $this->apply_recipes($value);
1005
                } else {
1006
                    throw new convert_path_exception('non_grouped_path_with_array_values');
1007
                }
1008
            }
1009
 
1010
            // drop legacy fields
1011
            if (in_array($name, $this->dropfields)) {
1012
                continue;
1013
            }
1014
 
1015
            // fields renaming
1016
            if (array_key_exists($name, $this->renamefields)) {
1017
                $name = $this->renamefields[$name];
1018
            }
1019
 
1020
            $cooked[$name] = $value;
1021
        }
1022
 
1023
        // adding new fields
1024
        foreach ($this->newfields as $name => $value) {
1025
            $cooked[$name] = $value;
1026
        }
1027
 
1028
        return $cooked;
1029
    }
1030
 
1031
    /**
1032
     * @return string the element given name
1033
     */
1034
    public function get_name() {
1035
        return $this->name;
1036
    }
1037
 
1038
    /**
1039
     * @return string the path to the element
1040
     */
1041
    public function get_path() {
1042
        return $this->path;
1043
    }
1044
 
1045
    /**
1046
     * @return bool flag to define if this element will get child ones grouped or no
1047
     */
1048
    public function is_grouped() {
1049
        return $this->grouped;
1050
    }
1051
 
1052
    /**
1053
     * @return object the processing object providing the processing method
1054
     */
1055
    public function get_processing_object() {
1056
        return $this->pobject;
1057
    }
1058
 
1059
    /**
1060
     * @return string the name of the method to call to process the element
1061
     */
1062
    public function get_processing_method() {
1063
        return $this->pmethod;
1064
    }
1065
 
1066
    /**
1067
     * @return string the name of the path start event listener
1068
     */
1069
    public function get_start_method() {
1070
        return $this->smethod;
1071
    }
1072
 
1073
    /**
1074
     * @return string the name of the path end event listener
1075
     */
1076
    public function get_end_method() {
1077
        return $this->emethod;
1078
    }
1079
 
1080
    /**
1081
     * @return mixed the element data
1082
     */
1083
    public function get_tags() {
1084
        return $this->tags;
1085
    }
1086
 
1087
 
1088
    /// end of public API //////////////////////////////////////////////////////
1089
 
1090
    /**
1091
     * Makes sure the given name is a valid element name
1092
     *
1093
     * Note it may look as if we used exceptions for code flow control here. That's not the case
1094
     * as we actually validate the code, not the user data. And the code is supposed to be
1095
     * correct.
1096
     *
1097
     * @param string @name the element given name
1098
     * @throws convert_path_exception
1099
     * @return void
1100
     */
1101
    protected function validate_name($name) {
1102
        // Validate various name constraints, throwing exception if needed
1103
        if (empty($name)) {
1104
            throw new convert_path_exception('convert_path_emptyname', $name);
1105
        }
1106
        if (preg_replace('/\s/', '', $name) != $name) {
1107
            throw new convert_path_exception('convert_path_whitespace', $name);
1108
        }
1109
        if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
1110
            throw new convert_path_exception('convert_path_notasciiname', $name);
1111
        }
1112
    }
1113
 
1114
    /**
1115
     * Makes sure that the given object is a valid processing object
1116
     *
1117
     * The processing object must be an object providing at least element's processing method
1118
     * or path-reached-end event listener or path-reached-start listener method.
1119
     *
1120
     * Note it may look as if we used exceptions for code flow control here. That's not the case
1121
     * as we actually validate the code, not the user data. And the code is supposed to be
1122
     * correct.
1123
      *
1124
     * @param object $pobject
1125
     * @throws convert_path_exception
1126
     * @return void
1127
     */
1128
    protected function validate_pobject($pobject) {
1129
        if (!is_object($pobject)) {
1130
            throw new convert_path_exception('convert_path_no_object', get_class($pobject));
1131
        }
1132
        if (!method_exists($pobject, $this->get_processing_method()) and
1133
            !method_exists($pobject, $this->get_end_method()) and
1134
            !method_exists($pobject, $this->get_start_method())) {
1135
            throw new convert_path_exception('convert_path_missing_method', get_class($pobject));
1136
        }
1137
    }
1138
}
1139
 
1140
 
1141
/**
1142
 * Exception being thrown by {@link convert_path} methods
1143
 */
1144
class convert_path_exception extends moodle_exception {
1145
 
1146
    /**
1147
     * Constructor
1148
     *
1149
     * @param string $errorcode key for the corresponding error string
1150
     * @param mixed $a extra words and phrases that might be required by the error string
1151
     * @param string $debuginfo optional debugging information
1152
     */
1153
    public function __construct($errorcode, $a = null, $debuginfo = null) {
1154
        parent::__construct($errorcode, '', '', $a, $debuginfo);
1155
    }
1156
}
1157
 
1158
 
1159
/**
1160
 * The class responsible for files migration
1161
 *
1162
 * The files in Moodle 1.9 backup are stored in moddata, user_files, group_files,
1163
 * course_files and site_files folders.
1164
 */
1165
class moodle1_file_manager implements loggable {
1166
 
1167
    /** @var moodle1_converter instance we serve to */
1168
    public $converter;
1169
 
1170
    /** @var int context id of the files being migrated */
1171
    public $contextid;
1172
 
1173
    /** @var string component name of the files being migrated */
1174
    public $component;
1175
 
1176
    /** @var string file area of the files being migrated */
1177
    public $filearea;
1178
 
1179
    /** @var int item id of the files being migrated */
1180
    public $itemid = 0;
1181
 
1182
    /** @var int user id */
1183
    public $userid;
1184
 
1185
    /** @var string the root of the converter temp directory */
1186
    protected $basepath;
1187
 
1188
    /** @var array of file ids that were migrated by this instance */
1189
    protected $fileids = array();
1190
 
1191
    /**
1192
     * Constructor optionally accepting some default values for the migrated files
1193
     *
1194
     * @param moodle1_converter $converter the converter instance we serve to
1195
     * @param int $contextid initial context id of the files being migrated
1196
     * @param string $component initial component name of the files being migrated
1197
     * @param string $filearea initial file area of the files being migrated
1198
     * @param int $itemid initial item id of the files being migrated
1199
     * @param int $userid initial user id of the files being migrated
1200
     */
1201
    public function __construct(moodle1_converter $converter, $contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
1202
        // set the initial destination of the migrated files
1203
        $this->converter = $converter;
1204
        $this->contextid = $contextid;
1205
        $this->component = $component;
1206
        $this->filearea  = $filearea;
1207
        $this->itemid    = $itemid;
1208
        $this->userid    = $userid;
1209
        // set other useful bits
1210
        $this->basepath  = $converter->get_tempdir_path();
1211
    }
1212
 
1213
    /**
1214
     * Migrates one given file stored on disk
1215
     *
1216
     * @param string $sourcepath the path to the source local file within the backup archive {@example 'moddata/foobar/file.ext'}
1217
     * @param string $filepath the file path of the migrated file, defaults to the root directory '/' {@example '/sub/dir/'}
1218
     * @param string $filename the name of the migrated file, defaults to the same as the source file has
1219
     * @param int $sortorder the sortorder of the file (main files have sortorder set to 1)
1220
     * @param int $timecreated override the timestamp of when the migrated file should appear as created
1221
     * @param int $timemodified override the timestamp of when the migrated file should appear as modified
1222
     * @return int id of the migrated file
1223
     */
1224
    public function migrate_file($sourcepath, $filepath = '/', $filename = null, $sortorder = 0, $timecreated = null, $timemodified = null) {
1225
 
1226
        // Normalise Windows paths a bit.
1227
        $sourcepath = str_replace('\\', '/', $sourcepath);
1228
 
1229
        // PARAM_PATH must not be used on full OS path!
1230
        if ($sourcepath !== clean_param($sourcepath, PARAM_PATH)) {
1231
            throw new moodle1_convert_exception('file_invalid_path', $sourcepath);
1232
        }
1233
 
1234
        $sourcefullpath = $this->basepath.'/'.$sourcepath;
1235
 
1236
        if (!is_readable($sourcefullpath)) {
1237
            throw new moodle1_convert_exception('file_not_readable', $sourcefullpath);
1238
        }
1239
 
1240
        // sanitize filepath
1241
        if (empty($filepath)) {
1242
            $filepath = '/';
1243
        }
1244
        if (substr($filepath, -1) !== '/') {
1245
            $filepath .= '/';
1246
        }
1247
        $filepath = clean_param($filepath, PARAM_PATH);
1248
 
1249
        if (core_text::strlen($filepath) > 255) {
1250
            throw new moodle1_convert_exception('file_path_longer_than_255_chars');
1251
        }
1252
 
1253
        if (is_null($filename)) {
1254
            $filename = basename($sourcefullpath);
1255
        }
1256
 
1257
        $filename = clean_param($filename, PARAM_FILE);
1258
 
1259
        if ($filename === '') {
1260
            throw new moodle1_convert_exception('unsupported_chars_in_filename');
1261
        }
1262
 
1263
        if (is_null($timecreated)) {
1264
            $timecreated = filectime($sourcefullpath);
1265
        }
1266
 
1267
        if (is_null($timemodified)) {
1268
            $timemodified = filemtime($sourcefullpath);
1269
        }
1270
 
1271
        $filerecord = $this->make_file_record(array(
1272
            'filepath'      => $filepath,
1273
            'filename'      => $filename,
1274
            'sortorder'     => $sortorder,
1275
            'mimetype'      => mimeinfo('type', $sourcefullpath),
1276
            'timecreated'   => $timecreated,
1277
            'timemodified'  => $timemodified,
1278
        ));
1279
 
1280
        list($filerecord['contenthash'], $filerecord['filesize'], $newfile) = $this->add_file_to_pool($sourcefullpath);
1281
        $this->stash_file($filerecord);
1282
 
1283
        return $filerecord['id'];
1284
    }
1285
 
1286
    /**
1287
     * Migrates all files in the given directory
1288
     *
1289
     * @param string $rootpath path within the backup archive to the root directory containing the files {@example 'course_files'}
1290
     * @param string $relpath relative path used during the recursion - do not provide when calling this!
1291
     * @return array ids of the migrated files, empty array if the $rootpath not found
1292
     */
1293
    public function migrate_directory($rootpath, $relpath='/') {
1294
 
1295
        // Check the trailing slash in the $rootpath
1296
        if (substr($rootpath, -1) === '/') {
1297
            debugging('moodle1_file_manager::migrate_directory() expects $rootpath without the trailing slash', DEBUG_DEVELOPER);
1298
            $rootpath = substr($rootpath, 0, strlen($rootpath) - 1);
1299
        }
1300
 
1301
        if (!file_exists($this->basepath.'/'.$rootpath.$relpath)) {
1302
            return array();
1303
        }
1304
 
1305
        $fileids = array();
1306
 
1307
        // make the fake file record for the directory itself
1308
        $filerecord = $this->make_file_record(array('filepath' => $relpath, 'filename' => '.'));
1309
        $this->stash_file($filerecord);
1310
        $fileids[] = $filerecord['id'];
1311
 
1312
        $items = new DirectoryIterator($this->basepath.'/'.$rootpath.$relpath);
1313
 
1314
        foreach ($items as $item) {
1315
 
1316
            if ($item->isDot()) {
1317
                continue;
1318
            }
1319
 
1320
            if ($item->isLink()) {
1321
                throw new moodle1_convert_exception('unexpected_symlink');
1322
            }
1323
 
1324
            if ($item->isFile()) {
1325
                $fileids[] = $this->migrate_file(substr($item->getPathname(), strlen($this->basepath.'/')),
1326
                    $relpath, $item->getFilename(), 0, $item->getCTime(), $item->getMTime());
1327
 
1328
            } else {
1329
                $dirname = clean_param($item->getFilename(), PARAM_PATH);
1330
 
1331
                if ($dirname === '') {
1332
                    throw new moodle1_convert_exception('unsupported_chars_in_filename');
1333
                }
1334
 
1335
                // migrate subdirectories recursively
1336
                $fileids = array_merge($fileids, $this->migrate_directory($rootpath, $relpath.$item->getFilename().'/'));
1337
            }
1338
        }
1339
 
1340
        return $fileids;
1341
    }
1342
 
1343
    /**
1344
     * Returns the list of all file ids migrated by this instance so far
1345
     *
1346
     * @return array of int
1347
     */
1348
    public function get_fileids() {
1349
        return $this->fileids;
1350
    }
1351
 
1352
    /**
1353
     * Explicitly clear the list of file ids migrated by this instance so far
1354
     */
1355
    public function reset_fileids() {
1356
        $this->fileids = array();
1357
    }
1358
 
1359
    /**
1360
     * Log a message using the converter's logging mechanism
1361
     *
1362
     * @param string $message message text
1363
     * @param int $level message level {@example backup::LOG_WARNING}
1364
     * @param null|mixed $a additional information
1365
     * @param null|int $depth the message depth
1366
     * @param bool $display whether the message should be sent to the output, too
1367
     */
1368
    public function log($message, $level, $a = null, $depth = null, $display = false) {
1369
        $this->converter->log($message, $level, $a, $depth, $display);
1370
    }
1371
 
1372
    /// internal implementation details ////////////////////////////////////////
1373
 
1374
    /**
1375
     * Prepares a fake record from the files table
1376
     *
1377
     * @param array $fileinfo explicit file data
1378
     * @return array
1379
     */
1380
    protected function make_file_record(array $fileinfo) {
1381
 
1382
        $defaultrecord = array(
1383
            'contenthash'   => file_storage::hash_from_string(''),
1384
            'contextid'     => $this->contextid,
1385
            'component'     => $this->component,
1386
            'filearea'      => $this->filearea,
1387
            'itemid'        => $this->itemid,
1388
            'filepath'      => null,
1389
            'filename'      => null,
1390
            'filesize'      => 0,
1391
            'userid'        => $this->userid,
1392
            'mimetype'      => null,
1393
            'status'        => 0,
1394
            'timecreated'   => $now = time(),
1395
            'timemodified'  => $now,
1396
            'source'        => null,
1397
            'author'        => null,
1398
            'license'       => null,
1399
            'sortorder'     => 0,
1400
        );
1401
 
1402
        if (!array_key_exists('id', $fileinfo)) {
1403
            $defaultrecord['id'] = $this->converter->get_nextid();
1404
        }
1405
 
1406
        // override the default values with the explicit data provided and return
1407
        return array_merge($defaultrecord, $fileinfo);
1408
    }
1409
 
1410
    /**
1411
     * Copies the given file to the pool directory
1412
     *
1413
     * Returns an array containing SHA1 hash of the file contents, the file size
1414
     * and a flag indicating whether the file was actually added to the pool or whether
1415
     * it was already there.
1416
     *
1417
     * @param string $pathname the full path to the file
1418
     * @return array with keys (string)contenthash, (int)filesize, (bool)newfile
1419
     */
1420
    protected function add_file_to_pool($pathname) {
1421
 
1422
        if (!is_readable($pathname)) {
1423
            throw new moodle1_convert_exception('file_not_readable');
1424
        }
1425
 
1426
        $contenthash = file_storage::hash_from_path($pathname);
1427
        $filesize    = filesize($pathname);
1428
        $hashpath    = $this->converter->get_workdir_path().'/files/'.substr($contenthash, 0, 2);
1429
        $hashfile    = "$hashpath/$contenthash";
1430
 
1431
        if (file_exists($hashfile)) {
1432
            if (filesize($hashfile) !== $filesize) {
1433
                // congratulations! you have found two files with different size and the same
1434
                // content hash. or, something were wrong (which is more likely)
1435
                throw new moodle1_convert_exception('same_hash_different_size');
1436
            }
1437
            $newfile = false;
1438
 
1439
        } else {
1440
            check_dir_exists($hashpath);
1441
            $newfile = true;
1442
 
1443
            if (!copy($pathname, $hashfile)) {
1444
                throw new moodle1_convert_exception('unable_to_copy_file');
1445
            }
1446
 
1447
            if (filesize($hashfile) !== $filesize) {
1448
                throw new moodle1_convert_exception('filesize_different_after_copy');
1449
            }
1450
        }
1451
 
1452
        return array($contenthash, $filesize, $newfile);
1453
    }
1454
 
1455
    /**
1456
     * Stashes the file record into 'files' stash and adds the record id to list of migrated files
1457
     *
1458
     * @param array $filerecord
1459
     */
1460
    protected function stash_file(array $filerecord) {
1461
        $this->converter->set_stash('files', $filerecord, $filerecord['id']);
1462
        $this->fileids[] = $filerecord['id'];
1463
    }
1464
}
1465
 
1466
 
1467
/**
1468
 * Helper class that handles ids annotations for inforef.xml files
1469
 */
1470
class moodle1_inforef_manager {
1471
 
1472
    /** @var string the name of the annotator we serve to (like course, section, activity, block) */
1473
    protected $annotator = null;
1474
 
1475
    /** @var int the id of the annotator if it can have multiple instances */
1476
    protected $annotatorid = null;
1477
 
1478
    /** @var array the actual storage of references, currently implemented as a in-memory structure */
1479
    private $refs = array();
1480
 
1481
    /**
1482
     * Creates new instance of the manager for the given annotator
1483
     *
1484
     * The identification of the annotator we serve to may be important in the future
1485
     * when we move the actual storage of the references from memory to a persistent storage.
1486
     *
1487
     * @param moodle1_converter $converter
1488
     * @param string $name the name of the annotator (like course, section, activity, block)
1489
     * @param int $id the id of the annotator if required
1490
     */
1491
    public function __construct(moodle1_converter $converter, $name, $id = 0) {
1492
        $this->annotator   = $name;
1493
        $this->annotatorid = $id;
1494
    }
1495
 
1496
    /**
1497
     * Adds a reference
1498
     *
1499
     * @param string $item the name of referenced item (like user, file, scale, outcome or grade_item)
1500
     * @param int $id the value of the reference
1501
     */
1502
    public function add_ref($item, $id) {
1503
        $this->validate_item($item);
1504
        $this->refs[$item][$id] = true;
1505
    }
1506
 
1507
    /**
1508
     * Adds a bulk of references
1509
     *
1510
     * @param string $item the name of referenced item (like user, file, scale, outcome or grade_item)
1511
     * @param array $ids the list of referenced ids
1512
     */
1513
    public function add_refs($item, array $ids) {
1514
        $this->validate_item($item);
1515
        foreach ($ids as $id) {
1516
            $this->refs[$item][$id] = true;
1517
        }
1518
    }
1519
 
1520
    /**
1521
     * Writes the current references using a given opened xml writer
1522
     *
1523
     * @param xml_writer $xmlwriter
1524
     */
1525
    public function write_refs(xml_writer $xmlwriter) {
1526
        $xmlwriter->begin_tag('inforef');
1527
        foreach ($this->refs as $item => $ids) {
1528
            $xmlwriter->begin_tag($item.'ref');
1529
            foreach (array_keys($ids) as $id) {
1530
                $xmlwriter->full_tag($item, $id);
1531
            }
1532
            $xmlwriter->end_tag($item.'ref');
1533
        }
1534
        $xmlwriter->end_tag('inforef');
1535
    }
1536
 
1537
    /**
1538
     * Makes sure that the given name is a valid citizen of inforef.xml file
1539
     *
1540
     * @see backup_helper::get_inforef_itemnames()
1541
     * @param string $item the name of reference (like user, file, scale, outcome or grade_item)
1542
     * @throws coding_exception
1543
     */
1544
    protected function validate_item($item) {
1545
 
1546
        $allowed = array(
1547
            'user'              => true,
1548
            'grouping'          => true,
1549
            'group'             => true,
1550
            'role'              => true,
1551
            'file'              => true,
1552
            'scale'             => true,
1553
            'outcome'           => true,
1554
            'grade_item'        => true,
1555
            'question_category' => true
1556
        );
1557
 
1558
        if (!isset($allowed[$item])) {
1559
            throw new coding_exception('Invalid inforef item type');
1560
        }
1561
    }
1562
}