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