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