Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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  }