Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

   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 {@link convert_helper} and {@link convert_helper_exception} classes
  20   *
  21   * @package    core
  22   * @subpackage backup-convert
  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/util/includes/convert_includes.php');
  30  
  31  /**
  32   * Provides various functionality via its static methods
  33   */
  34  abstract class convert_helper {
  35  
  36      /**
  37       * @param string $entropy
  38       * @return string random identifier
  39       */
  40      public static function generate_id($entropy) {
  41          return md5(time() . '-' . $entropy . '-' . random_string(20));
  42      }
  43  
  44      /**
  45       * Returns the list of all available converters and loads their classes
  46       *
  47       * Converter must be installed as a directory in backup/converter/ and its
  48       * method is_available() must return true to get to the list.
  49       *
  50       * @see base_converter::is_available()
  51       * @return array of strings
  52       */
  53      public static function available_converters($restore=true) {
  54          global $CFG;
  55  
  56          $converters = array();
  57  
  58          $plugins    = get_list_of_plugins('backup/converter');
  59          foreach ($plugins as $name) {
  60              $filename = $restore ? 'lib.php' : 'backuplib.php';
  61              $classuf  = $restore ? '_converter' : '_export_converter';
  62              $classfile = "{$CFG->dirroot}/backup/converter/{$name}/{$filename}";
  63              $classname = "{$name}{$classuf}";
  64              $zip_contents      = "{$name}_zip_contents";
  65              $store_backup_file = "{$name}_store_backup_file";
  66              $convert           = "{$name}_backup_convert";
  67  
  68              if (!file_exists($classfile)) {
  69                  throw new convert_helper_exception('converter_classfile_not_found', $classfile);
  70              }
  71  
  72              require_once($classfile);
  73  
  74              if (!class_exists($classname)) {
  75                  throw new convert_helper_exception('converter_classname_not_found', $classname);
  76              }
  77  
  78              if (call_user_func($classname .'::is_available')) {
  79                  if (!$restore) {
  80                      if (!class_exists($zip_contents)) {
  81                          throw new convert_helper_exception('converter_classname_not_found', $zip_contents);
  82                      }
  83                      if (!class_exists($store_backup_file)) {
  84                          throw new convert_helper_exception('converter_classname_not_found', $store_backup_file);
  85                      }
  86                      if (!class_exists($convert)) {
  87                          throw new convert_helper_exception('converter_classname_not_found', $convert);
  88                      }
  89                  }
  90  
  91                  $converters[] = $name;
  92              }
  93  
  94          }
  95  
  96          return $converters;
  97      }
  98  
  99      public static function export_converter_dependencies($converter, $dependency) {
 100          global $CFG;
 101  
 102          $result = array();
 103          $filename = 'backuplib.php';
 104          $classuf  = '_export_converter';
 105          $classfile = "{$CFG->dirroot}/backup/converter/{$converter}/{$filename}";
 106          $classname = "{$converter}{$classuf}";
 107  
 108          if (!file_exists($classfile)) {
 109              throw new convert_helper_exception('converter_classfile_not_found', $classfile);
 110          }
 111          require_once($classfile);
 112  
 113          if (!class_exists($classname)) {
 114              throw new convert_helper_exception('converter_classname_not_found', $classname);
 115          }
 116  
 117          if (call_user_func($classname .'::is_available')) {
 118              $deps = call_user_func($classname .'::get_deps');
 119              if (array_key_exists($dependency, $deps)) {
 120                  $result = $deps[$dependency];
 121              }
 122          }
 123  
 124          return $result;
 125      }
 126  
 127      /**
 128       * Detects if the given folder contains an unpacked moodle2 backup
 129       *
 130       * @param string $tempdir the name of the backup directory
 131       * @return boolean true if moodle2 format detected, false otherwise
 132       */
 133      public static function detect_moodle2_format($tempdir) {
 134          $dirpath = make_backup_temp_directory($tempdir, false);
 135          if (!is_dir($dirpath)) {
 136              throw new convert_helper_exception('tmp_backup_directory_not_found', $dirpath);
 137          }
 138  
 139          $filepath = $dirpath . '/moodle_backup.xml';
 140          if (!file_exists($filepath)) {
 141              return false;
 142          }
 143  
 144          $handle     = fopen($filepath, 'r');
 145          $firstchars = fread($handle, 200);
 146          $status     = fclose($handle);
 147  
 148          // Look for expected XML elements (case-insensitive to account for encoding attribute).
 149          if (stripos($firstchars, '<?xml version="1.0" encoding="UTF-8"?>') !== false &&
 150              strpos($firstchars, '<moodle_backup>') !== false &&
 151              strpos($firstchars, '<information>') !== false) {
 152  
 153                  return true;
 154          }
 155  
 156          return false;
 157      }
 158  
 159      /**
 160       * Converts the given directory with the backup into moodle2 format
 161       *
 162       * @param string $tempdir The directory to convert
 163       * @param string $format The current format, if already detected
 164       * @param base_logger|null if the conversion should be logged, use this logger
 165       * @throws convert_helper_exception
 166       * @return bool false if unable to find the conversion path, true otherwise
 167       */
 168      public static function to_moodle2_format($tempdir, $format = null, $logger = null) {
 169  
 170          if (is_null($format)) {
 171              $format = backup_general_helper::detect_backup_format($tempdir);
 172          }
 173  
 174          // get the supported conversion paths from all available converters
 175          $converters   = self::available_converters();
 176          $descriptions = array();
 177          foreach ($converters as $name) {
 178              $classname = "{$name}_converter";
 179              if (!class_exists($classname)) {
 180                  throw new convert_helper_exception('class_not_loaded', $classname);
 181              }
 182              if ($logger instanceof base_logger) {
 183                  backup_helper::log('available converter', backup::LOG_DEBUG, $classname, 1, false, $logger);
 184              }
 185              $descriptions[$name] = call_user_func($classname .'::description');
 186          }
 187  
 188          // choose the best conversion path for the given format
 189          $path = self::choose_conversion_path($format, $descriptions);
 190  
 191          if (empty($path)) {
 192              if ($logger instanceof base_logger) {
 193                  backup_helper::log('unable to find the conversion path', backup::LOG_ERROR, null, 0, false, $logger);
 194              }
 195              return false;
 196          }
 197  
 198          if ($logger instanceof base_logger) {
 199              backup_helper::log('conversion path established', backup::LOG_INFO,
 200                  implode(' => ', array_merge($path, array('moodle2'))), 0, false, $logger);
 201          }
 202  
 203          foreach ($path as $name) {
 204              if ($logger instanceof base_logger) {
 205                  backup_helper::log('running converter', backup::LOG_INFO, $name, 0, false, $logger);
 206              }
 207              $converter = convert_factory::get_converter($name, $tempdir, $logger);
 208              $converter->convert();
 209          }
 210  
 211          // make sure we ended with moodle2 format
 212          if (!self::detect_moodle2_format($tempdir)) {
 213              throw new convert_helper_exception('conversion_failed');
 214          }
 215  
 216          return true;
 217      }
 218  
 219     /**
 220      * Inserts an inforef into the conversion temp table
 221      */
 222      public static function set_inforef($contextid) {
 223          global $DB;
 224      }
 225  
 226      public static function get_inforef($contextid) {
 227      }
 228  
 229      /// end of public API //////////////////////////////////////////////////////
 230  
 231      /**
 232       * Choose the best conversion path for the given format
 233       *
 234       * Given the source format and the list of available converters and their properties,
 235       * this methods picks the most effective way how to convert the source format into
 236       * the target moodle2 format. The method returns a list of converters that should be
 237       * called, in order.
 238       *
 239       * This implementation uses Dijkstra's algorithm to find the shortest way through
 240       * the oriented graph.
 241       *
 242       * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm
 243       * @author David Mudrak <david@moodle.com>
 244       * @param string $format the source backup format, one of backup::FORMAT_xxx
 245       * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name
 246       * @return array ordered list of converter names to call (may be empty if not reachable)
 247       */
 248      protected static function choose_conversion_path($format, array $descriptions) {
 249  
 250          // construct an oriented graph of conversion paths. backup formats are nodes
 251          // and the the converters are edges of the graph.
 252          $paths = array();   // [fromnode][tonode] => converter
 253          foreach ($descriptions as $converter => $description) {
 254              $from   = $description['from'];
 255              $to     = $description['to'];
 256              $cost   = $description['cost'];
 257  
 258              if (is_null($from) or $from === backup::FORMAT_UNKNOWN or
 259                  is_null($to) or $to === backup::FORMAT_UNKNOWN or
 260                  is_null($cost) or $cost <= 0) {
 261                      throw new convert_helper_exception('invalid_converter_description', $converter);
 262              }
 263  
 264              if (!isset($paths[$from][$to])) {
 265                  $paths[$from][$to] = $converter;
 266              } else {
 267                  // if there are two converters available for the same conversion
 268                  // path, choose the one with the lowest cost. if there are more
 269                  // available converters with the same cost, the chosen one is
 270                  // undefined (depends on the order of processing)
 271                  if ($descriptions[$paths[$from][$to]]['cost'] > $cost) {
 272                      $paths[$from][$to] = $converter;
 273                  }
 274              }
 275          }
 276  
 277          if (empty($paths)) {
 278              // no conversion paths available
 279              return array();
 280          }
 281  
 282          // now use Dijkstra's algorithm and find the shortest conversion path
 283  
 284          $dist = array(); // list of nodes and their distances from the source format
 285          $prev = array(); // list of previous nodes in optimal path from the source format
 286          foreach ($paths as $fromnode => $tonodes) {
 287              $dist[$fromnode] = null; // infinitive distance, can't be reached
 288              $prev[$fromnode] = null; // unknown
 289              foreach ($tonodes as $tonode => $converter) {
 290                  $dist[$tonode] = null; // infinitive distance, can't be reached
 291                  $prev[$tonode] = null; // unknown
 292              }
 293          }
 294  
 295          if (!array_key_exists($format, $dist)) {
 296              return array();
 297          } else {
 298              $dist[$format] = 0;
 299          }
 300  
 301          $queue = array_flip(array_keys($dist));
 302          while (!empty($queue)) {
 303              // find the node with the smallest distance from the source in the queue
 304              // in the first iteration, this will find the original format node itself
 305              $closest = null;
 306              foreach ($queue as $node => $undefined) {
 307                  if (is_null($dist[$node])) {
 308                      continue;
 309                  }
 310                  if (is_null($closest) or ($dist[$node] < $dist[$closest])) {
 311                      $closest = $node;
 312                  }
 313              }
 314  
 315              if (is_null($closest) or is_null($dist[$closest])) {
 316                  // all remaining nodes are inaccessible from source
 317                  break;
 318              }
 319  
 320              if ($closest === backup::FORMAT_MOODLE) {
 321                  // bingo we can break now
 322                  break;
 323              }
 324  
 325              unset($queue[$closest]);
 326  
 327              // visit all neighbors and update distances to them eventually
 328  
 329              if (!isset($paths[$closest])) {
 330                  continue;
 331              }
 332              $neighbors = array_keys($paths[$closest]);
 333              // keep just neighbors that are in the queue yet
 334              foreach ($neighbors as $ix => $neighbor) {
 335                  if (!array_key_exists($neighbor, $queue)) {
 336                      unset($neighbors[$ix]);
 337                  }
 338              }
 339  
 340              foreach ($neighbors as $neighbor) {
 341                  // the alternative distance to the neighbor if we went thru the
 342                  // current $closest node
 343                  $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost'];
 344  
 345                  if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) {
 346                      // we found a shorter way to the $neighbor, remember it
 347                      $dist[$neighbor] = $alt;
 348                      $prev[$neighbor] = $closest;
 349                  }
 350              }
 351          }
 352  
 353          if (is_null($dist[backup::FORMAT_MOODLE])) {
 354              // unable to find a conversion path, the target format not reachable
 355              return array();
 356          }
 357  
 358          // reconstruct the optimal path from the source format to the target one
 359          $conversionpath = array();
 360          $target         = backup::FORMAT_MOODLE;
 361          while (isset($prev[$target])) {
 362              array_unshift($conversionpath, $paths[$prev[$target]][$target]);
 363              $target = $prev[$target];
 364          }
 365  
 366          return $conversionpath;
 367      }
 368  }
 369  
 370  /**
 371   * General convert_helper related exception
 372   *
 373   * @author David Mudrak <david@moodle.com>
 374   */
 375  class convert_helper_exception extends moodle_exception {
 376  
 377      /**
 378       * Constructor
 379       *
 380       * @param string $errorcode key for the corresponding error string
 381       * @param object $a extra words and phrases that might be required in the error string
 382       * @param string $debuginfo optional debugging information
 383       */
 384      public function __construct($errorcode, $a = null, $debuginfo = null) {
 385          parent::__construct($errorcode, '', '', $a, $debuginfo);
 386      }
 387  }