ddb2fe54729bb903d68c88cbc1a76043d0a68fa7
[moodle.git] / backup / util / plan / restore_structure_step.class.php
1 <?php
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/>.
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  */
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 {
32     protected $filename; // Name of the file to be parsed
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
38     protected $elementsoldid; // Array to store last oldid used on each element
39     protected $elementsnewid; // Array to store last newid used on each element
41     protected $pathlock;      // Path currently locking processing of children
43     const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore
44                                        // all children below path processor returning it
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;
54         $this->contentprocessor = null;
55         $this->pathelements = array();
56         $this->elementsoldid = array();
57         $this->elementsnewid = array();
58         $this->pathlock = null;
59         parent::__construct($name, $task);
60     }
62     final public function execute() {
64         if (!$this->execute_condition()) { // Check any condition to execute this
65             return;
66         }
68         $fullpath = $this->task->get_taskbasepath();
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         }
75         // Append the filename to the fullpath
76         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
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         }
83         // Get restore_path elements array adapting and preparing it for processing
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         }
88         $this->prepare_pathelements($structure);
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);
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!)
97         $xmlparser->set_processor($xmlprocessor);
99         // Add pathelements to processor
100         foreach ($this->pathelements as $element) {
101             $xmlprocessor->add_path($element->get_path(), $element->is_grouped());
102         }
104         // And process it, dispatch to target methods in step will start automatically
105         $xmlparser->process();
107         // Have finished, launch the after_execute method of all the processing objects
108         $this->launch_after_execute_methods();
109     }
111     /**
112      * Receive one chunk of information form the xml parser processor and
113      * dispatch it, following the naming rules
114      */
115     final public function process($data) {
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();
122         $rdata = null;
123         if (empty($object)) { // No processing object defined
124             throw new restore_step_exception('restore_structure_step_missing_pobject', $object);
125         }
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         }
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
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
146             $element->set_data($rdata);
147         } else {               // Else, put the original parsed data
148             $element->set_data($data);
149         }
150     }
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      */
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         }
165         // If we haven't specified one context for the files, use the task one
166         if (is_null($filesctxid)) {
167             $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null;
168         } else { // Use the specified one
169             $parentitemid = $restorefiles ? $filesctxid : null;
170         }
171         // We have passed one explicit parentid, apply it
172         $parentitemid = !is_null($parentid) ? $parentid : $parentitemid;
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     }
183     /**
184      * Returns the latest (parent) old id mapped by one pathelement
185      */
186     public function get_old_parentid($itemname) {
187         return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null;
188     }
190     /**
191      * Returns the latest (parent) new id mapped by one pathelement
192      */
193     public function get_new_parentid($itemname) {
194         return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null;
195     }
197     /**
198      * Return the new id of a mapping for the given itemname
199      *
200      * @param string $itemname the type of item
201      * @param int $oldid the item ID from the backup
202      * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false
203      */
204     public function get_mappingid($itemname, $oldid, $ifnotfound = false) {
205         $mapping = $this->get_mapping($itemname, $oldid);
206         return $mapping ? $mapping->newitemid : $ifnotfound;
207     }
209     /**
210      * Return the complete mapping from the given itemname, itemid
211      */
212     public function get_mapping($itemname, $oldid) {
213         return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid);
214     }
216     /**
217      * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
218      */
219     public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
220         $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
221         restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
222                                           $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
223     }
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      */
230     public function apply_date_offset($value) {
232         // empties don't offset - zeros (int and string), false and nulls return original value
233         if (empty($value)) {
234             return $value;
235         }
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;
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         }
249         // Original course has not startdate or setting doesn't exist, offset = 0
250         if (empty($original) || empty($setting)) {
251             $cache[$this->get_restoreid()] = 0;
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;
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()),
260                                    $this->task->get_userid())) {
261             $cache[$this->get_restoreid()] = 0;
263         // Arrived here, let's calculate the real offset
264         } else {
265             $cache[$this->get_restoreid()] = $setting - $original;
266         }
268         // Return the passed value with cached offset applied
269         return $value + $cache[$this->get_restoreid()];
270     }
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     }
280 // Protected API starts here
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) {
291         global $CFG;
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         }
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     }
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();
361     }
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         }
398     }
400     /**
401      * This method will be executed after the whole structure step have been processed
402      *
403      * After execution method for code needed to be executed after the whole structure
404      * has been processed. Useful for cleaning tasks, files process and others. Simply
405      * overwrite in in your steps if needed
406      */
407     protected function after_execute() {
408         // do nothing by default
409     }
411     /**
412      * Prepare the pathelements for processing, looking for duplicates, applying
413      * processing objects and other adjustments
414      */
415     protected function prepare_pathelements($elementsarr) {
417         // First iteration, push them to new array, indexed by name
418         // detecting duplicates in names or paths
419         $names = array();
420         $paths = array();
421         foreach($elementsarr as $element) {
422             if (!$element instanceof restore_path_element) {
423                 throw new restore_step_exception('restore_path_element_wrong_class', get_class($element));
424             }
425             if (array_key_exists($element->get_name(), $names)) {
426                 throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
427             }
428             if (array_key_exists($element->get_path(), $paths)) {
429                 throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
430             }
431             $names[$element->get_name()] = true;
432             $paths[$element->get_path()] = $element;
433         }
434         // Now, for each element not having one processing object, if
435         // not child of grouped element, assign $this (the step itself) as processing element
436         // Note method must exist or we'll get one @restore_path_element_exception
437         foreach($paths as $key => $pelement) {
438             if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
439                 $paths[$key]->set_processing_object($this);
440             }
441             // Populate $elementsoldid and $elementsoldid based on available pathelements
442             $this->elementsoldid[$pelement->get_name()] = null;
443             $this->elementsnewid[$pelement->get_name()] = null;
444         }
445         // Done, add them to pathelements (dupes by key - path - are discarded)
446         $this->pathelements = array_merge($this->pathelements, $paths);
447     }
449     /**
450      * Given one pathelement, return true if grouped parent was found
451      */
452     protected function grouped_parent_exists($pelement, $elements) {
453         foreach ($elements as $element) {
454             if ($pelement->get_path() == $element->get_path()) {
455                 continue; // Don't compare against itself
456             }
457             // If element is grouped and parent of pelement, return true
458             if ($element->is_grouped() and strpos($pelement->get_path() .  '/', $element->get_path()) === 0) {
459                 return true;
460             }
461         }
462         return false; // no grouped parent found
463     }
465     /**
466      * To conditionally decide if one step will be executed or no
467      *
468      * For steps needing to be executed conditionally, based in dynamic
469      * conditions (at execution time vs at declaration time) you must
470      * override this function. It will return true if the step must be
471      * executed and false if not
472      */
473     protected function execute_condition() {
474         return true;
475     }
477     /**
478      * Function that will return the structure to be processed by this restore_step.
479      * Must return one array of @restore_path_element elements
480      */
481     abstract protected function define_structure();