Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • Differences Between: [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

       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(dirname(__FILE__) . '/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  }
    

    Search This Site: