MDL-21432 backup - handle mnet_remoteusers and support for prechecks
[moodle.git] / backup / controller / restore_controller.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-controller
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  * Class implementing the controller of any restore process
27  *
28  * This final class is in charge of controlling all the restore architecture, for any
29  * type of backup.
30  *
31  * TODO: Finish phpdocs
32  */
33 class restore_controller extends backup implements loggable {
35     protected $tempdir;   // Directory under dataroot/temp/backup awaiting restore
36     protected $restoreid; // Unique identificator for this restore
38     protected $courseid; // courseid where restore is going to happen
40     protected $type;   // Type of backup (activity, section, course)
41     protected $format; // Format of backup (moodle, imscc)
42     protected $interactive; // yes/no
43     protected $mode;   // Purpose of the backup (default settings)
44     protected $userid; // user id executing the restore
45     protected $operation; // Type of operation (backup/restore)
46     protected $target;    // Restoring to new/existing/current_adding/_deleting
47     protected $samesite;  // Are we restoring to the same site where the backup was generated
49     protected $status; // Current status of the controller (created, planned, configured...)
50     protected $precheck;    // Results of the execution of restore prechecks
52     protected $info;   // Information retrieved from backup contents
53     protected $plan;   // Restore execution plan
55     protected $execution;     // inmediate/delayed
56     protected $executiontime; // epoch time when we want the restore to be executed (requires cron to run)
58     protected $logger;      // Logging chain object (moodle, inline, fs, db, syslog)
60     protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses
62     public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target){
63         $this->tempdir = $tempdir;
64         $this->courseid = $courseid;
65         $this->interactive = $interactive;
66         $this->mode = $mode;
67         $this->userid = $userid;
68         $this->target = $target;
70         // Apply some defaults
71         $this->type = '';
72         $this->format = backup::FORMAT_UNKNOWN;
73         $this->execution = backup::EXECUTION_INMEDIATE;
74         $this->operation = backup::OPERATION_RESTORE;
75         $this->executiontime = 0;
76         $this->samesite = false;
77         $this->checksum = '';
78         $this->precheck = null;
80         // Apply current backup version and release if necessary
81         backup_controller_dbops::apply_version_and_release();
83         // Check courseid is correct
84         restore_check::check_courseid($this->courseid);
86         // Check user is correct
87         restore_check::check_user($this->userid);
89         // Calculate unique $restoreid
90         $this->calculate_restoreid();
92         // Default logger chain (based on interactive/execution)
93         $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid);
95         // Instantiate the output_controller singleton and active it if interactive and inmediate
96         $oc = output_controller::get_instance();
97         if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
98             $oc->set_active(true);
99         }
101         $this->log('instantiating restore controller', backup::LOG_INFO, $this->restoreid);
103         // Set initial status
104         $this->set_status(backup::STATUS_CREATED);
106         // Calculate original restore format
107         $this->format = backup_general_helper::detect_backup_format($tempdir);
109         // If format is not moodle2, set to conversion needed
110         if ($this->format !== backup::FORMAT_MOODLE) {
111             $this->set_status(backup::STATUS_REQUIRE_CONV);
113         // Else, format is moodle2, load plan, apply security and set status based on interactivity
114         } else {
115             // Load plan
116             $this->load_plan();
118             // Perform all initial security checks and apply (2nd param) them to settings automatically
119             restore_check::check_security($this, true);
121             if ($this->interactive == backup::INTERACTIVE_YES) {
122                 $this->set_status(backup::STATUS_SETTING_UI);
123             } else {
124                 $this->set_status(backup::STATUS_NEED_PRECHECK);
125             }
126         }
127     }
129     public function finish_ui() {
130         if ($this->status != backup::STATUS_SETTING_UI) {
131             throw new backup_controller_exception('cannot_finish_ui_if_not_setting_ui');
132         }
133         $this->set_status(backup::STATUS_NEED_PRECHECK);
134     }
136     public function process_ui_event() {
138         // Perform security checks throwing exceptions (2nd param) if something is wrong
139         restore_check::check_security($this, false);
140     }
142     public function set_status($status) {
143         $this->log('setting controller status to', backup::LOG_DEBUG, $status);
144         // TODO: Check it's a correct status
145         $this->status = $status;
146         // Ensure that, once set to backup::STATUS_AWAITING | STATUS_NEED_PRECHECK, controller is stored in DB
147         if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_NEED_PRECHECK) {
148             $this->save_controller();
149             $this->logger = self::load_controller($this->restoreid)->logger; // wakeup loggers
150         }
151     }
153     public function set_execution($execution, $executiontime = 0) {
154         $this->log('setting controller execution', backup::LOG_DEBUG);
155         // TODO: Check valid execution mode
156         // TODO: Check time in future
157         // TODO: Check time = 0 if inmediate
158         $this->execution = $execution;
159         $this->executiontime = $executiontime;
161         // Default logger chain (based on interactive/execution)
162         $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid);
163     }
165 // checksumable interface methods
167     public function calculate_checksum() {
168         // Reset current checksum to take it out from calculations!
169         $this->checksum = '';
170         // Init checksum
171         $tempchecksum = md5('tempdir-'    . $this->tempdir .
172                             'restoreid-'  . $this->restoreid .
173                             'courseid-'   . $this->courseid .
174                             'type-'       . $this->type .
175                             'format-'     . $this->format .
176                             'interactive-'. $this->interactive .
177                             'mode-'       . $this->mode .
178                             'userid-'     . $this->userid .
179                             'target-'     . $this->target .
180                             'samesite-'   . $this->samesite .
181                             'operation-'  . $this->operation .
182                             'status-'     . $this->status .
183                             'precheck-'   . backup_general_helper::array_checksum_recursive(array($this->precheck)) .
184                             'execution-'  . $this->execution .
185                             'plan-'       . backup_general_helper::array_checksum_recursive(array($this->plan)) .
186                             'info-'       . backup_general_helper::array_checksum_recursive(array($this->info)) .
187                             'logger-'     . backup_general_helper::array_checksum_recursive(array($this->logger)));
188         $this->log('calculating controller checksum', backup::LOG_DEBUG, $tempchecksum);
189         return $tempchecksum;
190     }
192     public function is_checksum_correct($checksum) {
193         return $this->checksum === $checksum;
194     }
196     public function get_tempdir() {
197         return $this->tempdir;
198     }
200     public function get_restoreid() {
201         return $this->restoreid;
202     }
204     public function get_type() {
205         return $this->type;
206     }
208     public function get_operation() {
209         return $this->operation;
210     }
212     public function get_courseid() {
213         return $this->courseid;
214     }
216     public function get_format() {
217         return $this->format;
218     }
220     public function get_interactive() {
221         return $this->interactive;
222     }
224     public function get_mode() {
225         return $this->mode;
226     }
228     public function get_userid() {
229         return $this->userid;
230     }
232     public function get_target() {
233         return $this->target;
234     }
236     public function is_samesite() {
237         return $this->samesite;
238     }
240     public function get_status() {
241         return $this->status;
242     }
244     public function get_execution() {
245         return $this->execution;
246     }
248     public function get_executiontime() {
249         return $this->executiontime;
250     }
252     public function get_plan() {
253         return $this->plan;
254     }
256     public function get_info() {
257         return $this->info;
258     }
260     public function get_logger() {
261         return $this->logger;
262     }
264     public function execute_plan() {
265         return $this->plan->execute();
266     }
268     public function execute_precheck() {
269         if (is_array($this->precheck)) {
270             throw new restore_controller_exception('precheck_alredy_executed', $this->status);
271         }
272         if ($this->status != backup::STATUS_NEED_PRECHECK) {
273             throw new restore_controller_exception('cannot_precheck_wrong_status', $this->status);
274         }
275         $this->precheck = restore_prechecks_helper::execute_prechecks($this);
276         if (!array_key_exists('errors', $this->precheck)) { // No errors, can be executed
277             $this->set_status(backup::STATUS_AWAITING);
278         }
279         if (empty($this->precheck)) { // No errors nor warnings, return true
280             return true;
281         }
282         return false;
283     }
285     public function get_results() {
286         return $this->plan->get_results();
287     }
289     public function get_precheck_results() {
290         if (!is_array($this->precheck)) {
291             throw new restore_controller_exception('precheck_not_executed');
292         }
293         return $this->precheck;
294     }
296     public function log($message, $level, $a = null, $depth = null, $display = false) {
297         backup_helper::log($message, $level, $a, $depth, $display, $this->logger);
298     }
300     public function save_controller() {
301         // Going to save controller to persistent storage, calculate checksum for later checks and save it
302         // TODO: flag the controller as NA. Any operation on it should be forbidden util loaded back
303         $this->log('saving controller to db', backup::LOG_DEBUG);
304         $this->checksum = $this->calculate_checksum();
305         restore_controller_dbops::save_controller($this, $this->checksum);
306     }
308     public static function load_controller($restoreid) {
309         // Load controller from persistent storage
310         // TODO: flag the controller as available. Operations on it can continue
311         $controller = restore_controller_dbops::load_controller($restoreid);
312         $controller->log('loading controller from db', backup::LOG_DEBUG);
313         return $controller;
314     }
316     /**
317      * class method to provide pseudo random unique "correct" tempdir names
318      */
319     public static function get_tempdir_name($courseid = 0, $userid = 0) {
320         // Current epoch time + courseid + userid + random bits
321         return md5(time() . '-' . $courseid . '-'. $userid . '-'. random_string(20));
322     }
324     /**
325      * convert from current format to backup::MOODLE format
326      */
327     public function convert() {
328         if ($this->status != backup::STATUS_REQUIRE_CONV) {
329             throw new restore_controller_exception('cannot_convert_not_required_status');
330         }
331         if ($this->format == backup::FORMAT_UNKNOWN) {
332             throw new restore_controller_exception('cannot_convert_from_unknown_format');
333         }
334         if ($this->format == backup::FORMAT_MOODLE1) {
335             // TODO: Implement moodle1 => moodle2 conversion
336             throw new restore_controller_exception('cannot_convert_yet_from_moodle1_format');
337         }
339         // Once conversions have finished, we check again the format
340         $newformat = backup_general_helper::detect_backup_format($tempdir);
342         // If format is moodle2, load plan, apply security and set status based on interactivity
343         if ($newformat === backup::FORMAT_MOODLE) {
344             // Load plan
345             $this->load_plan();
347             // Perform all initial security checks and apply (2nd param) them to settings automatically
348             restore_check::check_security($this, true);
350             if ($this->interactive == backup::INTERACTIVE_YES) {
351                 $this->set_status(backup::STATUS_SETTING_UI);
352             } else {
353                 $this->set_status(backup::STATUS_NEED_PRECHECK);
354             }
355         } else {
356             throw new restore_controller_exception('conversion_ended_with_wrong_format', $newformat);
357         }
358     }
360 // Protected API starts here
362     protected function calculate_restoreid() {
363         // Current epoch time + tempdir + courseid + interactive + mode + userid + target + operation + random bits
364         $this->restoreid = md5(time() . '-' . $this->tempdir . '-' . $this->courseid . '-'. $this->interactive . '-' .
365                                $this->mode . '-' . $this->userid . '-'. $this->target . '-' . $this->operation . '-' .
366                                random_string(20));
367     }
369     protected function load_plan() {
370         // First of all, we need to introspect the moodle_backup.xml file
371         // in order to detect all the required stuff. So, create the
372         // monster $info structure where everything will be defined
373         $this->log('loading backup info', backup::LOG_DEBUG);
374         $this->info = backup_general_helper::get_backup_information($this->tempdir);
376         // Set the controller type to the one found in the information
377         $this->type = $this->info->type;
379         // Set the controller samesite flag as needed
380         $this->samesite = backup_general_helper::backup_is_samesite($this->info);
382         // Now we load the plan that will be configured following the
383         // information provided by the $info
384         $this->log('loading controller plan', backup::LOG_DEBUG);
385         $this->plan = new restore_plan($this);
386         $this->plan->build(); // Build plan for this controller
387         $this->set_status(backup::STATUS_PLANNED);
388     }
391 /*
392  * Exception class used by all the @restore_controller stuff
393  */
394 class restore_controller_exception extends backup_exception {
396     public function __construct($errorcode, $a=NULL, $debuginfo=null) {
397         parent::__construct($errorcode, $a, $debuginfo);
398     }