Converter file manager's methods take relative path within the backup instead of...
[moodle.git] / backup / converter / moodle1 / lib.php
CommitLineData
1e2c7351
DM
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
27defined('MOODLE_INTERNAL') || die();
28
29require_once($CFG->dirroot . '/backup/converter/convertlib.php');
30require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php');
31require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
a5fe5912
DM
32require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php');
33require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php');
34require_once($CFG->dirroot . '/backup/util/dbops/restore_dbops.class.php');
96f7c7ad 35require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php');
1e2c7351
DM
36require_once(dirname(__FILE__) . '/handlerlib.php');
37
38/**
39 * Converter of Moodle 1.9 backup into Moodle 2.x format
40 */
41class 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
6cfa5a32
DM
52 /** @var null|string the current module being processed - used to expand the MOD paths */
53 protected $currentmod = null;
1e2c7351 54
6cfa5a32
DM
55 /** @var null|string the current block being processed - used to expand the BLOCK paths */
56 protected $currentblock = null;
1e2c7351
DM
57
58 /** @var string path currently locking processing of children */
59 protected $pathlock;
60
61 /**
62 * Instructs the dispatcher to ignore all children below path processor returning it
63 */
64 const SKIP_ALL_CHILDREN = -991399;
65
66 /**
67 * Detects the Moodle 1.9 format of the backup directory
68 *
69 * @param string $tempdir the name of the backup directory
70 * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise
71 */
72 public static function detect_format($tempdir) {
73 global $CFG;
74
75 $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle.xml';
76 if (file_exists($filepath)) {
77 // looks promising, lets load some information
78 $handle = fopen($filepath, 'r');
79 $first_chars = fread($handle, 200);
80 fclose($handle);
81
82 // check if it has the required strings
83 if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
84 strpos($first_chars,'<MOODLE_BACKUP>') !== false and
85 strpos($first_chars,'<INFO>') !== false) {
86
87 return backup::FORMAT_MOODLE1;
88 }
89 }
90
91 return null;
92 }
93
94 /**
95 * Initialize the instance if needed, called by the constructor
96 *
97 * Here we create objects we need before the execution.
98 */
99 protected function init() {
100
101 // ask your mother first before going out playing with toys
102 parent::init();
103
104 // good boy, prepare XML parser and processor
105 $this->xmlparser = new progressive_parser();
106 $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
107 $this->xmlprocessor = new moodle1_parser_processor($this);
108 $this->xmlparser->set_processor($this->xmlprocessor);
109
110 // make sure that MOD and BLOCK paths are visited
111 $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/MODULES/MOD');
112 $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK');
113
114 // register the conversion handlers
115 foreach (moodle1_handlers_factory::get_handlers($this) as $handler) {
1e2c7351
DM
116 $this->register_handler($handler, $handler->get_paths());
117 }
118 }
119
120 /**
121 * Converts the contents of the tempdir into the target format in the workdir
122 */
123 protected function execute() {
9b5f1ad5 124 $this->create_stash_storage();
1e2c7351 125 $this->xmlparser->process();
9b5f1ad5 126 $this->drop_stash_storage();
1e2c7351
DM
127 }
128
129 /**
130 * Register a handler for the given path elements
131 */
132 protected function register_handler(moodle1_handler $handler, array $elements) {
133
134 // first iteration, push them to new array, indexed by name
135 // to detect duplicates in names or paths
136 $names = array();
137 $paths = array();
138 foreach($elements as $element) {
139 if (!$element instanceof convert_path) {
140 throw new convert_exception('path_element_wrong_class', get_class($element));
141 }
142 if (array_key_exists($element->get_name(), $names)) {
143 throw new convert_exception('path_element_name_alreadyexists', $element->get_name());
144 }
145 if (array_key_exists($element->get_path(), $paths)) {
146 throw new convert_exception('path_element_path_alreadyexists', $element->get_path());
147 }
148 $names[$element->get_name()] = true;
149 $paths[$element->get_path()] = $element;
150 }
151
152 // now, for each element not having a processing object yet, assign the handler
153 // if the element is not a memeber of a group
154 foreach($paths as $key => $element) {
155 if (is_null($element->get_processing_object()) and !$this->grouped_parent_exists($element, $paths)) {
156 $paths[$key]->set_processing_object($handler);
157 }
158 // add the element path to the processor
159 $this->xmlprocessor->add_path($element->get_path(), $element->is_grouped());
160 }
161
162 // done, store the paths (duplicates by path are discarded)
163 $this->pathelements = array_merge($this->pathelements, $paths);
164
165 // remove the injected plugin name element from the MOD and BLOCK paths
166 // and register such collapsed path, too
167 foreach ($elements as $element) {
168 $path = $element->get_path();
169 $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/MODULES\/MOD\/(\w+)\//', '/MOODLE_BACKUP/COURSE/MODULES/MOD/', $path);
170 $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/BLOCKS\/BLOCK\/(\w+)\//', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/', $path);
171 if (!empty($path) and $path != $element->get_path()) {
172 $this->xmlprocessor->add_path($path, false);
173 }
174 }
175 }
176
177 /**
178 * Helper method used by {@link self::register_handler()}
179 *
180 * @param convert_path $pelement path element
181 * @param array of convert_path instances
182 * @return bool true if grouped parent was found, false otherwise
183 */
184 protected function grouped_parent_exists($pelement, $elements) {
185
186 foreach ($elements as $element) {
187 if ($pelement->get_path() == $element->get_path()) {
188 // don't compare against itself
189 continue;
190 }
191 // if the element is grouped and it is a parent of pelement, return true
192 if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
193 return true;
194 }
195 }
196
197 // no grouped parent found
198 return false;
199 }
200
201 /**
202 * Process the data obtained from the XML parser processor
203 *
204 * This methods receives one chunk of information from the XML parser
205 * processor and dispatches it, following the naming rules.
206 * We are expanding the modules and blocks paths here to include the plugin's name.
207 *
208 * @param array $data
209 */
210 public function process_chunk($data) {
211
212 $path = $data['path'];
213
214 // expand the MOD paths so that they contain the module name
215 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
216 $this->currentmod = strtoupper($data['tags']['MODTYPE']);
217 $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
218
219 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
220 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
221 }
222
223 // expand the BLOCK paths so that they contain the module name
224 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
225 $this->currentblock = strtoupper($data['tags']['NAME']);
226 $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
227
228 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
229 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
230 }
231
232 if ($path !== $data['path']) {
233 if (!array_key_exists($path, $this->pathelements)) {
234 // no handler registered for the transformed MOD or BLOCK path
235 // todo add this event to the convert log instead of debugging
236 //debugging('No handler registered for the path ' . $path);
237 return;
238
239 } else {
240 // pretend as if the original $data contained the tranformed path
241 $data['path'] = $path;
242 }
243 }
244
245 if (!array_key_exists($data['path'], $this->pathelements)) {
246 // path added to the processor without the handler
247 throw new convert_exception('missing_path_handler', $data['path']);
248 }
249
beb7de37
DM
250 $element = $this->pathelements[$data['path']];
251 $object = $element->get_processing_object();
252 $method = $element->get_processing_method();
253 $returned = null; // data returned by the processing method, if any
1e2c7351
DM
254
255 if (empty($object)) {
256 throw new convert_exception('missing_processing_object', $object);
257 }
258
259 // release the lock if we aren't anymore within children of it
260 if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
261 $this->pathlock = null;
262 }
263
264 // if the path is not locked, apply the element's recipes and dispatch
265 // the cooked tags to the processing method
266 if (is_null($this->pathlock)) {
beb7de37
DM
267 $rawdatatags = $data['tags'];
268 $data['tags'] = $element->apply_recipes($data['tags']);
46ff8b0e
DM
269
270 // if the processing method exists, give it a chance to modify data
271 if (method_exists($object, $method)) {
272 $returned = $object->$method($data['tags'], $rawdatatags);
273 }
1e2c7351
DM
274 }
275
276 // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path
277 // and lock it so that its children are not dispatched
beb7de37 278 if ($returned === self::SKIP_ALL_CHILDREN) {
1e2c7351
DM
279 // check we haven't any previous lock
280 if (!is_null($this->pathlock)) {
281 throw new convert_exception('already_locked_path', $data['path']);
282 }
283 // set the lock - nothing below the current path will be dispatched
284 $this->pathlock = $data['path'] . '/';
285
286 // if the method has returned any info, set element data to it
beb7de37
DM
287 } else if (!is_null($returned)) {
288 $element->set_data($returned);
1e2c7351
DM
289
290 // use just the cooked parsed data otherwise
291 } else {
292 $element->set_data($data);
293 }
294 }
295
296 /**
297 * Executes operations required at the start of a watched path
298 *
6cfa5a32
DM
299 * For MOD and BLOCK paths, this is supported only for the sub-paths, not the root
300 * module/block element. For the illustration:
301 *
302 * You CAN'T attach on_xxx_start() listener to a path like
303 * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP because the <MOD> must
304 * be processed first in {@link self::process_chunk()} where $this->currentmod
305 * is set.
306 *
307 * You CAN attach some on_xxx_start() listener to a path like
308 * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP/SUBMISSIONS because it is
309 * a sub-path under <MOD> and we have $this->currentmod already set when the
310 * <SUBMISSIONS> is reached.
1e2c7351 311 *
1e2c7351
DM
312 * @param string $path in the original file
313 */
314 public function path_start_reached($path) {
a5fe5912 315
6cfa5a32
DM
316 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
317 $this->currentmod = null;
318 $forbidden = true;
319
320 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
321 // expand the MOD paths so that they contain the module name
322 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
323 }
324
325 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
326 $this->currentmod = null;
327 $forbidden = true;
328
329 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
330 // expand the BLOCK paths so that they contain the module name
331 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
332 }
333
a5fe5912
DM
334 if (empty($this->pathelements[$path])) {
335 return;
336 }
337
338 $element = $this->pathelements[$path];
339 $pobject = $element->get_processing_object();
46ff8b0e 340 $method = $element->get_start_method();
a5fe5912
DM
341
342 if (method_exists($pobject, $method)) {
6cfa5a32
DM
343 if (empty($forbidden)) {
344 $pobject->$method();
345
346 } else {
347 // this path is not supported because we do not know the module/block yet
348 throw new coding_exception('Attaching the on-start event listener to the root MOD or BLOCK element is forbidden.');
349 }
a5fe5912 350 }
1e2c7351
DM
351 }
352
353 /**
354 * Executes operations required at the end of a watched path
355 *
1e2c7351
DM
356 * @param string $path in the original file
357 */
358 public function path_end_reached($path) {
a5fe5912
DM
359
360 // expand the MOD paths so that they contain the current module name
361 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
362 $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
363
364 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
365 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
366 }
367
368 // expand the BLOCK paths so that they contain the module name
369 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
370 $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
371
372 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
373 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
374 }
375
376 if (empty($this->pathelements[$path])) {
377 return;
378 }
379
380 $element = $this->pathelements[$path];
381 $pobject = $element->get_processing_object();
46ff8b0e 382 $method = $element->get_end_method();
1cc0e42a 383 $data = $element->get_data();
a5fe5912
DM
384
385 if (method_exists($pobject, $method)) {
1cc0e42a 386 $pobject->$method($data['tags']);
a5fe5912
DM
387 }
388 }
389
390 /**
9b5f1ad5 391 * Creates the temporary storage for stashed data
a5fe5912 392 *
9b5f1ad5 393 * This implementation uses backup_ids_temp table.
a5fe5912 394 */
9b5f1ad5
DM
395 public function create_stash_storage() {
396 backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
a5fe5912
DM
397 }
398
399 /**
9b5f1ad5 400 * Drops the temporary storage of stashed data
a5fe5912 401 *
9b5f1ad5 402 * This implementation uses backup_ids_temp table.
a5fe5912 403 */
9b5f1ad5
DM
404 public function drop_stash_storage() {
405 backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
1e2c7351 406 }
a5fe5912 407
beb7de37 408 /**
9b5f1ad5 409 * Stores some information for later processing
beb7de37 410 *
9b5f1ad5
DM
411 * This implementation uses backup_ids_temp table to store data. Make
412 * sure that the $stashname + $itemid combo is unique.
beb7de37
DM
413 *
414 * @param string $stashname name of the stash
415 * @param mixed $info information to stash
9b5f1ad5 416 * @param int $itemid optional id for multiple infos within the same stashname
beb7de37 417 */
9b5f1ad5
DM
418 public function set_stash($stashname, $info, $itemid = 0) {
419 try {
420 restore_dbops::set_backup_ids_record($this->get_id(), $stashname, $itemid, 0, null, $info);
421
422 } catch (dml_exception $e) {
423 throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage());
424 }
beb7de37
DM
425 }
426
427 /**
428 * Restores a given stash stored previously by {@link self::set_stash()}
429 *
430 * @param string $stashname name of the stash
9b5f1ad5
DM
431 * @param int $itemid optional id for multiple infos within the same stashname
432 * @throws moodle1_convert_empty_storage_exception if the info has not been stashed previously
beb7de37
DM
433 * @return mixed stashed data
434 */
9b5f1ad5
DM
435 public function get_stash($stashname, $itemid = 0) {
436
437 $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid);
438
439 if (empty($record)) {
6357693c 440 throw new moodle1_convert_empty_storage_exception('required_not_stashed_data', array($stashname, $itemid));
9b5f1ad5
DM
441 } else {
442 return $record->info;
443 }
beb7de37
DM
444 }
445
6d73f185
DM
446 /**
447 * Returns the list of stashed $itemids in the given stash
448 *
449 * @param string $stashname
450 * @return array
451 */
452 public function get_stash_itemids($stashname) {
453 global $DB;
454
455 $search = array(
456 'backupid' => $this->get_id(),
457 'itemname' => $stashname
458 );
459
460 return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemid'));
461 }
462
beb7de37
DM
463 /**
464 * Generates an artificial context id
465 *
466 * Moodle 1.9 backups do not contain any context information. But we need them
467 * in Moodle 2.x format so here we generate fictive context id for every given
468 * context level + instance combo.
469 *
26cac34a
DM
470 * CONTEXT_SYSTEM and CONTEXT_COURSE ignore the $instance as they represent a
471 * single system or the course being restored.
472 *
beb7de37
DM
473 * @see get_context_instance()
474 * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE
475 * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules
476 * @return int the context id
477 */
26cac34a 478 public function get_contextid($level, $instance = 0) {
beb7de37 479
9b5f1ad5 480 $stashname = 'context' . $level;
beb7de37 481
26cac34a
DM
482 if ($level == CONTEXT_SYSTEM or $level == CONTEXT_COURSE) {
483 $instance = 0;
484 }
485
d5d02635
DM
486 try {
487 // try the previously stashed id
488 return $this->get_stash($stashname, $instance);
beb7de37 489
d5d02635 490 } catch (moodle1_convert_empty_storage_exception $e) {
beb7de37 491 // this context level + instance is required for the first time
26cac34a
DM
492 $newid = $this->get_nextid();
493 $this->set_stash($stashname, $newid, $instance);
494 return $newid;
9b5f1ad5 495 }
beb7de37 496 }
179982a4 497
6700d288
DM
498 /**
499 * Simple autoincrement generator
500 *
501 * @return int the next number in a row of numbers
502 */
503 public function get_nextid() {
504 static $autoincrement = 0;
505 return ++$autoincrement;
506 }
507
66f79e50
DM
508 /**
509 * Creates and returns new instance of the file manager
510 *
511 * @param int $contextid the default context id of the files being migrated
512 * @param string $component the default component name of the files being migrated
513 * @param string $filearea the default file area of the files being migrated
514 * @param int $itemid the default item id of the files being migrated
515 * @param int $userid initial user id of the files being migrated
516 * @return moodle1_file_manager
517 */
518 public function get_file_manager($contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
519 return new moodle1_file_manager($this, $contextid, $component, $filearea, $itemid, $userid);
520 }
521
179982a4
DM
522 /**
523 * @see parent::description()
524 */
525 public static function description() {
526
527 return array(
528 'from' => backup::FORMAT_MOODLE1,
529 'to' => backup::FORMAT_MOODLE,
530 'cost' => 10,
531 );
532 }
1e2c7351
DM
533}
534
535
9b5f1ad5
DM
536/**
537 * Exception thrown by this converter
538 */
539class moodle1_convert_exception extends convert_exception {
540}
541
542
543/**
544 * Exception thrown by the temporary storage subsystem of moodle1_converter
545 */
546class moodle1_convert_storage_exception extends moodle1_convert_exception {
547}
548
549
550/**
551 * Exception thrown by the temporary storage subsystem of moodle1_converter
552 */
553class moodle1_convert_empty_storage_exception extends moodle1_convert_exception {
554}
555
556
1e2c7351 557/**
96f7c7ad 558 * XML parser processor used for processing parsed moodle.xml
1e2c7351
DM
559 */
560class moodle1_parser_processor extends grouped_parser_processor {
561
562 /** @var moodle1_converter */
563 protected $converter;
564
565 public function __construct(moodle1_converter $converter) {
566 $this->converter = $converter;
567 parent::__construct();
568 }
569
570 /**
8312ab67
DM
571 * Provides NULL decoding
572 *
573 * Note that we do not decode $@FILEPHP@$ and friends here as we are going to write them
574 * back immediately into another XML file.
1e2c7351
DM
575 */
576 public function process_cdata($cdata) {
1e2c7351 577
8312ab67 578 if ($cdata === '$@NULL@$') {
1e2c7351 579 return null;
1e2c7351 580 }
8312ab67
DM
581
582 return $cdata;
1e2c7351
DM
583 }
584
585 /**
586 * Override this method so we'll be able to skip
587 * dispatching some well-known chunks, like the
588 * ones being 100% part of subplugins stuff. Useful
589 * for allowing development without having all the
590 * possible restore subplugins defined
8312ab67
DM
591 *
592 * @todo review why is this here (it is a relict)
1e2c7351
DM
593 */
594 protected function postprocess_chunk($data) {
595
596 // Iterate over all the data tags, if any of them is
597 // not 'subplugin_XXXX' or has value, then it's a valid chunk,
598 // pass it to standard (parent) processing of chunks.
599 foreach ($data['tags'] as $key => $value) {
600 if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) {
601 parent::postprocess_chunk($data);
602 return;
603 }
604 }
605 // Arrived here, all the tags correspond to sublplugins and are empty,
606 // skip the chunk, and debug_developer notice
607 $this->chunks--; // not counted
608 debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) .
609 ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER);
610 }
611
612 /**
613 * Dispatches the data chunk to the converter class
614 *
615 * @param array $data the chunk of parsed data
616 */
617 protected function dispatch_chunk($data) {
618 $this->converter->process_chunk($data);
619 }
620
621 /**
622 * Informs the converter at the start of a watched path
623 *
624 * @param string $path
625 */
626 protected function notify_path_start($path) {
627 $this->converter->path_start_reached($path);
628 }
629
630 /**
631 * Informs the converter at the end of a watched path
632 *
633 * @param string $path
634 */
635 protected function notify_path_end($path) {
636 $this->converter->path_end_reached($path);
637 }
638}
639
640
96f7c7ad
DM
641/**
642 * XML transformer that modifies the content of the files being written during the conversion
643 *
644 * @see backup_xml_transformer
645 */
646class moodle1_xml_transformer extends xml_contenttransformer {
647
648 /**
649 * Modify the content before it is writter to a file
650 *
651 * @param string|mixed $content
652 */
653 public function process($content) {
654
655 // the content should be a string. If array or object is given, try our best recursively
656 // but inform the developer
657 if (is_array($content)) {
658 debugging('Moodle1 XML transformer should not process arrays but plain content always', DEBUG_DEVELOPER);
659 foreach($content as $key => $plaincontent) {
660 $content[$key] = $this->process($plaincontent);
661 }
662 return $content;
663
664 } else if (is_object($content)) {
665 debugging('Moodle1 XML transformer should not process objects but plain content always', DEBUG_DEVELOPER);
666 foreach((array)$content as $key => $plaincontent) {
667 $content[$key] = $this->process($plaincontent);
668 }
669 return (object)$content;
670 }
671
672 // try to deal with some trivial cases first
673 if (is_null($content)) {
674 return '$@NULL@$';
675
676 } else if ($content === '') {
677 return '';
678
679 } else if (is_numeric($content)) {
680 return $content;
681
682 } else if (strlen($content) < 32) {
683 return $content;
684 }
685
96f7c7ad
DM
686 return $content;
687 }
688}
689
690
1e2c7351
DM
691/**
692 * Class representing a path to be converted from XML file
693 *
694 * This was created as a copy of {@link restore_path_element} and should be refactored
695 * probably.
696 */
697class convert_path {
698
699 /** @var string name of the element */
700 protected $name;
701
702 /** @var string path within the XML file this element will handle */
703 protected $path;
704
705 /** @var bool flag to define if this element will get child ones grouped or no */
706 protected $grouped;
707
708 /** @var object object instance in charge of processing this element. */
709 protected $pobject = null;
710
711 /** @var string the name of the processing method */
712 protected $pmethod = null;
713
46ff8b0e
DM
714 /** @var string the name of the path start event handler */
715 protected $smethod = null;
716
717 /** @var string the name of the path end event handler */
718 protected $emethod = null;
719
1e2c7351
DM
720 /** @var mixed last data read for this element or returned data by processing method */
721 protected $data = null;
722
a5fe5912
DM
723 /** @var array of deprecated fields that are dropped */
724 protected $dropfields = array();
1e2c7351
DM
725
726 /** @var array of fields renaming */
727 protected $renamefields = array();
728
729 /** @var array of new fields to add and their initial values */
730 protected $newfields = array();
731
732 /**
733 * Constructor
734 *
735 * @param string $name name of the element
736 * @param string $path path of the element
737 * @param array $recipe basic description of the structure conversion
738 * @param bool $grouped to gather information in grouped mode or no
739 */
740 public function __construct($name, $path, array $recipe = array(), $grouped = false) {
741
742 $this->validate_name($name);
743
744 $this->name = $name;
745 $this->path = $path;
746 $this->grouped = $grouped;
747
46ff8b0e 748 // set the default method names
1e2c7351 749 $this->set_processing_method('process_' . $name);
46ff8b0e
DM
750 $this->set_start_method('on_'.$name.'_start');
751 $this->set_end_method('on_'.$name.'_end');
1e2c7351 752
a5fe5912
DM
753 if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
754 $this->set_dropped_fields($recipe['dropfields']);
1e2c7351
DM
755 }
756 if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
757 $this->set_renamed_fields($recipe['renamefields']);
758 }
759 if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
760 $this->set_new_fields($recipe['newfields']);
761 }
762 }
763
764 /**
765 * Validates and sets the given processing object
766 *
767 * @param object $pobject processing object, must provide a method to be called
768 */
769 public function set_processing_object($pobject) {
770 $this->validate_pobject($pobject);
771 $this->pobject = $pobject;
772 }
773
774 /**
775 * Sets the name of the processing method
776 *
777 * @param string $pmethod
778 */
779 public function set_processing_method($pmethod) {
780 $this->pmethod = $pmethod;
781 }
782
46ff8b0e
DM
783 /**
784 * Sets the name of the path start event listener
785 *
786 * @param string $smethod
787 */
788 public function set_start_method($smethod) {
789 $this->smethod = $smethod;
790 }
791
792 /**
793 * Sets the name of the path end event listener
794 *
795 * @param string $emethod
796 */
797 public function set_end_method($emethod) {
798 $this->emethod = $emethod;
799 }
800
1e2c7351
DM
801 /**
802 * Sets the element data
803 *
804 * @param mixed
805 */
806 public function set_data($data) {
807 $this->data = $data;
808 }
809
810 /**
a5fe5912 811 * Sets the list of deprecated fields to drop
1e2c7351
DM
812 *
813 * @param array $fields
814 */
a5fe5912
DM
815 public function set_dropped_fields(array $fields) {
816 $this->dropfields = $fields;
1e2c7351
DM
817 }
818
819 /**
820 * Sets the required new names of the current fields
821 *
822 * @param array $fields (string)$currentname => (string)$newname
823 */
824 public function set_renamed_fields(array $fields) {
825 $this->renamefields = $fields;
826 }
827
828 /**
829 * Sets the new fields and their values
830 *
831 * @param array $fields (string)$field => (mixed)value
832 */
833 public function set_new_fields(array $fields) {
834 $this->newfields = $fields;
835 }
836
837 /**
838 * Cooks the parsed tags data by applying known recipes
839 *
840 * Recipes are used for common trivial operations like adding new fields
841 * or renaming fields. The handler's processing method receives cooked
842 * data.
843 *
844 * @param array $data the contents of the element
845 * @return array
846 */
847 public function apply_recipes(array $data) {
848
849 $cooked = array();
850
851 foreach ($data as $name => $value) {
852 // lower case rocks!
853 $name = strtolower($name);
854
a5fe5912
DM
855 // drop legacy fields
856 if (in_array($name, $this->dropfields)) {
857 continue;
858 }
859
1e2c7351
DM
860 // fields renaming
861 if (array_key_exists($name, $this->renamefields)) {
862 $name = $this->renamefields[$name];
863 }
864
865 $cooked[$name] = $value;
866 }
867
868 // adding new fields
869 foreach ($this->newfields as $name => $value) {
870 $cooked[$name] = $value;
871 }
872
873 return $cooked;
874 }
875
876 /**
877 * @return string the element given name
878 */
879 public function get_name() {
880 return $this->name;
881 }
882
883 /**
884 * @return string the path to the element
885 */
886 public function get_path() {
887 return $this->path;
888 }
889
890 /**
891 * @return bool flag to define if this element will get child ones grouped or no
892 */
893 public function is_grouped() {
894 return $this->grouped;
895 }
896
897 /**
898 * @return object the processing object providing the processing method
899 */
900 public function get_processing_object() {
901 return $this->pobject;
902 }
903
904 /**
905 * @return string the name of the method to call to process the element
906 */
907 public function get_processing_method() {
908 return $this->pmethod;
909 }
910
46ff8b0e
DM
911 /**
912 * @return string the name of the path start event listener
913 */
914 public function get_start_method() {
915 return $this->smethod;
916 }
917
918 /**
919 * @return string the name of the path end event listener
920 */
921 public function get_end_method() {
922 return $this->emethod;
923 }
924
1e2c7351
DM
925 /**
926 * @return mixed the element data
927 */
928 public function get_data() {
929 return $this->data;
930 }
931
932
933 /// end of public API //////////////////////////////////////////////////////
934
935 /**
936 * Makes sure the given name is a valid element name
937 *
938 * Note it may look as if we used exceptions for code flow control here. That's not the case
939 * as we actually validate the code, not the user data. And the code is supposed to be
940 * correct.
941 *
942 * @param string @name the element given name
943 * @throws convert_path_exception
944 * @return void
945 */
946 protected function validate_name($name) {
947 // Validate various name constraints, throwing exception if needed
948 if (empty($name)) {
949 throw new convert_path_exception('convert_path_emptyname', $name);
950 }
951 if (preg_replace('/\s/', '', $name) != $name) {
952 throw new convert_path_exception('convert_path_whitespace', $name);
953 }
954 if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
955 throw new convert_path_exception('convert_path_notasciiname', $name);
956 }
957 }
958
959 /**
960 * Makes sure that the given object is a valid processing object
961 *
46ff8b0e
DM
962 * The processing object must be an object providing at least element's processing method
963 * or path-reached-end event listener or path-reached-start listener method.
964 *
1e2c7351
DM
965 * Note it may look as if we used exceptions for code flow control here. That's not the case
966 * as we actually validate the code, not the user data. And the code is supposed to be
967 * correct.
968 *
969 * @param object $pobject
970 * @throws convert_path_exception
971 * @return void
972 */
973 protected function validate_pobject($pobject) {
974 if (!is_object($pobject)) {
46ff8b0e 975 throw new convert_path_exception('convert_path_no_object', get_class($pobject));
1e2c7351 976 }
46ff8b0e
DM
977 if (!method_exists($pobject, $this->get_processing_method()) and
978 !method_exists($pobject, $this->get_end_method()) and
979 !method_exists($pobject, $this->get_start_method())) {
980 throw new convert_path_exception('convert_path_missing_method', get_class($pobject));
1e2c7351
DM
981 }
982 }
983}
984
985
986/**
987 * Exception being thrown by {@link convert_path} methods
988 */
989class convert_path_exception extends moodle_exception {
990
991 /**
992 * Constructor
993 *
994 * @param string $errorcode key for the corresponding error string
995 * @param mixed $a extra words and phrases that might be required by the error string
996 * @param string $debuginfo optional debugging information
997 */
998 public function __construct($errorcode, $a = null, $debuginfo = null) {
999 parent::__construct($errorcode, '', '', $a, $debuginfo);
1000 }
1001}
66f79e50
DM
1002
1003
1004/**
1005 * The class responsible for files migration
1006 *
1007 * The files in Moodle 1.9 backup are stored in moddata, user_files, group_files,
1008 * course_files and site_files folders.
1009 */
1010class moodle1_file_manager {
1011
1012 /** @var moodle1_converter instance we serve to */
1013 public $converter;
1014
1015 /** @var int context id of the files being migrated */
1016 public $contextid;
1017
1018 /** @var string component name of the files being migrated */
1019 public $component;
1020
1021 /** @var string file area of the files being migrated */
1022 public $filearea;
1023
1024 /** @var int item id of the files being migrated */
1025 public $itemid = 0;
1026
1027 /** @var int user id */
1028 public $userid;
1029
214c4924
DM
1030 /** @var string the root of the converter temp directory */
1031 protected $basepath;
1032
66f79e50
DM
1033 /** @var textlib instance used during the migration */
1034 protected $textlib;
1035
1036 /** @var array of file ids that were migrated by this instance */
1037 protected $fileids = array();
1038
1039 /**
1040 * Constructor optionally accepting some default values for the migrated files
1041 *
1042 * @param moodle1_converter $converter the converter instance we serve to
1043 * @param int $contextid initial context id of the files being migrated
1044 * @param string $component initial component name of the files being migrated
1045 * @param string $filearea initial file area of the files being migrated
1046 * @param int $itemid initial item id of the files being migrated
1047 * @param int $userid initial user id of the files being migrated
1048 */
1049 public function __construct(moodle1_converter $converter, $contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
214c4924 1050 // set the initial destination of the migrated files
66f79e50
DM
1051 $this->converter = $converter;
1052 $this->contextid = $contextid;
1053 $this->component = $component;
1054 $this->filearea = $filearea;
1055 $this->itemid = $itemid;
1056 $this->userid = $userid;
214c4924
DM
1057 // set other useful bits
1058 $this->basepath = $converter->get_tempdir_path();
66f79e50
DM
1059 $this->textlib = textlib_get_instance();
1060 }
1061
1062 /**
1063 * Migrates one given file stored on disk
1064 *
214c4924 1065 * @param string $sourcepath the path to the source local file within the backup archive {@example 'moddata/foobar/file.ext'}
66f79e50
DM
1066 * @param string $filepath the file path of the migrated file, defaults to the root directory '/'
1067 * @param string $filename the name of the migrated file, defaults to the same as the source file has
1068 * @param int $timecreated override the timestamp of when the migrated file should appear as created
1069 * @param int $timemodified override the timestamp of when the migrated file should appear as modified
1070 * @return int id of the migrated file
1071 */
214c4924
DM
1072 public function migrate_file($sourcepath, $filepath = '/', $filename = null, $timecreated = null, $timemodified = null) {
1073
1074 $sourcefullpath = $this->basepath.'/'.$sourcepath;
66f79e50
DM
1075
1076 if (!is_readable($sourcefullpath)) {
214c4924 1077 throw new moodle1_convert_exception('file_not_readable', $sourcefullpath);
66f79e50
DM
1078 }
1079
1080 $filepath = clean_param($filepath, PARAM_PATH);
1081
1082 if ($this->textlib->strlen($filepath) > 255) {
1083 throw new moodle1_convert_exception('file_path_longer_than_255_chars');
1084 }
1085
1086 if (is_null($filename)) {
1087 $filename = basename($sourcefullpath);
1088 }
1089
1090 $filename = clean_param($filename, PARAM_FILE);
1091
1092 if ($filename === '') {
1093 throw new moodle1_convert_exception('unsupported_chars_in_filename');
1094 }
1095
1096 if (is_null($timecreated)) {
1097 $timecreated = filectime($sourcefullpath);
1098 }
1099
1100 if (is_null($timemodified)) {
1101 $timemodified = filemtime($sourcefullpath);
1102 }
1103
1104 $filerecord = $this->make_file_record(array(
1105 'filepath' => $filepath,
1106 'filename' => $filename,
1107 'mimetype' => mimeinfo('type', $sourcefullpath),
1108 'timecreated' => $timecreated,
1109 'timemodified' => $timemodified,
1110 ));
1111
1112 list($filerecord['contenthash'], $filerecord['filesize'], $newfile) = $this->add_file_to_pool($sourcefullpath);
1113 $this->stash_file($filerecord);
1114
1115 return $filerecord['id'];
1116 }
1117
1118 /**
1119 * Migrates all files in the given directory
1120 *
214c4924 1121 * @param string $rootpath path within the backup archive to the root directory containing the files {@example 'course_files'}
66f79e50
DM
1122 * @param string $relpath relative path used during the recursion - do not provide when calling this!
1123 * @return array ids of the migrated files
1124 */
1125 public function migrate_directory($rootpath, $relpath='/') {
1126
1127 $fileids = array();
1128
1129 // make the fake file record for the directory itself
1130 $filerecord = $this->make_file_record(array('filepath' => $relpath, 'filename' => '.'));
1131 $this->stash_file($filerecord);
1132 $fileids[] = $filerecord['id'];
1133
214c4924 1134 $items = new DirectoryIterator($this->basepath.'/'.$rootpath.$relpath);
66f79e50
DM
1135
1136 foreach ($items as $item) {
1137
1138 if ($item->isDot()) {
1139 continue;
1140 }
1141
1142 if ($item->isLink()) {
1143 throw new moodle1_convert_exception('unexpected_symlink');
1144 }
1145
1146 if ($item->isFile()) {
214c4924
DM
1147 $fileids[] = $this->migrate_file(substr($item->getPathname(), strlen($this->basepath.'/')),
1148 $relpath, $item->getFilename(), $item->getCTime(), $item->getMTime());
66f79e50
DM
1149
1150 } else {
1151 $dirname = clean_param($item->getFilename(), PARAM_PATH);
1152
1153 if ($dirname === '') {
1154 throw new moodle1_convert_exception('unsupported_chars_in_filename');
1155 }
1156
1157 // migrate subdirectories recursively
1158 $fileids = array_merge($fileids, $this->migrate_directory($rootpath, $relpath.$item->getFilename().'/'));
1159 }
1160 }
1161
1162 return $fileids;
1163 }
1164
1165 /**
1166 * Returns the list of all file ids migrated by this instance so far
1167 *
1168 * @return array of int
1169 */
1170 public function get_fileids() {
1171 return $this->fileids;
1172 }
1173
1174 /// internal implementation details ////////////////////////////////////////
1175
1176 /**
1177 * Prepares a fake record from the files table
1178 *
1179 * @param array $fileinfo explicit file data
1180 * @return array
1181 */
1182 protected function make_file_record(array $fileinfo) {
1183
1184 $defaultrecord = array(
1185 'contenthash' => 'da39a3ee5e6b4b0d3255bfef95601890afd80709', // sha1 of an empty file
1186 'contextid' => $this->contextid,
1187 'component' => $this->component,
1188 'filearea' => $this->filearea,
1189 'itemid' => $this->itemid,
1190 'filepath' => null,
1191 'filename' => null,
1192 'filesize' => 0,
1193 'userid' => $this->userid,
1194 'mimetype' => null,
1195 'status' => 0,
1196 'timecreated' => $now = time(),
1197 'timemodified' => $now,
1198 'source' => null,
1199 'author' => null,
1200 'license' => null,
1201 'sortorder' => 0,
1202 );
1203
1204 if (!array_key_exists('id', $fileinfo)) {
1205 $defaultrecord['id'] = $this->converter->get_nextid();
1206 }
1207
1208 // override the default values with the explicit data provided and return
1209 return array_merge($defaultrecord, $fileinfo);
1210 }
1211
1212 /**
1213 * Copies the given file to the pool directory
1214 *
1215 * Returns an array containing SHA1 hash of the file contents, the file size
1216 * and a flag indicating whether the file was actually added to the pool or whether
1217 * it was already there.
1218 *
1219 * @param string $pathname the full path to the file
1220 * @return array with keys (string)contenthash, (int)filesize, (bool)newfile
1221 */
1222 protected function add_file_to_pool($pathname) {
1223
1224 if (!is_readable($pathname)) {
1225 throw new moodle1_convert_exception('file_not_readable');
1226 }
1227
1228 $contenthash = sha1_file($pathname);
1229 $filesize = filesize($pathname);
1230 $hashpath = $this->converter->get_workdir_path().'/files/'.substr($contenthash, 0, 2);
1231 $hashfile = "$hashpath/$contenthash";
1232
1233 if (file_exists($hashfile)) {
1234 if (filesize($hashfile) !== $filesize) {
1235 // congratulations! you have found two files with different size and the same
1236 // content hash. or, something were wrong (which is more likely)
1237 throw new moodle1_convert_exception('same_hash_different_size');
1238 }
1239 $newfile = false;
1240
1241 } else {
1242 check_dir_exists($hashpath);
1243 $newfile = true;
1244
1245 if (!copy($pathname, $hashfile)) {
1246 throw new moodle1_convert_exception('unable_to_copy_file');
1247 }
1248
1249 if (filesize($hashfile) !== $filesize) {
1250 throw new moodle1_convert_exception('filesize_different_after_copy');
1251 }
1252 }
1253
1254 return array($contenthash, $filesize, $newfile);
1255 }
1256
1257 /**
1258 * Stashes the file record into 'files' stash and adds the record id to list of migrated files
1259 *
1260 * @param array $filerecord
1261 */
1262 protected function stash_file(array $filerecord) {
1263 $this->converter->set_stash('files', $filerecord, $filerecord['id']);
1264 $this->fileids[] = $filerecord['id'];
1265 }
1266}