Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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          if (strpos($firstchars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
 149              strpos($firstchars,'<moodle_backup>') !== false and
 150              strpos($firstchars,'<information>') !== false) {
 151                  return true;
 152          }
 153  
 154          return false;
 155      }
 156  
 157      /**
 158       * Converts the given directory with the backup into moodle2 format
 159       *
 160       * @param string $tempdir The directory to convert
 161       * @param string $format The current format, if already detected
 162       * @param base_logger|null if the conversion should be logged, use this logger
 163       * @throws convert_helper_exception
 164       * @return bool false if unable to find the conversion path, true otherwise
 165       */
 166      public static function to_moodle2_format($tempdir, $format = null, $logger = null) {
 167  
 168          if (is_null($format)) {
 169              $format = backup_general_helper::detect_backup_format($tempdir);
 170          }
 171  
 172          // get the supported conversion paths from all available converters
 173          $converters   = self::available_converters();
 174          $descriptions = array();
 175          foreach ($converters as $name) {
 176              $classname = "{$name}_converter";
 177              if (!class_exists($classname)) {
 178                  throw new convert_helper_exception('class_not_loaded', $classname);
 179              }
 180              if ($logger instanceof base_logger) {
 181                  backup_helper::log('available converter', backup::LOG_DEBUG, $classname, 1, false, $logger);
 182              }
 183              $descriptions[$name] = call_user_func($classname .'::description');
 184          }
 185  
 186          // choose the best conversion path for the given format
 187          $path = self::choose_conversion_path($format, $descriptions);
 188  
 189          if (empty($path)) {
 190              if ($logger instanceof base_logger) {
 191                  backup_helper::log('unable to find the conversion path', backup::LOG_ERROR, null, 0, false, $logger);
 192              }
 193              return false;
 194          }
 195  
 196          if ($logger instanceof base_logger) {
 197              backup_helper::log('conversion path established', backup::LOG_INFO,
 198                  implode(' => ', array_merge($path, array('moodle2'))), 0, false, $logger);
 199          }
 200  
 201          foreach ($path as $name) {
 202              if ($logger instanceof base_logger) {
 203                  backup_helper::log('running converter', backup::LOG_INFO, $name, 0, false, $logger);
 204              }
 205              $converter = convert_factory::get_converter($name, $tempdir, $logger);
 206              $converter->convert();
 207          }
 208  
 209          // make sure we ended with moodle2 format
 210          if (!self::detect_moodle2_format($tempdir)) {
 211              throw new convert_helper_exception('conversion_failed');
 212          }
 213  
 214          return true;
 215      }
 216  
 217     /**
 218      * Inserts an inforef into the conversion temp table
 219      */
 220      public static function set_inforef($contextid) {
 221          global $DB;
 222      }
 223  
 224      public static function get_inforef($contextid) {
 225      }
 226  
 227      /// end of public API //////////////////////////////////////////////////////
 228  
 229      /**
 230       * Choose the best conversion path for the given format
 231       *
 232       * Given the source format and the list of available converters and their properties,
 233       * this methods picks the most effective way how to convert the source format into
 234       * the target moodle2 format. The method returns a list of converters that should be
 235       * called, in order.
 236       *
 237       * This implementation uses Dijkstra's algorithm to find the shortest way through
 238       * the oriented graph.
 239       *
 240       * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm
 241       * @author David Mudrak <david@moodle.com>
 242       * @param string $format the source backup format, one of backup::FORMAT_xxx
 243       * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name
 244       * @return array ordered list of converter names to call (may be empty if not reachable)
 245       */
 246      protected static function choose_conversion_path($format, array $descriptions) {
 247  
 248          // construct an oriented graph of conversion paths. backup formats are nodes
 249          // and the the converters are edges of the graph.
 250          $paths = array();   // [fromnode][tonode] => converter
 251          foreach ($descriptions as $converter => $description) {
 252              $from   = $description['from'];
 253              $to     = $description['to'];
 254              $cost   = $description['cost'];
 255  
 256              if (is_null($from) or $from === backup::FORMAT_UNKNOWN or
 257                  is_null($to) or $to === backup::FORMAT_UNKNOWN or
 258                  is_null($cost) or $cost <= 0) {
 259                      throw new convert_helper_exception('invalid_converter_description', $converter);
 260              }
 261  
 262              if (!isset($paths[$from][$to])) {
 263                  $paths[$from][$to] = $converter;
 264              } else {
 265                  // if there are two converters available for the same conversion
 266                  // path, choose the one with the lowest cost. if there are more
 267                  // available converters with the same cost, the chosen one is
 268                  // undefined (depends on the order of processing)
 269                  if ($descriptions[$paths[$from][$to]]['cost'] > $cost) {
 270                      $paths[$from][$to] = $converter;
 271                  }
 272              }
 273          }
 274  
 275          if (empty($paths)) {
 276              // no conversion paths available
 277              return array();
 278          }
 279  
 280          // now use Dijkstra's algorithm and find the shortest conversion path
 281  
 282          $dist = array(); // list of nodes and their distances from the source format
 283          $prev = array(); // list of previous nodes in optimal path from the source format
 284          foreach ($paths as $fromnode => $tonodes) {
 285              $dist[$fromnode] = null; // infinitive distance, can't be reached
 286              $prev[$fromnode] = null; // unknown
 287              foreach ($tonodes as $tonode => $converter) {
 288                  $dist[$tonode] = null; // infinitive distance, can't be reached
 289                  $prev[$tonode] = null; // unknown
 290              }
 291          }
 292  
 293          if (!array_key_exists($format, $dist)) {
 294              return array();
 295          } else {
 296              $dist[$format] = 0;
 297          }
 298  
 299          $queue = array_flip(array_keys($dist));
 300          while (!empty($queue)) {
 301              // find the node with the smallest distance from the source in the queue
 302              // in the first iteration, this will find the original format node itself
 303              $closest = null;
 304              foreach ($queue as $node => $undefined) {
 305                  if (is_null($dist[$node])) {
 306                      continue;
 307                  }
 308                  if (is_null($closest) or ($dist[$node] < $dist[$closest])) {
 309                      $closest = $node;
 310                  }
 311              }
 312  
 313              if (is_null($closest) or is_null($dist[$closest])) {
 314                  // all remaining nodes are inaccessible from source
 315                  break;
 316              }
 317  
 318              if ($closest === backup::FORMAT_MOODLE) {
 319                  // bingo we can break now
 320                  break;
 321              }
 322  
 323              unset($queue[$closest]);
 324  
 325              // visit all neighbors and update distances to them eventually
 326  
 327              if (!isset($paths[$closest])) {
 328                  continue;
 329              }
 330              $neighbors = array_keys($paths[$closest]);
 331              // keep just neighbors that are in the queue yet
 332              foreach ($neighbors as $ix => $neighbor) {
 333                  if (!array_key_exists($neighbor, $queue)) {
 334                      unset($neighbors[$ix]);
 335                  }
 336              }
 337  
 338              foreach ($neighbors as $neighbor) {
 339                  // the alternative distance to the neighbor if we went thru the
 340                  // current $closest node
 341                  $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost'];
 342  
 343                  if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) {
 344                      // we found a shorter way to the $neighbor, remember it
 345                      $dist[$neighbor] = $alt;
 346                      $prev[$neighbor] = $closest;
 347                  }
 348              }
 349          }
 350  
 351          if (is_null($dist[backup::FORMAT_MOODLE])) {
 352              // unable to find a conversion path, the target format not reachable
 353              return array();
 354          }
 355  
 356          // reconstruct the optimal path from the source format to the target one
 357          $conversionpath = array();
 358          $target         = backup::FORMAT_MOODLE;
 359          while (isset($prev[$target])) {
 360              array_unshift($conversionpath, $paths[$prev[$target]][$target]);
 361              $target = $prev[$target];
 362          }
 363  
 364          return $conversionpath;
 365      }
 366  }
 367  
 368  /**
 369   * General convert_helper related exception
 370   *
 371   * @author David Mudrak <david@moodle.com>
 372   */
 373  class convert_helper_exception extends moodle_exception {
 374  
 375      /**
 376       * Constructor
 377       *
 378       * @param string $errorcode key for the corresponding error string
 379       * @param object $a extra words and phrases that might be required in the error string
 380       * @param string $debuginfo optional debugging information
 381       */
 382      public function __construct($errorcode, $a = null, $debuginfo = null) {
 383          parent::__construct($errorcode, '', '', $a, $debuginfo);
 384      }
 385  }