The write_xml() does not put 'id' into attributes implicitly
[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
01b922fe 52 /** @var string the current module being processed - used to expand the MOD paths */
1e2c7351
DM
53 protected $currentmod = '';
54
01b922fe 55 /** @var string the current block being processed - used to expand the BLOCK paths */
1e2c7351
DM
56 protected $currentblock = '';
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 *
299 * Note that this is called before the MOD and BLOCK paths are expanded
a5fe5912
DM
300 * so the current plugin is not known yet. Also note that this is
301 * triggered before the previous path is actually dispatched.
1e2c7351 302 *
1e2c7351
DM
303 * @param string $path in the original file
304 */
305 public function path_start_reached($path) {
a5fe5912
DM
306
307 if (empty($this->pathelements[$path])) {
308 return;
309 }
310
311 $element = $this->pathelements[$path];
312 $pobject = $element->get_processing_object();
46ff8b0e 313 $method = $element->get_start_method();
a5fe5912
DM
314
315 if (method_exists($pobject, $method)) {
316 $pobject->$method();
317 }
1e2c7351
DM
318 }
319
320 /**
321 * Executes operations required at the end of a watched path
322 *
1e2c7351
DM
323 * @param string $path in the original file
324 */
325 public function path_end_reached($path) {
a5fe5912
DM
326
327 // expand the MOD paths so that they contain the current module name
328 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
329 $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
330
331 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
332 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
333 }
334
335 // expand the BLOCK paths so that they contain the module name
336 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
337 $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
338
339 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
340 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
341 }
342
343 if (empty($this->pathelements[$path])) {
344 return;
345 }
346
347 $element = $this->pathelements[$path];
348 $pobject = $element->get_processing_object();
46ff8b0e 349 $method = $element->get_end_method();
1cc0e42a 350 $data = $element->get_data();
a5fe5912
DM
351
352 if (method_exists($pobject, $method)) {
1cc0e42a 353 $pobject->$method($data['tags']);
a5fe5912
DM
354 }
355 }
356
357 /**
9b5f1ad5 358 * Creates the temporary storage for stashed data
a5fe5912 359 *
9b5f1ad5 360 * This implementation uses backup_ids_temp table.
a5fe5912 361 */
9b5f1ad5
DM
362 public function create_stash_storage() {
363 backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
a5fe5912
DM
364 }
365
366 /**
9b5f1ad5 367 * Drops the temporary storage of stashed data
a5fe5912 368 *
9b5f1ad5 369 * This implementation uses backup_ids_temp table.
a5fe5912 370 */
9b5f1ad5
DM
371 public function drop_stash_storage() {
372 backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
1e2c7351 373 }
a5fe5912 374
beb7de37 375 /**
9b5f1ad5 376 * Stores some information for later processing
beb7de37 377 *
9b5f1ad5
DM
378 * This implementation uses backup_ids_temp table to store data. Make
379 * sure that the $stashname + $itemid combo is unique.
beb7de37
DM
380 *
381 * @param string $stashname name of the stash
382 * @param mixed $info information to stash
9b5f1ad5 383 * @param int $itemid optional id for multiple infos within the same stashname
beb7de37 384 */
9b5f1ad5
DM
385 public function set_stash($stashname, $info, $itemid = 0) {
386 try {
387 restore_dbops::set_backup_ids_record($this->get_id(), $stashname, $itemid, 0, null, $info);
388
389 } catch (dml_exception $e) {
390 throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage());
391 }
beb7de37
DM
392 }
393
394 /**
395 * Restores a given stash stored previously by {@link self::set_stash()}
396 *
397 * @param string $stashname name of the stash
9b5f1ad5
DM
398 * @param int $itemid optional id for multiple infos within the same stashname
399 * @throws moodle1_convert_empty_storage_exception if the info has not been stashed previously
beb7de37
DM
400 * @return mixed stashed data
401 */
9b5f1ad5
DM
402 public function get_stash($stashname, $itemid = 0) {
403
404 $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid);
405
406 if (empty($record)) {
407 throw new moodle1_convert_empty_storage_exception('required_not_stashed_data');
408 } else {
409 return $record->info;
410 }
beb7de37
DM
411 }
412
6d73f185
DM
413 /**
414 * Returns the list of stashed $itemids in the given stash
415 *
416 * @param string $stashname
417 * @return array
418 */
419 public function get_stash_itemids($stashname) {
420 global $DB;
421
422 $search = array(
423 'backupid' => $this->get_id(),
424 'itemname' => $stashname
425 );
426
427 return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemid'));
428 }
429
beb7de37
DM
430 /**
431 * Generates an artificial context id
432 *
433 * Moodle 1.9 backups do not contain any context information. But we need them
434 * in Moodle 2.x format so here we generate fictive context id for every given
435 * context level + instance combo.
436 *
26cac34a
DM
437 * CONTEXT_SYSTEM and CONTEXT_COURSE ignore the $instance as they represent a
438 * single system or the course being restored.
439 *
beb7de37
DM
440 * @see get_context_instance()
441 * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE
442 * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules
443 * @return int the context id
444 */
26cac34a 445 public function get_contextid($level, $instance = 0) {
beb7de37 446
9b5f1ad5 447 $stashname = 'context' . $level;
beb7de37 448
26cac34a
DM
449 if ($level == CONTEXT_SYSTEM or $level == CONTEXT_COURSE) {
450 $instance = 0;
451 }
452
d5d02635
DM
453 try {
454 // try the previously stashed id
455 return $this->get_stash($stashname, $instance);
beb7de37 456
d5d02635 457 } catch (moodle1_convert_empty_storage_exception $e) {
beb7de37 458 // this context level + instance is required for the first time
26cac34a
DM
459 $newid = $this->get_nextid();
460 $this->set_stash($stashname, $newid, $instance);
461 return $newid;
9b5f1ad5 462 }
beb7de37 463 }
179982a4 464
6700d288
DM
465 /**
466 * Simple autoincrement generator
467 *
468 * @return int the next number in a row of numbers
469 */
470 public function get_nextid() {
471 static $autoincrement = 0;
472 return ++$autoincrement;
473 }
474
179982a4
DM
475 /**
476 * @see parent::description()
477 */
478 public static function description() {
479
480 return array(
481 'from' => backup::FORMAT_MOODLE1,
482 'to' => backup::FORMAT_MOODLE,
483 'cost' => 10,
484 );
485 }
1e2c7351
DM
486}
487
488
9b5f1ad5
DM
489/**
490 * Exception thrown by this converter
491 */
492class moodle1_convert_exception extends convert_exception {
493}
494
495
496/**
497 * Exception thrown by the temporary storage subsystem of moodle1_converter
498 */
499class moodle1_convert_storage_exception extends moodle1_convert_exception {
500}
501
502
503/**
504 * Exception thrown by the temporary storage subsystem of moodle1_converter
505 */
506class moodle1_convert_empty_storage_exception extends moodle1_convert_exception {
507}
508
509
1e2c7351 510/**
96f7c7ad 511 * XML parser processor used for processing parsed moodle.xml
1e2c7351
DM
512 */
513class moodle1_parser_processor extends grouped_parser_processor {
514
515 /** @var moodle1_converter */
516 protected $converter;
517
518 public function __construct(moodle1_converter $converter) {
519 $this->converter = $converter;
520 parent::__construct();
521 }
522
523 /**
524 * Provide NULL and legacy file.php uses decoding
525 */
526 public function process_cdata($cdata) {
527 global $CFG;
528
529 if ($cdata === '$@NULL@$') { // Some cases we know we can skip complete processing
530 return null;
531 } else if ($cdata === '') {
532 return '';
533 } else if (is_numeric($cdata)) {
534 return $cdata;
535 } else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
536 return $cdata; // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
537 } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
538 return $cdata;
539 }
540 // Decode file.php calls
541 $search = array ("$@FILEPHP@$");
542 $replace = array(get_file_url($this->courseid));
543 $result = str_replace($search, $replace, $cdata);
544 // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
545 $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
546 if ($CFG->slasharguments) {
547 $replace = array('/', '?forcedownload=1');
548 } else {
549 $replace = array('%2F', '&amp;forcedownload=1');
550 }
551 return str_replace($search, $replace, $result);
552 }
553
554 /**
555 * Override this method so we'll be able to skip
556 * dispatching some well-known chunks, like the
557 * ones being 100% part of subplugins stuff. Useful
558 * for allowing development without having all the
559 * possible restore subplugins defined
560 */
561 protected function postprocess_chunk($data) {
562
563 // Iterate over all the data tags, if any of them is
564 // not 'subplugin_XXXX' or has value, then it's a valid chunk,
565 // pass it to standard (parent) processing of chunks.
566 foreach ($data['tags'] as $key => $value) {
567 if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) {
568 parent::postprocess_chunk($data);
569 return;
570 }
571 }
572 // Arrived here, all the tags correspond to sublplugins and are empty,
573 // skip the chunk, and debug_developer notice
574 $this->chunks--; // not counted
575 debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) .
576 ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER);
577 }
578
579 /**
580 * Dispatches the data chunk to the converter class
581 *
582 * @param array $data the chunk of parsed data
583 */
584 protected function dispatch_chunk($data) {
585 $this->converter->process_chunk($data);
586 }
587
588 /**
589 * Informs the converter at the start of a watched path
590 *
591 * @param string $path
592 */
593 protected function notify_path_start($path) {
594 $this->converter->path_start_reached($path);
595 }
596
597 /**
598 * Informs the converter at the end of a watched path
599 *
600 * @param string $path
601 */
602 protected function notify_path_end($path) {
603 $this->converter->path_end_reached($path);
604 }
605}
606
607
96f7c7ad
DM
608/**
609 * XML transformer that modifies the content of the files being written during the conversion
610 *
611 * @see backup_xml_transformer
612 */
613class moodle1_xml_transformer extends xml_contenttransformer {
614
615 /**
616 * Modify the content before it is writter to a file
617 *
618 * @param string|mixed $content
619 */
620 public function process($content) {
621
622 // the content should be a string. If array or object is given, try our best recursively
623 // but inform the developer
624 if (is_array($content)) {
625 debugging('Moodle1 XML transformer should not process arrays but plain content always', DEBUG_DEVELOPER);
626 foreach($content as $key => $plaincontent) {
627 $content[$key] = $this->process($plaincontent);
628 }
629 return $content;
630
631 } else if (is_object($content)) {
632 debugging('Moodle1 XML transformer should not process objects but plain content always', DEBUG_DEVELOPER);
633 foreach((array)$content as $key => $plaincontent) {
634 $content[$key] = $this->process($plaincontent);
635 }
636 return (object)$content;
637 }
638
639 // try to deal with some trivial cases first
640 if (is_null($content)) {
641 return '$@NULL@$';
642
643 } else if ($content === '') {
644 return '';
645
646 } else if (is_numeric($content)) {
647 return $content;
648
649 } else if (strlen($content) < 32) {
650 return $content;
651 }
652
653 // todo will we need this?
654 //$content = $this->process_filephp_links($content); // Replace all calls to file.php by $@FILEPHP@$ in a normalised way
655 //$content = $this->encode_absolute_links($content); // Pass the content against all the found encoders
656
657 return $content;
658 }
659}
660
661
1e2c7351
DM
662/**
663 * Class representing a path to be converted from XML file
664 *
665 * This was created as a copy of {@link restore_path_element} and should be refactored
666 * probably.
667 */
668class convert_path {
669
670 /** @var string name of the element */
671 protected $name;
672
673 /** @var string path within the XML file this element will handle */
674 protected $path;
675
676 /** @var bool flag to define if this element will get child ones grouped or no */
677 protected $grouped;
678
679 /** @var object object instance in charge of processing this element. */
680 protected $pobject = null;
681
682 /** @var string the name of the processing method */
683 protected $pmethod = null;
684
46ff8b0e
DM
685 /** @var string the name of the path start event handler */
686 protected $smethod = null;
687
688 /** @var string the name of the path end event handler */
689 protected $emethod = null;
690
1e2c7351
DM
691 /** @var mixed last data read for this element or returned data by processing method */
692 protected $data = null;
693
a5fe5912
DM
694 /** @var array of deprecated fields that are dropped */
695 protected $dropfields = array();
1e2c7351
DM
696
697 /** @var array of fields renaming */
698 protected $renamefields = array();
699
700 /** @var array of new fields to add and their initial values */
701 protected $newfields = array();
702
703 /**
704 * Constructor
705 *
706 * @param string $name name of the element
707 * @param string $path path of the element
708 * @param array $recipe basic description of the structure conversion
709 * @param bool $grouped to gather information in grouped mode or no
710 */
711 public function __construct($name, $path, array $recipe = array(), $grouped = false) {
712
713 $this->validate_name($name);
714
715 $this->name = $name;
716 $this->path = $path;
717 $this->grouped = $grouped;
718
46ff8b0e 719 // set the default method names
1e2c7351 720 $this->set_processing_method('process_' . $name);
46ff8b0e
DM
721 $this->set_start_method('on_'.$name.'_start');
722 $this->set_end_method('on_'.$name.'_end');
1e2c7351 723
a5fe5912
DM
724 if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
725 $this->set_dropped_fields($recipe['dropfields']);
1e2c7351
DM
726 }
727 if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
728 $this->set_renamed_fields($recipe['renamefields']);
729 }
730 if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
731 $this->set_new_fields($recipe['newfields']);
732 }
733 }
734
735 /**
736 * Validates and sets the given processing object
737 *
738 * @param object $pobject processing object, must provide a method to be called
739 */
740 public function set_processing_object($pobject) {
741 $this->validate_pobject($pobject);
742 $this->pobject = $pobject;
743 }
744
745 /**
746 * Sets the name of the processing method
747 *
748 * @param string $pmethod
749 */
750 public function set_processing_method($pmethod) {
751 $this->pmethod = $pmethod;
752 }
753
46ff8b0e
DM
754 /**
755 * Sets the name of the path start event listener
756 *
757 * @param string $smethod
758 */
759 public function set_start_method($smethod) {
760 $this->smethod = $smethod;
761 }
762
763 /**
764 * Sets the name of the path end event listener
765 *
766 * @param string $emethod
767 */
768 public function set_end_method($emethod) {
769 $this->emethod = $emethod;
770 }
771
1e2c7351
DM
772 /**
773 * Sets the element data
774 *
775 * @param mixed
776 */
777 public function set_data($data) {
778 $this->data = $data;
779 }
780
781 /**
a5fe5912 782 * Sets the list of deprecated fields to drop
1e2c7351
DM
783 *
784 * @param array $fields
785 */
a5fe5912
DM
786 public function set_dropped_fields(array $fields) {
787 $this->dropfields = $fields;
1e2c7351
DM
788 }
789
790 /**
791 * Sets the required new names of the current fields
792 *
793 * @param array $fields (string)$currentname => (string)$newname
794 */
795 public function set_renamed_fields(array $fields) {
796 $this->renamefields = $fields;
797 }
798
799 /**
800 * Sets the new fields and their values
801 *
802 * @param array $fields (string)$field => (mixed)value
803 */
804 public function set_new_fields(array $fields) {
805 $this->newfields = $fields;
806 }
807
808 /**
809 * Cooks the parsed tags data by applying known recipes
810 *
811 * Recipes are used for common trivial operations like adding new fields
812 * or renaming fields. The handler's processing method receives cooked
813 * data.
814 *
815 * @param array $data the contents of the element
816 * @return array
817 */
818 public function apply_recipes(array $data) {
819
820 $cooked = array();
821
822 foreach ($data as $name => $value) {
823 // lower case rocks!
824 $name = strtolower($name);
825
a5fe5912
DM
826 // drop legacy fields
827 if (in_array($name, $this->dropfields)) {
828 continue;
829 }
830
1e2c7351
DM
831 // fields renaming
832 if (array_key_exists($name, $this->renamefields)) {
833 $name = $this->renamefields[$name];
834 }
835
836 $cooked[$name] = $value;
837 }
838
839 // adding new fields
840 foreach ($this->newfields as $name => $value) {
841 $cooked[$name] = $value;
842 }
843
844 return $cooked;
845 }
846
847 /**
848 * @return string the element given name
849 */
850 public function get_name() {
851 return $this->name;
852 }
853
854 /**
855 * @return string the path to the element
856 */
857 public function get_path() {
858 return $this->path;
859 }
860
861 /**
862 * @return bool flag to define if this element will get child ones grouped or no
863 */
864 public function is_grouped() {
865 return $this->grouped;
866 }
867
868 /**
869 * @return object the processing object providing the processing method
870 */
871 public function get_processing_object() {
872 return $this->pobject;
873 }
874
875 /**
876 * @return string the name of the method to call to process the element
877 */
878 public function get_processing_method() {
879 return $this->pmethod;
880 }
881
46ff8b0e
DM
882 /**
883 * @return string the name of the path start event listener
884 */
885 public function get_start_method() {
886 return $this->smethod;
887 }
888
889 /**
890 * @return string the name of the path end event listener
891 */
892 public function get_end_method() {
893 return $this->emethod;
894 }
895
1e2c7351
DM
896 /**
897 * @return mixed the element data
898 */
899 public function get_data() {
900 return $this->data;
901 }
902
903
904 /// end of public API //////////////////////////////////////////////////////
905
906 /**
907 * Makes sure the given name is a valid element name
908 *
909 * Note it may look as if we used exceptions for code flow control here. That's not the case
910 * as we actually validate the code, not the user data. And the code is supposed to be
911 * correct.
912 *
913 * @param string @name the element given name
914 * @throws convert_path_exception
915 * @return void
916 */
917 protected function validate_name($name) {
918 // Validate various name constraints, throwing exception if needed
919 if (empty($name)) {
920 throw new convert_path_exception('convert_path_emptyname', $name);
921 }
922 if (preg_replace('/\s/', '', $name) != $name) {
923 throw new convert_path_exception('convert_path_whitespace', $name);
924 }
925 if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
926 throw new convert_path_exception('convert_path_notasciiname', $name);
927 }
928 }
929
930 /**
931 * Makes sure that the given object is a valid processing object
932 *
46ff8b0e
DM
933 * The processing object must be an object providing at least element's processing method
934 * or path-reached-end event listener or path-reached-start listener method.
935 *
1e2c7351
DM
936 * Note it may look as if we used exceptions for code flow control here. That's not the case
937 * as we actually validate the code, not the user data. And the code is supposed to be
938 * correct.
939 *
940 * @param object $pobject
941 * @throws convert_path_exception
942 * @return void
943 */
944 protected function validate_pobject($pobject) {
945 if (!is_object($pobject)) {
46ff8b0e 946 throw new convert_path_exception('convert_path_no_object', get_class($pobject));
1e2c7351 947 }
46ff8b0e
DM
948 if (!method_exists($pobject, $this->get_processing_method()) and
949 !method_exists($pobject, $this->get_end_method()) and
950 !method_exists($pobject, $this->get_start_method())) {
951 throw new convert_path_exception('convert_path_missing_method', get_class($pobject));
1e2c7351
DM
952 }
953 }
954}
955
956
957/**
958 * Exception being thrown by {@link convert_path} methods
959 */
960class convert_path_exception extends moodle_exception {
961
962 /**
963 * Constructor
964 *
965 * @param string $errorcode key for the corresponding error string
966 * @param mixed $a extra words and phrases that might be required by the error string
967 * @param string $debuginfo optional debugging information
968 */
969 public function __construct($errorcode, $a = null, $debuginfo = null) {
970 parent::__construct($errorcode, '', '', $a, $debuginfo);
971 }
972}