Merge branch 'MDL-41146-master' of git://github.com/sammarshallou/moodle
[moodle.git] / backup / util / dbops / backup_controller_dbops.class.php
CommitLineData
69dd0c8c
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-dbops
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 * Non instantiable helper class providing DB support to the @backup_controller
27 *
28 * This class contains various static methods available for all the DB operations
29 * performed by the backup_controller class
30 *
31 * TODO: Finish phpdocs
32 */
33abstract class backup_controller_dbops extends backup_dbops {
34
07e72001 35 /**
36 * @var string Backup id for cached backup_includes_files result.
37 */
38 protected static $includesfilescachebackupid;
39
40 /**
41 * @var int Cached backup_includes_files result
42 */
43 protected static $includesfilescache;
44
6e803b9f
EL
45 /**
46 * Send one backup controller to DB
47 *
48 * @param backup_controller $controller controller to send to DB
49 * @param string $checksum hash of the controller to be checked
50 * @param bool $includeobj to decide if the object itself must be updated (true) or no (false)
51 * @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false)
52 * @return int id of the controller record in the DB
53 * @throws backup_controller_exception|backup_dbops_exception
54 */
55 public static function save_controller($controller, $checksum, $includeobj = true, $cleanobj = false) {
69dd0c8c
EL
56 global $DB;
57 // Check we are going to save one backup_controller
58 if (! $controller instanceof backup_controller) {
59 throw new backup_controller_exception('backup_controller_expected');
60 }
6e803b9f
EL
61 // Check checksum is ok. Only if we are including object info. Sounds silly but it isn't ;-).
62 if ($includeobj and !$controller->is_checksum_correct($checksum)) {
69dd0c8c
EL
63 throw new backup_dbops_exception('backup_controller_dbops_saving_checksum_mismatch');
64 }
6e803b9f
EL
65 // Cannot request to $includeobj and $cleanobj at the same time.
66 if ($includeobj and $cleanobj) {
67 throw new backup_dbops_exception('backup_controller_dbops_saving_cannot_include_and_delete');
68 }
69dd0c8c
EL
69 // Get all the columns
70 $rec = new stdclass();
71 $rec->backupid = $controller->get_backupid();
f60f4666 72 $rec->operation = $controller->get_operation();
69dd0c8c
EL
73 $rec->type = $controller->get_type();
74 $rec->itemid = $controller->get_id();
75 $rec->format = $controller->get_format();
76 $rec->interactive = $controller->get_interactive();
77 $rec->purpose = $controller->get_mode();
78 $rec->userid = $controller->get_userid();
79 $rec->status = $controller->get_status();
80 $rec->execution = $controller->get_execution();
81 $rec->executiontime= $controller->get_executiontime();
82 $rec->checksum = $checksum;
83 // Serialize information
6e803b9f
EL
84 if ($includeobj) {
85 $rec->controller = base64_encode(serialize($controller));
86 } else if ($cleanobj) {
87 $rec->controller = '';
88 }
69dd0c8c
EL
89 // Send it to DB
90 if ($recexists = $DB->get_record('backup_controllers', array('backupid' => $rec->backupid))) {
91 $rec->id = $recexists->id;
92 $rec->timemodified = time();
93 $DB->update_record('backup_controllers', $rec);
94 } else {
95 $rec->timecreated = time();
96 $rec->timemodified = 0;
97 $rec->id = $DB->insert_record('backup_controllers', $rec);
98 }
99 return $rec->id;
100 }
101
102 public static function load_controller($backupid) {
103 global $DB;
104 if (! $controllerrec = $DB->get_record('backup_controllers', array('backupid' => $backupid))) {
105 throw new backup_dbops_exception('backup_controller_dbops_nonexisting');
106 }
107 $controller = unserialize(base64_decode($controllerrec->controller));
108 // Check checksum is ok. Sounds silly but it isn't ;-)
109 if (!$controller->is_checksum_correct($controllerrec->checksum)) {
110 throw new backup_dbops_exception('backup_controller_dbops_loading_checksum_mismatch');
111 }
112 return $controller;
113 }
114
115 public static function create_backup_ids_temp_table($backupid) {
62d6f183
RS
116 global $CFG, $DB;
117 $dbman = $DB->get_manager(); // We are going to use database_manager services
118
119 $xmldb_table = new xmldb_table('backup_ids_temp');
d745d2d4 120 $xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
62d6f183
RS
121 // Set default backupid (not needed but this enforce any missing backupid). That's hackery in action!
122 $xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid);
123 $xmldb_table->add_field('itemname', XMLDB_TYPE_CHAR, 160, null, XMLDB_NOTNULL, null, null);
124 $xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
125 $xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
d745d2d4
EL
126 $xmldb_table->add_field('parentitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
127 $xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null);
62d6f183
RS
128 $xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
129 $xmldb_table->add_key('backupid_itemname_itemid_uk', XMLDB_KEY_UNIQUE, array('backupid','itemname','itemid'));
d745d2d4
EL
130 $xmldb_table->add_index('backupid_parentitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','parentitemid'));
131 $xmldb_table->add_index('backupid_itemname_newitemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','itemname','newitemid'));
62d6f183
RS
132
133 $dbman->create_temp_table($xmldb_table); // And create it
134
b8bb45b0
EL
135 }
136
62d6f183 137 public static function create_backup_files_temp_table($backupid) {
69dd0c8c
EL
138 global $CFG, $DB;
139 $dbman = $DB->get_manager(); // We are going to use database_manager services
140
62d6f183 141 $xmldb_table = new xmldb_table('backup_files_temp');
d745d2d4 142 $xmldb_table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
69dd0c8c 143 // Set default backupid (not needed but this enforce any missing backupid). That's hackery in action!
62d6f183
RS
144 $xmldb_table->add_field('backupid', XMLDB_TYPE_CHAR, 32, null, XMLDB_NOTNULL, null, $backupid);
145 $xmldb_table->add_field('contextid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
146 $xmldb_table->add_field('component', XMLDB_TYPE_CHAR, 100, null, XMLDB_NOTNULL, null, null);
147 $xmldb_table->add_field('filearea', XMLDB_TYPE_CHAR, 50, null, XMLDB_NOTNULL, null, null);
148 $xmldb_table->add_field('itemid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
d745d2d4
EL
149 $xmldb_table->add_field('info', XMLDB_TYPE_TEXT, null, null, null, null, null);
150 $xmldb_table->add_field('newcontextid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
151 $xmldb_table->add_field('newitemid', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
62d6f183 152 $xmldb_table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
d745d2d4 153 $xmldb_table->add_index('backupid_contextid_component_filearea_itemid_ix', XMLDB_INDEX_NOTUNIQUE, array('backupid','contextid','component','filearea','itemid'));
69dd0c8c
EL
154
155 $dbman->create_temp_table($xmldb_table); // And create it
156 }
157
158 public static function drop_backup_ids_temp_table($backupid) {
159 global $DB;
160 $dbman = $DB->get_manager(); // We are going to use database_manager services
161
162 $targettablename = 'backup_ids_temp';
163 $table = new xmldb_table($targettablename);
a66b2ae4 164 $dbman->drop_table($table); // And drop it
69dd0c8c
EL
165 }
166
3e7e2ab2
RS
167 /**
168 * Decode the info field from backup_ids_temp or backup_files_temp.
169 *
170 * @param mixed $info The info field data to decode, may be an object or a simple integer.
171 * @return mixed The decoded information. For simple types it returns, for complex ones we decode.
172 */
173 public static function decode_backup_temp_info($info) {
174 // We encode all data except null.
175 if ($info != null) {
176 if (extension_loaded('zlib')) {
177 return unserialize(gzuncompress(base64_decode($info)));
178 } else {
179 return unserialize(base64_decode($info));
180 }
181 }
182 return $info;
183 }
184
185 /**
186 * Encode the info field for backup_ids_temp or backup_files_temp.
187 *
188 * @param mixed $info string The info field data to encode.
189 * @return string An encoded string of data or null if the input is null.
190 */
191 public static function encode_backup_temp_info($info) {
192 // We encode if there is any information to keep the translations simpler.
193 if ($info != null) {
194 // We compress if possible. It reduces db, network and memory storage. The saving is greater than CPU compression cost.
195 // Compression level 1 is chosen has it produces good compression with the smallest possible overhead, see MDL-40618.
196 if (extension_loaded('zlib')) {
197 return base64_encode(gzcompress(serialize($info), 1));
198 } else {
199 return base64_encode(serialize($info));
200 }
201 }
202 return $info;
203 }
204
69dd0c8c
EL
205 /**
206 * Given one type and id from controller, return the corresponding courseid
207 */
208 public static function get_courseid_from_type_id($type, $id) {
209 global $DB;
210 if ($type == backup::TYPE_1COURSE) {
211 return $id; // id is the course id
212
213 } else if ($type == backup::TYPE_1SECTION) {
214 if (! $courseid = $DB->get_field('course_sections', 'course', array('id' => $id))) {
215 throw new backup_dbops_exception('course_not_found_for_section', $id);
216 }
217 return $courseid;
218 } else if ($type == backup::TYPE_1ACTIVITY) {
219 if (! $courseid = $DB->get_field('course_modules', 'course', array('id' => $id))) {
220 throw new backup_dbops_exception('course_not_found_for_moduleid', $id);
221 }
222 return $courseid;
223 }
224 }
225
226 /**
227 * Given one activity task, return the activity information and related settings
228 * Used by get_moodle_backup_information()
229 */
230 private static function get_activity_backup_information($task) {
231
232 $contentinfo = array(
233 'moduleid' => $task->get_moduleid(),
234 'sectionid' => $task->get_sectionid(),
235 'modulename' => $task->get_modulename(),
236 'title' => $task->get_name(),
237 'directory' => 'activities/' . $task->get_modulename() . '_' . $task->get_moduleid());
238
239 // Now get activity settings
240 // Calculate prefix to find valid settings
241 $prefix = basename($contentinfo['directory']);
242 $settingsinfo = array();
243 foreach ($task->get_settings() as $setting) {
244 // Discard ones without valid prefix
245 if (strpos($setting->get_name(), $prefix) !== 0) {
246 continue;
247 }
248 // Validate level is correct (activity)
249 if ($setting->get_level() != backup_setting::ACTIVITY_LEVEL) {
250 throw new backup_controller_exception('setting_not_activity_level', $setting);
251 }
252 $settinginfo = array(
253 'level' => 'activity',
254 'activity' => $prefix,
255 'name' => $setting->get_name(),
256 'value' => $setting->get_value());
ce937f99 257 $settingsinfo[$setting->get_name()] = (object)$settinginfo;
69dd0c8c
EL
258 }
259 return array($contentinfo, $settingsinfo);
260 }
261
262 /**
263 * Given one section task, return the section information and related settings
264 * Used by get_moodle_backup_information()
265 */
266 private static function get_section_backup_information($task) {
267
268 $contentinfo = array(
269 'sectionid' => $task->get_sectionid(),
270 'title' => $task->get_name(),
271 'directory' => 'sections/' . 'section_' . $task->get_sectionid());
272
273 // Now get section settings
274 // Calculate prefix to find valid settings
275 $prefix = basename($contentinfo['directory']);
276 $settingsinfo = array();
277 foreach ($task->get_settings() as $setting) {
278 // Discard ones without valid prefix
279 if (strpos($setting->get_name(), $prefix) !== 0) {
280 continue;
281 }
282 // Validate level is correct (section)
283 if ($setting->get_level() != backup_setting::SECTION_LEVEL) {
284 throw new backup_controller_exception('setting_not_section_level', $setting);
285 }
286 $settinginfo = array(
287 'level' => 'section',
d12fd69b 288 'section' => $prefix,
69dd0c8c
EL
289 'name' => $setting->get_name(),
290 'value' => $setting->get_value());
ce937f99 291 $settingsinfo[$setting->get_name()] = (object)$settinginfo;
69dd0c8c
EL
292 }
293 return array($contentinfo, $settingsinfo);
294 }
295
296 /**
297 * Given one course task, return the course information and related settings
298 * Used by get_moodle_backup_information()
299 */
300 private static function get_course_backup_information($task) {
301
302 $contentinfo = array(
303 'courseid' => $task->get_courseid(),
304 'title' => $task->get_name(),
305 'directory' => 'course');
306
307 // Now get course settings
308 // Calculate prefix to find valid settings
309 $prefix = basename($contentinfo['directory']);
310 $settingsinfo = array();
311 foreach ($task->get_settings() as $setting) {
312 // Discard ones without valid prefix
313 if (strpos($setting->get_name(), $prefix) !== 0) {
314 continue;
315 }
316 // Validate level is correct (course)
317 if ($setting->get_level() != backup_setting::COURSE_LEVEL) {
318 throw new backup_controller_exception('setting_not_course_level', $setting);
319 }
320 $settinginfo = array(
321 'level' => 'course',
322 'name' => $setting->get_name(),
323 'value' => $setting->get_value());
ce937f99 324 $settingsinfo[$setting->get_name()] = (object)$settinginfo;
69dd0c8c
EL
325 }
326 return array($contentinfo, $settingsinfo);
327 }
328
329 /**
330 * Given one root task, return the course information and related settings
331 * Used by get_moodle_backup_information()
332 */
333 private static function get_root_backup_information($task) {
334
335 // Now get root settings
336 $settingsinfo = array();
337 foreach ($task->get_settings() as $setting) {
338 // Validate level is correct (root)
339 if ($setting->get_level() != backup_setting::ROOT_LEVEL) {
340 throw new backup_controller_exception('setting_not_root_level', $setting);
341 }
342 $settinginfo = array(
343 'level' => 'root',
344 'name' => $setting->get_name(),
345 'value' => $setting->get_value());
ce937f99 346 $settingsinfo[$setting->get_name()] = (object)$settinginfo;
69dd0c8c
EL
347 }
348 return array(null, $settingsinfo);
349 }
350
351 /**
352 * Get details information for main moodle_backup.xml file, extracting it from
353 * the specified controller
354 */
355 public static function get_moodle_backup_information($backupid) {
356
357 $detailsinfo = array(); // Information details
358 $contentsinfo= array(); // Information about backup contents
359 $settingsinfo= array(); // Information about backup settings
360 $bc = self::load_controller($backupid); // Load controller
361
362 // Details info
cd0034d8 363 $detailsinfo['id'] = $bc->get_id();
69dd0c8c
EL
364 $detailsinfo['backup_id'] = $bc->get_backupid();
365 $detailsinfo['type'] = $bc->get_type();
366 $detailsinfo['format'] = $bc->get_format();
367 $detailsinfo['interactive'] = $bc->get_interactive();
368 $detailsinfo['mode'] = $bc->get_mode();
369 $detailsinfo['execution'] = $bc->get_execution();
370 $detailsinfo['executiontime'] = $bc->get_executiontime();
ce937f99 371 $detailsinfo['userid'] = $bc->get_userid();
cd0034d8 372 $detailsinfo['courseid'] = $bc->get_courseid();
69dd0c8c
EL
373
374
375 // Init content placeholders
376 $contentsinfo['activities'] = array();
377 $contentsinfo['sections'] = array();
378 $contentsinfo['course'] = array();
379
380 // Contents info (extract information from tasks)
381 foreach ($bc->get_plan()->get_tasks() as $task) {
382
383 if ($task instanceof backup_activity_task) { // Activity task
384
fbd74137
EL
385 if ($task->get_setting_value('included')) { // Only return info about included activities
386 list($contentinfo, $settings) = self::get_activity_backup_information($task);
387 $contentsinfo['activities'][] = $contentinfo;
388 $settingsinfo = array_merge($settingsinfo, $settings);
389 }
69dd0c8c
EL
390
391 } else if ($task instanceof backup_section_task) { // Section task
392
fbd74137
EL
393 if ($task->get_setting_value('included')) { // Only return info about included sections
394 list($contentinfo, $settings) = self::get_section_backup_information($task);
395 $contentsinfo['sections'][] = $contentinfo;
396 $settingsinfo = array_merge($settingsinfo, $settings);
397 }
69dd0c8c
EL
398
399 } else if ($task instanceof backup_course_task) { // Course task
400
401 list($contentinfo, $settings) = self::get_course_backup_information($task);
402 $contentsinfo['course'][] = $contentinfo;
403 $settingsinfo = array_merge($settingsinfo, $settings);
404
405 } else if ($task instanceof backup_root_task) { // Root task
406
407 list($contentinfo, $settings) = self::get_root_backup_information($task);
408 $settingsinfo = array_merge($settingsinfo, $settings);
409 }
410 }
411
f2364c93
EL
412 $bc->destroy(); // Always need to destroy controller to handle circular references
413
69dd0c8c
EL
414 return array(array((object)$detailsinfo), $contentsinfo, $settingsinfo);
415 }
416
417 /**
418 * Update CFG->backup_version and CFG->backup_release if change in
419 * version is detected.
420 */
421 public static function apply_version_and_release() {
422 global $CFG;
423
424 if ($CFG->backup_version < backup::VERSION) {
425 set_config('backup_version', backup::VERSION);
426 set_config('backup_release', backup::RELEASE);
427 }
428 }
61ebef8f 429
c3ea499d
EL
430 /**
431 * Given the backupid, detect if the backup includes "mnet" remote users or no
432 */
433 public static function backup_includes_mnet_remote_users($backupid) {
434 global $CFG, $DB;
435
436 $sql = "SELECT COUNT(*)
437 FROM {backup_ids_temp} b
438 JOIN {user} u ON u.id = b.itemid
439 WHERE b.backupid = ?
440 AND b.itemname = 'userfinal'
441 AND u.mnethostid != ?";
442 $count = $DB->count_records_sql($sql, array($backupid, $CFG->mnet_localhost_id));
443 return (int)(bool)$count;
444 }
445
b1850c12
AN
446 /**
447 * Given the backupid, determine whether this backup should include
448 * files from the moodle file storage system.
449 *
450 * @param string $backupid The ID of the backup.
451 * @return int Indicates whether files should be included in backups.
452 */
453 public static function backup_includes_files($backupid) {
07e72001 454 // This function is called repeatedly in a backup with many files.
455 // Loading the controller is a nontrivial operation (in a large test
456 // backup it took 0.3 seconds), so we do a temporary cache of it within
457 // this request.
458 if (self::$includesfilescachebackupid === $backupid) {
459 return self::$includesfilescache;
460 }
461
462 // Load controller, get value, then destroy controller and return result.
463 self::$includesfilescachebackupid = $backupid;
b1850c12 464 $bc = self::load_controller($backupid);
07e72001 465 self::$includesfilescache = $bc->get_include_files();
466 $bc->destroy();
467 return self::$includesfilescache;
b1850c12
AN
468 }
469
67233725
DC
470 /**
471 * Given the backupid, detect if the backup contains references to external contents
472 *
473 * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
474 * @return int
475 */
476 public static function backup_includes_file_references($backupid) {
477 global $CFG, $DB;
478
479 $sql = "SELECT count(r.repositoryid)
480 FROM {files} f
f1bd371c 481 LEFT JOIN {files_reference} r
67233725
DC
482 ON r.id = f.referencefileid
483 JOIN {backup_ids_temp} bi
484 ON f.id = bi.itemid
485 WHERE bi.backupid = ?
486 AND bi.itemname = 'filefinal'";
487 $count = $DB->count_records_sql($sql, array($backupid));
488 return (int)(bool)$count;
489 }
490
560811a9
EL
491 /**
492 * Given the courseid, return some course related information we want to transport
493 *
494 * @param int $course the id of the course this backup belongs to
495 */
496 public static function backup_get_original_course_info($courseid) {
497 global $DB;
498 return $DB->get_record('course', array('id' => $courseid), 'fullname, shortname, startdate');
499 }
500
59fc0cbd
EL
501 /**
502 * Sets the default values for the settings in a backup operation
503 *
504 * Based on the mode of the backup it will delegate the process to
505 * other methods like {@link apply_general_config_defaults} ...
506 * to get proper defaults loaded
507 *
508 * @param backup_controller $controller
509 */
510 public static function apply_config_defaults(backup_controller $controller) {
511 // Based on the mode of the backup (general, automated, import, hub...)
512 // decide the action to perform to get defaults loaded
513 $mode = $controller->get_mode();
514
515 switch ($mode) {
516 case backup::MODE_GENERAL:
517 // Load the general defaults
518 self::apply_general_config_defaults($controller);
519 break;
520 case backup::MODE_AUTOMATED:
521 // TODO: Move the loading from automatic stuff to here
522 break;
523 default:
524 // Nothing to do for other modes (IMPORT/HUB...). Some day we
525 // can define defaults (admin UI...) for them if we want to
526 }
527 }
528
61ebef8f
SH
529 /**
530 * Sets the controller settings default values from the backup config.
560811a9 531 *
61ebef8f
SH
532 * @param backup_controller $controller
533 */
59fc0cbd 534 private static function apply_general_config_defaults(backup_controller $controller) {
61ebef8f
SH
535 $settings = array(
536 // Config name => Setting name
537 'backup_general_users' => 'users',
538 'backup_general_anonymize' => 'anonymize',
539 'backup_general_role_assignments' => 'role_assignments',
61ebef8f
SH
540 'backup_general_activities' => 'activities',
541 'backup_general_blocks' => 'blocks',
542 'backup_general_filters' => 'filters',
543 'backup_general_comments' => 'comments',
2832093f 544 'backup_general_badges' => 'badges',
61ebef8f
SH
545 'backup_general_userscompletion' => 'userscompletion',
546 'backup_general_logs' => 'logs',
547 'backup_general_histories' => 'grade_histories'
548 );
549 $plan = $controller->get_plan();
550 foreach ($settings as $config=>$settingname) {
551 $value = get_config('backup', $config);
552 $locked = (get_config('backup', $config.'_locked') == true);
553 if ($plan->setting_exists($settingname)) {
554 $setting = $plan->get_setting($settingname);
555 if ($setting->get_value() != $value || 1==1) {
556 $setting->set_value($value);
557 if ($locked) {
558 $setting->set_status(base_setting::LOCKED_BY_CONFIG);
559 }
560 }
561 }
562 }
563 }
69dd0c8c 564}