MDL-33079 Enable IMS-CC export (backup converter)
[moodle.git] / backup / util / helper / convert_helper.class.php
CommitLineData
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
27defined('MOODLE_INTERNAL') || die();
28
1e2c7351
DM
29require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php');
30
17252e2d 31/**
0164592b 32 * Provides various functionality via its static methods
17252e2d
MN
33 */
34abstract 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 */
376class 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}