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