MDL-27376 MDL-27377 Backup converters API refactored
[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/**
0164592b
DM
19 * Provides {@link convert_helper} class
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
17252e2d 29/**
0164592b 30 * Provides various functionality via its static methods
17252e2d
MN
31 */
32abstract class convert_helper {
0164592b
DM
33
34 /**
35 * @param string $entropy
36 * @return string random identifier
37 */
17252e2d
MN
38 public static function generate_id($entropy) {
39 return md5(time() . '-' . $entropy . '-' . random_string(20));
40 }
c5c8b350
MN
41
42 /**
0164592b
DM
43 * Returns the list of all available converters and loads their classes
44 *
45 * Converter must be installed as a directory in backup/converter/ and its
46 * method is_available() must return true to get to the list.
47 *
48 * @see base_converter::is_available()
49 * @return array of strings
50 */
51 public static function available_converters() {
52 global $CFG;
53
54 $converters = array();
55 $plugins = get_list_of_plugins('backup/converter');
56 foreach ($plugins as $name) {
57 $classfile = "$CFG->dirroot/backup/converter/$name/converter.class.php";
58 $classname = "{$name}_converter";
59
60 if (!file_exists($classfile)) {
61 throw new coding_exception("Converter factory error: class file not found $classfile");
62 }
63 require_once($classfile);
64
65 if (!class_exists($classname)) {
66 throw new coding_exception("Converter factory error: class not found $classname");
67 }
68
69 if (call_user_func($classname .'::is_available')) {
70 $converters[] = $name;
71 }
72 }
73
74 return $converters;
75 }
76
77 /**
78 * Detects if the given folder contains an unpacked moodle2 backup
79 *
80 * @param string $tempdir the name of the backup directory
81 * @return boolean true if moodle2 format detected, false otherwise
82 */
83 public static function detect_moodle2_format($tempdir) {
84 global $CFG;
85
86 $dirpath = $CFG->dataroot . '/temp/backup/' . $tempdir;
87 $filepath = $dirpath . '/moodle_backup.xml';
88
89 if (!is_dir($dirpath)) {
90 throw new backup_helper_exception('tmp_backup_directory_not_found', $dirpath);
91 }
92
93 if (!file_exists($filepath)) {
94 return false;
95 }
96
97 $handle = fopen($filepath, 'r');
98 $firstchars = fread($handle, 200);
99 $status = fclose($handle);
100
101 if (strpos($firstchars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
102 strpos($firstchars,'<moodle_backup>') !== false and
103 strpos($firstchars,'<information>') !== false) {
104 return true;
105 }
106
107 return false;
108 }
109
110 /**
111 * Converts the given directory with the backup into moodle2 format
112 *
c5c8b350
MN
113 * @throws coding_exception|restore_controller_exception
114 * @param string $tempdir The directory to convert
115 * @param string $format The current format, if already detected
116 * @return void
117 */
0164592b
DM
118 public static function to_moodle2_format($tempdir, $format = null) {
119
c5c8b350
MN
120 if (is_null($format)) {
121 $format = backup_general_helper::detect_backup_format($tempdir);
122 }
c5c8b350 123
0164592b
DM
124 // get the supported conversion paths from all available converters
125 $converters = convert_factory::available_converters();
126 $descriptions = array();
127 foreach ($converters as $name) {
128 $classname = "{$name}_converter";
129 if (!class_exists($classname)) {
130 throw new coding_exception("available_converters() is supposed to load
131 converter classes but class $classname not found");
c5c8b350 132 }
0164592b
DM
133 $descriptions[$name] = call_user_func($classname .'::description');
134 }
c5c8b350 135
0164592b
DM
136 // choose the best conversion path for the given format
137 $path = self::choose_conversion_path($format, $descriptions);
138
139 if (empty($path)) {
140 // unable to convert
141 // todo throwing exception is not a good way to control the flow here
142 throw new coding_exception('Unable to find conversion path');
c5c8b350 143 }
0164592b
DM
144
145 foreach ($path as $name) {
146 $converter = convert_factory::converter($name, $tempdir);
147 $converter->convert();
148 }
149
150 // make sure we ended with moodle2 format
151 if (!self::detect_moodle2_format($tempdir)) {
152 throw new coding_exception('Conversion failed');
c5c8b350
MN
153 }
154 }
142ec51f
PC
155
156 /**
157 * Inserts an inforef into the conversion temp table
158 */
159 public static function set_inforef($contextid) {
160 global $DB;
142ec51f
PC
161 }
162
163 public static function get_inforef($contextid) {
164 }
165
166 /**
167 * Converts a plain old php object (popo?) into a string...
168 * Useful for debuging failed db inserts, or anything like that
169 */
170 public static function obj_to_readable($obj) {
171 $mapper = function($field, $value) { return "$field=$value"; };
172 $fields = get_object_vars($obj);
173
174 return implode(", ", array_map($mapper, array_keys($fields), array_values($fields)));
175 }
176
6c0235b3
MN
177 /**
178 * Generate an artificial context ID
179 *
180 * @static
181 * @throws Exception
182 * @param int $instance The moodle component instance ID, same value used for get_context_instance()
183 * @param string $component The moodle component, like block_html, mod_quiz, etc
184 * @param string $converterid The converter ID
185 * @return int
62c5daf1
MN
186 * @todo Add caching?
187 * @todo Can we make the lookup faster? Not taking advantage of indexes
6c0235b3 188 */
eba6af75 189 public static function get_contextid($instance, $component = 'moodle', $converterid = NULL) {
142ec51f
PC
190 global $DB;
191
192 // Attempt to retrieve the contextid
56bd1ab0
MN
193 $contextid = $DB->get_field_select('backup_ids_temp', 'id',
194 $DB->sql_compare_text('info', 100).' = ? AND itemid = ? AND itemname = ?',
195 array($component, $instance, 'context')
196 );
eba6af75 197
e10e8879
MN
198 if (!empty($contextid)) {
199 return $contextid;
142ec51f
PC
200 }
201
202 $context = new stdClass;
eba6af75 203 $context->itemid = $instance;
142ec51f 204 $context->itemname = 'context';
eba6af75
MN
205 $context->info = $component;
206
207 if (!is_null($converterid)) {
208 $context->backupid = $converterid;
209 }
56bd1ab0 210 if ($id = $DB->insert_record('backup_ids_temp', $context)) {
142ec51f
PC
211 return $id;
212 } else {
213 $msg = self::obj_to_readable($context);
214 throw new Exception(sprintf("Could not insert context record into temp table: %s", $msg));
eba6af75 215 }
142ec51f 216 }
0164592b
DM
217
218 /// end of public API //////////////////////////////////////////////////////
219
220 /**
221 * Choose the best conversion path for the given format
222 *
223 * Given the source format and the list of available converters and their properties,
224 * this methods picks the most effective way how to convert the source format into
225 * the target moodle2 format. The method returns a list of converters that should be
226 * called, in order.
227 *
228 * This implementation uses Dijkstra's algorithm to find the shortest way through
229 * the oriented graph.
230 *
231 * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm
232 * @param string $format the source backup format, one of backup::FORMAT_xxx
233 * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name
234 * @return array ordered list of converter names to call (may be empty if not reachable)
235 */
236 protected static function choose_conversion_path($format, array $descriptions) {
237
238 // construct an oriented graph of conversion paths. backup formats are nodes
239 // and the the converters are edges of the graph.
240 $paths = array(); // [fromnode][tonode] => converter
241 foreach ($descriptions as $converter => $description) {
242 $from = $description['from'];
243 $to = $description['to'];
244 $cost = $description['cost'];
245
246 if (is_null($from) or $from === backup::FORMAT_UNKNOWN or
247 is_null($to) or $to === backup::FORMAT_UNKNOWN or
248 is_null($cost) or $cost <= 0) {
249 throw new coding_exception('Invalid converter description:' . $converter);
250 }
251
252 if (!isset($paths[$from][$to])) {
253 $paths[$from][$to] = $converter;
254 } else {
255 // if there are two converters available for the same conversion
256 // path, choose the one with the lowest cost. if there are more
257 // available converters with the same cost, the chosen one is
258 // undefined (depends on the order of processing)
259 if ($descriptions[$paths[$from][$to]]['cost'] > $cost) {
260 $paths[$from][$to] = $converter;
261 }
262 }
263 }
264
265 if (empty($paths)) {
266 // no conversion paths available
267 return array();
268 }
269
270 // now use Dijkstra's algorithm and find the shortest conversion path
271
272 $dist = array(); // list of nodes and their distances from the source format
273 $prev = array(); // list of previous nodes in optimal path from the source format
274 foreach ($paths as $fromnode => $tonodes) {
275 $dist[$fromnode] = null; // infinitive distance, can't be reached
276 $prev[$fromnode] = null; // unknown
277 foreach ($tonodes as $tonode => $converter) {
278 $dist[$tonode] = null; // infinitive distance, can't be reached
279 $prev[$tonode] = null; // unknown
280 }
281 }
282
283 if (!array_key_exists($format, $dist)) {
284 return array();
285 } else {
286 $dist[$format] = 0;
287 }
288
289 $queue = array_flip(array_keys($dist));
290 while (!empty($queue)) {
291 // find the node with the smallest distance from the source in the queue
292 // in the first iteration, this will find the original format node itself
293 $closest = null;
294 foreach ($queue as $node => $undefined) {
295 if (is_null($dist[$node])) {
296 continue;
297 }
298 if (is_null($closest) or ($dist[$node] < $dist[$closest])) {
299 $closest = $node;
300 }
301 }
302
303 if (is_null($closest) or is_null($dist[$closest])) {
304 // all remaining nodes are inaccessible from source
305 break;
306 }
307
308 if ($closest === backup::FORMAT_MOODLE) {
309 // bingo we can break now
310 break;
311 }
312
313 unset($queue[$closest]);
314
315 // visit all neighbors and update distances to them eventually
316
317 if (!isset($paths[$closest])) {
318 continue;
319 }
320 $neighbors = array_keys($paths[$closest]);
321 // keep just neighbors that are in the queue yet
322 foreach ($neighbors as $ix => $neighbor) {
323 if (!array_key_exists($neighbor, $queue)) {
324 unset($neighbors[$ix]);
325 }
326 }
327
328 foreach ($neighbors as $neighbor) {
329 // the alternative distance to the neighbor if we went thru the
330 // current $closest node
331 $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost'];
332
333 if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) {
334 // we found a shorter way to the $neighbor, remember it
335 $dist[$neighbor] = $alt;
336 $prev[$neighbor] = $closest;
337 }
338 }
339 }
340
341 if (is_null($dist[backup::FORMAT_MOODLE])) {
342 // unable to find a conversion path, the target format not reachable
343 return array();
344 }
345
346 // reconstruct the optimal path from the source format to the target one
347 $conversionpath = array();
348 $target = backup::FORMAT_MOODLE;
349 while (isset($prev[$target])) {
350 array_unshift($conversionpath, $paths[$prev[$target]][$target]);
351 $target = $prev[$target];
352 }
353
354 return $conversionpath;
355 }
142ec51f 356}