MDL-24419 (1): Core restore steps should have after_restore function
[moodle.git] / backup / util / plan / restore_structure_step.class.php
CommitLineData
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 */
30abstract 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
c44299c1
EL
363 /**
364 * Launch all the after_restore methods present in all the processing objects
365 *
366 * This method will launch all the after_restore methods that can be defined
367 * both in restore_plugin class
368 *
369 * For restore_plugin classes the name of the method to be executed will be
370 * "after_restore_" + connection point (as far as can be multiple connection
371 * points in the same class)
372 */
373 public function launch_after_restore_methods() {
374 $alreadylaunched = array(); // To avoid multiple executions
375 foreach ($this->pathelements as $pathelement) {
376 // Get the processing object
377 $pobject = $pathelement->get_processing_object();
378 // Skip null processors (child of grouped ones for sure)
379 if (is_null($pobject)) {
380 continue;
381 }
382 // Skip restore structure step processors (this)
383 if ($pobject instanceof restore_structure_step) {
384 continue;
385 }
386 // Skip already launched processing objects
387 if (in_array($pobject, $alreadylaunched, true)) {
388 continue;
389 }
390 // Add processing object to array of launched ones
391 $alreadylaunched[] = $pobject;
392 // If the processing object has support for
393 // launching after_restore methods, use it
394 if (method_exists($pobject, 'launch_after_restore_methods')) {
395 $pobject->launch_after_restore_methods();
396 }
397 }
1cf121e1 398 // Finally execute own (restore_structure_step) after_restore method
399 $this->after_restore();
c44299c1
EL
400 }
401
91d11057
EL
402 /**
403 * This method will be executed after the whole structure step have been processed
404 *
405 * After execution method for code needed to be executed after the whole structure
406 * has been processed. Useful for cleaning tasks, files process and others. Simply
407 * overwrite in in your steps if needed
408 */
409 protected function after_execute() {
410 // do nothing by default
411 }
412
1cf121e1 413 /**
414 * This method will be executed after the rest of the restore has been processed.
415 *
416 * Use if you need to update IDs based on things which are restored after this
417 * step has completed.
418 */
419 protected function after_restore() {
420 // do nothing by default
421 }
422
91d11057
EL
423 /**
424 * Prepare the pathelements for processing, looking for duplicates, applying
425 * processing objects and other adjustments
426 */
427 protected function prepare_pathelements($elementsarr) {
428
429 // First iteration, push them to new array, indexed by name
430 // detecting duplicates in names or paths
431 $names = array();
432 $paths = array();
433 foreach($elementsarr as $element) {
434 if (!$element instanceof restore_path_element) {
435 throw new restore_step_exception('restore_path_element_wrong_class', get_class($element));
436 }
437 if (array_key_exists($element->get_name(), $names)) {
438 throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
439 }
440 if (array_key_exists($element->get_path(), $paths)) {
441 throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
442 }
443 $names[$element->get_name()] = true;
444 $paths[$element->get_path()] = $element;
445 }
446 // Now, for each element not having one processing object, if
447 // not child of grouped element, assign $this (the step itself) as processing element
448 // Note method must exist or we'll get one @restore_path_element_exception
449 foreach($paths as $key => $pelement) {
450 if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
451 $paths[$key]->set_processing_object($this);
452 }
453 // Populate $elementsoldid and $elementsoldid based on available pathelements
454 $this->elementsoldid[$pelement->get_name()] = null;
455 $this->elementsnewid[$pelement->get_name()] = null;
456 }
457 // Done, add them to pathelements (dupes by key - path - are discarded)
458 $this->pathelements = array_merge($this->pathelements, $paths);
459 }
460
461 /**
462 * Given one pathelement, return true if grouped parent was found
463 */
464 protected function grouped_parent_exists($pelement, $elements) {
465 foreach ($elements as $element) {
466 if ($pelement->get_path() == $element->get_path()) {
467 continue; // Don't compare against itself
468 }
469 // If element is grouped and parent of pelement, return true
470 if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
471 return true;
472 }
473 }
474 return false; // no grouped parent found
475 }
476
477 /**
478 * To conditionally decide if one step will be executed or no
479 *
480 * For steps needing to be executed conditionally, based in dynamic
481 * conditions (at execution time vs at declaration time) you must
482 * override this function. It will return true if the step must be
483 * executed and false if not
484 */
485 protected function execute_condition() {
486 return true;
487 }
488
69023455
EL
489 /**
490 * Function that will return the structure to be processed by this restore_step.
491 * Must return one array of @restore_path_element elements
492 */
493 abstract protected function define_structure();
494}