More backup-converter API improvements
[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');
1e2c7351
DM
35require_once(dirname(__FILE__) . '/handlerlib.php');
36
37/**
38 * Converter of Moodle 1.9 backup into Moodle 2.x format
39 */
40class moodle1_converter extends base_converter {
41
42 /** @var progressive_parser moodle.xml file parser */
43 protected $xmlparser;
44
45 /** @var moodle1_parser_processor */
46 protected $xmlprocessor;
47
48 /** @var array of {@link convert_path} to process */
49 protected $pathelements = array();
50
51 /** @var string the current module being processed */
52 protected $currentmod = '';
53
54 /** @var string the current block being processed */
55 protected $currentblock = '';
56
57 /** @var string path currently locking processing of children */
58 protected $pathlock;
59
60 /**
61 * Instructs the dispatcher to ignore all children below path processor returning it
62 */
63 const SKIP_ALL_CHILDREN = -991399;
64
65 /**
66 * Detects the Moodle 1.9 format of the backup directory
67 *
68 * @param string $tempdir the name of the backup directory
69 * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise
70 */
71 public static function detect_format($tempdir) {
72 global $CFG;
73
74 $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle.xml';
75 if (file_exists($filepath)) {
76 // looks promising, lets load some information
77 $handle = fopen($filepath, 'r');
78 $first_chars = fread($handle, 200);
79 fclose($handle);
80
81 // check if it has the required strings
82 if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
83 strpos($first_chars,'<MOODLE_BACKUP>') !== false and
84 strpos($first_chars,'<INFO>') !== false) {
85
86 return backup::FORMAT_MOODLE1;
87 }
88 }
89
90 return null;
91 }
92
93 /**
94 * Initialize the instance if needed, called by the constructor
95 *
96 * Here we create objects we need before the execution.
97 */
98 protected function init() {
99
100 // ask your mother first before going out playing with toys
101 parent::init();
102
103 // good boy, prepare XML parser and processor
104 $this->xmlparser = new progressive_parser();
105 $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
106 $this->xmlprocessor = new moodle1_parser_processor($this);
107 $this->xmlparser->set_processor($this->xmlprocessor);
108
109 // make sure that MOD and BLOCK paths are visited
110 $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/MODULES/MOD');
111 $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK');
112
113 // register the conversion handlers
114 foreach (moodle1_handlers_factory::get_handlers($this) as $handler) {
1e2c7351
DM
115 $this->register_handler($handler, $handler->get_paths());
116 }
117 }
118
119 /**
120 * Converts the contents of the tempdir into the target format in the workdir
121 */
122 protected function execute() {
123 $this->xmlparser->process();
124 }
125
126 /**
127 * Register a handler for the given path elements
128 */
129 protected function register_handler(moodle1_handler $handler, array $elements) {
130
131 // first iteration, push them to new array, indexed by name
132 // to detect duplicates in names or paths
133 $names = array();
134 $paths = array();
135 foreach($elements as $element) {
136 if (!$element instanceof convert_path) {
137 throw new convert_exception('path_element_wrong_class', get_class($element));
138 }
139 if (array_key_exists($element->get_name(), $names)) {
140 throw new convert_exception('path_element_name_alreadyexists', $element->get_name());
141 }
142 if (array_key_exists($element->get_path(), $paths)) {
143 throw new convert_exception('path_element_path_alreadyexists', $element->get_path());
144 }
145 $names[$element->get_name()] = true;
146 $paths[$element->get_path()] = $element;
147 }
148
149 // now, for each element not having a processing object yet, assign the handler
150 // if the element is not a memeber of a group
151 foreach($paths as $key => $element) {
152 if (is_null($element->get_processing_object()) and !$this->grouped_parent_exists($element, $paths)) {
153 $paths[$key]->set_processing_object($handler);
154 }
155 // add the element path to the processor
156 $this->xmlprocessor->add_path($element->get_path(), $element->is_grouped());
157 }
158
159 // done, store the paths (duplicates by path are discarded)
160 $this->pathelements = array_merge($this->pathelements, $paths);
161
162 // remove the injected plugin name element from the MOD and BLOCK paths
163 // and register such collapsed path, too
164 foreach ($elements as $element) {
165 $path = $element->get_path();
166 $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/MODULES\/MOD\/(\w+)\//', '/MOODLE_BACKUP/COURSE/MODULES/MOD/', $path);
167 $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/BLOCKS\/BLOCK\/(\w+)\//', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/', $path);
168 if (!empty($path) and $path != $element->get_path()) {
169 $this->xmlprocessor->add_path($path, false);
170 }
171 }
172 }
173
174 /**
175 * Helper method used by {@link self::register_handler()}
176 *
177 * @param convert_path $pelement path element
178 * @param array of convert_path instances
179 * @return bool true if grouped parent was found, false otherwise
180 */
181 protected function grouped_parent_exists($pelement, $elements) {
182
183 foreach ($elements as $element) {
184 if ($pelement->get_path() == $element->get_path()) {
185 // don't compare against itself
186 continue;
187 }
188 // if the element is grouped and it is a parent of pelement, return true
189 if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
190 return true;
191 }
192 }
193
194 // no grouped parent found
195 return false;
196 }
197
198 /**
199 * Process the data obtained from the XML parser processor
200 *
201 * This methods receives one chunk of information from the XML parser
202 * processor and dispatches it, following the naming rules.
203 * We are expanding the modules and blocks paths here to include the plugin's name.
204 *
205 * @param array $data
206 */
207 public function process_chunk($data) {
208
209 $path = $data['path'];
210
211 // expand the MOD paths so that they contain the module name
212 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
213 $this->currentmod = strtoupper($data['tags']['MODTYPE']);
214 $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
215
216 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
217 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
218 }
219
220 // expand the BLOCK paths so that they contain the module name
221 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
222 $this->currentblock = strtoupper($data['tags']['NAME']);
223 $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
224
225 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
226 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
227 }
228
229 if ($path !== $data['path']) {
230 if (!array_key_exists($path, $this->pathelements)) {
231 // no handler registered for the transformed MOD or BLOCK path
232 // todo add this event to the convert log instead of debugging
233 //debugging('No handler registered for the path ' . $path);
234 return;
235
236 } else {
237 // pretend as if the original $data contained the tranformed path
238 $data['path'] = $path;
239 }
240 }
241
242 if (!array_key_exists($data['path'], $this->pathelements)) {
243 // path added to the processor without the handler
244 throw new convert_exception('missing_path_handler', $data['path']);
245 }
246
beb7de37
DM
247 $element = $this->pathelements[$data['path']];
248 $object = $element->get_processing_object();
249 $method = $element->get_processing_method();
250 $returned = null; // data returned by the processing method, if any
1e2c7351
DM
251
252 if (empty($object)) {
253 throw new convert_exception('missing_processing_object', $object);
254 }
255
256 // release the lock if we aren't anymore within children of it
257 if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
258 $this->pathlock = null;
259 }
260
261 // if the path is not locked, apply the element's recipes and dispatch
262 // the cooked tags to the processing method
263 if (is_null($this->pathlock)) {
beb7de37
DM
264 $rawdatatags = $data['tags'];
265 $data['tags'] = $element->apply_recipes($data['tags']);
266 $returned = $object->$method($data['tags'], $rawdatatags);
1e2c7351
DM
267 }
268
269 // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path
270 // and lock it so that its children are not dispatched
beb7de37 271 if ($returned === self::SKIP_ALL_CHILDREN) {
1e2c7351
DM
272 // check we haven't any previous lock
273 if (!is_null($this->pathlock)) {
274 throw new convert_exception('already_locked_path', $data['path']);
275 }
276 // set the lock - nothing below the current path will be dispatched
277 $this->pathlock = $data['path'] . '/';
278
279 // if the method has returned any info, set element data to it
beb7de37
DM
280 } else if (!is_null($returned)) {
281 $element->set_data($returned);
1e2c7351
DM
282
283 // use just the cooked parsed data otherwise
284 } else {
285 $element->set_data($data);
286 }
287 }
288
289 /**
290 * Executes operations required at the start of a watched path
291 *
292 * Note that this is called before the MOD and BLOCK paths are expanded
a5fe5912
DM
293 * so the current plugin is not known yet. Also note that this is
294 * triggered before the previous path is actually dispatched.
1e2c7351 295 *
1e2c7351
DM
296 * @param string $path in the original file
297 */
298 public function path_start_reached($path) {
a5fe5912
DM
299
300 if (empty($this->pathelements[$path])) {
301 return;
302 }
303
304 $element = $this->pathelements[$path];
305 $pobject = $element->get_processing_object();
306 $method = 'on_' . $element->get_name() . '_start';
307
308 if (method_exists($pobject, $method)) {
309 $pobject->$method();
310 }
1e2c7351
DM
311 }
312
313 /**
314 * Executes operations required at the end of a watched path
315 *
1e2c7351
DM
316 * @param string $path in the original file
317 */
318 public function path_end_reached($path) {
a5fe5912
DM
319
320 // expand the MOD paths so that they contain the current module name
321 if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
322 $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
323
324 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
325 $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
326 }
327
328 // expand the BLOCK paths so that they contain the module name
329 if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
330 $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
331
332 } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
333 $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentmod, $path);
334 }
335
336 if (empty($this->pathelements[$path])) {
337 return;
338 }
339
340 $element = $this->pathelements[$path];
341 $pobject = $element->get_processing_object();
342 $method = 'on_' . $element->get_name() . '_end';
343
344 if (method_exists($pobject, $method)) {
345 $pobject->$method();
346 }
347 }
348
349 /**
350 * Creates the backup_ids_temp table
351 */
352 public function create_backup_ids_temp_table() {
353 backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
354 }
355
356 /**
357 * Drops the backup_ids_temp table
358 */
359 public function drop_backup_ids_temp_table() {
360 backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
361 }
362
363 /**
364 * Stores a record in the temporary backup_ids table
365 *
366 * @param string $itemname
367 * @param int $itemid
368 * @param int $newitemid
369 * @param int $parentitemid
370 * @param mixed $info
371 * @return void
372 */
373 public function set_backup_ids_record($itemname, $itemid, $newitemid = 0, $parentitemid = null, $info = null) {
374 restore_dbops::set_backup_ids_record($this->get_id(), $itemname, $itemid, $newitemid, $parentitemid, $info);
375 }
376
377 /**
378 * Restores a previously saved record from backup_ids temporary table
379 *
380 * @param string $itemname
381 * @param int $itemid
382 * @return stdClass
383 */
384 public function get_backup_ids_record($itemname, $itemid) {
385 return restore_dbops::get_backup_ids_record($this->get_id(), $itemname, $itemid);
1e2c7351 386 }
a5fe5912 387
beb7de37
DM
388 /**
389 * Store some information for later processing
390 *
391 * This implenentation uses backup_ids_temp table to store data. Make
392 * sure that the stash name is unique.
393 *
394 * @param string $stashname name of the stash
395 * @param mixed $info information to stash
396 */
397 public function set_stash($stashname, $info) {
398 $this->set_backup_ids_record($stashname, 0, 0, null, $info);
399 }
400
401 /**
402 * Restores a given stash stored previously by {@link self::set_stash()}
403 *
404 * @param string $stashname name of the stash
405 * @return mixed stashed data
406 */
407 public function get_stash($stashname) {
408 return $this->get_backup_ids_record($stashname, 0);
409 }
410
411 /**
412 * Generates an artificial context id
413 *
414 * Moodle 1.9 backups do not contain any context information. But we need them
415 * in Moodle 2.x format so here we generate fictive context id for every given
416 * context level + instance combo.
417 *
418 * This implementation maps context level and instanceid to the columns
419 * of the backup_ids_temp table and uses the id of the record in that table
420 * as the context id.
421 *
422 * @see get_context_instance()
423 * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE
424 * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules
425 * @return int the context id
426 */
427 public function get_contextid($level, $instance) {
428
429 $itemname = 'context' . $level;
430 $itemid = $instance;
431
432 $existing = $this->get_backup_ids_record($itemname, $itemid);
433
434 if (empty($existing)) {
435 // this context level + instance is required for the first time
436 // store it and re-read to obtain its record id
437 $this->set_backup_ids_record($itemname, $itemid);
438 $existing = $this->get_backup_ids_record($itemname, $itemid);
439 }
440
441 return $existing->id;
442 }
1e2c7351
DM
443}
444
445
446/**
447 * XML parser processor
448 */
449class moodle1_parser_processor extends grouped_parser_processor {
450
451 /** @var moodle1_converter */
452 protected $converter;
453
454 public function __construct(moodle1_converter $converter) {
455 $this->converter = $converter;
456 parent::__construct();
457 }
458
459 /**
460 * Provide NULL and legacy file.php uses decoding
461 */
462 public function process_cdata($cdata) {
463 global $CFG;
464
465 if ($cdata === '$@NULL@$') { // Some cases we know we can skip complete processing
466 return null;
467 } else if ($cdata === '') {
468 return '';
469 } else if (is_numeric($cdata)) {
470 return $cdata;
471 } else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
472 return $cdata; // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
473 } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
474 return $cdata;
475 }
476 // Decode file.php calls
477 $search = array ("$@FILEPHP@$");
478 $replace = array(get_file_url($this->courseid));
479 $result = str_replace($search, $replace, $cdata);
480 // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
481 $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
482 if ($CFG->slasharguments) {
483 $replace = array('/', '?forcedownload=1');
484 } else {
485 $replace = array('%2F', '&amp;forcedownload=1');
486 }
487 return str_replace($search, $replace, $result);
488 }
489
490 /**
491 * Override this method so we'll be able to skip
492 * dispatching some well-known chunks, like the
493 * ones being 100% part of subplugins stuff. Useful
494 * for allowing development without having all the
495 * possible restore subplugins defined
496 */
497 protected function postprocess_chunk($data) {
498
499 // Iterate over all the data tags, if any of them is
500 // not 'subplugin_XXXX' or has value, then it's a valid chunk,
501 // pass it to standard (parent) processing of chunks.
502 foreach ($data['tags'] as $key => $value) {
503 if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) {
504 parent::postprocess_chunk($data);
505 return;
506 }
507 }
508 // Arrived here, all the tags correspond to sublplugins and are empty,
509 // skip the chunk, and debug_developer notice
510 $this->chunks--; // not counted
511 debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) .
512 ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER);
513 }
514
515 /**
516 * Dispatches the data chunk to the converter class
517 *
518 * @param array $data the chunk of parsed data
519 */
520 protected function dispatch_chunk($data) {
521 $this->converter->process_chunk($data);
522 }
523
524 /**
525 * Informs the converter at the start of a watched path
526 *
527 * @param string $path
528 */
529 protected function notify_path_start($path) {
530 $this->converter->path_start_reached($path);
531 }
532
533 /**
534 * Informs the converter at the end of a watched path
535 *
536 * @param string $path
537 */
538 protected function notify_path_end($path) {
539 $this->converter->path_end_reached($path);
540 }
541}
542
543
544/**
545 * Class representing a path to be converted from XML file
546 *
547 * This was created as a copy of {@link restore_path_element} and should be refactored
548 * probably.
549 */
550class convert_path {
551
552 /** @var string name of the element */
553 protected $name;
554
555 /** @var string path within the XML file this element will handle */
556 protected $path;
557
558 /** @var bool flag to define if this element will get child ones grouped or no */
559 protected $grouped;
560
561 /** @var object object instance in charge of processing this element. */
562 protected $pobject = null;
563
564 /** @var string the name of the processing method */
565 protected $pmethod = null;
566
567 /** @var mixed last data read for this element or returned data by processing method */
568 protected $data = null;
569
a5fe5912
DM
570 /** @var array of deprecated fields that are dropped */
571 protected $dropfields = array();
1e2c7351
DM
572
573 /** @var array of fields renaming */
574 protected $renamefields = array();
575
576 /** @var array of new fields to add and their initial values */
577 protected $newfields = array();
578
579 /**
580 * Constructor
581 *
582 * @param string $name name of the element
583 * @param string $path path of the element
584 * @param array $recipe basic description of the structure conversion
585 * @param bool $grouped to gather information in grouped mode or no
586 */
587 public function __construct($name, $path, array $recipe = array(), $grouped = false) {
588
589 $this->validate_name($name);
590
591 $this->name = $name;
592 $this->path = $path;
593 $this->grouped = $grouped;
594
595 // set the default processing method name
596 $this->set_processing_method('process_' . $name);
597
a5fe5912
DM
598 if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
599 $this->set_dropped_fields($recipe['dropfields']);
1e2c7351
DM
600 }
601 if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
602 $this->set_renamed_fields($recipe['renamefields']);
603 }
604 if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
605 $this->set_new_fields($recipe['newfields']);
606 }
607 }
608
609 /**
610 * Validates and sets the given processing object
611 *
612 * @param object $pobject processing object, must provide a method to be called
613 */
614 public function set_processing_object($pobject) {
615 $this->validate_pobject($pobject);
616 $this->pobject = $pobject;
617 }
618
619 /**
620 * Sets the name of the processing method
621 *
622 * @param string $pmethod
623 */
624 public function set_processing_method($pmethod) {
625 $this->pmethod = $pmethod;
626 }
627
628 /**
629 * Sets the element data
630 *
631 * @param mixed
632 */
633 public function set_data($data) {
634 $this->data = $data;
635 }
636
637 /**
a5fe5912 638 * Sets the list of deprecated fields to drop
1e2c7351
DM
639 *
640 * @param array $fields
641 */
a5fe5912
DM
642 public function set_dropped_fields(array $fields) {
643 $this->dropfields = $fields;
1e2c7351
DM
644 }
645
646 /**
647 * Sets the required new names of the current fields
648 *
649 * @param array $fields (string)$currentname => (string)$newname
650 */
651 public function set_renamed_fields(array $fields) {
652 $this->renamefields = $fields;
653 }
654
655 /**
656 * Sets the new fields and their values
657 *
658 * @param array $fields (string)$field => (mixed)value
659 */
660 public function set_new_fields(array $fields) {
661 $this->newfields = $fields;
662 }
663
664 /**
665 * Cooks the parsed tags data by applying known recipes
666 *
667 * Recipes are used for common trivial operations like adding new fields
668 * or renaming fields. The handler's processing method receives cooked
669 * data.
670 *
671 * @param array $data the contents of the element
672 * @return array
673 */
674 public function apply_recipes(array $data) {
675
676 $cooked = array();
677
678 foreach ($data as $name => $value) {
679 // lower case rocks!
680 $name = strtolower($name);
681
a5fe5912
DM
682 // drop legacy fields
683 if (in_array($name, $this->dropfields)) {
684 continue;
685 }
686
1e2c7351
DM
687 // fields renaming
688 if (array_key_exists($name, $this->renamefields)) {
689 $name = $this->renamefields[$name];
690 }
691
692 $cooked[$name] = $value;
693 }
694
695 // adding new fields
696 foreach ($this->newfields as $name => $value) {
697 $cooked[$name] = $value;
698 }
699
700 return $cooked;
701 }
702
703 /**
704 * @return string the element given name
705 */
706 public function get_name() {
707 return $this->name;
708 }
709
710 /**
711 * @return string the path to the element
712 */
713 public function get_path() {
714 return $this->path;
715 }
716
717 /**
718 * @return bool flag to define if this element will get child ones grouped or no
719 */
720 public function is_grouped() {
721 return $this->grouped;
722 }
723
724 /**
725 * @return object the processing object providing the processing method
726 */
727 public function get_processing_object() {
728 return $this->pobject;
729 }
730
731 /**
732 * @return string the name of the method to call to process the element
733 */
734 public function get_processing_method() {
735 return $this->pmethod;
736 }
737
738 /**
739 * @return mixed the element data
740 */
741 public function get_data() {
742 return $this->data;
743 }
744
745
746 /// end of public API //////////////////////////////////////////////////////
747
748 /**
749 * Makes sure the given name is a valid element name
750 *
751 * Note it may look as if we used exceptions for code flow control here. That's not the case
752 * as we actually validate the code, not the user data. And the code is supposed to be
753 * correct.
754 *
755 * @param string @name the element given name
756 * @throws convert_path_exception
757 * @return void
758 */
759 protected function validate_name($name) {
760 // Validate various name constraints, throwing exception if needed
761 if (empty($name)) {
762 throw new convert_path_exception('convert_path_emptyname', $name);
763 }
764 if (preg_replace('/\s/', '', $name) != $name) {
765 throw new convert_path_exception('convert_path_whitespace', $name);
766 }
767 if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
768 throw new convert_path_exception('convert_path_notasciiname', $name);
769 }
770 }
771
772 /**
773 * Makes sure that the given object is a valid processing object
774 *
775 * The processing object must be an object providing the element's processing method.
776 * Note it may look as if we used exceptions for code flow control here. That's not the case
777 * as we actually validate the code, not the user data. And the code is supposed to be
778 * correct.
779 *
780 * @param object $pobject
781 * @throws convert_path_exception
782 * @return void
783 */
784 protected function validate_pobject($pobject) {
785 if (!is_object($pobject)) {
786 throw new convert_path_exception('convert_path_no_object', $pobject);
787 }
788 if (!method_exists($pobject, $this->get_processing_method())) {
789 throw new convert_path_exception('convert_path_missingmethod', $this->get_processing_method());
790 }
791 }
792}
793
794
795/**
796 * Exception being thrown by {@link convert_path} methods
797 */
798class convert_path_exception extends moodle_exception {
799
800 /**
801 * Constructor
802 *
803 * @param string $errorcode key for the corresponding error string
804 * @param mixed $a extra words and phrases that might be required by the error string
805 * @param string $debuginfo optional debugging information
806 */
807 public function __construct($errorcode, $a = null, $debuginfo = null) {
808 parent::__construct($errorcode, '', '', $a, $debuginfo);
809 }
810}