MDL-27729 Glossary module 1.9 backup converts to 2.0 format
[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 */
53 public static function available_converters() {
54 global $CFG;
55
56 $converters = array();
57 $plugins = get_list_of_plugins('backup/converter');
58 foreach ($plugins as $name) {
1e2c7351 59 $classfile = "$CFG->dirroot/backup/converter/$name/lib.php";
0164592b
DM
60 $classname = "{$name}_converter";
61
62 if (!file_exists($classfile)) {
1e2c7351 63 throw new convert_helper_exception('converter_classfile_not_found', $classfile);
0164592b
DM
64 }
65 require_once($classfile);
66
67 if (!class_exists($classname)) {
1e2c7351 68 throw new convert_helper_exception('converter_classname_not_found', $classname);
0164592b
DM
69 }
70
71 if (call_user_func($classname .'::is_available')) {
72 $converters[] = $name;
73 }
74 }
75
76 return $converters;
77 }
78
79 /**
80 * Detects if the given folder contains an unpacked moodle2 backup
81 *
82 * @param string $tempdir the name of the backup directory
83 * @return boolean true if moodle2 format detected, false otherwise
84 */
85 public static function detect_moodle2_format($tempdir) {
86 global $CFG;
87
88 $dirpath = $CFG->dataroot . '/temp/backup/' . $tempdir;
89 $filepath = $dirpath . '/moodle_backup.xml';
90
91 if (!is_dir($dirpath)) {
1e2c7351 92 throw new converter_helper_exception('tmp_backup_directory_not_found', $dirpath);
0164592b
DM
93 }
94
95 if (!file_exists($filepath)) {
96 return false;
97 }
98
99 $handle = fopen($filepath, 'r');
100 $firstchars = fread($handle, 200);
101 $status = fclose($handle);
102
103 if (strpos($firstchars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
104 strpos($firstchars,'<moodle_backup>') !== false and
105 strpos($firstchars,'<information>') !== false) {
106 return true;
107 }
108
109 return false;
110 }
111
112 /**
113 * Converts the given directory with the backup into moodle2 format
114 *
c5c8b350
MN
115 * @param string $tempdir The directory to convert
116 * @param string $format The current format, if already detected
1e2c7351
DM
117 * @throws convert_helper_exception
118 * @return bool false if unable to find the conversion path, true otherwise
c5c8b350 119 */
0164592b
DM
120 public static function to_moodle2_format($tempdir, $format = null) {
121
c5c8b350
MN
122 if (is_null($format)) {
123 $format = backup_general_helper::detect_backup_format($tempdir);
124 }
c5c8b350 125
0164592b 126 // get the supported conversion paths from all available converters
d51345c7 127 $converters = self::available_converters();
0164592b
DM
128 $descriptions = array();
129 foreach ($converters as $name) {
130 $classname = "{$name}_converter";
131 if (!class_exists($classname)) {
1e2c7351 132 throw new convert_helper_exception('class_not_loaded', $classname);
c5c8b350 133 }
0164592b
DM
134 $descriptions[$name] = call_user_func($classname .'::description');
135 }
c5c8b350 136
0164592b
DM
137 // choose the best conversion path for the given format
138 $path = self::choose_conversion_path($format, $descriptions);
139
140 if (empty($path)) {
141 // unable to convert
1e2c7351 142 return false;
c5c8b350 143 }
0164592b
DM
144
145 foreach ($path as $name) {
17d2e210 146 $converter = convert_factory::get_converter($name, $tempdir);
0164592b
DM
147 $converter->convert();
148 }
149
150 // make sure we ended with moodle2 format
151 if (!self::detect_moodle2_format($tempdir)) {
1e2c7351 152 throw new convert_helper_exception('conversion_failed');
c5c8b350 153 }
1e2c7351
DM
154
155 return true;
c5c8b350 156 }
142ec51f
PC
157
158 /**
159 * Inserts an inforef into the conversion temp table
160 */
161 public static function set_inforef($contextid) {
162 global $DB;
142ec51f
PC
163 }
164
165 public static function get_inforef($contextid) {
166 }
167
168 /**
169 * Converts a plain old php object (popo?) into a string...
170 * Useful for debuging failed db inserts, or anything like that
171 */
172 public static function obj_to_readable($obj) {
173 $mapper = function($field, $value) { return "$field=$value"; };
174 $fields = get_object_vars($obj);
175
176 return implode(", ", array_map($mapper, array_keys($fields), array_values($fields)));
177 }
178
0164592b
DM
179 /// end of public API //////////////////////////////////////////////////////
180
181 /**
182 * Choose the best conversion path for the given format
183 *
184 * Given the source format and the list of available converters and their properties,
185 * this methods picks the most effective way how to convert the source format into
186 * the target moodle2 format. The method returns a list of converters that should be
187 * called, in order.
188 *
189 * This implementation uses Dijkstra's algorithm to find the shortest way through
190 * the oriented graph.
191 *
192 * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm
1e2c7351 193 * @author David Mudrak <david@moodle.com>
0164592b
DM
194 * @param string $format the source backup format, one of backup::FORMAT_xxx
195 * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name
196 * @return array ordered list of converter names to call (may be empty if not reachable)
197 */
198 protected static function choose_conversion_path($format, array $descriptions) {
199
200 // construct an oriented graph of conversion paths. backup formats are nodes
201 // and the the converters are edges of the graph.
202 $paths = array(); // [fromnode][tonode] => converter
203 foreach ($descriptions as $converter => $description) {
204 $from = $description['from'];
205 $to = $description['to'];
206 $cost = $description['cost'];
207
208 if (is_null($from) or $from === backup::FORMAT_UNKNOWN or
209 is_null($to) or $to === backup::FORMAT_UNKNOWN or
210 is_null($cost) or $cost <= 0) {
1e2c7351 211 throw new convert_helper_exception('invalid_converter_description', $converter);
0164592b
DM
212 }
213
214 if (!isset($paths[$from][$to])) {
215 $paths[$from][$to] = $converter;
216 } else {
217 // if there are two converters available for the same conversion
218 // path, choose the one with the lowest cost. if there are more
219 // available converters with the same cost, the chosen one is
220 // undefined (depends on the order of processing)
221 if ($descriptions[$paths[$from][$to]]['cost'] > $cost) {
222 $paths[$from][$to] = $converter;
223 }
224 }
225 }
226
227 if (empty($paths)) {
228 // no conversion paths available
229 return array();
230 }
231
232 // now use Dijkstra's algorithm and find the shortest conversion path
233
234 $dist = array(); // list of nodes and their distances from the source format
235 $prev = array(); // list of previous nodes in optimal path from the source format
236 foreach ($paths as $fromnode => $tonodes) {
237 $dist[$fromnode] = null; // infinitive distance, can't be reached
238 $prev[$fromnode] = null; // unknown
239 foreach ($tonodes as $tonode => $converter) {
240 $dist[$tonode] = null; // infinitive distance, can't be reached
241 $prev[$tonode] = null; // unknown
242 }
243 }
244
245 if (!array_key_exists($format, $dist)) {
246 return array();
247 } else {
248 $dist[$format] = 0;
249 }
250
251 $queue = array_flip(array_keys($dist));
252 while (!empty($queue)) {
253 // find the node with the smallest distance from the source in the queue
254 // in the first iteration, this will find the original format node itself
255 $closest = null;
256 foreach ($queue as $node => $undefined) {
257 if (is_null($dist[$node])) {
258 continue;
259 }
260 if (is_null($closest) or ($dist[$node] < $dist[$closest])) {
261 $closest = $node;
262 }
263 }
264
265 if (is_null($closest) or is_null($dist[$closest])) {
266 // all remaining nodes are inaccessible from source
267 break;
268 }
269
270 if ($closest === backup::FORMAT_MOODLE) {
271 // bingo we can break now
272 break;
273 }
274
275 unset($queue[$closest]);
276
277 // visit all neighbors and update distances to them eventually
278
279 if (!isset($paths[$closest])) {
280 continue;
281 }
282 $neighbors = array_keys($paths[$closest]);
283 // keep just neighbors that are in the queue yet
284 foreach ($neighbors as $ix => $neighbor) {
285 if (!array_key_exists($neighbor, $queue)) {
286 unset($neighbors[$ix]);
287 }
288 }
289
290 foreach ($neighbors as $neighbor) {
291 // the alternative distance to the neighbor if we went thru the
292 // current $closest node
293 $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost'];
294
295 if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) {
296 // we found a shorter way to the $neighbor, remember it
297 $dist[$neighbor] = $alt;
298 $prev[$neighbor] = $closest;
299 }
300 }
301 }
302
303 if (is_null($dist[backup::FORMAT_MOODLE])) {
304 // unable to find a conversion path, the target format not reachable
305 return array();
306 }
307
308 // reconstruct the optimal path from the source format to the target one
309 $conversionpath = array();
310 $target = backup::FORMAT_MOODLE;
311 while (isset($prev[$target])) {
312 array_unshift($conversionpath, $paths[$prev[$target]][$target]);
313 $target = $prev[$target];
314 }
315
316 return $conversionpath;
317 }
142ec51f 318}
1e2c7351
DM
319
320/**
321 * General convert_helper related exception
322 *
323 * @author David Mudrak <david@moodle.com>
324 */
325class convert_helper_exception extends moodle_exception {
326
327 /**
328 * Constructor
329 *
330 * @param string $errorcode key for the corresponding error string
331 * @param object $a extra words and phrases that might be required in the error string
332 * @param string $debuginfo optional debugging information
333 */
334 public function __construct($errorcode, $a = null, $debuginfo = null) {
335 parent::__construct($errorcode, '', '', $a, $debuginfo);
336 }
337}