diff --git a/backup/backup.class.php b/backup/backup.class.php
index 31ae5b13715..936ff8a4e1c 100644
--- a/backup/backup.class.php
+++ b/backup/backup.class.php
@@ -40,6 +40,8 @@ abstract class backup implements checksumable {
// Backup format
const FORMAT_MOODLE = 'moodle2';
+ const FORMAT_MOODLE1 = 'moodle1';
+ const FORMAT_IMSCC = 'imscc';
const FORMAT_UNKNOWN = 'unknown';
// Interactive
diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php
index 93806276314..c2078172d98 100644
--- a/backup/controller/restore_controller.class.php
+++ b/backup/controller/restore_controller.class.php
@@ -377,17 +377,17 @@ class restore_controller extends backup implements loggable {
}
/**
- * convert from current format to backup::MOODLE format
+ * Converts from current format to backup::MOODLE format
*/
public function convert() {
global $CFG;
+ require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');
if ($this->status != backup::STATUS_REQUIRE_CONV) {
throw new restore_controller_exception('cannot_convert_not_required_status');
}
- require_once($CFG->dirroot.'/backup/util/includes/convert_includes.php');
- // Run conversion until we have the proper format
+ // Run conversion to the proper format
convert_helper::to_moodle2_format($this->get_tempdir(), $this->format);
// If no exceptions were thrown, then we are in the proper format
diff --git a/backup/converter/moodle1/converter.class.php b/backup/converter/moodle1/converter.class.php
index 28a52271fbd..4395c103613 100644
--- a/backup/converter/moodle1/converter.class.php
+++ b/backup/converter/moodle1/converter.class.php
@@ -16,8 +16,6 @@
// along with Moodle. If not, see .
/**
- * All of the task and step classes specific to moodle1 conversion
- *
* @package core
* @subpackage backup-convert
* @copyright 2011 Mark Nielsen
@@ -26,49 +24,47 @@
defined('MOODLE_INTERNAL') || die();
-require_once($CFG->dirroot.'/backup/converter/moodle1/taskslib.php');
-require_once($CFG->dirroot.'/backup/converter/moodle1/stepslib.php');
+require_once($CFG->dirroot . '/backup/converter/moodle1/taskslib.php');
+require_once($CFG->dirroot . '/backup/converter/moodle1/stepslib.php');
/**
- * This will be the Moodle 1 to Moodle 2 Converter
+ * Converter of Moodle 1.9 backup into Moodle 2.x format
*/
class moodle1_converter extends plan_converter {
- /**
- * The current module being processed
- *
- * @var string
- */
+
+ /** @var string the current module being processed */
protected $currentmod = '';
- /**
- * The current block being processed
- *
- * @var string
- */
+ /** @var string the current block being processed */
protected $currentblock = '';
/**
- * @return boolean
+ * Detects the Moodle 1.9 format of the backup directory
+ *
+ * @param string $tempdir the name of the backup directory
+ * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise
*/
- public function can_convert() {
- // Then look for MOODLE1 (moodle1) format
- $filepath = $this->get_tempdir() . '/moodle.xml';
- if (file_exists($filepath)) { // Looks promising, lets load some information
- $handle = fopen($filepath, "r");
- $first_chars = fread($handle,200);
+ public static function detect_format($tempdir) {
+ global $CFG;
+
+ $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle.xml';
+ if (file_exists($filepath)) {
+ // looks promising, lets load some information
+ $handle = fopen($filepath, 'r');
+ $first_chars = fread($handle, 200);
fclose($handle);
- // Check if it has the required strings
- if (strpos($first_chars,'') !== false &&
- strpos($first_chars,'') !== false &&
+ // check if it has the required strings
+ if (strpos($first_chars,'') !== false and
+ strpos($first_chars,'') !== false and
strpos($first_chars,'') !== false) {
- return true;
+ return backup::FORMAT_MOODLE1;
}
}
- return false;
- }
+ return null;
+ }
/**
* Path transformation for modules and blocks. Here we
@@ -116,7 +112,7 @@ class moodle1_converter extends plan_converter {
public function build_plan() {
$this->xmlparser = new progressive_parser();
- $this->xmlparser->set_file($this->get_tempdir() . '/moodle.xml');
+ $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
$this->xmlprocessor = new convert_structure_parser_processor($this); // @todo Probably move this
$this->xmlparser->set_processor($this->xmlprocessor);
diff --git a/backup/converter/moodle1/simpletest/testconverter.php b/backup/converter/moodle1/simpletest/testconverter.php
index 20969ea4006..7c07530d83f 100644
--- a/backup/converter/moodle1/simpletest/testconverter.php
+++ b/backup/converter/moodle1/simpletest/testconverter.php
@@ -26,15 +26,13 @@
defined('MOODLE_INTERNAL') || die();
-require_once($CFG->dirroot.'/backup/util/includes/convert_includes.php');
+require_once($CFG->dirroot . '/backup/converter/moodle1/converter.class.php');
class moodle1_converter_test extends UnitTestCase {
public static $includecoverage = array();
- /**
- * @var string
- */
+ /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */
protected $tempdir;
public function setUp() {
@@ -43,7 +41,7 @@ class moodle1_converter_test extends UnitTestCase {
$this->tempdir = convert_helper::generate_id('simpletest');
check_dir_exists("$CFG->dataroot/temp/backup/$this->tempdir");
copy(
- $CFG->dirroot.'/backup/converter/moodle1/simpletest/files/moodle.xml',
+ "$CFG->dirroot/backup/converter/moodle1/simpletest/files/moodle.xml",
"$CFG->dataroot/temp/backup/$this->tempdir/moodle.xml"
);
}
@@ -55,10 +53,9 @@ class moodle1_converter_test extends UnitTestCase {
}
}
- public function test_can_convert() {
- $converter = convert_factory::converter('moodle1', $this->tempdir);
- $this->assertIsA($converter, 'moodle1_converter');
- $this->assertTrue($converter->can_convert());
+ public function test_detect_format() {
+ $detected = moodle1_converter::detect_format($this->tempdir);
+ $this->assertEqual(backup::FORMAT_MOODLE1, $detected);
}
public function test_convert() {
diff --git a/backup/converter/moodle1/taskslib.php b/backup/converter/moodle1/taskslib.php
index bd301ca5c36..a6dc80d9050 100644
--- a/backup/converter/moodle1/taskslib.php
+++ b/backup/converter/moodle1/taskslib.php
@@ -24,6 +24,8 @@
defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');
+
class moodle1_root_task extends convert_task {
/**
* Function responsible for building the steps of any task
diff --git a/backup/util/converter/base_converter.class.php b/backup/util/converter/base_converter.class.php
index 3ed93608617..364dcb2308c 100644
--- a/backup/util/converter/base_converter.class.php
+++ b/backup/util/converter/base_converter.class.php
@@ -25,95 +25,195 @@
defined('MOODLE_INTERNAL') || die();
/**
- * Base Abstract Converter
+ * Base abstract converter
*
* @throws backup_exception|Exception|null
*/
abstract class base_converter {
+ /** @var string unique identifier of this converter instance */
protected $id;
+ /** @var string the name of the directory containing the unpacked backup being converted */
protected $tempdir;
- protected $convertdir;
+ /** @var string the name of the directory where the backup is converted to */
+ protected $workdir;
- // do we want absolute path instead of tempdir?
- // Do we need to create a new tempdir to convert into? EG: target...
+ /**
+ * Constructor
+ *
+ * @param string $tempdir the relative path to the directory containing the unpacked backup to convert
+ */
public function __construct($tempdir) {
- $this->tempdir = $tempdir;
- $this->convertdir = $this->tempdir.'_'.$this->get_name();
- $this->id = convert_helper::generate_id($this->convertdir);
+
+ $this->tempdir = $tempdir;
+ $this->id = convert_helper::generate_id($this->workdir);
+ $this->workdir = $tempdir . '_' . $this->get_name() . '_' . $this->id;
$this->init();
}
- public function init() {
- }
-
+ /**
+ * Get instance identifier
+ *
+ * @return string the unique identifier of this converter instance
+ */
public function get_id() {
return $this->id;
}
+ /**
+ * Get converter name
+ *
+ * @return string the system name of the converter
+ */
public function get_name() {
return array_shift(explode('_', get_class($this)));
}
- public function get_convertdir() {
- global $CFG;
-
- return "$CFG->dataroot/temp/backup/$this->convertdir";
- }
-
- public function get_tempdir() {
- global $CFG;
-
- return "$CFG->dataroot/temp/backup/$this->tempdir";
- }
-
- public function delete_convertdir() {
- fulldelete($this->get_convertdir());
- }
-
- public function create_convertdir() {
- $this->delete_convertdir();
- if (!check_dir_exists($this->get_convertdir())) {
- throw new backup_exception('failedtomakeconvertdir'); // @todo Define this string
- }
- }
-
- public function replace_tempdir() {
- fulldelete($this->get_tempdir());
-
- if (!rename($this->get_convertdir(), $this->get_tempdir())) {
- throw new backup_exception('failedmoveconvertedintoplace'); // @todo Define this string
- }
- }
-
/**
- * @abstract
- * @return boolean
+ * Converts the backup directory
*/
- abstract public function can_convert();
-
- // Kicks things off
public function convert() {
- $e = NULL;
+ $e = null;
+ // try to execute the converter
try {
- $this->create_convertdir();
+ $this->create_workdir();
$this->execute();
$this->replace_tempdir();
} catch (Exception $e) {
}
- // Do cleanup...
+
+ // clean-up stuff if needed
$this->destroy();
+ // eventually re-throw the execution exception
if ($e instanceof Exception) {
throw $e;
}
}
- abstract public function execute();
+ /// public static methods //////////////////////////////////////////////////
- public function destroy() {
- $this->delete_convertdir();
+ /**
+ * Makes sure that this converter is available at this site
+ *
+ * This is intended for eventual PHP extensions check, environment check etc.
+ * All checks that do not depend on actual backup data should be done here.
+ *
+ * @return boolean true if this converter should be considered as available
+ */
+ public static function is_available() {
+ return true;
+ }
+
+ /**
+ * Detects the format of the backup directory
+ *
+ * Moodle 2.x format is being detected by the core itself. The converters are
+ * therefore supposed to detect the source format. Eventually, if the target
+ * format os not {@link backup::FORMAT_MOODLE} then they should be able to
+ * detect both source and target formats.
+ *
+ * @param string $tempdir the name of the backup directory
+ * @return null|string null if not recognized, backup::FORMAT_xxx otherwise
+ */
+ public static function detect_format($tempdir) {
+ return null;
+ }
+
+ /**
+ * Returns the basic information about the converter
+ *
+ * The returned array must contain the following keys:
+ * 'from' - the supported source format, eg. backup::FORMAT_MOODLE1
+ * 'to' - the supported target format, eg. backup::FORMAT_MOODLE
+ * 'cost' - the cost of the conversion, non-negative non-zero integer
+ */
+ public static function description() {
+
+ return array(
+ 'from' => null,
+ 'to' => null,
+ 'cost' => null,
+ );
+ }
+
+ /// end of public API //////////////////////////////////////////////////////
+
+ /**
+ * Initialize the instance if needed, called by the constructor
+ */
+ protected function init() {
+ }
+
+ /**
+ * Converts the contents of the tempdir into the target format in the workdir
+ */
+ protected abstract function execute();
+
+ /**
+ * @return string the full path to the working directory
+ */
+ protected function get_workdir_path() {
+ global $CFG;
+
+ return "$CFG->dataroot/temp/backup/$this->workdir";
+ }
+
+ /**
+ * @return string the full path to the directory with the source backup
+ */
+ protected function get_tempdir_path() {
+ global $CFG;
+
+ return "$CFG->dataroot/temp/backup/$this->tempdir";
+ }
+
+ /**
+ * Prepares a new empty working directory
+ */
+ protected function create_workdir() {
+
+ fulldelete($this->get_workdir_path());
+ if (!check_dir_exists($this->get_workdir_path())) {
+ throw new backup_exception('failedtocreateworkdir');
+ }
+ }
+
+ /**
+ * Replaces the source backup directory with the converted version
+ *
+ * If $CFG->keeptempdirectoriesonbackup is defined, the original source
+ * source backup directory is kept for debugging purposes.
+ */
+ protected function replace_tempdir() {
+ global $CFG;
+
+ if (empty($CFG->keeptempdirectoriesonbackup)) {
+ fulldelete($this->get_tempdir_path);
+ } else {
+ if (!rename($this->get_tempdir_path, $this->get_tempdir_path . '_' . $this->get_name() . '_' . $this->id . '_source')) {
+ throw new backup_exception('failedrenamesourcetempdir');
+ }
+ }
+
+ if (!rename($this->get_workdir_path(), $this->get_tempdir_path())) {
+ throw new backup_exception('failedmoveconvertedintoplace');
+ }
+ }
+
+ /**
+ * Cleans up stuff after the execution
+ *
+ * Note that we do not know if the execution was successful or not.
+ * An exception might have been thrown.
+ */
+ protected function destroy() {
+ global $CFG;
+
+ if (empty($CFG->keeptempdirectoriesonbackup)) {
+ fulldelete($this->get_workdir_path);
+ }
}
}
diff --git a/backup/util/converter/plan_converter.class.php b/backup/util/converter/plan_converter.class.php
index 00742e6321b..3301cc690b7 100644
--- a/backup/util/converter/plan_converter.class.php
+++ b/backup/util/converter/plan_converter.class.php
@@ -25,40 +25,31 @@
defined('MOODLE_INTERNAL') || die();
/**
- * Plan based abstract converter
+ * Base class for all converters using plan/tasks/steps pattern
*
- * All backup converters that use {@link convert_plan} should extend this class.
+ * All converters that use {@link convert_plan} must extend this class.
*/
-abstract class plan_converter extends base_converter {
+abstract class planned_converter extends base_converter {
- /**
- * @var convert_plan
- */
+ /** @var convert_plan */
protected $plan;
-
- /**
- * @var progressive_parser
- */
+ /** @var progressive_parser */
protected $xmlparser;
-
- /**
- * @var convert_structure_parser_processor
- */
+ /** @var convert_structure_parser_processor */
protected $xmlprocessor;
+ /** @var array path elements to process */
+ protected $pathelements = array();
+ /** @todo needed? redo? path currently locking processing of children */
+ protected $pathlock;
/**
- * @var array
+ * Instructs the dispatcher to ignore all children below path processor returning it
*/
- protected $pathelements = array(); // Array of pathelements to process
-
- // @todo needed? redo?
- protected $pathlock; // Path currently locking processing of children
-
- // @todo IDK what this is really...
- const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore
- // all children below path processor returning it
+ const SKIP_ALL_CHILDREN = -991399;
/**
+ * Return the plan instance, instatinate it if it does not exist yet
+ *
* @return convert_plan
*/
public function get_plan() {
diff --git a/backup/util/factories/convert_factory.class.php b/backup/util/factories/convert_factory.class.php
index 414674343cc..6dd6bbab19c 100644
--- a/backup/util/factories/convert_factory.class.php
+++ b/backup/util/factories/convert_factory.class.php
@@ -55,23 +55,6 @@ abstract class convert_factory {
}
return new $classname($tempdir);
}
-
- /**
- * Instantiates a list of all installed converters operating on a given directory
- *
- * @param string $tempdir The temp directory to operate on
- * @return array
- */
- public static function converters($tempdir) {
- global $CFG;
-
- $converters = array();
- $plugins = get_list_of_plugins('backup/converter');
- foreach ($plugins as $name) {
- $converters[$name] = self::converter($name, $tempdir);
- }
- return $converters;
- }
/**
* Runs through all plugins of a specific type and instantiates their task class
diff --git a/backup/util/helper/backup_general_helper.class.php b/backup/util/helper/backup_general_helper.class.php
index 00c18fcbfac..4fd2d082e6b 100644
--- a/backup/util/helper/backup_general_helper.class.php
+++ b/backup/util/helper/backup_general_helper.class.php
@@ -228,42 +228,34 @@ abstract class backup_general_helper extends backup_helper {
}
/**
- * Given one temp/backup/xxx dir, detect its format
+ * Detects the format of the given unpacked backup directory
*
- * TODO: Move harcoded detection here to delegated classes under backup/format (moodle1, imscc..)
- * conversion code will be there too.
+ * @param string $tempdir the name of the backup directory
+ * @return string one of backup::FORMAT_xxx constants
*/
public static function detect_backup_format($tempdir) {
global $CFG;
- $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle_backup.xml';
+ require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');
- // Does tempdir exist and is dir
- if (!is_dir(dirname($filepath))) {
- throw new backup_helper_exception('tmp_backup_directory_not_found', dirname($filepath));
+ if (convert_helper::detect_moodle2_format($tempdir)) {
+ return backup::FORMAT_MOODLE;
}
- // First look for MOODLE (moodle2) format
- if (file_exists($filepath)) { // Looks promising, lets load some information
- $handle = fopen ($filepath, "r");
- $first_chars = fread($handle,200);
- $status = fclose ($handle);
- // Check if it has the required strings
- if (strpos($first_chars,'') !== false &&
- strpos($first_chars,'') !== false &&
- strpos($first_chars,'') !== false) {
- return backup::FORMAT_MOODLE;
+ // see if a converter can identify the format
+ $converters = convert_factory::available_converters();
+ foreach ($converters as $name) {
+ $classname = "{$name}_converter";
+ if (!class_exists($classname)) {
+ throw new coding_exception("available_converters() is supposed to load
+ converter classes but class $classname not found");
+ }
+
+ $detected = call_user_func($classname .'::detect_format', $tempdir);
+ if (!empty($detected)) {
+ return $detected;
}
}
- // See if a converter can identify the format as its own
- $converters = convert_factory::converters($tempdir);
- foreach ($converters as $name => $converter) {
- if ($converter->can_convert()) {
- return $name;
- }
- }
-
- // Arrived here, unknown format
return backup::FORMAT_UNKNOWN;
}
}
diff --git a/backup/util/helper/convert_helper.class.php b/backup/util/helper/convert_helper.class.php
index 2ac07850e0b..86184618cdf 100644
--- a/backup/util/helper/convert_helper.class.php
+++ b/backup/util/helper/convert_helper.class.php
@@ -16,6 +16,8 @@
// along with Moodle. If not, see .
/**
+ * Provides {@link convert_helper} class
+ *
* @package core
* @subpackage backup-convert
* @copyright 2011 Mark Nielsen
@@ -25,37 +27,129 @@
defined('MOODLE_INTERNAL') || die();
/**
- * General convert helper
+ * 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));
}
/**
- * @static
+ * 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() {
+ global $CFG;
+
+ $converters = array();
+ $plugins = get_list_of_plugins('backup/converter');
+ foreach ($plugins as $name) {
+ $classfile = "$CFG->dirroot/backup/converter/$name/converter.class.php";
+ $classname = "{$name}_converter";
+
+ if (!file_exists($classfile)) {
+ throw new coding_exception("Converter factory error: class file not found $classfile");
+ }
+ require_once($classfile);
+
+ if (!class_exists($classname)) {
+ throw new coding_exception("Converter factory error: class not found $classname");
+ }
+
+ if (call_user_func($classname .'::is_available')) {
+ $converters[] = $name;
+ }
+ }
+
+ return $converters;
+ }
+
+ /**
+ * 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) {
+ global $CFG;
+
+ $dirpath = $CFG->dataroot . '/temp/backup/' . $tempdir;
+ $filepath = $dirpath . '/moodle_backup.xml';
+
+ if (!is_dir($dirpath)) {
+ throw new backup_helper_exception('tmp_backup_directory_not_found', $dirpath);
+ }
+
+ if (!file_exists($filepath)) {
+ return false;
+ }
+
+ $handle = fopen($filepath, 'r');
+ $firstchars = fread($handle, 200);
+ $status = fclose($handle);
+
+ if (strpos($firstchars,'') !== false and
+ strpos($firstchars,'') !== false and
+ strpos($firstchars,'') !== false) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Converts the given directory with the backup into moodle2 format
+ *
* @throws coding_exception|restore_controller_exception
* @param string $tempdir The directory to convert
* @param string $format The current format, if already detected
* @return void
*/
- public static function to_moodle2_format($tempdir, $format = NULL) {
+ public static function to_moodle2_format($tempdir, $format = null) {
+
if (is_null($format)) {
$format = backup_general_helper::detect_backup_format($tempdir);
}
- while (!in_array($format, array(backup::FORMAT_MOODLE, backup::FORMAT_UNKNOWN))) {
- $converter = convert_factory::converter($format, $tempdir);
- if (!$converter->can_convert()) {
- throw new coding_exception('Converter detection failed, the loaded converter cannot convert this format');
+ // get the supported conversion paths from all available converters
+ $converters = convert_factory::available_converters();
+ $descriptions = array();
+ foreach ($converters as $name) {
+ $classname = "{$name}_converter";
+ if (!class_exists($classname)) {
+ throw new coding_exception("available_converters() is supposed to load
+ converter classes but class $classname not found");
}
- $converter->convert();
-
- // Re-detect format
- $format = backup_general_helper::detect_backup_format($tempdir);
+ $descriptions[$name] = call_user_func($classname .'::description');
}
- if ($format == backup::FORMAT_UNKNOWN) {
- throw new restore_controller_exception('cannot_convert_from_unknown_format'); // @todo Change exception class
+
+ // choose the best conversion path for the given format
+ $path = self::choose_conversion_path($format, $descriptions);
+
+ if (empty($path)) {
+ // unable to convert
+ // todo throwing exception is not a good way to control the flow here
+ throw new coding_exception('Unable to find conversion path');
+ }
+
+ foreach ($path as $name) {
+ $converter = convert_factory::converter($name, $tempdir);
+ $converter->convert();
+ }
+
+ // make sure we ended with moodle2 format
+ if (!self::detect_moodle2_format($tempdir)) {
+ throw new coding_exception('Conversion failed');
}
}
@@ -64,8 +158,6 @@ abstract class convert_helper {
*/
public static function set_inforef($contextid) {
global $DB;
-
-
}
public static function get_inforef($contextid) {
@@ -122,4 +214,143 @@ abstract class convert_helper {
throw new Exception(sprintf("Could not insert context record into temp table: %s", $msg));
}
}
+
+ /// 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
+ * @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 coding_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;
+ }
}
diff --git a/backup/util/helper/simpletest/testconverthelper.php b/backup/util/helper/simpletest/testconverthelper.php
new file mode 100644
index 00000000000..90b83d57a23
--- /dev/null
+++ b/backup/util/helper/simpletest/testconverthelper.php
@@ -0,0 +1,146 @@
+.
+
+/**
+ * Unit tests for backup/util/helper/convert_helper.class.php
+ *
+ * @package core
+ * @subpackage backup-convert
+ * @copyright 2011 David Mudrak
+ * @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 access to the protected methods we need to test
+ */
+class testable_convert_helper extends convert_helper {
+
+ public static function choose_conversion_path($format, array $descriptions) {
+ return parent::choose_conversion_path($format, $descriptions);
+ }
+}
+
+/**
+ * Defines the test methods
+ */
+class convert_helper_test extends UnitTestCase {
+
+ public static $includecoverage = array();
+
+ public function test_choose_conversion_path() {
+
+ // no converters available
+ $descriptions = array();
+ $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+ $this->assertEqual($path, array());
+
+ // missing source and/or targets
+ $descriptions = array(
+ // some custom converter
+ 'exporter' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => 'some_custom_format',
+ 'cost' => 10,
+ ),
+ // another custom converter
+ 'converter' => array(
+ 'from' => 'yet_another_crazy_custom_format',
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 10,
+ ),
+ );
+ $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+ $this->assertEqual($path, array());
+
+ $path = testable_convert_helper::choose_conversion_path('some_other_custom_format', $descriptions);
+ $this->assertEqual($path, array());
+
+ // single step conversion
+ $path = testable_convert_helper::choose_conversion_path('yet_another_crazy_custom_format', $descriptions);
+ $this->assertEqual($path, array('converter'));
+
+ // no conversion needed - this is supposed to be detected by the caller
+ $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE, $descriptions);
+ $this->assertEqual($path, array());
+
+ // two alternatives
+ $descriptions = array(
+ // standard moodle 1.9 -> 2.x converter
+ 'moodle1' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 10,
+ ),
+ // alternative moodle 1.9 -> 2.x converter
+ 'alternative' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 8,
+ )
+ );
+ $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+ $this->assertEqual($path, array('alternative'));
+
+ // complex case
+ $descriptions = array(
+ // standard moodle 1.9 -> 2.x converter
+ 'moodle1' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 10,
+ ),
+ // alternative moodle 1.9 -> 2.x converter
+ 'alternative' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 8,
+ ),
+ // custom converter from 1.9 -> custom 'CFv1' format
+ 'cc1' => array(
+ 'from' => backup::FORMAT_MOODLE1,
+ 'to' => 'CFv1',
+ 'cost' => 2,
+ ),
+ // custom converter from custom 'CFv1' format -> moodle 2.0 format
+ 'cc2' => array(
+ 'from' => 'CFv1',
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 5,
+ ),
+ // custom converter from CFv1 -> CFv2 format
+ 'cc3' => array(
+ 'from' => 'CFv1',
+ 'to' => 'CFv2',
+ 'cost' => 2,
+ ),
+ // custom converter from CFv2 -> moodle 2.0 format
+ 'cc4' => array(
+ 'from' => 'CFv2',
+ 'to' => backup::FORMAT_MOODLE,
+ 'cost' => 2,
+ ),
+ );
+
+ // ask the helper to find the most effective way
+ $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions);
+ $this->assertEqual($path, array('cc1', 'cc3', 'cc4'));
+ }
+}
diff --git a/backup/util/plan/convert_plan.class.php b/backup/util/plan/convert_plan.class.php
index 835423dbebb..633bbde8cbd 100644
--- a/backup/util/plan/convert_plan.class.php
+++ b/backup/util/plan/convert_plan.class.php
@@ -48,7 +48,7 @@ class convert_plan extends base_plan implements loggable {
}
public function get_basepath() {
- return $this->converter->get_convertdir();
+ return $this->converter->get_workdir_path();
}
/**
diff --git a/mod/forum/backup/moodle1/convert_forum_stepslib.php b/mod/forum/backup/moodle1/convert_forum_stepslib.php
index 218c6f992d1..acc2acaba15 100644
--- a/mod/forum/backup/moodle1/convert_forum_stepslib.php
+++ b/mod/forum/backup/moodle1/convert_forum_stepslib.php
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
* Convert forum
*/
class moodle1_forum_activity_structure_step extends convert_structure_step {
+
/**
* Function that will return the structure to be processed by this convert_step.
* Must return one array of @convert_path_element elements