Improved the moodle1_convert stashing subsystem
[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() {
9b5f1ad5 123 $this->create_stash_storage();
1e2c7351 124 $this->xmlparser->process();
9b5f1ad5 125 $this->drop_stash_storage();
1e2c7351
DM
126 }
127
128 /**
129 * Register a handler for the given path elements
130 */
131 protected function register_handler(moodle1_handler $handler, array $elements) {
132
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 }
150
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 }
160
161 // done, store the paths (duplicates by path are discarded)
162 $this->pathelements = array_merge($this->pathelements, $paths);
163
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 }
175
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) {
184
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 }
195
196 // no grouped parent found
197 return false;
198 }
199
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) {
210
211 $path = $data['path'];
212
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;
217
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 }
221
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;
226
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 }
230
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;
237
238 } else {
239 // pretend as if the original $data contained the tranformed path
240 $data['path'] = $path;
241 }
242 }
243
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 }
248
beb7de37
DM
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
1e2c7351
DM
253
254 if (empty($object)) {
255 throw new convert_exception('missing_processing_object', $object);
256 }
257
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 }
262
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)) {
beb7de37
DM
266 $rawdatatags = $data['tags'];
267 $data['tags'] = $element->apply_recipes($data['tags']);
268 $returned = $object->$method($data['tags'], $rawdatatags);
1e2c7351
DM
269 }
270
271 // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path
272 // and lock it so that its children are not dispatched
beb7de37 273 if ($returned === self::SKIP_ALL_CHILDREN) {
1e2c7351
DM
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'] . '/';
280
281 // if the method has returned any info, set element data to it
beb7de37
DM
282 } else if (!is_null($returned)) {
283 $element->set_data($returned);
1e2c7351
DM
284
285 // use just the cooked parsed data otherwise
286 } else {
287 $element->set_data($data);
288 }
289 }
290
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
a5fe5912
DM
295 * so the current plugin is not known yet. Also note that this is
296 * triggered before the previous path is actually dispatched.
1e2c7351 297 *
1e2c7351
DM
298 * @param string $path in the original file
299 */
300 public function path_start_reached($path) {
a5fe5912
DM
301
302 if (empty($this->pathelements[$path])) {
303 return;
304 }
305
306 $element = $this->pathelements[$path];
307 $pobject = $element->get_processing_object();
308 $method = 'on_' . $element->get_name() . '_start';
309
310 if (method_exists($pobject, $method)) {
311 $pobject->$method();
312 }
1e2c7351
DM
313 }
314
315 /**
316 * Executes operations required at the end of a watched path
317 *
1e2c7351
DM
318 * @param string $path in the original file
319 */
320 public function path_end_reached($path) {
a5fe5912
DM
321
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;
325
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 }
329
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;
333
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 }
337
338 if (empty($this->pathelements[$path])) {
339 return;
340 }
341
342 $element = $this->pathelements[$path];
343 $pobject = $element->get_processing_object();
344 $method = 'on_' . $element->get_name() . '_end';
345
346 if (method_exists($pobject, $method)) {
347 $pobject->$method();
348 }
349 }
350
351 /**
9b5f1ad5 352 * Creates the temporary storage for stashed data
a5fe5912 353 *
9b5f1ad5 354 * This implementation uses backup_ids_temp table.
a5fe5912 355 */
9b5f1ad5
DM
356 public function create_stash_storage() {
357 backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
a5fe5912
DM
358 }
359
360 /**
9b5f1ad5 361 * Drops the temporary storage of stashed data
a5fe5912 362 *
9b5f1ad5 363 * This implementation uses backup_ids_temp table.
a5fe5912 364 */
9b5f1ad5
DM
365 public function drop_stash_storage() {
366 backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
1e2c7351 367 }
a5fe5912 368
beb7de37 369 /**
9b5f1ad5 370 * Stores some information for later processing
beb7de37 371 *
9b5f1ad5
DM
372 * This implementation uses backup_ids_temp table to store data. Make
373 * sure that the $stashname + $itemid combo is unique.
beb7de37
DM
374 *
375 * @param string $stashname name of the stash
376 * @param mixed $info information to stash
9b5f1ad5 377 * @param int $itemid optional id for multiple infos within the same stashname
beb7de37 378 */
9b5f1ad5
DM
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);
382
383 } catch (dml_exception $e) {
384 throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage());
385 }
beb7de37
DM
386 }
387
388 /**
389 * Restores a given stash stored previously by {@link self::set_stash()}
390 *
391 * @param string $stashname name of the stash
9b5f1ad5
DM
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
beb7de37
DM
394 * @return mixed stashed data
395 */
9b5f1ad5
DM
396 public function get_stash($stashname, $itemid = 0) {
397
398 $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid);
399
400 if (empty($record)) {
401 throw new moodle1_convert_empty_storage_exception('required_not_stashed_data');
402 } else {
403 return $record->info;
404 }
beb7de37
DM
405 }
406
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 *
beb7de37
DM
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) {
9b5f1ad5 420 static $autoincrement = 0;
beb7de37 421
9b5f1ad5 422 $stashname = 'context' . $level;
beb7de37 423
9b5f1ad5 424 $existing = $this->get_stash($stashname, $instance);
beb7de37
DM
425
426 if (empty($existing)) {
427 // this context level + instance is required for the first time
428 // store it and re-read to obtain its record id
9b5f1ad5
DM
429 $this->set_stash($stashname, $autoincrement++, $instance);
430 return $autoincrement;
beb7de37 431
9b5f1ad5
DM
432 } else {
433 return $existing;
434 }
beb7de37 435 }
1e2c7351
DM
436}
437
438
9b5f1ad5
DM
439/**
440 * Exception thrown by this converter
441 */
442class moodle1_convert_exception extends convert_exception {
443}
444
445
446/**
447 * Exception thrown by the temporary storage subsystem of moodle1_converter
448 */
449class moodle1_convert_storage_exception extends moodle1_convert_exception {
450}
451
452
453/**
454 * Exception thrown by the temporary storage subsystem of moodle1_converter
455 */
456class moodle1_convert_empty_storage_exception extends moodle1_convert_exception {
457}
458
459
1e2c7351
DM
460/**
461 * XML parser processor
462 */
463class moodle1_parser_processor extends grouped_parser_processor {
464
465 /** @var moodle1_converter */
466 protected $converter;
467
468 public function __construct(moodle1_converter $converter) {
469 $this->converter = $converter;
470 parent::__construct();
471 }
472
473 /**
474 * Provide NULL and legacy file.php uses decoding
475 */
476 public function process_cdata($cdata) {
477 global $CFG;
478
479 if ($cdata === '$@NULL@$') { // Some cases we know we can skip complete processing
480 return null;
481 } else if ($cdata === '') {
482 return '';
483 } else if (is_numeric($cdata)) {
484 return $cdata;
485 } else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
486 return $cdata; // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
487 } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
488 return $cdata;
489 }
490 // Decode file.php calls
491 $search = array ("$@FILEPHP@$");
492 $replace = array(get_file_url($this->courseid));
493 $result = str_replace($search, $replace, $cdata);
494 // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
495 $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
496 if ($CFG->slasharguments) {
497 $replace = array('/', '?forcedownload=1');
498 } else {
499 $replace = array('%2F', '&amp;forcedownload=1');
500 }
501 return str_replace($search, $replace, $result);
502 }
503
504 /**
505 * Override this method so we'll be able to skip
506 * dispatching some well-known chunks, like the
507 * ones being 100% part of subplugins stuff. Useful
508 * for allowing development without having all the
509 * possible restore subplugins defined
510 */
511 protected function postprocess_chunk($data) {
512
513 // Iterate over all the data tags, if any of them is
514 // not 'subplugin_XXXX' or has value, then it's a valid chunk,
515 // pass it to standard (parent) processing of chunks.
516 foreach ($data['tags'] as $key => $value) {
517 if (trim($value) !== '' || strpos($key, 'subplugin_') !== 0) {
518 parent::postprocess_chunk($data);
519 return;
520 }
521 }
522 // Arrived here, all the tags correspond to sublplugins and are empty,
523 // skip the chunk, and debug_developer notice
524 $this->chunks--; // not counted
525 debugging('Missing support on restore for ' . clean_param($data['path'], PARAM_PATH) .
526 ' subplugin (' . implode(', ', array_keys($data['tags'])) .')', DEBUG_DEVELOPER);
527 }
528
529 /**
530 * Dispatches the data chunk to the converter class
531 *
532 * @param array $data the chunk of parsed data
533 */
534 protected function dispatch_chunk($data) {
535 $this->converter->process_chunk($data);
536 }
537
538 /**
539 * Informs the converter at the start of a watched path
540 *
541 * @param string $path
542 */
543 protected function notify_path_start($path) {
544 $this->converter->path_start_reached($path);
545 }
546
547 /**
548 * Informs the converter at the end of a watched path
549 *
550 * @param string $path
551 */
552 protected function notify_path_end($path) {
553 $this->converter->path_end_reached($path);
554 }
555}
556
557
558/**
559 * Class representing a path to be converted from XML file
560 *
561 * This was created as a copy of {@link restore_path_element} and should be refactored
562 * probably.
563 */
564class convert_path {
565
566 /** @var string name of the element */
567 protected $name;
568
569 /** @var string path within the XML file this element will handle */
570 protected $path;
571
572 /** @var bool flag to define if this element will get child ones grouped or no */
573 protected $grouped;
574
575 /** @var object object instance in charge of processing this element. */
576 protected $pobject = null;
577
578 /** @var string the name of the processing method */
579 protected $pmethod = null;
580
581 /** @var mixed last data read for this element or returned data by processing method */
582 protected $data = null;
583
a5fe5912
DM
584 /** @var array of deprecated fields that are dropped */
585 protected $dropfields = array();
1e2c7351
DM
586
587 /** @var array of fields renaming */
588 protected $renamefields = array();
589
590 /** @var array of new fields to add and their initial values */
591 protected $newfields = array();
592
593 /**
594 * Constructor
595 *
596 * @param string $name name of the element
597 * @param string $path path of the element
598 * @param array $recipe basic description of the structure conversion
599 * @param bool $grouped to gather information in grouped mode or no
600 */
601 public function __construct($name, $path, array $recipe = array(), $grouped = false) {
602
603 $this->validate_name($name);
604
605 $this->name = $name;
606 $this->path = $path;
607 $this->grouped = $grouped;
608
609 // set the default processing method name
610 $this->set_processing_method('process_' . $name);
611
a5fe5912
DM
612 if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
613 $this->set_dropped_fields($recipe['dropfields']);
1e2c7351
DM
614 }
615 if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
616 $this->set_renamed_fields($recipe['renamefields']);
617 }
618 if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
619 $this->set_new_fields($recipe['newfields']);
620 }
621 }
622
623 /**
624 * Validates and sets the given processing object
625 *
626 * @param object $pobject processing object, must provide a method to be called
627 */
628 public function set_processing_object($pobject) {
629 $this->validate_pobject($pobject);
630 $this->pobject = $pobject;
631 }
632
633 /**
634 * Sets the name of the processing method
635 *
636 * @param string $pmethod
637 */
638 public function set_processing_method($pmethod) {
639 $this->pmethod = $pmethod;
640 }
641
642 /**
643 * Sets the element data
644 *
645 * @param mixed
646 */
647 public function set_data($data) {
648 $this->data = $data;
649 }
650
651 /**
a5fe5912 652 * Sets the list of deprecated fields to drop
1e2c7351
DM
653 *
654 * @param array $fields
655 */
a5fe5912
DM
656 public function set_dropped_fields(array $fields) {
657 $this->dropfields = $fields;
1e2c7351
DM
658 }
659
660 /**
661 * Sets the required new names of the current fields
662 *
663 * @param array $fields (string)$currentname => (string)$newname
664 */
665 public function set_renamed_fields(array $fields) {
666 $this->renamefields = $fields;
667 }
668
669 /**
670 * Sets the new fields and their values
671 *
672 * @param array $fields (string)$field => (mixed)value
673 */
674 public function set_new_fields(array $fields) {
675 $this->newfields = $fields;
676 }
677
678 /**
679 * Cooks the parsed tags data by applying known recipes
680 *
681 * Recipes are used for common trivial operations like adding new fields
682 * or renaming fields. The handler's processing method receives cooked
683 * data.
684 *
685 * @param array $data the contents of the element
686 * @return array
687 */
688 public function apply_recipes(array $data) {
689
690 $cooked = array();
691
692 foreach ($data as $name => $value) {
693 // lower case rocks!
694 $name = strtolower($name);
695
a5fe5912
DM
696 // drop legacy fields
697 if (in_array($name, $this->dropfields)) {
698 continue;
699 }
700
1e2c7351
DM
701 // fields renaming
702 if (array_key_exists($name, $this->renamefields)) {
703 $name = $this->renamefields[$name];
704 }
705
706 $cooked[$name] = $value;
707 }
708
709 // adding new fields
710 foreach ($this->newfields as $name => $value) {
711 $cooked[$name] = $value;
712 }
713
714 return $cooked;
715 }
716
717 /**
718 * @return string the element given name
719 */
720 public function get_name() {
721 return $this->name;
722 }
723
724 /**
725 * @return string the path to the element
726 */
727 public function get_path() {
728 return $this->path;
729 }
730
731 /**
732 * @return bool flag to define if this element will get child ones grouped or no
733 */
734 public function is_grouped() {
735 return $this->grouped;
736 }
737
738 /**
739 * @return object the processing object providing the processing method
740 */
741 public function get_processing_object() {
742 return $this->pobject;
743 }
744
745 /**
746 * @return string the name of the method to call to process the element
747 */
748 public function get_processing_method() {
749 return $this->pmethod;
750 }
751
752 /**
753 * @return mixed the element data
754 */
755 public function get_data() {
756 return $this->data;
757 }
758
759
760 /// end of public API //////////////////////////////////////////////////////
761
762 /**
763 * Makes sure the given name is a valid element name
764 *
765 * Note it may look as if we used exceptions for code flow control here. That's not the case
766 * as we actually validate the code, not the user data. And the code is supposed to be
767 * correct.
768 *
769 * @param string @name the element given name
770 * @throws convert_path_exception
771 * @return void
772 */
773 protected function validate_name($name) {
774 // Validate various name constraints, throwing exception if needed
775 if (empty($name)) {
776 throw new convert_path_exception('convert_path_emptyname', $name);
777 }
778 if (preg_replace('/\s/', '', $name) != $name) {
779 throw new convert_path_exception('convert_path_whitespace', $name);
780 }
781 if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
782 throw new convert_path_exception('convert_path_notasciiname', $name);
783 }
784 }
785
786 /**
787 * Makes sure that the given object is a valid processing object
788 *
789 * The processing object must be an object providing the element's processing method.
790 * Note it may look as if we used exceptions for code flow control here. That's not the case
791 * as we actually validate the code, not the user data. And the code is supposed to be
792 * correct.
793 *
794 * @param object $pobject
795 * @throws convert_path_exception
796 * @return void
797 */
798 protected function validate_pobject($pobject) {
799 if (!is_object($pobject)) {
800 throw new convert_path_exception('convert_path_no_object', $pobject);
801 }
802 if (!method_exists($pobject, $this->get_processing_method())) {
803 throw new convert_path_exception('convert_path_missingmethod', $this->get_processing_method());
804 }
805 }
806}
807
808
809/**
810 * Exception being thrown by {@link convert_path} methods
811 */
812class convert_path_exception extends moodle_exception {
813
814 /**
815 * Constructor
816 *
817 * @param string $errorcode key for the corresponding error string
818 * @param mixed $a extra words and phrases that might be required by the error string
819 * @param string $debuginfo optional debugging information
820 */
821 public function __construct($errorcode, $a = null, $debuginfo = null) {
822 parent::__construct($errorcode, '', '', $a, $debuginfo);
823 }
824}