8545f07c4b03a6bc732fb6bbc8251225c62e5482
[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     /**
42      * Constructor - instantiates one object of this class
43      */
44     public function __construct($name, $filename, $task = null) {
45         if (!is_null($task) && !($task instanceof restore_task)) {
46             throw new restore_step_exception('wrong_restore_task_specified');
47         }
48         $this->filename = $filename;
49         $this->contentprocessor = null;
50         $this->pathelements = array();
51         $this->elementsoldid = array();
52         $this->elementsnewid = array();
53         parent::__construct($name, $task);
54     }
56     public function execute() {
58         if (!$this->execute_condition()) { // Check any condition to execute this
59             return;
60         }
62         $fullpath = $this->task->get_taskbasepath();
64         // We MUST have one fullpath here, else, error
65         if (empty($fullpath)) {
66             throw new restore_step_exception('restore_structure_step_undefined_fullpath');
67         }
69         // Append the filename to the fullpath
70         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
72         // And it MUST exist
73         if (!file_exists($fullpath)) { // Shouldn't happen ever, but...
74             throw new restore_step_exception('missing_moodle_backup_xml_file', $fullpath);
75         }
77         // Get restore_path elements array adapting and preparing it for processing
78         $this->pathelements = $this->prepare_pathelements($this->define_structure());
80         // Populate $elementsoldid and $elementsoldid based on available pathelements
81         foreach ($this->pathelements as $pathelement) {
82             $this->elementsoldid[$pathelement->get_name()] = null;
83             $this->elementsnewid[$pathelement->get_name()] = null;
84         }
86         // Create parser and processor
87         $xmlparser = new progressive_parser();
88         $xmlparser->set_file($fullpath);
89         $xmlprocessor = new restore_structure_parser_processor($this->task->get_courseid(), $this);
90         $this->contentprocessor = $xmlprocessor; // Save the reference to the contentprocessor
91                                                  // as far as we are going to need it out
92                                                  // from parser (blame serialized data!)
93         $xmlparser->set_processor($xmlprocessor);
95         // Add pathelements to processor
96         foreach ($this->pathelements as $element) {
97             $xmlprocessor->add_path($element->get_path(), $element->is_grouped());
98         }
100         // And process it, dispatch to target methods in step will start automatically
101         $xmlparser->process();
103         // Have finished, call to the after_execute method
104         $this->after_execute();
105     }
107     /**
108      * Receive one chunk of information form the xml parser processor and
109      * dispatch it, following the naming rules
110      */
111     public function process($data) {
112         if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen
113             throw new restore_step_exception('restore_structure_step_missing_path', $data['path']);
114         }
115         $element = $this->pathelements[$data['path']];
116         $object = $element->get_processing_object();
117         $method = $element->get_processing_method();
118         if (empty($object)) { // No processing object defined
119             throw new restore_step_exception('restore_structure_step_missing_pobject', $object);
120         }
121         $rdata = $object->$method($data['tags']); // Dispatch to proper object/method
122         if ($rdata !== null) { // If the method has returned any info, set element data to it
123             $element->set_data($rdata);
124         } else {               // Else, put the original parsed data
125             $element->set_data($data);
126         }
127     }
129 // Protected API starts here
131     /**
132      * This method will be executed after the whole structure step have been processed
133      *
134      * After execution method for code needed to be executed after the whole structure
135      * has been processed. Useful for cleaning tasks, files process and others. Simply
136      * overwrite in in your steps if needed
137      */
138     protected function after_execute() {
139         // do nothing by default
140     }
142     /**
143      * Prepare the pathelements for processing, looking for duplicates, applying
144      * processing objects and other adjustments
145      */
146     protected function prepare_pathelements($elementsarr) {
148         // First iteration, push them to new array, indexed by name
149         // detecting duplicates in names or paths
150         $names = array();
151         $paths = array();
152         foreach($elementsarr as $element) {
153             if (array_key_exists($element->get_name(), $names)) {
154                 throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
155             }
156             if (array_key_exists($element->get_path(), $paths)) {
157                 throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
158             }
159             $names[$element->get_name()] = true;
160             $elements[$element->get_path()] = $element;
161         }
162         // Now, for each element not having one processing object, if
163         // not child of grouped element, assign $this (the step itself) as processing element
164         // Note method must exist or we'll get one @restore_path_element_exception
165         foreach($elements as $key => $pelement) {
166             if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $elements)) {
167                 $elements[$key]->set_processing_object($this);
168             }
169         }
170         // Done, return them
171         return $elements;
172     }
174     /**
175      * Given one pathelement, return true if grouped parent was found
176      */
177     protected function grouped_parent_exists($pelement, $elements) {
178         foreach ($elements as $element) {
179             if ($pelement->get_path() == $element->get_path()) {
180                 continue; // Don't compare against itself
181             }
182             // If element is grouped and parent of pelement, return true
183             if ($element->is_grouped() and strpos($pelement->get_path() .  '/', $element->get_path()) === 0) {
184                 return true;
185             }
186         }
187         return false; // no grouped parent found
188     }
190     /**
191      * To conditionally decide if one step will be executed or no
192      *
193      * For steps needing to be executed conditionally, based in dynamic
194      * conditions (at execution time vs at declaration time) you must
195      * override this function. It will return true if the step must be
196      * executed and false if not
197      */
198     protected function execute_condition() {
199         return true;
200     }
202     /**
203      * To send ids pairs to backup_ids_table and to store them into paths
204      *
205      * This method will send the given itemname and old/new ids to the
206      * backup_ids_temp table, and, at the same time, will save the new id
207      * into the corresponding restore_path_element for easier access
208      * by children. Also will inject the known old context id for the task
209      * in case it's going to be used for restoring files later
210      */
211     protected function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null) {
212         // If we haven't specified one context for the files, use the task one
213         if ($filesctxid == null) {
214             $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null;
215         } else { // Use the specified one
216             $parentitemid = $restorefiles ? $filesctxid : null;
217         }
218         // Let's call the low level one
219         restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid);
220         // Now, if the itemname matches any pathelement->name, store the latest $newid
221         if (array_key_exists($itemname, $this->elementsoldid)) { // If present in  $this->elementsoldid, is valid, put both ids
222             $this->elementsoldid[$itemname] = $oldid;
223             $this->elementsnewid[$itemname] = $newid;
224         }
225     }
227     /**
228      * Returns the latest (parent) old id mapped by one pathelement
229      */
230     protected function get_old_parentid($itemname) {
231         return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null;
232     }
234     /**
235      * Returns the latest (parent) new id mapped by one pathelement
236      */
237     protected function get_new_parentid($itemname) {
238         return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null;
239     }
241     /**
242      * Return the new id of a mapping for the given itemname
243      *
244      */
245     protected function get_mappingid($itemname, $oldid) {
246         $mapping = $this->get_mapping($itemname, $oldid);
247         return $mapping ? $mapping->newitemid : false;
248     }
250     /**
251      * Return the complete mapping from the given itemname, itemid
252      */
253     protected function get_mapping($itemname, $oldid) {
254         return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid);
255     }
257     /**
258      * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
259      */
260     protected function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null) {
261         $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
262         restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
263                                           $filearea, $filesctxid, $mappingitemname);
264     }
266     /**
267      * Apply course startdate offset based in original course startdate and course_offset_startdate setting
268      * Note we are using one static cache here, but *by restoreid*, so it's ok for concurrence/multiple
269      * executions in the same request
270      */
271     protected function apply_date_offset($value) {
273         static $cache = array();
274         // Lookup cache
275         if (isset($cache[$this->get_restoreid()])) {
276             return $value + $cache[$this->get_restoreid()];
277         }
278         // No cache, let's calculate the offset
279         $original = $this->task->get_info()->original_course_startdate;
280         $setting  = $this->get_setting_value('course_startdate');
282         // Original course has not startdate, offset = 0
283         if (empty($original)) {
284             $cache[$this->get_restoreid()] = 0;
286         // Less than 24h of difference, offset = 0 (this avoids some problems with timezones)
287         } else if (abs($setting - $original) < 24 * 60 * 60) {
288             $cache[$this->get_restoreid()] = 0;
290         // Re-enforce 'moodle/restore:rolldates' capability for the user in the course, just in case
291         } else if (!has_capability('moodle/restore:rolldates',
292                                    get_context_instance(CONTEXT_COURSE, $this->get_courseid()),
293                                    $this->task->get_userid())) {
294             $cache[$this->get_restoreid()] = 0;
296         // Arrived here, let's calculate the real offset
297         } else {
298             $cache[$this->get_restoreid()] = $setting - $original;
299         }
301         // Return the passed value with cached offset applied
302         return $value + $cache[$this->get_restoreid()];
303     }
305     /**
306      * Function that will return the structure to be processed by this restore_step.
307      * Must return one array of @restore_path_element elements
308      */
309     abstract protected function define_structure();