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.
<?php

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Provides {@link convert_helper} and {@link convert_helper_exception} classes
 *
 * @package    core
 * @subpackage backup-convert
 * @copyright  2011 Mark Nielsen <mark@moodlerooms.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');

/**
 * Provides various functionality via its static methods
 */
abstract class convert_helper {

    /**
     * @param string $entropy
     * @return string random identifier
     */
    public static function generate_id($entropy) {
        return md5(time() . '-' . $entropy . '-' . random_string(20));
    }

    /**
     * Returns the list of all available converters and loads their classes
     *
     * Converter must be installed as a directory in backup/converter/ and its
     * method is_available() must return true to get to the list.
     *
     * @see base_converter::is_available()
     * @return array of strings
     */
    public static function available_converters($restore=true) {
        global $CFG;

        $converters = array();

        $plugins    = get_list_of_plugins('backup/converter');
        foreach ($plugins as $name) {
            $filename = $restore ? 'lib.php' : 'backuplib.php';
            $classuf  = $restore ? '_converter' : '_export_converter';
            $classfile = "{$CFG->dirroot}/backup/converter/{$name}/{$filename}";
            $classname = "{$name}{$classuf}";
            $zip_contents      = "{$name}_zip_contents";
            $store_backup_file = "{$name}_store_backup_file";
            $convert           = "{$name}_backup_convert";

            if (!file_exists($classfile)) {
                throw new convert_helper_exception('converter_classfile_not_found', $classfile);
            }

            require_once($classfile);

            if (!class_exists($classname)) {
                throw new convert_helper_exception('converter_classname_not_found', $classname);
            }

            if (call_user_func($classname .'::is_available')) {
                if (!$restore) {
                    if (!class_exists($zip_contents)) {
                        throw new convert_helper_exception('converter_classname_not_found', $zip_contents);
                    }
                    if (!class_exists($store_backup_file)) {
                        throw new convert_helper_exception('converter_classname_not_found', $store_backup_file);
                    }
                    if (!class_exists($convert)) {
                        throw new convert_helper_exception('converter_classname_not_found', $convert);
                    }
                }

                $converters[] = $name;
            }

        }

        return $converters;
    }

    public static function export_converter_dependencies($converter, $dependency) {
        global $CFG;

        $result = array();
        $filename = 'backuplib.php';
        $classuf  = '_export_converter';
        $classfile = "{$CFG->dirroot}/backup/converter/{$converter}/{$filename}";
        $classname = "{$converter}{$classuf}";

        if (!file_exists($classfile)) {
            throw new convert_helper_exception('converter_classfile_not_found', $classfile);
        }
        require_once($classfile);

        if (!class_exists($classname)) {
            throw new convert_helper_exception('converter_classname_not_found', $classname);
        }

        if (call_user_func($classname .'::is_available')) {
            $deps = call_user_func($classname .'::get_deps');
            if (array_key_exists($dependency, $deps)) {
                $result = $deps[$dependency];
            }
        }

        return $result;
    }

    /**
     * Detects if the given folder contains an unpacked moodle2 backup
     *
     * @param string $tempdir the name of the backup directory
     * @return boolean true if moodle2 format detected, false otherwise
     */
    public static function detect_moodle2_format($tempdir) {
        $dirpath = make_backup_temp_directory($tempdir, false);
        if (!is_dir($dirpath)) {
            throw new convert_helper_exception('tmp_backup_directory_not_found', $dirpath);
        }

        $filepath = $dirpath . '/moodle_backup.xml';
        if (!file_exists($filepath)) {
            return false;
        }

        $handle     = fopen($filepath, 'r');
        $firstchars = fread($handle, 200);
        $status     = fclose($handle);

< if (strpos($firstchars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and < strpos($firstchars,'<moodle_backup>') !== false and
> // Look for expected XML elements (case-insensitive to account for encoding attribute). > if (stripos($firstchars, '<?xml version="1.0" encoding="UTF-8"?>') !== false && > strpos($firstchars, '<moodle_backup>') !== false &&
strpos($firstchars,'<information>') !== false) {
>
return true; } return false; } /** * Converts the given directory with the backup into moodle2 format * * @param string $tempdir The directory to convert * @param string $format The current format, if already detected * @param base_logger|null if the conversion should be logged, use this logger * @throws convert_helper_exception * @return bool false if unable to find the conversion path, true otherwise */ public static function to_moodle2_format($tempdir, $format = null, $logger = null) { if (is_null($format)) { $format = backup_general_helper::detect_backup_format($tempdir); } // get the supported conversion paths from all available converters $converters = self::available_converters(); $descriptions = array(); foreach ($converters as $name) { $classname = "{$name}_converter"; if (!class_exists($classname)) { throw new convert_helper_exception('class_not_loaded', $classname); } if ($logger instanceof base_logger) { backup_helper::log('available converter', backup::LOG_DEBUG, $classname, 1, false, $logger); } $descriptions[$name] = call_user_func($classname .'::description'); } // choose the best conversion path for the given format $path = self::choose_conversion_path($format, $descriptions); if (empty($path)) { if ($logger instanceof base_logger) { backup_helper::log('unable to find the conversion path', backup::LOG_ERROR, null, 0, false, $logger); } return false; } if ($logger instanceof base_logger) { backup_helper::log('conversion path established', backup::LOG_INFO, implode(' => ', array_merge($path, array('moodle2'))), 0, false, $logger); } foreach ($path as $name) { if ($logger instanceof base_logger) { backup_helper::log('running converter', backup::LOG_INFO, $name, 0, false, $logger); } $converter = convert_factory::get_converter($name, $tempdir, $logger); $converter->convert(); } // make sure we ended with moodle2 format if (!self::detect_moodle2_format($tempdir)) { throw new convert_helper_exception('conversion_failed'); } return true; } /** * Inserts an inforef into the conversion temp table */ public static function set_inforef($contextid) { global $DB; } public static function get_inforef($contextid) { } /// end of public API ////////////////////////////////////////////////////// /** * Choose the best conversion path for the given format * * Given the source format and the list of available converters and their properties, * this methods picks the most effective way how to convert the source format into * the target moodle2 format. The method returns a list of converters that should be * called, in order. * * This implementation uses Dijkstra's algorithm to find the shortest way through * the oriented graph. * * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm * @author David Mudrak <david@moodle.com> * @param string $format the source backup format, one of backup::FORMAT_xxx * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name * @return array ordered list of converter names to call (may be empty if not reachable) */ protected static function choose_conversion_path($format, array $descriptions) { // construct an oriented graph of conversion paths. backup formats are nodes // and the the converters are edges of the graph. $paths = array(); // [fromnode][tonode] => converter foreach ($descriptions as $converter => $description) { $from = $description['from']; $to = $description['to']; $cost = $description['cost']; if (is_null($from) or $from === backup::FORMAT_UNKNOWN or is_null($to) or $to === backup::FORMAT_UNKNOWN or is_null($cost) or $cost <= 0) { throw new convert_helper_exception('invalid_converter_description', $converter); } if (!isset($paths[$from][$to])) { $paths[$from][$to] = $converter; } else { // if there are two converters available for the same conversion // path, choose the one with the lowest cost. if there are more // available converters with the same cost, the chosen one is // undefined (depends on the order of processing) if ($descriptions[$paths[$from][$to]]['cost'] > $cost) { $paths[$from][$to] = $converter; } } } if (empty($paths)) { // no conversion paths available return array(); } // now use Dijkstra's algorithm and find the shortest conversion path $dist = array(); // list of nodes and their distances from the source format $prev = array(); // list of previous nodes in optimal path from the source format foreach ($paths as $fromnode => $tonodes) { $dist[$fromnode] = null; // infinitive distance, can't be reached $prev[$fromnode] = null; // unknown foreach ($tonodes as $tonode => $converter) { $dist[$tonode] = null; // infinitive distance, can't be reached $prev[$tonode] = null; // unknown } } if (!array_key_exists($format, $dist)) { return array(); } else { $dist[$format] = 0; } $queue = array_flip(array_keys($dist)); while (!empty($queue)) { // find the node with the smallest distance from the source in the queue // in the first iteration, this will find the original format node itself $closest = null; foreach ($queue as $node => $undefined) { if (is_null($dist[$node])) { continue; } if (is_null($closest) or ($dist[$node] < $dist[$closest])) { $closest = $node; } } if (is_null($closest) or is_null($dist[$closest])) { // all remaining nodes are inaccessible from source break; } if ($closest === backup::FORMAT_MOODLE) { // bingo we can break now break; } unset($queue[$closest]); // visit all neighbors and update distances to them eventually if (!isset($paths[$closest])) { continue; } $neighbors = array_keys($paths[$closest]); // keep just neighbors that are in the queue yet foreach ($neighbors as $ix => $neighbor) { if (!array_key_exists($neighbor, $queue)) { unset($neighbors[$ix]); } } foreach ($neighbors as $neighbor) { // the alternative distance to the neighbor if we went thru the // current $closest node $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost']; if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) { // we found a shorter way to the $neighbor, remember it $dist[$neighbor] = $alt; $prev[$neighbor] = $closest; } } } if (is_null($dist[backup::FORMAT_MOODLE])) { // unable to find a conversion path, the target format not reachable return array(); } // reconstruct the optimal path from the source format to the target one $conversionpath = array(); $target = backup::FORMAT_MOODLE; while (isset($prev[$target])) { array_unshift($conversionpath, $paths[$prev[$target]][$target]); $target = $prev[$target]; } return $conversionpath; } } /** * General convert_helper related exception * * @author David Mudrak <david@moodle.com> */ class convert_helper_exception extends moodle_exception { /** * Constructor * * @param string $errorcode key for the corresponding error string * @param object $a extra words and phrases that might be required in the error string * @param string $debuginfo optional debugging information */ public function __construct($errorcode, $a = null, $debuginfo = null) { parent::__construct($errorcode, '', '', $a, $debuginfo); } }