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