<?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);
}
}