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