MDL-52523 backup of subplugins: add missing global
[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
4fb31ae6 104 // Set up progress tracking.
105 $progress = $this->get_task()->get_progress();
809fdb83 106 $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
4fb31ae6 107 $xmlparser->set_progress($progress);
108
69023455
EL
109 // And process it, dispatch to target methods in step will start automatically
110 $xmlparser->process();
69023455 111
41941110
EL
112 // Have finished, launch the after_execute method of all the processing objects
113 $this->launch_after_execute_methods();
37ff843d 114 $progress->end_progress();
2df0f295 115 }
69023455
EL
116
117 /**
118 * Receive one chunk of information form the xml parser processor and
119 * dispatch it, following the naming rules
120 */
0149e6c7 121 final public function process($data) {
69023455
EL
122 if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen
123 throw new restore_step_exception('restore_structure_step_missing_path', $data['path']);
124 }
125 $element = $this->pathelements[$data['path']];
126 $object = $element->get_processing_object();
127 $method = $element->get_processing_method();
0149e6c7 128 $rdata = null;
69023455
EL
129 if (empty($object)) { // No processing object defined
130 throw new restore_step_exception('restore_structure_step_missing_pobject', $object);
131 }
0149e6c7
EL
132 // Release the lock if we aren't anymore within children of it
133 if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
134 $this->pathlock = null;
135 }
136 if (is_null($this->pathlock)) { // Only dispatch if there isn't any lock
137 $rdata = $object->$method($data['tags']); // Dispatch to proper object/method
138 }
139
140 // If the dispatched method returns SKIP_ALL_CHILDREN, we grab current path in order to
141 // lock dispatching to any children
142 if ($rdata === self::SKIP_ALL_CHILDREN) {
143 // Check we haven't any previous lock
144 if (!is_null($this->pathlock)) {
145 throw new restore_step_exception('restore_structure_step_already_skipping', $data['path']);
146 }
147 // Set the lock
148 $this->pathlock = $data['path'] . '/'; // Lock everything below current path
149
150 // Continue with normal processing of return values
151 } else if ($rdata !== null) { // If the method has returned any info, set element data to it
69023455
EL
152 $element->set_data($rdata);
153 } else { // Else, put the original parsed data
154 $element->set_data($data);
155 }
156 }
157
2df0f295
EL
158 /**
159 * To send ids pairs to backup_ids_table and to store them into paths
160 *
161 * This method will send the given itemname and old/new ids to the
162 * backup_ids_temp table, and, at the same time, will save the new id
163 * into the corresponding restore_path_element for easier access
164 * by children. Also will inject the known old context id for the task
165 * in case it's going to be used for restoring files later
166 */
2a918d8f
EL
167 public function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) {
168 if ($restorefiles && $parentid) {
169 throw new restore_step_exception('set_mapping_cannot_specify_both_restorefiles_and_parentitemid');
170 }
c0440b3f 171 // If we haven't specified one context for the files, use the task one
2a918d8f 172 if (is_null($filesctxid)) {
c0440b3f
EL
173 $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null;
174 } else { // Use the specified one
175 $parentitemid = $restorefiles ? $filesctxid : null;
176 }
2a918d8f
EL
177 // We have passed one explicit parentid, apply it
178 $parentitemid = !is_null($parentid) ? $parentid : $parentitemid;
179
2df0f295
EL
180 // Let's call the low level one
181 restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid);
182 // Now, if the itemname matches any pathelement->name, store the latest $newid
183 if (array_key_exists($itemname, $this->elementsoldid)) { // If present in $this->elementsoldid, is valid, put both ids
184 $this->elementsoldid[$itemname] = $oldid;
185 $this->elementsnewid[$itemname] = $newid;
186 }
187 }
188
189 /**
190 * Returns the latest (parent) old id mapped by one pathelement
191 */
91d11057 192 public function get_old_parentid($itemname) {
2df0f295
EL
193 return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null;
194 }
195
196 /**
197 * Returns the latest (parent) new id mapped by one pathelement
198 */
91d11057 199 public function get_new_parentid($itemname) {
2df0f295
EL
200 return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null;
201 }
202
203 /**
204 * Return the new id of a mapping for the given itemname
4b43b107
EL
205 *
206 * @param string $itemname the type of item
98529b00 207 * @param int $oldid the item ID from the backup
4b43b107 208 * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false
2df0f295 209 */
98529b00 210 public function get_mappingid($itemname, $oldid, $ifnotfound = false) {
2df0f295 211 $mapping = $this->get_mapping($itemname, $oldid);
98529b00 212 return $mapping ? $mapping->newitemid : $ifnotfound;
2df0f295
EL
213 }
214
215 /**
216 * Return the complete mapping from the given itemname, itemid
217 */
91d11057 218 public function get_mapping($itemname, $oldid) {
2df0f295
EL
219 return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid);
220 }
221
222 /**
223 * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
224 */
f2745cbe 225 public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
37ff843d 226 // If the current progress object is set up and ready to receive
227 // indeterminate progress, then use it, otherwise don't. (This check is
228 // just in case this function is ever called from somewhere not within
229 // the execute() method here, which does set up progress like this.)
230 $progress = $this->get_task()->get_progress();
231 if (!$progress->is_in_progress_section() ||
809fdb83 232 $progress->get_current_max() !== \core\progress\base::INDETERMINATE) {
37ff843d 233 $progress = null;
234 }
235
c0440b3f 236 $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
3b232aef 237 $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
37ff843d 238 $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid, null, false,
239 $progress);
3b232aef
FM
240 $resultstoadd = array();
241 foreach ($results as $result) {
242 $this->log($result->message, $result->level);
243 $resultstoadd[$result->code] = true;
244 }
245 $this->task->add_result($resultstoadd);
2df0f295
EL
246 }
247
41941110
EL
248 /**
249 * As far as restore structure steps are implementing restore_plugin stuff, they need to
250 * have the parent task available for wrapping purposes (get course/context....)
7a7b8a1f 251 * @return restore_task|null
41941110
EL
252 */
253 public function get_task() {
254 return $this->task;
255 }
256
91d11057
EL
257// Protected API starts here
258
41941110
EL
259 /**
260 * Add plugin structure to any element in the structure restore tree
261 *
46f6f7f2 262 * @param string $plugintype type of plugin as defined by core_component::get_plugin_types()
41941110
EL
263 * @param restore_path_element $element element in the structure restore tree that
264 * we are going to add plugin information to
265 */
266 protected function add_plugin_structure($plugintype, $element) {
267
268 global $CFG;
269
270 // Check the requested plugintype is a valid one
46f6f7f2 271 if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) {
41941110
EL
272 throw new restore_step_exception('incorrect_plugin_type', $plugintype);
273 }
274
275 // Get all the restore path elements, looking across all the plugin dirs
bd3b3bba 276 $pluginsdirs = core_component::get_plugin_list($plugintype);
41941110
EL
277 foreach ($pluginsdirs as $name => $pluginsdir) {
278 // We need to add also backup plugin classes on restore, they may contain
279 // some stuff used both in backup & restore
280 $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin';
281 $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php';
282 if (file_exists($backupfile)) {
283 require_once($backupfile);
284 }
285 // Now add restore plugin classes and prepare stuff
286 $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin';
287 $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php';
288 if (file_exists($restorefile)) {
289 require_once($restorefile);
290 $restoreplugin = new $restoreclassname($plugintype, $name, $this);
291 // Add plugin paths to the step
292 $this->prepare_pathelements($restoreplugin->define_plugin_structure($element));
293 }
294 }
295 }
296
6a45e6b1
EL
297 /**
298 * Add subplugin structure for a given plugin to any element in the structure restore tree
299 *
300 * This method allows the injection of subplugins (of a specific plugin) parsing and proccessing
301 * to any element in the restore structure.
302 *
303 * NOTE: Initially subplugins were only available for activities (mod), so only the
304 * {@link restore_activity_structure_step} class had support for them, always
305 * looking for /mod/modulenanme subplugins. This new method is a generalization of the
306 * existing one for activities, supporting all subplugins injecting information everywhere.
307 *
308 * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.php.
309 * @param restore_path_element $element element in the structure restore tree that
310 * we are going to add subplugin information to.
311 * @param string $plugintype type of the plugin.
312 * @param string $pluginname name of the plugin.
313 * @return void
314 */
315 protected function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) {
8eef9244
TH
316 global $CFG;
317 // This global declaration is required, because where we do require_once($backupfile);
318 // That file may in turn try to do require_once($CFG->dirroot ...).
319 // That worked in the past, we should keep it working.
6a45e6b1
EL
320
321 // Verify if this is a BC call for an activity restore. See NOTE above for this special case.
322 if ($plugintype === null and $pluginname === null) {
323 $plugintype = 'mod';
324 $pluginname = $this->task->get_modulename();
325 // TODO: Once all the calls have been changed to add both not null plugintype and pluginname, add a debugging here.
326 }
327
328 // Check the requested plugintype is a valid one.
329 if (!array_key_exists($plugintype, core_component::get_plugin_types())) {
330 throw new restore_step_exception('incorrect_plugin_type', $plugintype);
331 }
332
333 // Check the requested pluginname, for the specified plugintype, is a valid one.
334 if (!array_key_exists($pluginname, core_component::get_plugin_list($plugintype))) {
335 throw new restore_step_exception('incorrect_plugin_name', array($plugintype, $pluginname));
336 }
337
338 // Check the requested subplugintype is a valid one.
339 $subpluginsfile = core_component::get_component_directory($plugintype . '_' . $pluginname) . '/db/subplugins.php';
340 if (!file_exists($subpluginsfile)) {
341 throw new restore_step_exception('plugin_missing_subplugins_php_file', array($plugintype, $pluginname));
342 }
343 include($subpluginsfile);
344 if (!array_key_exists($subplugintype, $subplugins)) {
345 throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
346 }
347
348 // Every subplugin optionally can have a common/parent subplugin
349 // class for shared stuff.
350 $parentclass = 'restore_' . $plugintype . '_' . $pluginname . '_' . $subplugintype . '_subplugin';
351 $parentfile = core_component::get_component_directory($plugintype . '_' . $pluginname) .
352 '/backup/moodle2/' . $parentclass . '.class.php';
353 if (file_exists($parentfile)) {
354 require_once($parentfile);
355 }
356
357 // Get all the restore path elements, looking across all the subplugin dirs.
358 $subpluginsdirs = core_component::get_plugin_list($subplugintype);
359 foreach ($subpluginsdirs as $name => $subpluginsdir) {
360 $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
361 $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
362 if (file_exists($restorefile)) {
363 require_once($restorefile);
364 $restoresubplugin = new $classname($subplugintype, $name, $this);
365 // Add subplugin paths to the step.
366 $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
367 }
368 }
369 }
370
41941110
EL
371 /**
372 * Launch all the after_execute methods present in all the processing objects
373 *
374 * This method will launch all the after_execute methods that can be defined
375 * both in restore_plugin and restore_structure_step classes
376 *
377 * For restore_plugin classes the name of the method to be executed will be
378 * "after_execute_" + connection point (as far as can be multiple connection
379 * points in the same class)
380 *
381 * For restore_structure_step classes is will be, simply, "after_execute". Note
382 * that this is executed *after* the plugin ones
383 */
384 protected function launch_after_execute_methods() {
385 $alreadylaunched = array(); // To avoid multiple executions
386 foreach ($this->pathelements as $key => $pathelement) {
387 // Get the processing object
388 $pobject = $pathelement->get_processing_object();
389 // Skip null processors (child of grouped ones for sure)
390 if (is_null($pobject)) {
391 continue;
392 }
393 // Skip restore structure step processors (this)
394 if ($pobject instanceof restore_structure_step) {
395 continue;
396 }
397 // Skip already launched processing objects
398 if (in_array($pobject, $alreadylaunched, true)) {
399 continue;
400 }
401 // Add processing object to array of launched ones
402 $alreadylaunched[] = $pobject;
403 // If the processing object has support for
404 // launching after_execute methods, use it
405 if (method_exists($pobject, 'launch_after_execute_methods')) {
406 $pobject->launch_after_execute_methods();
407 }
408 }
409 // Finally execute own (restore_structure_step) after_execute method
410 $this->after_execute();
411
412 }
413
c44299c1
EL
414 /**
415 * Launch all the after_restore methods present in all the processing objects
416 *
417 * This method will launch all the after_restore methods that can be defined
418 * both in restore_plugin class
419 *
420 * For restore_plugin classes the name of the method to be executed will be
421 * "after_restore_" + connection point (as far as can be multiple connection
422 * points in the same class)
423 */
424 public function launch_after_restore_methods() {
425 $alreadylaunched = array(); // To avoid multiple executions
426 foreach ($this->pathelements as $pathelement) {
427 // Get the processing object
428 $pobject = $pathelement->get_processing_object();
429 // Skip null processors (child of grouped ones for sure)
430 if (is_null($pobject)) {
431 continue;
432 }
433 // Skip restore structure step processors (this)
434 if ($pobject instanceof restore_structure_step) {
435 continue;
436 }
437 // Skip already launched processing objects
438 if (in_array($pobject, $alreadylaunched, true)) {
439 continue;
440 }
441 // Add processing object to array of launched ones
442 $alreadylaunched[] = $pobject;
443 // If the processing object has support for
444 // launching after_restore methods, use it
445 if (method_exists($pobject, 'launch_after_restore_methods')) {
446 $pobject->launch_after_restore_methods();
447 }
448 }
1cf121e1 449 // Finally execute own (restore_structure_step) after_restore method
450 $this->after_restore();
c44299c1
EL
451 }
452
91d11057
EL
453 /**
454 * This method will be executed after the whole structure step have been processed
455 *
456 * After execution method for code needed to be executed after the whole structure
457 * has been processed. Useful for cleaning tasks, files process and others. Simply
458 * overwrite in in your steps if needed
459 */
460 protected function after_execute() {
461 // do nothing by default
462 }
463
1cf121e1 464 /**
465 * This method will be executed after the rest of the restore has been processed.
466 *
467 * Use if you need to update IDs based on things which are restored after this
468 * step has completed.
469 */
470 protected function after_restore() {
471 // do nothing by default
472 }
473
91d11057
EL
474 /**
475 * Prepare the pathelements for processing, looking for duplicates, applying
476 * processing objects and other adjustments
477 */
478 protected function prepare_pathelements($elementsarr) {
479
480 // First iteration, push them to new array, indexed by name
481 // detecting duplicates in names or paths
482 $names = array();
483 $paths = array();
484 foreach($elementsarr as $element) {
485 if (!$element instanceof restore_path_element) {
486 throw new restore_step_exception('restore_path_element_wrong_class', get_class($element));
487 }
488 if (array_key_exists($element->get_name(), $names)) {
489 throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
490 }
491 if (array_key_exists($element->get_path(), $paths)) {
492 throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
493 }
494 $names[$element->get_name()] = true;
495 $paths[$element->get_path()] = $element;
496 }
497 // Now, for each element not having one processing object, if
498 // not child of grouped element, assign $this (the step itself) as processing element
499 // Note method must exist or we'll get one @restore_path_element_exception
500 foreach($paths as $key => $pelement) {
501 if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
502 $paths[$key]->set_processing_object($this);
503 }
504 // Populate $elementsoldid and $elementsoldid based on available pathelements
505 $this->elementsoldid[$pelement->get_name()] = null;
506 $this->elementsnewid[$pelement->get_name()] = null;
507 }
508 // Done, add them to pathelements (dupes by key - path - are discarded)
509 $this->pathelements = array_merge($this->pathelements, $paths);
510 }
511
512 /**
513 * Given one pathelement, return true if grouped parent was found
514 */
515 protected function grouped_parent_exists($pelement, $elements) {
516 foreach ($elements as $element) {
517 if ($pelement->get_path() == $element->get_path()) {
518 continue; // Don't compare against itself
519 }
520 // If element is grouped and parent of pelement, return true
521 if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
522 return true;
523 }
524 }
525 return false; // no grouped parent found
526 }
527
528 /**
529 * To conditionally decide if one step will be executed or no
530 *
531 * For steps needing to be executed conditionally, based in dynamic
532 * conditions (at execution time vs at declaration time) you must
533 * override this function. It will return true if the step must be
534 * executed and false if not
535 */
536 protected function execute_condition() {
537 return true;
538 }
539
69023455
EL
540 /**
541 * Function that will return the structure to be processed by this restore_step.
542 * Must return one array of @restore_path_element elements
543 */
544 abstract protected function define_structure();
545}