Commit | Line | Data |
---|---|---|
69023455 EL |
1 | <?php |
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 | /** | |
19 | * @package moodlecore | |
20 | * @subpackage backup-plan | |
21 | * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | ||
25 | /** | |
26 | * Abstract class defining the needed stuff to restore one xml file | |
27 | * | |
28 | * TODO: Finish phpdocs | |
29 | */ | |
30 | abstract class restore_structure_step extends restore_step { | |
31 | ||
32 | protected $filename; // Name of the file to be parsed | |
2d7cd798 EL |
33 | protected $contentprocessor; // xml parser processor being used |
34 | // (need it here, apart from parser | |
35 | // thanks to serialized data to process - | |
36 | // say thanks to blocks!) | |
37 | protected $pathelements; // Array of pathelements to process | |
2df0f295 EL |
38 | protected $elementsoldid; // Array to store last oldid used on each element |
39 | protected $elementsnewid; // Array to store last newid used on each element | |
69023455 | 40 | |
0149e6c7 EL |
41 | protected $pathlock; // Path currently locking processing of children |
42 | ||
43 | const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore | |
44 | // all children below path processor returning it | |
45 | ||
69023455 EL |
46 | /** |
47 | * Constructor - instantiates one object of this class | |
48 | */ | |
49 | public function __construct($name, $filename, $task = null) { | |
50 | if (!is_null($task) && !($task instanceof restore_task)) { | |
51 | throw new restore_step_exception('wrong_restore_task_specified'); | |
52 | } | |
53 | $this->filename = $filename; | |
2d7cd798 | 54 | $this->contentprocessor = null; |
69023455 | 55 | $this->pathelements = array(); |
2df0f295 EL |
56 | $this->elementsoldid = array(); |
57 | $this->elementsnewid = array(); | |
0149e6c7 | 58 | $this->pathlock = null; |
69023455 EL |
59 | parent::__construct($name, $task); |
60 | } | |
61 | ||
0149e6c7 | 62 | final public function execute() { |
69023455 | 63 | |
482aac65 EL |
64 | if (!$this->execute_condition()) { // Check any condition to execute this |
65 | return; | |
66 | } | |
67 | ||
69023455 EL |
68 | $fullpath = $this->task->get_taskbasepath(); |
69 | ||
70 | // We MUST have one fullpath here, else, error | |
71 | if (empty($fullpath)) { | |
72 | throw new restore_step_exception('restore_structure_step_undefined_fullpath'); | |
73 | } | |
74 | ||
75 | // Append the filename to the fullpath | |
76 | $fullpath = rtrim($fullpath, '/') . '/' . $this->filename; | |
77 | ||
78 | // And it MUST exist | |
79 | if (!file_exists($fullpath)) { // Shouldn't happen ever, but... | |
80 | throw new restore_step_exception('missing_moodle_backup_xml_file', $fullpath); | |
81 | } | |
82 | ||
83 | // Get restore_path elements array adapting and preparing it for processing | |
245491b0 EL |
84 | $structure = $this->define_structure(); |
85 | if (!is_array($structure)) { | |
86 | throw new restore_step_exception('restore_step_structure_not_array', $this->get_name()); | |
87 | } | |
91d11057 | 88 | $this->prepare_pathelements($structure); |
2df0f295 | 89 | |
69023455 EL |
90 | // Create parser and processor |
91 | $xmlparser = new progressive_parser(); | |
92 | $xmlparser->set_file($fullpath); | |
93 | $xmlprocessor = new restore_structure_parser_processor($this->task->get_courseid(), $this); | |
2d7cd798 EL |
94 | $this->contentprocessor = $xmlprocessor; // Save the reference to the contentprocessor |
95 | // as far as we are going to need it out | |
96 | // from parser (blame serialized data!) | |
69023455 EL |
97 | $xmlparser->set_processor($xmlprocessor); |
98 | ||
99 | // Add pathelements to processor | |
100 | foreach ($this->pathelements as $element) { | |
101 | $xmlprocessor->add_path($element->get_path(), $element->is_grouped()); | |
102 | } | |
103 | ||
104 | // And process it, dispatch to target methods in step will start automatically | |
105 | $xmlparser->process(); | |
69023455 | 106 | |
41941110 EL |
107 | // Have finished, launch the after_execute method of all the processing objects |
108 | $this->launch_after_execute_methods(); | |
2df0f295 | 109 | } |
69023455 EL |
110 | |
111 | /** | |
112 | * Receive one chunk of information form the xml parser processor and | |
113 | * dispatch it, following the naming rules | |
114 | */ | |
0149e6c7 | 115 | final public function process($data) { |
69023455 EL |
116 | if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen |
117 | throw new restore_step_exception('restore_structure_step_missing_path', $data['path']); | |
118 | } | |
119 | $element = $this->pathelements[$data['path']]; | |
120 | $object = $element->get_processing_object(); | |
121 | $method = $element->get_processing_method(); | |
0149e6c7 | 122 | $rdata = null; |
69023455 EL |
123 | if (empty($object)) { // No processing object defined |
124 | throw new restore_step_exception('restore_structure_step_missing_pobject', $object); | |
125 | } | |
0149e6c7 EL |
126 | // Release the lock if we aren't anymore within children of it |
127 | if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) { | |
128 | $this->pathlock = null; | |
129 | } | |
130 | if (is_null($this->pathlock)) { // Only dispatch if there isn't any lock | |
131 | $rdata = $object->$method($data['tags']); // Dispatch to proper object/method | |
132 | } | |
133 | ||
134 | // If the dispatched method returns SKIP_ALL_CHILDREN, we grab current path in order to | |
135 | // lock dispatching to any children | |
136 | if ($rdata === self::SKIP_ALL_CHILDREN) { | |
137 | // Check we haven't any previous lock | |
138 | if (!is_null($this->pathlock)) { | |
139 | throw new restore_step_exception('restore_structure_step_already_skipping', $data['path']); | |
140 | } | |
141 | // Set the lock | |
142 | $this->pathlock = $data['path'] . '/'; // Lock everything below current path | |
143 | ||
144 | // Continue with normal processing of return values | |
145 | } else if ($rdata !== null) { // If the method has returned any info, set element data to it | |
69023455 EL |
146 | $element->set_data($rdata); |
147 | } else { // Else, put the original parsed data | |
148 | $element->set_data($data); | |
149 | } | |
150 | } | |
151 | ||
2df0f295 EL |
152 | /** |
153 | * To send ids pairs to backup_ids_table and to store them into paths | |
154 | * | |
155 | * This method will send the given itemname and old/new ids to the | |
156 | * backup_ids_temp table, and, at the same time, will save the new id | |
157 | * into the corresponding restore_path_element for easier access | |
158 | * by children. Also will inject the known old context id for the task | |
159 | * in case it's going to be used for restoring files later | |
160 | */ | |
2a918d8f EL |
161 | public function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) { |
162 | if ($restorefiles && $parentid) { | |
163 | throw new restore_step_exception('set_mapping_cannot_specify_both_restorefiles_and_parentitemid'); | |
164 | } | |
c0440b3f | 165 | // If we haven't specified one context for the files, use the task one |
2a918d8f | 166 | if (is_null($filesctxid)) { |
c0440b3f EL |
167 | $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null; |
168 | } else { // Use the specified one | |
169 | $parentitemid = $restorefiles ? $filesctxid : null; | |
170 | } | |
2a918d8f EL |
171 | // We have passed one explicit parentid, apply it |
172 | $parentitemid = !is_null($parentid) ? $parentid : $parentitemid; | |
173 | ||
2df0f295 EL |
174 | // Let's call the low level one |
175 | restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid); | |
176 | // Now, if the itemname matches any pathelement->name, store the latest $newid | |
177 | if (array_key_exists($itemname, $this->elementsoldid)) { // If present in $this->elementsoldid, is valid, put both ids | |
178 | $this->elementsoldid[$itemname] = $oldid; | |
179 | $this->elementsnewid[$itemname] = $newid; | |
180 | } | |
181 | } | |
182 | ||
183 | /** | |
184 | * Returns the latest (parent) old id mapped by one pathelement | |
185 | */ | |
91d11057 | 186 | public function get_old_parentid($itemname) { |
2df0f295 EL |
187 | return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null; |
188 | } | |
189 | ||
190 | /** | |
191 | * Returns the latest (parent) new id mapped by one pathelement | |
192 | */ | |
91d11057 | 193 | public function get_new_parentid($itemname) { |
2df0f295 EL |
194 | return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null; |
195 | } | |
196 | ||
197 | /** | |
198 | * Return the new id of a mapping for the given itemname | |
4b43b107 EL |
199 | * |
200 | * @param string $itemname the type of item | |
98529b00 | 201 | * @param int $oldid the item ID from the backup |
4b43b107 | 202 | * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false |
2df0f295 | 203 | */ |
98529b00 | 204 | public function get_mappingid($itemname, $oldid, $ifnotfound = false) { |
2df0f295 | 205 | $mapping = $this->get_mapping($itemname, $oldid); |
98529b00 | 206 | return $mapping ? $mapping->newitemid : $ifnotfound; |
2df0f295 EL |
207 | } |
208 | ||
209 | /** | |
210 | * Return the complete mapping from the given itemname, itemid | |
211 | */ | |
91d11057 | 212 | public function get_mapping($itemname, $oldid) { |
2df0f295 EL |
213 | return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid); |
214 | } | |
215 | ||
216 | /** | |
217 | * Add all the existing file, given their component and filearea and one backup_ids itemname to match with | |
218 | */ | |
f2745cbe | 219 | public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) { |
c0440b3f | 220 | $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid; |
b8bb45b0 | 221 | restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, |
f2745cbe | 222 | $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid); |
2df0f295 EL |
223 | } |
224 | ||
4bca307a EL |
225 | /** |
226 | * Apply course startdate offset based in original course startdate and course_offset_startdate setting | |
227 | * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple | |
228 | * executions in the same request | |
229 | */ | |
91d11057 | 230 | public function apply_date_offset($value) { |
4bca307a | 231 | |
fc92674d EL |
232 | // empties don't offset - zeros (int and string), false and nulls return original value |
233 | if (empty($value)) { | |
fc46764d EL |
234 | return $value; |
235 | } | |
236 | ||
4bca307a EL |
237 | static $cache = array(); |
238 | // Lookup cache | |
239 | if (isset($cache[$this->get_restoreid()])) { | |
240 | return $value + $cache[$this->get_restoreid()]; | |
241 | } | |
242 | // No cache, let's calculate the offset | |
243 | $original = $this->task->get_info()->original_course_startdate; | |
636444e8 EL |
244 | $setting = 0; |
245 | if ($this->setting_exists('course_startdate')) { // Seting may not exist (MDL-25019) | |
246 | $setting = $this->get_setting_value('course_startdate'); | |
247 | } | |
4bca307a | 248 | |
636444e8 EL |
249 | // Original course has not startdate or setting doesn't exist, offset = 0 |
250 | if (empty($original) || empty($setting)) { | |
4bca307a EL |
251 | $cache[$this->get_restoreid()] = 0; |
252 | ||
253 | // Less than 24h of difference, offset = 0 (this avoids some problems with timezones) | |
254 | } else if (abs($setting - $original) < 24 * 60 * 60) { | |
255 | $cache[$this->get_restoreid()] = 0; | |
256 | ||
257 | // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case | |
258 | } else if (!has_capability('moodle/restore:rolldates', | |
259 | get_context_instance(CONTEXT_COURSE, $this->get_courseid()), | |
ba8bead5 | 260 | $this->task->get_userid())) { |
4bca307a EL |
261 | $cache[$this->get_restoreid()] = 0; |
262 | ||
263 | // Arrived here, let's calculate the real offset | |
264 | } else { | |
265 | $cache[$this->get_restoreid()] = $setting - $original; | |
266 | } | |
267 | ||
268 | // Return the passed value with cached offset applied | |
269 | return $value + $cache[$this->get_restoreid()]; | |
270 | } | |
271 | ||
41941110 EL |
272 | /** |
273 | * As far as restore structure steps are implementing restore_plugin stuff, they need to | |
274 | * have the parent task available for wrapping purposes (get course/context....) | |
275 | */ | |
276 | public function get_task() { | |
277 | return $this->task; | |
278 | } | |
279 | ||
91d11057 EL |
280 | // Protected API starts here |
281 | ||
41941110 EL |
282 | /** |
283 | * Add plugin structure to any element in the structure restore tree | |
284 | * | |
285 | * @param string $plugintype type of plugin as defined by get_plugin_types() | |
286 | * @param restore_path_element $element element in the structure restore tree that | |
287 | * we are going to add plugin information to | |
288 | */ | |
289 | protected function add_plugin_structure($plugintype, $element) { | |
290 | ||
291 | global $CFG; | |
292 | ||
293 | // Check the requested plugintype is a valid one | |
294 | if (!array_key_exists($plugintype, get_plugin_types($plugintype))) { | |
295 | throw new restore_step_exception('incorrect_plugin_type', $plugintype); | |
296 | } | |
297 | ||
298 | // Get all the restore path elements, looking across all the plugin dirs | |
299 | $pluginsdirs = get_plugin_list($plugintype); | |
300 | foreach ($pluginsdirs as $name => $pluginsdir) { | |
301 | // We need to add also backup plugin classes on restore, they may contain | |
302 | // some stuff used both in backup & restore | |
303 | $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin'; | |
304 | $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php'; | |
305 | if (file_exists($backupfile)) { | |
306 | require_once($backupfile); | |
307 | } | |
308 | // Now add restore plugin classes and prepare stuff | |
309 | $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin'; | |
310 | $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php'; | |
311 | if (file_exists($restorefile)) { | |
312 | require_once($restorefile); | |
313 | $restoreplugin = new $restoreclassname($plugintype, $name, $this); | |
314 | // Add plugin paths to the step | |
315 | $this->prepare_pathelements($restoreplugin->define_plugin_structure($element)); | |
316 | } | |
317 | } | |
318 | } | |
319 | ||
320 | /** | |
321 | * Launch all the after_execute methods present in all the processing objects | |
322 | * | |
323 | * This method will launch all the after_execute methods that can be defined | |
324 | * both in restore_plugin and restore_structure_step classes | |
325 | * | |
326 | * For restore_plugin classes the name of the method to be executed will be | |
327 | * "after_execute_" + connection point (as far as can be multiple connection | |
328 | * points in the same class) | |
329 | * | |
330 | * For restore_structure_step classes is will be, simply, "after_execute". Note | |
331 | * that this is executed *after* the plugin ones | |
332 | */ | |
333 | protected function launch_after_execute_methods() { | |
334 | $alreadylaunched = array(); // To avoid multiple executions | |
335 | foreach ($this->pathelements as $key => $pathelement) { | |
336 | // Get the processing object | |
337 | $pobject = $pathelement->get_processing_object(); | |
338 | // Skip null processors (child of grouped ones for sure) | |
339 | if (is_null($pobject)) { | |
340 | continue; | |
341 | } | |
342 | // Skip restore structure step processors (this) | |
343 | if ($pobject instanceof restore_structure_step) { | |
344 | continue; | |
345 | } | |
346 | // Skip already launched processing objects | |
347 | if (in_array($pobject, $alreadylaunched, true)) { | |
348 | continue; | |
349 | } | |
350 | // Add processing object to array of launched ones | |
351 | $alreadylaunched[] = $pobject; | |
352 | // If the processing object has support for | |
353 | // launching after_execute methods, use it | |
354 | if (method_exists($pobject, 'launch_after_execute_methods')) { | |
355 | $pobject->launch_after_execute_methods(); | |
356 | } | |
357 | } | |
358 | // Finally execute own (restore_structure_step) after_execute method | |
359 | $this->after_execute(); | |
360 | ||
361 | } | |
362 | ||
91d11057 EL |
363 | /** |
364 | * This method will be executed after the whole structure step have been processed | |
365 | * | |
366 | * After execution method for code needed to be executed after the whole structure | |
367 | * has been processed. Useful for cleaning tasks, files process and others. Simply | |
368 | * overwrite in in your steps if needed | |
369 | */ | |
370 | protected function after_execute() { | |
371 | // do nothing by default | |
372 | } | |
373 | ||
374 | /** | |
375 | * Prepare the pathelements for processing, looking for duplicates, applying | |
376 | * processing objects and other adjustments | |
377 | */ | |
378 | protected function prepare_pathelements($elementsarr) { | |
379 | ||
380 | // First iteration, push them to new array, indexed by name | |
381 | // detecting duplicates in names or paths | |
382 | $names = array(); | |
383 | $paths = array(); | |
384 | foreach($elementsarr as $element) { | |
385 | if (!$element instanceof restore_path_element) { | |
386 | throw new restore_step_exception('restore_path_element_wrong_class', get_class($element)); | |
387 | } | |
388 | if (array_key_exists($element->get_name(), $names)) { | |
389 | throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name()); | |
390 | } | |
391 | if (array_key_exists($element->get_path(), $paths)) { | |
392 | throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path()); | |
393 | } | |
394 | $names[$element->get_name()] = true; | |
395 | $paths[$element->get_path()] = $element; | |
396 | } | |
397 | // Now, for each element not having one processing object, if | |
398 | // not child of grouped element, assign $this (the step itself) as processing element | |
399 | // Note method must exist or we'll get one @restore_path_element_exception | |
400 | foreach($paths as $key => $pelement) { | |
401 | if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) { | |
402 | $paths[$key]->set_processing_object($this); | |
403 | } | |
404 | // Populate $elementsoldid and $elementsoldid based on available pathelements | |
405 | $this->elementsoldid[$pelement->get_name()] = null; | |
406 | $this->elementsnewid[$pelement->get_name()] = null; | |
407 | } | |
408 | // Done, add them to pathelements (dupes by key - path - are discarded) | |
409 | $this->pathelements = array_merge($this->pathelements, $paths); | |
410 | } | |
411 | ||
412 | /** | |
413 | * Given one pathelement, return true if grouped parent was found | |
414 | */ | |
415 | protected function grouped_parent_exists($pelement, $elements) { | |
416 | foreach ($elements as $element) { | |
417 | if ($pelement->get_path() == $element->get_path()) { | |
418 | continue; // Don't compare against itself | |
419 | } | |
420 | // If element is grouped and parent of pelement, return true | |
421 | if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) { | |
422 | return true; | |
423 | } | |
424 | } | |
425 | return false; // no grouped parent found | |
426 | } | |
427 | ||
428 | /** | |
429 | * To conditionally decide if one step will be executed or no | |
430 | * | |
431 | * For steps needing to be executed conditionally, based in dynamic | |
432 | * conditions (at execution time vs at declaration time) you must | |
433 | * override this function. It will return true if the step must be | |
434 | * executed and false if not | |
435 | */ | |
436 | protected function execute_condition() { | |
437 | return true; | |
438 | } | |
439 | ||
69023455 EL |
440 | /** |
441 | * Function that will return the structure to be processed by this restore_step. | |
442 | * Must return one array of @restore_path_element elements | |
443 | */ | |
444 | abstract protected function define_structure(); | |
445 | } |