Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Backup controller and related exception classes.
19
 *
20
 * @package core_backup
21
 * @subpackage backup-controller
22
 * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
23
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
/**
27
 * Class implementing the controller of any backup process
28
 *
29
 * This final class is in charge of controlling all the backup architecture, for any
30
 * type of backup. Based in type, format, interactivity and target, it stores the
31
 * whole execution plan and settings that will be used later by the @backup_worker,
32
 * applies all the defaults, performs all the security contraints and is in charge
33
 * of handling the ui if necessary. Also logging strategy is defined here.
34
 *
35
 * Note the class is 100% neutral and usable for *any* backup. It just stores/requests
36
 * all the needed information from other backup classes in order to have everything well
37
 * structured in order to allow the @backup_worker classes to do their job.
38
 *
39
 * In other words, a mammoth class, but don't worry, practically everything is delegated/
40
 * aggregated!)
41
 */
42
class backup_controller extends base_controller {
43
    /** @var string Unique identifier for this backup */
44
    protected $backupid;
45
 
46
    /**
47
     * Type of item that is being stored in the backup.
48
     *
49
     * Should be selected from one of the backup::TYPE_ constants
50
     * for example backup::TYPE_1ACTIVITY
51
     *
52
     * @var string
53
     */
54
    protected $type;
55
 
56
    /** @var int Course/section/course_module id to backup */
57
    protected $id;
58
 
59
    /** @var false|int The id of the course the backup belongs to, or false if no course. */
60
    protected $courseid;
61
 
62
    /**
63
     * Format of backup (moodle, imscc).
64
     *
65
     * Should be one of the backup::FORMAT_ constants.
66
     * for example backup::FORMAT_MOODLE
67
     *
68
     * @var string
69
     */
70
    protected $format;
71
 
72
    /**
73
     * Whether this backup will require user interaction.
74
     *
75
     * Should be one of backup::INTERACTIVE_YES or INTERACTIVE_NO
76
     *
77
     * @var bool
78
     */
79
    protected $interactive;
80
 
81
    /**
82
     * Purpose of the backup (default settings)
83
     *
84
     * Should be one of the the backup::MODE_ constants,
85
     * for example backup::MODE_GENERAL
86
     *
87
     * @var int
88
     */
89
    protected $mode;
90
 
91
    /** @var int The id of the user executing the backup. */
92
    protected $userid;
93
 
94
    /**
95
     * Type of operation (backup/restore)
96
     *
97
     * Should be selected from: backup::OPERATION_BACKUP or OPERATION_RESTORE
98
     *
99
     * @var string
100
     */
101
    protected $operation;
102
 
103
    /**
104
     * Current status of the controller (created, planned, configured...)
105
     *
106
     * It should be one of the backup::STATUS_ constants,
107
     * for example backup::STATUS_AWAITING.
108
     *
109
     * @var int
110
     */
111
    protected $status;
112
 
113
    /** @var backup_plan Backup execution plan. */
114
    protected $plan;
115
 
116
    /** @var int Whether this backup includes files (1) or not (0). */
117
    protected $includefiles;
118
 
119
    /**
120
     * Immediate/delayed execution type.
121
     * @var int
122
     */
123
    protected $execution;
124
 
125
    /** @var int Epoch time when we want the backup to be executed (requires cron to run). */
126
    protected $executiontime;
127
 
128
    /** @var null Destination chain object (fs_moodle, fs_os, db, email...). */
129
    protected $destination;
130
 
131
    /** @var string Cache {@see \checksumable} results for lighter {@see \backup_controller::is_checksum_correct()} uses. */
132
    protected $checksum;
133
 
134
    /**
135
     * The role ids to keep in a copy operation.
136
     * @var array
137
     */
138
    protected $keptroles = array();
139
 
140
    /**
141
     * Constructor for the backup controller class.
142
     *
143
     * @param string $type Type of the backup; One of backup::TYPE_1COURSE, TYPE_1SECTION, TYPE_1ACTIVITY
144
     * @param int $id The ID of the item to backup; e.g the course id
145
     * @param string $format The backup format to use; Most likely backup::FORMAT_MOODLE
146
     * @param bool $interactive Whether this backup will require user interaction; backup::INTERACTIVE_YES or INTERACTIVE_NO
147
     * @param int $mode One of backup::MODE_GENERAL, MODE_IMPORT, MODE_SAMESITE, MODE_HUB, MODE_AUTOMATED
148
     * @param int $userid The id of the user making the backup
149
     * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
150
     */
151
    public function __construct($type, $id, $format, $interactive, $mode, $userid, $releasesession = backup::RELEASESESSION_NO) {
152
        $this->type = $type;
153
        $this->id   = $id;
154
        $this->courseid = backup_controller_dbops::get_courseid_from_type_id($this->type, $this->id);
155
        $this->format = $format;
156
        $this->interactive = $interactive;
157
        $this->mode = $mode;
158
        $this->userid = $userid;
159
        $this->releasesession = $releasesession;
160
 
161
        // Apply some defaults
162
        $this->operation = backup::OPERATION_BACKUP;
163
        $this->executiontime = 0;
164
        $this->checksum = '';
165
 
166
        // Set execution based on backup mode.
167
        if ($mode == backup::MODE_ASYNC || $mode == backup::MODE_COPY) {
168
            $this->execution = backup::EXECUTION_DELAYED;
169
        } else {
170
            $this->execution = backup::EXECUTION_INMEDIATE;
171
        }
172
 
173
        // Apply current backup version and release if necessary
174
        backup_controller_dbops::apply_version_and_release();
175
 
176
        // Check format and type are correct
177
        backup_check::check_format_and_type($this->format, $this->type);
178
 
179
        // Check id is correct
180
        backup_check::check_id($this->type, $this->id);
181
 
182
        // Check user is correct
183
        backup_check::check_user($this->userid);
184
 
185
        // Calculate unique $backupid
186
        $this->calculate_backupid();
187
 
188
        // Default logger chain (based on interactive/execution)
189
        $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid);
190
 
191
        // By default there is no progress reporter. Interfaces that wish to
192
        // display progress must set it.
193
        $this->progress = new \core\progress\none();
194
 
195
        // Instantiate the output_controller singleton and active it if interactive and immediate.
196
        $oc = output_controller::get_instance();
197
        if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
198
            $oc->set_active(true);
199
        }
200
 
201
        $this->log('instantiating backup controller', backup::LOG_INFO, $this->backupid);
202
 
203
        // Default destination chain (based on type/mode/execution)
204
        $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution);
205
 
206
        // Set initial status
207
        $this->set_status(backup::STATUS_CREATED);
208
 
209
        // Load plan (based on type/format)
210
        $this->load_plan();
211
 
212
        // Apply all default settings (based on type/format/mode)
213
        $this->apply_defaults();
214
 
215
        // Perform all initial security checks and apply (2nd param) them to settings automatically
216
        backup_check::check_security($this, true);
217
 
218
        // Set status based on interactivity
219
        if ($this->interactive == backup::INTERACTIVE_YES) {
220
            $this->set_status(backup::STATUS_SETTING_UI);
221
        } else {
222
            $this->set_status(backup::STATUS_AWAITING);
223
        }
224
    }
225
 
226
    /**
227
     * Clean structures used by the backup_controller
228
     *
229
     * This method clean various structures used by the backup_controller,
230
     * destroying them in an ordered way, so their memory will be gc properly
231
     * by PHP (mainly circular references).
232
     *
233
     * Note that, while it's not mandatory to execute this method, it's highly
234
     * recommended to do so, specially in scripts performing multiple operations
235
     * (like the automated backups) or the system will run out of memory after
236
     * a few dozens of backups)
237
     */
238
    public function destroy() {
239
        // Only need to destroy circulars under the plan. Delegate to it.
240
        $this->plan->destroy();
241
        // Loggers may have also chained references, destroy them. Also closing resources when needed.
242
        $this->logger->destroy();
243
    }
244
 
245
    /**
246
     * Declare that all user interaction with the backup controller is complete.
247
     *
248
     * After this the backup controller is waiting for processing.
249
     */
250
    public function finish_ui() {
251
        if ($this->status != backup::STATUS_SETTING_UI) {
252
            throw new backup_controller_exception('cannot_finish_ui_if_not_setting_ui');
253
        }
254
        $this->set_status(backup::STATUS_AWAITING);
255
    }
256
 
257
    /**
258
     * Validates the backup is valid after any user changes.
259
     *
260
     * A backup_controller_exception will be thrown if there is an issue.
261
     */
262
    public function process_ui_event() {
263
 
264
        // Perform security checks throwing exceptions (2nd param) if something is wrong
265
        backup_check::check_security($this, false);
266
    }
267
 
268
    /**
269
     * Sets the new status of the backup.
270
     *
271
     * @param int $status
272
     */
273
    public function set_status($status) {
274
        // Note: never save_controller() with the object info after STATUS_EXECUTING or the whole controller,
275
        // containing all the steps will be sent to DB. 100% (monster) useless.
276
        $this->log('setting controller status to', backup::LOG_DEBUG, $status);
277
        // TODO: Check it's a correct status.
278
        $this->status = $status;
279
        // Ensure that, once set to backup::STATUS_AWAITING, controller is stored in DB.
280
        // Also save if executing so we can better track progress.
281
        if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_EXECUTING) {
282
            $this->save_controller();
283
            $tbc = self::load_controller($this->backupid);
284
            $this->logger = $tbc->logger; // wakeup loggers
285
            $tbc->plan->destroy(); // Clean plan controller structures, keeping logger alive.
286
 
287
        } else if ($status == backup::STATUS_FINISHED_OK) {
288
            // If the operation has ended without error (backup::STATUS_FINISHED_OK)
289
            // proceed by cleaning the object from database. MDL-29262.
290
            $this->save_controller(false, true);
291
        } else if ($status == backup::STATUS_FINISHED_ERR) {
292
            // If the operation has ended with an error save the controller
293
            // preserving the object in the database. We may want it for debugging.
294
            $this->save_controller();
295
        }
296
    }
297
 
298
    /**
299
     * Sets if the backup will be processed immediately, or later.
300
     *
301
     * @param int $execution Use backup::EXECUTION_INMEDIATE or backup::EXECUTION_DELAYED
302
     * @param int $executiontime The timestamp in the future when the task should be executed, or 0 for immediately.
303
     */
304
    public function set_execution($execution, $executiontime = 0) {
305
        $this->log('setting controller execution', backup::LOG_DEBUG);
306
        // TODO: Check valid execution mode.
307
        // TODO: Check time in future.
308
        // TODO: Check time = 0 if immediate.
309
        $this->execution = $execution;
310
        $this->executiontime = $executiontime;
311
 
312
        // Default destination chain (based on type/mode/execution)
313
        $this->destination = backup_factory::get_destination_chain($this->type, $this->id, $this->mode, $this->execution);
314
 
315
        // Default logger chain (based on interactive/execution)
316
        $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid);
317
    }
318
 
319
// checksumable interface methods
320
 
321
    public function calculate_checksum() {
322
        // Reset current checksum to take it out from calculations!
323
        $this->checksum = '';
324
        // Init checksum
325
        $tempchecksum = md5('backupid-'   . $this->backupid .
326
                            'type-'       . $this->type .
327
                            'id-'         . $this->id .
328
                            'format-'     . $this->format .
329
                            'interactive-'. $this->interactive .
330
                            'mode-'       . $this->mode .
331
                            'userid-'     . $this->userid .
332
                            'operation-'  . $this->operation .
333
                            'status-'     . $this->status .
334
                            'execution-'  . $this->execution .
335
                            'plan-'       . backup_general_helper::array_checksum_recursive(array($this->plan)) .
336
                            'destination-'. backup_general_helper::array_checksum_recursive(array($this->destination)) .
337
                            'logger-'     . backup_general_helper::array_checksum_recursive(array($this->logger)));
338
        $this->log('calculating controller checksum', backup::LOG_DEBUG, $tempchecksum);
339
        return $tempchecksum;
340
    }
341
 
342
    public function is_checksum_correct($checksum) {
343
        return $this->checksum === $checksum;
344
    }
345
 
346
    /**
347
     * Gets the unique identifier for this backup controller.
348
     *
349
     * @return string
350
     */
351
    public function get_backupid() {
352
        return $this->backupid;
353
    }
354
 
355
    /**
356
     * Gets the type of backup to be performed.
357
     *
358
     * Use {@see \backup_controller::get_id()} to find the instance being backed up.
359
     *
360
     * @return string
361
     */
362
    public function get_type() {
363
        return $this->type;
364
    }
365
 
366
    /**
367
     * Returns the current value of the include_files setting.
368
     * This setting is intended to ensure that files are not included in
369
     * generated backups.
370
     *
371
     * @return int Indicates whether files should be included in backups.
372
     */
373
    public function get_include_files() {
374
        return $this->includefiles;
375
    }
376
 
377
    /**
378
     * Returns the default value for $this->includefiles before we consider any settings.
379
     *
380
     * @return bool
381
     * @throws dml_exception
382
     */
383
    protected function get_include_files_default(): bool {
384
        // We normally include files.
385
        $includefiles = true;
386
 
387
        // In an import, we don't need to include files.
388
        if ($this->get_mode() === backup::MODE_IMPORT) {
389
            $includefiles = false;
390
        }
391
 
392
        // When a backup is intended for the same site, we don't need to include the files.
393
        // Note, this setting is only used for duplication of an entire course.
394
        if ($this->get_mode() === backup::MODE_SAMESITE || $this->get_mode() === backup::MODE_COPY) {
395
            $includefiles = false;
396
        }
397
 
398
        // If backup is automated and we have set auto backup config to exclude
399
        // files then set them to be excluded here.
400
        $backupautofiles = (bool) get_config('backup', 'backup_auto_files');
401
        if ($this->get_mode() === backup::MODE_AUTOMATED && !$backupautofiles) {
402
            $includefiles = false;
403
        }
404
 
405
        return $includefiles;
406
    }
407
 
408
    /**
409
     * Gets if this is a backup or restore.
410
     *
411
     * @return string
412
     */
413
    public function get_operation() {
414
        return $this->operation;
415
    }
416
 
417
    /**
418
     * Gets the instance id of the item being backed up.
419
     *
420
     * It's meaning is related to the type of backup {@see \backup_controller::get_type()}.
421
     *
422
     * @return int
423
     */
424
    public function get_id() {
425
        return $this->id;
426
    }
427
 
428
    /**
429
     * Gets the course that the item being backed up is in.
430
     *
431
     * @return false|int
432
     */
433
    public function get_courseid() {
434
        return $this->courseid;
435
    }
436
 
437
    /**
438
     * Gets the format the backup is stored in.
439
     *
440
     * @return string
441
     */
442
    public function get_format() {
443
        return $this->format;
444
    }
445
 
446
    /**
447
     * Gets if user interaction is expected during the backup.
448
     *
449
     * @return bool
450
     */
451
    public function get_interactive() {
452
        return $this->interactive;
453
    }
454
 
455
    /**
456
     * Gets the mode that the backup will be performed in.
457
     *
458
     * @return int
459
     */
460
    public function get_mode() {
461
        return $this->mode;
462
    }
463
 
464
    /**
465
     * Get the id of the user who started the backup.
466
     *
467
     * @return int
468
     */
469
    public function get_userid() {
470
        return $this->userid;
471
    }
472
 
473
    /**
474
     * Get the current status of the backup.
475
     *
476
     * @return int
477
     */
478
    public function get_status() {
479
        return $this->status;
480
    }
481
 
482
    /**
483
     * Get if the backup will be executed immediately, or later.
484
     *
485
     * @return int
486
     */
487
    public function get_execution() {
488
        return $this->execution;
489
    }
490
 
491
    /**
492
     * Get when the backup will be executed.
493
     *
494
     * @return int 0 means now, otherwise a Unix timestamp
495
     */
496
    public function get_executiontime() {
497
        return $this->executiontime;
498
    }
499
 
500
    /**
501
     * Gets the plan that will be run during the backup.
502
     *
503
     * @return backup_plan
504
     */
505
    public function get_plan() {
506
        return $this->plan;
507
    }
508
 
509
    /**
510
     * For debug only. Get a simple test display of all the settings.
511
     *
512
     * @return string
513
     */
514
    public function debug_display_all_settings_values(): string {
515
        return $this->get_plan()->debug_display_all_settings_values();
516
    }
517
 
518
    /**
519
     * Sets the user roles that should be kept in the destination course
520
     * for a course copy operation.
521
     *
522
     * @param array $roleids
523
     * @throws backup_controller_exception
524
     */
525
    public function set_kept_roles(array $roleids): void {
526
        // Only allow of keeping user roles when controller is in copy mode.
527
        if ($this->mode != backup::MODE_COPY) {
528
            throw new backup_controller_exception('cannot_set_keep_roles_wrong_mode');
529
        }
530
 
531
        $this->keptroles = $roleids;
532
    }
533
 
534
    /**
535
     * Executes the backup
536
     * @return void Throws and exception of completes
537
     */
538
    public function execute_plan() {
539
        // Basic/initial prevention against time/memory limits
540
        core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted
541
        raise_memory_limit(MEMORY_EXTRA);
542
 
543
        // Release the session so other tabs in the same session are not blocked.
544
        if ($this->get_releasesession() === backup::RELEASESESSION_YES) {
545
            \core\session\manager::write_close();
546
        }
547
 
548
        // If the controller has decided that we can include files, then check the setting, otherwise do not include files.
549
        if ($this->get_include_files()) {
550
            $this->set_include_files((bool) $this->get_plan()->get_setting('files')->get_value());
551
        }
552
 
553
        // If this is not a course backup, or single activity backup (e.g. duplicate) inform the plan we are not
554
        // including all the activities for sure. This will affect any
555
        // task/step executed conditionally to stop including information
556
        // for section and activity backup. MDL-28180.
557
        if ($this->get_type() !== backup::TYPE_1COURSE && $this->get_type() !== backup::TYPE_1ACTIVITY) {
558
            $this->log('notifying plan about excluded activities by type', backup::LOG_DEBUG);
559
            $this->plan->set_excluding_activities();
560
        }
561
 
562
        // Handle copy operation specific settings.
563
        if ($this->mode == backup::MODE_COPY) {
564
            $this->plan->set_kept_roles($this->keptroles);
565
        }
566
 
567
        return $this->plan->execute();
568
    }
569
 
570
    /**
571
     * Gets the results of the plan execution for this backup.
572
     *
573
     * @return array
574
     */
575
    public function get_results() {
576
        return $this->plan->get_results();
577
    }
578
 
579
    /**
580
     * Save controller information
581
     *
582
     * @param bool $includeobj to decide if the object itself must be updated (true) or no (false)
583
     * @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false)
584
     */
585
    public function save_controller($includeobj = true, $cleanobj = false) {
586
        // Going to save controller to persistent storage, calculate checksum for later checks and save it.
587
        // TODO: flag the controller as NA. Any operation on it should be forbidden until loaded back.
588
        $this->log('saving controller to db', backup::LOG_DEBUG);
589
        if ($includeobj ) {  // Only calculate checksum if we are going to include the object.
590
            $this->checksum = $this->calculate_checksum();
591
        }
592
        backup_controller_dbops::save_controller($this, $this->checksum, $includeobj, $cleanobj);
593
    }
594
 
595
    /**
596
     * Loads a backup controller from the database.
597
     *
598
     * @param string $backupid The id of the backup controller.
599
     * @return \backup_controller
600
     */
601
    public static function load_controller($backupid) {
602
        // Load controller from persistent storage
603
        // TODO: flag the controller as available. Operations on it can continue
604
        $controller = backup_controller_dbops::load_controller($backupid);
605
        $controller->log('loading controller from db', backup::LOG_DEBUG);
606
        return $controller;
607
    }
608
 
609
// Protected API starts here
610
 
611
    /**
612
     * Creates a unique id for this backup.
613
     */
614
    protected function calculate_backupid() {
615
        // Current epoch time + type + id + format + interactive + mode + userid + operation
616
        // should be unique enough. Add one random part at the end
617
        $this->backupid = md5(time() . '-' . $this->type . '-' . $this->id . '-' . $this->format . '-' .
618
                              $this->interactive . '-' . $this->mode . '-' . $this->userid . '-' .
619
                              $this->operation . '-' . random_string(20));
620
    }
621
 
622
    /**
623
     * Builds the plan for this backup job so that it may be executed.
624
     */
625
    protected function load_plan() {
626
        $this->log('loading controller plan', backup::LOG_DEBUG);
627
        $this->plan = new backup_plan($this);
628
        $this->plan->build(); // Build plan for this controller
629
        $this->set_status(backup::STATUS_PLANNED);
630
    }
631
 
632
    /**
633
     * Sets default values for the backup controller.
634
     */
635
    protected function apply_defaults() {
636
        $this->log('applying plan defaults', backup::LOG_DEBUG);
637
        backup_controller_dbops::apply_config_defaults($this);
638
        $this->set_status(backup::STATUS_CONFIGURED);
639
        $this->set_include_files($this->get_include_files_default());
640
    }
641
 
642
    /**
643
     * Set the initial value for the include_files setting.
644
     *
645
     * @param bool $includefiles
646
     * @see backup_controller::get_include_files for further information on the purpose of this setting.
647
     */
648
    protected function set_include_files(bool $includefiles) {
649
        $this->log("setting file inclusion to {$this->includefiles}", backup::LOG_DEBUG);
650
        $this->includefiles = (int) $includefiles;
651
    }
652
}
653
 
654
/**
655
 * Exception class used by all the @backup_controller stuff
656
 */
657
class backup_controller_exception extends backup_exception {
658
 
659
    public function __construct($errorcode, $a=NULL, $debuginfo=null) {
660
        parent::__construct($errorcode, $a, $debuginfo);
661
    }
662
}