Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is 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   * @package moodlecore
  20   * @subpackage backup-plan
  21   * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  /**
  26   * Abstract class defining the needed stuff to restore one xml file
  27   *
  28   * TODO: Finish phpdocs
  29   */
  30  abstract class restore_structure_step extends restore_step {
  31  
  32      protected $filename; // Name of the file to be parsed
  33      protected $contentprocessor; // xml parser processor being used
  34                                   // (need it here, apart from parser
  35                                   // thanks to serialized data to process -
  36                                   // say thanks to blocks!)
  37      protected $pathelements;  // Array of pathelements to process
  38      protected $elementsoldid; // Array to store last oldid used on each element
  39      protected $elementsnewid; // Array to store last newid used on each element
  40  
  41      protected $pathlock;      // Path currently locking processing of children
  42  
  43      const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore
  44                                         // all children below path processor returning it
  45  
  46      /**
  47       * Constructor - instantiates one object of this class
  48       */
  49      public function __construct($name, $filename, $task = null) {
  50          if (!is_null($task) && !($task instanceof restore_task)) {
  51              throw new restore_step_exception('wrong_restore_task_specified');
  52          }
  53          $this->filename = $filename;
  54          $this->contentprocessor = null;
  55          $this->pathelements = array();
  56          $this->elementsoldid = array();
  57          $this->elementsnewid = array();
  58          $this->pathlock = null;
  59          parent::__construct($name, $task);
  60      }
  61  
  62      final public function execute() {
  63  
  64          if (!$this->execute_condition()) { // Check any condition to execute this
  65              return;
  66          }
  67  
  68          $fullpath = $this->task->get_taskbasepath();
  69  
  70          // We MUST have one fullpath here, else, error
  71          if (empty($fullpath)) {
  72              throw new restore_step_exception('restore_structure_step_undefined_fullpath');
  73          }
  74  
  75          // Append the filename to the fullpath
  76          $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  77  
  78          // And it MUST exist
  79          if (!file_exists($fullpath)) { // Shouldn't happen ever, but...
  80              throw new restore_step_exception('missing_moodle_backup_xml_file', $fullpath);
  81          }
  82  
  83          // Get restore_path elements array adapting and preparing it for processing
  84          $structure = $this->define_structure();
  85          if (!is_array($structure)) {
  86              throw new restore_step_exception('restore_step_structure_not_array', $this->get_name());
  87          }
  88          $this->prepare_pathelements($structure);
  89  
  90          // Create parser and processor
  91          $xmlparser = new progressive_parser();
  92          $xmlparser->set_file($fullpath);
  93          $xmlprocessor = new restore_structure_parser_processor($this->task->get_courseid(), $this);
  94          $this->contentprocessor = $xmlprocessor; // Save the reference to the contentprocessor
  95                                                   // as far as we are going to need it out
  96                                                   // from parser (blame serialized data!)
  97          $xmlparser->set_processor($xmlprocessor);
  98  
  99          // Add pathelements to processor
 100          foreach ($this->pathelements as $element) {
 101              $xmlprocessor->add_path($element->get_path(), $element->is_grouped());
 102          }
 103  
 104          // Set up progress tracking.
 105          $progress = $this->get_task()->get_progress();
 106          $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
 107          $xmlparser->set_progress($progress);
 108  
 109          // And process it, dispatch to target methods in step will start automatically
 110          $xmlparser->process();
 111  
 112          // Have finished, launch the after_execute method of all the processing objects
 113          $this->launch_after_execute_methods();
 114          $progress->end_progress();
 115      }
 116  
 117      /**
 118       * Receive one chunk of information form the xml parser processor and
 119       * dispatch it, following the naming rules
 120       */
 121      final public function process($data) {
 122          if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen
 123              throw new restore_step_exception('restore_structure_step_missing_path', $data['path']);
 124          }
 125          $element = $this->pathelements[$data['path']];
 126          $object = $element->get_processing_object();
 127          $method = $element->get_processing_method();
 128          $rdata = null;
 129          if (empty($object)) { // No processing object defined
 130              throw new restore_step_exception('restore_structure_step_missing_pobject', $object);
 131          }
 132          // Release the lock if we aren't anymore within children of it
 133          if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
 134              $this->pathlock = null;
 135          }
 136          if (is_null($this->pathlock)) { // Only dispatch if there isn't any lock
 137              $rdata = $object->$method($data['tags']); // Dispatch to proper object/method
 138          }
 139  
 140          // If the dispatched method returns SKIP_ALL_CHILDREN, we grab current path in order to
 141          // lock dispatching to any children
 142          if ($rdata === self::SKIP_ALL_CHILDREN) {
 143              // Check we haven't any previous lock
 144              if (!is_null($this->pathlock)) {
 145                  throw new restore_step_exception('restore_structure_step_already_skipping', $data['path']);
 146              }
 147              // Set the lock
 148              $this->pathlock = $data['path'] . '/'; // Lock everything below current path
 149  
 150          // Continue with normal processing of return values
 151          } else if ($rdata !== null) { // If the method has returned any info, set element data to it
 152              $element->set_data($rdata);
 153          } else {               // Else, put the original parsed data
 154              $element->set_data($data);
 155          }
 156      }
 157  
 158      /**
 159       * To send ids pairs to backup_ids_table and to store them into paths
 160       *
 161       * This method will send the given itemname and old/new ids to the
 162       * backup_ids_temp table, and, at the same time, will save the new id
 163       * into the corresponding restore_path_element for easier access
 164       * by children. Also will inject the known old context id for the task
 165       * in case it's going to be used for restoring files later
 166       */
 167      public function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) {
 168          if ($restorefiles && $parentid) {
 169              throw new restore_step_exception('set_mapping_cannot_specify_both_restorefiles_and_parentitemid');
 170          }
 171          // If we haven't specified one context for the files, use the task one
 172          if (is_null($filesctxid)) {
 173              $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null;
 174          } else { // Use the specified one
 175              $parentitemid = $restorefiles ? $filesctxid : null;
 176          }
 177          // We have passed one explicit parentid, apply it
 178          $parentitemid = !is_null($parentid) ? $parentid : $parentitemid;
 179  
 180          // Let's call the low level one
 181          restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid);
 182          // Now, if the itemname matches any pathelement->name, store the latest $newid
 183          if (array_key_exists($itemname, $this->elementsoldid)) { // If present in  $this->elementsoldid, is valid, put both ids
 184              $this->elementsoldid[$itemname] = $oldid;
 185              $this->elementsnewid[$itemname] = $newid;
 186          }
 187      }
 188  
 189      /**
 190       * Returns the latest (parent) old id mapped by one pathelement
 191       */
 192      public function get_old_parentid($itemname) {
 193          return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null;
 194      }
 195  
 196      /**
 197       * Returns the latest (parent) new id mapped by one pathelement
 198       */
 199      public function get_new_parentid($itemname) {
 200          return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null;
 201      }
 202  
 203      /**
 204       * Return the new id of a mapping for the given itemname
 205       *
 206       * @param string $itemname the type of item
 207       * @param int $oldid the item ID from the backup
 208       * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false
 209       */
 210      public function get_mappingid($itemname, $oldid, $ifnotfound = false) {
 211          $mapping = $this->get_mapping($itemname, $oldid);
 212          return $mapping ? $mapping->newitemid : $ifnotfound;
 213      }
 214  
 215      /**
 216       * Return the complete mapping from the given itemname, itemid
 217       */
 218      public function get_mapping($itemname, $oldid) {
 219          return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid);
 220      }
 221  
 222      /**
 223       * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
 224       */
 225      public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
 226          // If the current progress object is set up and ready to receive
 227          // indeterminate progress, then use it, otherwise don't. (This check is
 228          // just in case this function is ever called from somewhere not within
 229          // the execute() method here, which does set up progress like this.)
 230          $progress = $this->get_task()->get_progress();
 231          if (!$progress->is_in_progress_section() ||
 232                  $progress->get_current_max() !== \core\progress\base::INDETERMINATE) {
 233              $progress = null;
 234          }
 235  
 236          $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
 237          $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
 238                  $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid, null, false,
 239                  $progress);
 240          $resultstoadd = array();
 241          foreach ($results as $result) {
 242              $this->log($result->message, $result->level);
 243              $resultstoadd[$result->code] = true;
 244          }
 245          $this->task->add_result($resultstoadd);
 246      }
 247  
 248      /**
 249       * As far as restore structure steps are implementing restore_plugin stuff, they need to
 250       * have the parent task available for wrapping purposes (get course/context....)
 251       * @return restore_task|null
 252       */
 253      public function get_task() {
 254          return $this->task;
 255      }
 256  
 257  // Protected API starts here
 258  
 259      /**
 260       * Add plugin structure to any element in the structure restore tree
 261       *
 262       * @param string $plugintype type of plugin as defined by core_component::get_plugin_types()
 263       * @param restore_path_element $element element in the structure restore tree that
 264       *                                       we are going to add plugin information to
 265       */
 266      protected function add_plugin_structure($plugintype, $element) {
 267  
 268          global $CFG;
 269  
 270          // Check the requested plugintype is a valid one
 271          if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) {
 272               throw new restore_step_exception('incorrect_plugin_type', $plugintype);
 273          }
 274  
 275          // Get all the restore path elements, looking across all the plugin dirs
 276          $pluginsdirs = core_component::get_plugin_list($plugintype);
 277          foreach ($pluginsdirs as $name => $pluginsdir) {
 278              // We need to add also backup plugin classes on restore, they may contain
 279              // some stuff used both in backup & restore
 280              $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin';
 281              $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php';
 282              if (file_exists($backupfile)) {
 283                  require_once($backupfile);
 284              }
 285              // Now add restore plugin classes and prepare stuff
 286              $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin';
 287              $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php';
 288              if (file_exists($restorefile)) {
 289                  require_once($restorefile);
 290                  $restoreplugin = new $restoreclassname($plugintype, $name, $this);
 291                  // Add plugin paths to the step
 292                  $this->prepare_pathelements($restoreplugin->define_plugin_structure($element));
 293              }
 294          }
 295      }
 296  
 297      /**
 298       * Add subplugin structure for a given plugin to any element in the structure restore tree
 299       *
 300       * This method allows the injection of subplugins (of a specific plugin) parsing and proccessing
 301       * to any element in the restore structure.
 302       *
 303       * NOTE: Initially subplugins were only available for activities (mod), so only the
 304       * {@link restore_activity_structure_step} class had support for them, always
 305       * looking for /mod/modulenanme subplugins. This new method is a generalization of the
 306       * existing one for activities, supporting all subplugins injecting information everywhere.
 307       *
 308       * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
 309       * @param restore_path_element $element element in the structure restore tree that
 310       *                              we are going to add subplugin information to.
 311       * @param string $plugintype type of the plugin.
 312       * @param string $pluginname name of the plugin.
 313       * @return void
 314       */
 315      protected function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) {
 316          global $CFG;
 317          // This global declaration is required, because where we do require_once($backupfile);
 318          // That file may in turn try to do require_once($CFG->dirroot ...).
 319          // That worked in the past, we should keep it working.
 320  
 321          // Verify if this is a BC call for an activity restore. See NOTE above for this special case.
 322          if ($plugintype === null and $pluginname === null) {
 323              $plugintype = 'mod';
 324              $pluginname = $this->task->get_modulename();
 325              // TODO: Once all the calls have been changed to add both not null plugintype and pluginname, add a debugging here.
 326          }
 327  
 328          // Check the requested plugintype is a valid one.
 329          if (!array_key_exists($plugintype, core_component::get_plugin_types())) {
 330              throw new restore_step_exception('incorrect_plugin_type', $plugintype);
 331          }
 332  
 333          // Check the requested pluginname, for the specified plugintype, is a valid one.
 334          if (!array_key_exists($pluginname, core_component::get_plugin_list($plugintype))) {
 335              throw new restore_step_exception('incorrect_plugin_name', array($plugintype, $pluginname));
 336          }
 337  
 338          // Check the requested subplugintype is a valid one.
 339          $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
 340          if (null === $subplugins) {
 341              throw new restore_step_exception('plugin_missing_subplugins_configuration', array($plugintype, $pluginname));
 342          }
 343          if (!array_key_exists($subplugintype, $subplugins)) {
 344               throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
 345          }
 346  
 347          // Every subplugin optionally can have a common/parent subplugin
 348          // class for shared stuff.
 349          $parentclass = 'restore_' . $plugintype . '_' . $pluginname . '_' . $subplugintype . '_subplugin';
 350          $parentfile = core_component::get_component_directory($plugintype . '_' . $pluginname) .
 351              '/backup/moodle2/' . $parentclass . '.class.php';
 352          if (file_exists($parentfile)) {
 353              require_once($parentfile);
 354          }
 355  
 356          // Get all the restore path elements, looking across all the subplugin dirs.
 357          $subpluginsdirs = core_component::get_plugin_list($subplugintype);
 358          foreach ($subpluginsdirs as $name => $subpluginsdir) {
 359              $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
 360              $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
 361              if (file_exists($restorefile)) {
 362                  require_once($restorefile);
 363                  $restoresubplugin = new $classname($subplugintype, $name, $this);
 364                  // Add subplugin paths to the step.
 365                  $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
 366              }
 367          }
 368      }
 369  
 370      /**
 371       * Launch all the after_execute methods present in all the processing objects
 372       *
 373       * This method will launch all the after_execute methods that can be defined
 374       * both in restore_plugin and restore_structure_step classes
 375       *
 376       * For restore_plugin classes the name of the method to be executed will be
 377       * "after_execute_" + connection point (as far as can be multiple connection
 378       * points in the same class)
 379       *
 380       * For restore_structure_step classes is will be, simply, "after_execute". Note
 381       * that this is executed *after* the plugin ones
 382       */
 383      protected function launch_after_execute_methods() {
 384          $alreadylaunched = array(); // To avoid multiple executions
 385          foreach ($this->pathelements as $key => $pathelement) {
 386              // Get the processing object
 387              $pobject = $pathelement->get_processing_object();
 388              // Skip null processors (child of grouped ones for sure)
 389              if (is_null($pobject)) {
 390                  continue;
 391              }
 392              // Skip restore structure step processors (this)
 393              if ($pobject instanceof restore_structure_step) {
 394                  continue;
 395              }
 396              // Skip already launched processing objects
 397              if (in_array($pobject, $alreadylaunched, true)) {
 398                  continue;
 399              }
 400              // Add processing object to array of launched ones
 401              $alreadylaunched[] = $pobject;
 402              // If the processing object has support for
 403              // launching after_execute methods, use it
 404              if (method_exists($pobject, 'launch_after_execute_methods')) {
 405                  $pobject->launch_after_execute_methods();
 406              }
 407          }
 408          // Finally execute own (restore_structure_step) after_execute method
 409          $this->after_execute();
 410  
 411      }
 412  
 413      /**
 414       * Launch all the after_restore methods present in all the processing objects
 415       *
 416       * This method will launch all the after_restore methods that can be defined
 417       * both in restore_plugin class
 418       *
 419       * For restore_plugin classes the name of the method to be executed will be
 420       * "after_restore_" + connection point (as far as can be multiple connection
 421       * points in the same class)
 422       */
 423      public function launch_after_restore_methods() {
 424          $alreadylaunched = array(); // To avoid multiple executions
 425          foreach ($this->pathelements as $pathelement) {
 426              // Get the processing object
 427              $pobject = $pathelement->get_processing_object();
 428              // Skip null processors (child of grouped ones for sure)
 429              if (is_null($pobject)) {
 430                  continue;
 431              }
 432              // Skip restore structure step processors (this)
 433              if ($pobject instanceof restore_structure_step) {
 434                  continue;
 435              }
 436              // Skip already launched processing objects
 437              if (in_array($pobject, $alreadylaunched, true)) {
 438                  continue;
 439              }
 440              // Add processing object to array of launched ones
 441              $alreadylaunched[] = $pobject;
 442              // If the processing object has support for
 443              // launching after_restore methods, use it
 444              if (method_exists($pobject, 'launch_after_restore_methods')) {
 445                  $pobject->launch_after_restore_methods();
 446              }
 447          }
 448          // Finally execute own (restore_structure_step) after_restore method
 449          $this->after_restore();
 450      }
 451  
 452      /**
 453       * This method will be executed after the whole structure step have been processed
 454       *
 455       * After execution method for code needed to be executed after the whole structure
 456       * has been processed. Useful for cleaning tasks, files process and others. Simply
 457       * overwrite in in your steps if needed
 458       */
 459      protected function after_execute() {
 460          // do nothing by default
 461      }
 462  
 463      /**
 464       * This method will be executed after the rest of the restore has been processed.
 465       *
 466       * Use if you need to update IDs based on things which are restored after this
 467       * step has completed.
 468       */
 469      protected function after_restore() {
 470          // do nothing by default
 471      }
 472  
 473      /**
 474       * Prepare the pathelements for processing, looking for duplicates, applying
 475       * processing objects and other adjustments
 476       */
 477      protected function prepare_pathelements($elementsarr) {
 478  
 479          // First iteration, push them to new array, indexed by name
 480          // detecting duplicates in names or paths
 481          $names = array();
 482          $paths = array();
 483          foreach($elementsarr as $element) {
 484              if (!$element instanceof restore_path_element) {
 485                  throw new restore_step_exception('restore_path_element_wrong_class', get_class($element));
 486              }
 487              if (array_key_exists($element->get_name(), $names)) {
 488                  throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
 489              }
 490              if (array_key_exists($element->get_path(), $paths)) {
 491                  throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
 492              }
 493              $names[$element->get_name()] = true;
 494              $paths[$element->get_path()] = $element;
 495          }
 496          // Now, for each element not having one processing object, if
 497          // not child of grouped element, assign $this (the step itself) as processing element
 498          // Note method must exist or we'll get one @restore_path_element_exception
 499          foreach ($paths as $pelement) {
 500              if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
 501                  $pelement->set_processing_object($this);
 502              }
 503              // Populate $elementsoldid and $elementsoldid based on available pathelements
 504              $this->elementsoldid[$pelement->get_name()] = null;
 505              $this->elementsnewid[$pelement->get_name()] = null;
 506          }
 507          // Done, add them to pathelements (dupes by key - path - are discarded)
 508          $this->pathelements = array_merge($this->pathelements, $paths);
 509      }
 510  
 511      /**
 512       * Given one pathelement, return true if grouped parent was found
 513       *
 514       * @param restore_path_element $pelement the element we are interested in.
 515       * @param restore_path_element[] $elements the elements that exist.
 516       * @return bool true if this element is inside a grouped parent.
 517       */
 518      public function grouped_parent_exists($pelement, $elements) {
 519          foreach ($elements as $element) {
 520              if ($pelement->get_path() == $element->get_path()) {
 521                  continue; // Don't compare against itself.
 522              }
 523              // If element is grouped and parent of pelement, return true.
 524              if ($element->is_grouped() and strpos($pelement->get_path() .  '/', $element->get_path()) === 0) {
 525                  return true;
 526              }
 527          }
 528          return false; // No grouped parent found.
 529      }
 530  
 531      /**
 532       * To conditionally decide if one step will be executed or no
 533       *
 534       * For steps needing to be executed conditionally, based in dynamic
 535       * conditions (at execution time vs at declaration time) you must
 536       * override this function. It will return true if the step must be
 537       * executed and false if not
 538       */
 539      protected function execute_condition() {
 540          return true;
 541      }
 542  
 543      /**
 544       * Function that will return the structure to be processed by this restore_step.
 545       * Must return one array of @restore_path_element elements
 546       */
 547      abstract protected function define_structure();
 548  }