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