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