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