PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / class-updraftplus.php

class-updraftplus.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at class-updraftplus.php

4,663 lines 206.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
4
5 class UpdraftPlus {
6
7 public $version;
8
9 public $plugin_title = 'UpdraftPlus Backup/Restore';
10
11 // Choices will be shown in the admin menu in the order used here
12 public $backup_methods = array(
13 'updraftvault' => 'UpdraftPlus Vault',
14 'dropbox' => 'Dropbox',
15 's3' => 'Amazon S3',
16 'cloudfiles' => 'Rackspace Cloud Files',
17 'googledrive' => 'Google Drive',
18 'onedrive' => 'Microsoft OneDrive',
19 'ftp' => 'FTP',
20 'azure' => 'Microsoft Azure',
21 'sftp' => 'SFTP / SCP',
22 'googlecloud' => 'Google Cloud',
23 'backblaze' => 'Backblaze',
24 'webdav' => 'WebDAV',
25 's3generic' => 'S3-Compatible (Generic)',
26 'openstack' => 'OpenStack (Swift)',
27 'dreamobjects' => 'DreamObjects',
28 'email' => 'Email'
29 );
30
31 public $errors = array();
32
33 public $nonce;
34
35 public $file_nonce;
36
37 public $logfile_name = "";
38
39 public $logfile_handle = false;
40
41 public $backup_time;
42
43 public $job_time_ms;
44
45 public $opened_log_time;
46
47 private $backup_dir;
48
49 private $jobdata;
50
51 public $something_useful_happened = false;
52
53 public $have_addons = false;
54
55 // Used to schedule resumption attempts beyond the tenth, if needed
56 public $current_resumption;
57
58 public $newresumption_scheduled = false;
59
60 public $cpanel_quota_readable = false;
61
62 public $error_reporting_stop_when_logged = false;
63
64 private $combine_jobs_around;
65
66 /**
67 * Class constructor
68 */
69 public function __construct() {
70 global $pagenow;
71 // Initialisation actions - takes place on plugin load
72
73 if ($fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
74 $file_data = fread($fp, 1024);
75 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
76 $this->version = $matches[1];
77 }
78 fclose($fp);
79 }
80
81 $load_classes = array(
82 'UpdraftPlus_Backup_History' => 'includes/class-backup-history.php',
83 'UpdraftPlus_Encryption' => 'includes/class-updraftplus-encryption.php',
84 'UpdraftPlus_Manipulation_Functions' => 'includes/class-manipulation-functions.php',
85 'UpdraftPlus_Filesystem_Functions' => 'includes/class-filesystem-functions.php',
86 'UpdraftPlus_Storage_Methods_Interface' => 'includes/class-storage-methods-interface.php',
87 'UpdraftPlus_Job_Scheduler' => 'includes/class-job-scheduler.php'
88 );
89
90 foreach ($load_classes as $class => $relative_path) {
91 if (!class_exists($class)) include_once(UPDRAFTPLUS_DIR.'/'.$relative_path);
92 }
93
94 // Create admin page
95 add_action('init', array($this, 'handle_url_actions'));
96 // Run earlier than default - hence earlier than other components
97 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
98 add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
99 // Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
100 add_action('admin_init', array($this, 'admin_menu'), 9);
101
102 // The two actions which we schedule upon
103 add_action('updraft_backup', array($this, 'backup_files'));
104 add_action('updraft_backup_database', array($this, 'backup_database'));
105
106 // The three actions that can be called from "Backup Now"
107 add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
108 add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
109 add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
110
111 // backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
112 add_action('updraft_backup_all', array($this, 'backup_all'));
113
114 // This is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
115 add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
116
117 // If files + db are on different schedules but are scheduled for the same time, then combine them
118 add_filter('schedule_event', array($this, 'schedule_event'));
119
120 add_action('plugins_loaded', array($this, 'plugins_loaded'));
121
122 // Auto update plugin
123 add_filter('auto_update_plugin', array($this, 'maybe_auto_update_plugin'), 20, 2);
124
125 // Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
126 add_filter('itsec_has_external_backup', '__return_true', 999);
127 add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
128 add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
129
130 // Prevent people upgrading from being baffled by WP's obscure error message. See: https://core.trac.wordpress.org/ticket/27196
131 add_filter('upgrader_source_selection', array($this, 'upgrader_source_selection'), 10, 4);
132
133 // register_deactivation_hook(__FILE__, array($this, 'deactivation'));
134 if (!empty($_POST) && !empty($_GET['udm_action']) && 'vault_disconnect' == $_GET['udm_action'] && !empty($_POST['udrpc_message']) && !empty($_POST['reset_hash'])) {
135 add_action('wp_loaded', array($this, 'wp_loaded_vault_disconnect'), 1);
136 }
137
138 // Remove the notice on the Updates page that confuses users who already have backups installed
139 if ('update-core.php' == $pagenow) {
140 // added filter here instead of admin.php because the jetpack_just_in_time_msgs filter applied in init hook
141 add_filter('jetpack_just_in_time_msgs', '__return_false', 20);
142 }
143 }
144
145 /**
146 * Enables automatic updates for the plugin.
147 *
148 * Enables automatic updates for the plugin..
149 *
150 * @access public
151 * @see __construct
152 * @internal uses auto_update_plugin filter
153 *
154 * @param bool $update Whether the item has automatic updates enabled
155 * @param object $item Object holding the asset to be updated
156 * @return bool True of automatic updates enabled, false if not
157 */
158 public function maybe_auto_update_plugin($update, $item) {
159 if (!isset($item->plugin) || basename(UPDRAFTPLUS_DIR).'/updraftplus.php' !== $item->plugin) return $update;
160 $option_auto_update_settings = UpdraftPlus_Options::get_updraft_option('updraft_auto_updates');
161 return (1 == $option_auto_update_settings);
162 }
163
164 /**
165 * WP filter upgrader_source_selection. We use it to tweak the error message shown when an install of a new version is prevented by the existence of an existing version (i.e. us!), to give the user some actual useful information instead of WP's default.
166 *
167 * @param String $source File source location.
168 * @param String $remote_source Remote file source location.
169 * @param WP_Upgrader $upgrader_object WP_Upgrader instance.
170 * @param Array $hook_extra Extra arguments passed to hooked filters.
171 *
172 * @return String - filtered value
173 */
174 public function upgrader_source_selection($source, $remote_source, $upgrader_object, $hook_extra = array()) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
175
176 static $been_here_already = false;
177
178 if ($been_here_already || !is_array($hook_extra) || empty($hook_extra['type']) || 'plugin' !== $hook_extra['type'] || empty($hook_extra['action']) || 'install' !== $hook_extra['action'] || empty($source) || 'updraftplus' !== basename(untrailingslashit($source)) || !class_exists('ReflectionObject')) return $source;
179
180 $been_here_already = true;
181
182 $reflect = new ReflectionObject($upgrader_object);
183
184 $properties = $reflect->getProperty('strings');
185
186 if (!$properties->isPublic() || !is_array($upgrader_object->strings) || empty($upgrader_object->strings['folder_exists'])) return $source;
187
188 $upgrader_object->strings['folder_exists'] .= ' '.__('A version of UpdraftPlus is already installed. WordPress will only allow you to install your new version after first de-installing the existing one. That is safe - all your settings and backups will be retained. So, go to the "Plugins" page, de-activate and de-install UpdraftPlus, and then try again.', 'updraftplus');
189
190 return $source;
191
192 }
193
194 /**
195 * WordPress filter itsec_scheduled_external_backup - from iThemes Security
196 *
197 * @param Boolean $x - whether a backup is scheduled
198 *
199 * @return Boolean - filtered value
200 */
201 public function itsec_scheduled_external_backup($x) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
202 return wp_next_scheduled('updraft_backup') ? true : false;
203 }
204
205 /**
206 * WordPress filter itsec_external_backup_link - from iThemes security
207 *
208 * @param String $x - link
209 *
210 * @return String - filtered value
211 */
212 public function itsec_external_backup_link($x) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
213 return UpdraftPlus_Options::admin_page_url().'?page=updraftplus';
214 }
215
216 /**
217 * This method will disconnect UpdraftVault accounts.
218 *
219 * @return Array - returns the saved options if an error is encountered.
220 */
221 public function wp_loaded_vault_disconnect() {
222 $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('updraftvault');
223
224 if (is_wp_error($opts)) {
225 if ('recursion' !== $opts->get_error_code()) {
226 $msg = "UpdraftVault (".$opts->get_error_code()."): ".$opts->get_error_message();
227 $this->log($msg);
228 error_log("UpdraftPlus: $msg");
229 }
230 // The saved options had a problem; so, return the new ones
231 return $opts;
232 } elseif (!empty($opts['settings'])) {
233
234 foreach ($opts['settings'] as $instance_id => $storage_options) {
235 if (!empty($storage_options['token']) && $storage_options['token']) {
236 $site_id = $this->siteid();
237 $hash = hash('sha256', $site_id.':::'.$storage_options['token']);
238 if ($hash == $_POST['reset_hash']) {
239 $this->log('This site has been remotely disconnected from UpdraftPlus Vault');
240 include_once(UPDRAFTPLUS_DIR.'/methods/updraftvault.php');
241 $vault = new UpdraftPlus_BackupModule_updraftvault();
242 $vault->ajax_vault_disconnect();
243 // Die, as the vault method has already sent output
244 die;
245 } else {
246 $this->log('An invalid request was received to disconnect this site from UpdraftPlus Vault');
247 }
248 }
249 echo json_encode(array('disconnected' => 0));
250 }
251 }
252 die;
253 }
254
255 /**
256 * Gets an RPC object, and sets some defaults on it that we always want
257 *
258 * @param string $indicator_name indicator name
259 * @return array
260 */
261 public function get_udrpc($indicator_name = 'migrator.updraftplus.com') {
262 if (!class_exists('UpdraftPlus_Remote_Communications')) include_once(apply_filters('updraftplus_class_udrpc_path', UPDRAFTPLUS_DIR.'/includes/class-udrpc.php', $this->version));
263 $ud_rpc = new UpdraftPlus_Remote_Communications($indicator_name);
264 $ud_rpc->set_can_generate(true);
265 return $ud_rpc;
266 }
267
268 /**
269 * Ensure that the indicated phpseclib classes are available
270 *
271 * @param String|Array $classes - a class, or list of classes
272 * @param String|Array $class_paths - paths to include
273 *
274 * @return Boolean|WP_Error
275 */
276 public function ensure_phpseclib($classes = array(), $class_paths = array()) {
277
278 $this->no_deprecation_warnings_on_php7();
279
280 if (!empty($classes)) {
281 $any_missing = false;
282 if (is_string($classes)) $classes = array($classes);
283 foreach ($classes as $cl) {
284 if (!class_exists($cl)) $any_missing = true;
285 }
286 if (!$any_missing) return true;
287 }
288
289 $ret = true;
290
291 // From phpseclib/phpseclib/phpseclib/bootstrap.php - we nullify it there, but log here instead
292 if (extension_loaded('mbstring')) {
293 // 2 - MB_OVERLOAD_STRING
294 // @codingStandardsIgnoreLine
295 if (ini_get('mbstring.func_overload') & 2) {
296 // We go on to try anyway, in case the caller wasn't using an affected part of phpseclib
297 // @codingStandardsIgnoreLine
298 $ret = new WP_Error('mbstring_func_overload', 'Overloading of string functions using mbstring.func_overload is not supported by phpseclib.');
299 }
300 }
301
302 if (!empty($class_paths)) {
303 $phpseclib_dir = UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib';
304 if (false === strpos(get_include_path(), $phpseclib_dir)) set_include_path(get_include_path().PATH_SEPARATOR.$phpseclib_dir);
305 if (is_string($class_paths)) $class_paths = array($class_paths);
306 foreach ($class_paths as $cp) {
307 include_once($phpseclib_dir.'/'.$cp.'.php');
308 }
309 }
310
311 return $ret;
312 }
313
314 /**
315 * Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
316 */
317 private function no_deprecation_warnings_on_php7() {
318 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
319 // We don't test for PHP > 7 because the specific deprecated element will be removed in PHP 8 - and so no warning should come anyway (and we shouldn't suppress other stuff until we know we need to).
320 // @codingStandardsIgnoreLine
321 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
322 $old_level = error_reporting();
323 // @codingStandardsIgnoreLine
324 $new_level = $old_level & ~E_DEPRECATED;
325 if ($old_level != $new_level) error_reporting($new_level);
326 $this->no_deprecation_warnings = true;
327 }
328 }
329
330 public function close_browser_connection($txt = '') {
331 // Close browser connection so that it can resume AJAX polling
332 header('Content-Length: '.(empty($txt) ? '0' : 4+strlen($txt)));
333 header('Connection: close');
334 header('Content-Encoding: none');
335 if (session_id()) session_write_close();
336 echo "\r\n\r\n";
337 echo $txt;
338 // These two added - 19-Feb-15 - started being required on local dev machine, for unknown reason (probably some plugin that started an output buffer).
339 $ob_level = ob_get_level();
340 while ($ob_level > 0) {
341 ob_end_flush();
342 $ob_level--;
343 }
344 flush();
345 if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
346 }
347
348 /**
349 * Returns the number of bytes free, if it can be detected; otherwise, false
350 * Presently, we only detect CPanel. If you know of others, then feel free to contribute!
351 */
352 public function get_hosting_disk_quota_free() {
353 if (!@is_dir('/usr/local/cpanel') || $this->detect_safe_mode() || !function_exists('popen') || (!@is_executable('/usr/local/bin/perl') && !@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) || (defined('UPDRAFTPLUS_SKIP_CPANEL_QUOTA_CHECK') && UPDRAFTPLUS_SKIP_CPANEL_QUOTA_CHECK)) return false;
354
355 $perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';
356
357 $exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
358
359 $handle = @popen($exec, 'r');
360 if (!is_resource($handle)) return false;
361
362 $found = false;
363 $lines = 0;
364 while (false === $found && !feof($handle) && $lines<100) {
365 $lines++;
366 $w = fgets($handle);
367 // Used, limit, remain
368 if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) {
369 $found = true;
370 }
371 }
372 $ret = pclose($handle);
373 if (false === $found || 0 != $ret) return false;
374
375 if ((int) $matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
376
377 $this->cpanel_quota_readable = true;
378
379 return $matches;
380 }
381
382 public function last_modified_log() {
383 $updraft_dir = $this->backups_dir_location();
384
385 $log_file = '';
386 $mod_time = false;
387 $nonce = '';
388
389 if ($handle = @opendir($updraft_dir)) {
390 while (false !== ($entry = readdir($handle))) {
391 // The latter match is for files created internally by zipArchive::addFile
392 if (preg_match('/^log\.([a-z0-9]+)\.txt$/i', $entry, $matches)) {
393 $mtime = filemtime($updraft_dir.'/'.$entry);
394 if ($mtime > $mod_time) {
395 $mod_time = $mtime;
396 $log_file = $updraft_dir.'/'.$entry;
397 $nonce = $matches[1];
398 }
399 }
400 }
401 @closedir($handle);
402 }
403
404 return array($mod_time, $log_file, $nonce);
405 }
406
407 /**
408 * This function may get called multiple times, so write accordingly
409 */
410 public function admin_menu() {
411 // We are in the admin area: now load all that code
412 global $updraftplus_admin;
413 if (empty($updraftplus_admin)) include_once(UPDRAFTPLUS_DIR.'/admin.php');
414
415 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && 'updraftplus' == $_GET['page'] && 'downloadlatestmodlog' == $_GET['action'] && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
416
417 list ($mod_time, $log_file, $nonce) = $this->last_modified_log();
418
419 if ($mod_time >0) {
420 if (is_readable($log_file)) {
421 header('Content-type: text/plain');
422 readfile($log_file);
423 exit;
424 } else {
425 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablelog'));
426 }
427 } else {
428 add_action('all_admin_notices', array($this, 'show_admin_warning_nolog'));
429 }
430 }
431
432 }
433
434 /**
435 * WP action http_api_curl
436 *
437 * @param Resource $handle A curl handle returned by curl_init()
438 *
439 * @return the handle (having potentially had some options set upon it)
440 */
441 public function http_api_curl($handle) {
442 if (defined('UPDRAFTPLUS_IPV4_ONLY') && UPDRAFTPLUS_IPV4_ONLY) {
443 curl_setopt($handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
444 }
445 return $handle;
446 }
447
448 /**
449 * Used as a central location (to avoid repetition) to register or de-register hooks into the WP HTTP API
450 *
451 * @param Boolean $register - true to register, false to de-register
452 */
453 public function register_wp_http_option_hooks($register = true) {
454 if ($register) {
455 add_filter('http_request_args', array($this, 'modify_http_options'));
456 add_action('http_api_curl', array($this, 'http_api_curl'));
457 } else {
458 remove_filter('http_request_args', array($this, 'modify_http_options'));
459 remove_action('http_api_curl', array($this, 'http_api_curl'));
460 }
461 }
462
463 /**
464 * Used as a WordPress options filter (http_request_args)
465 *
466 * @param Array $opts - existing options
467 *
468 * @return Array - modified options
469 */
470 public function modify_http_options($opts) {
471
472 if (!is_array($opts)) return $opts;
473
474 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) $opts['sslcertificates'] = UPDRAFTPLUS_DIR.'/includes/cacert.pem';
475
476 $opts['sslverify'] = UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify') ? false : true;
477
478 return $opts;
479
480 }
481
482 /**
483 * Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?action=updraftmethod-googledrive-auth&page=updraftplus
484 * Nov 2013: Google's new cloud console, for reasons as yet unknown, only allows you to enter a redirect_uri with a single URL parameter... thus, we put page second, and re-add it if necessary. Apr 2014: Bitcasa already do this, so perhaps it is part of the OAuth2 standard or best practice somewhere.
485 * Also handle action=downloadlog
486 *
487 * @return Void - may not necessarily return at all, depending on the action
488 */
489 public function handle_url_actions() {
490
491 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
492 // Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
493 if (isset($_SERVER['REQUEST_METHOD']) && ('GET' == $_SERVER['REQUEST_METHOD'] || 'POST' == $_SERVER['REQUEST_METHOD']) && isset($_GET['action'])) {
494 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php') && UpdraftPlus_Options::user_can_manage()) {
495 $_GET['page'] = 'updraftplus';
496 $_REQUEST['page'] = 'updraftplus';
497 $method = $matches[1];
498 $call_method = "action_".$matches[2];
499 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($method));
500
501 $instance_id = isset($_GET['updraftplus_instance']) ? $_GET['updraftplus_instance'] : '';
502
503 if ("POST" == $_SERVER['REQUEST_METHOD'] && isset($_POST['state'])) {
504 $state = urldecode($_POST['state']);
505 } elseif (isset($_GET['state'])) {
506 $state = $_GET['state'];
507 }
508
509 // If we don't have an instance_id but the state is set then we are coming back to finish the auth and should extract the instance_id from the state
510 if ('' == $instance_id && isset($state) && false !== strpos($state, ':')) {
511 $parts = explode(':', $state);
512 $instance_id = $parts[1];
513 }
514
515 if (isset($storage_objects_and_ids[$method]['instance_settings'][$instance_id])) {
516 $opts = $storage_objects_and_ids[$method]['instance_settings'][$instance_id];
517 $backup_obj = $storage_objects_and_ids[$method]['object'];
518 $backup_obj->set_options($opts, false, $instance_id);
519 } else {
520 include_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
521 $call_class = "UpdraftPlus_BackupModule_".$method;
522 $backup_obj = new $call_class;
523 }
524
525 $this->register_wp_http_option_hooks();
526
527 try {
528 if (method_exists($backup_obj, $call_method)) {
529 call_user_func(array($backup_obj, $call_method));
530 }
531 } catch (Exception $e) {
532 $this->log(sprintf(__("%s error: %s", 'updraftplus'), $method, $e->getMessage().' ('.$e->getCode().')', 'error'));
533 }
534 $this->register_wp_http_option_hooks(false);
535 } elseif (isset($_GET['page']) && 'updraftplus' == $_GET['page'] && 'downloadlog' == $_GET['action'] && isset($_GET['updraftplus_backup_nonce']) && preg_match("/^[0-9a-f]{12}$/", $_GET['updraftplus_backup_nonce']) && UpdraftPlus_Options::user_can_manage()) {
536 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
537 $updraft_dir = $this->backups_dir_location();
538 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
539 if (is_readable($log_file)) {
540 header('Content-type: text/plain');
541 if (!empty($_GET['force_download'])) header('Content-Disposition: attachment; filename="'.basename($log_file).'"');
542 readfile($log_file);
543 exit;
544 } else {
545 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablelog'));
546 }
547 } elseif (isset($_GET['page']) && 'updraftplus' == $_GET['page'] && 'downloadfile' == $_GET['action'] && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-db([0-9]+)?+\.(gz\.crypt)$/i', $_GET['updraftplus_file']) && UpdraftPlus_Options::user_can_manage()) {
548 // Though this (venerable) code uses the action 'downloadfile', in fact, it's not that general: it's just for downloading a decrypted copy of encrypted databases, and nothing else
549 $updraft_dir = $this->backups_dir_location();
550 $file = $_GET['updraftplus_file'];
551 $spool_file = $updraft_dir.'/'.basename($file);
552 if (is_readable($spool_file)) {
553 $dkey = isset($_GET['decrypt_key']) ? stripslashes($_GET['decrypt_key']) : '';
554 $this->spool_file($spool_file, $dkey);
555 exit;
556 } else {
557 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablefile'));
558 }
559 } elseif ('updraftplus_spool_file' == $_GET['action'] && !empty($_GET['what']) && !empty($_GET['backup_timestamp']) && is_numeric($_GET['backup_timestamp']) && UpdraftPlus_Options::user_can_manage()) {
560 // At some point, it may be worth merging this with the previous section
561 $updraft_dir = $this->backups_dir_location();
562
563 $findex = isset($_GET['findex']) ? (int) $_GET['findex'] : 0;
564 $backup_timestamp = $_GET['backup_timestamp'];
565 $what = $_GET['what'];
566
567 $backup_set = UpdraftPlus_Backup_History::get_history($backup_timestamp);
568
569 $filename = null;
570 if (!empty($backup_set)) {
571 if ('db' != substr($what, 0, 2)) {
572 $backupable_entities = $this->get_backupable_file_entities();
573 if (!isset($backupable_entities[$what])) $filename = false;
574 }
575 if (false !== $filename && isset($backup_set[$what])) {
576 if (is_string($backup_set[$what]) && 0 == $findex) {
577 $filename = $backup_set[$what];
578 } elseif (isset($backup_set[$what][$findex])) {
579 $filename = $backup_set[$what][$findex];
580 }
581 }
582 }
583 if (empty($filename) || !is_readable($updraft_dir.'/'.basename($filename))) {
584 echo json_encode(array('result' => __('UpdraftPlus notice:', 'updraftplus').' '.__('The given file was not found, or could not be read.', 'updraftplus')));
585 exit;
586 }
587
588 $dkey = isset($_GET['decrypt_key']) ? stripslashes($_GET['decrypt_key']) : "";
589
590 $this->spool_file($updraft_dir.'/'.basename($filename), $dkey);
591 exit;
592
593 }
594 }
595 }
596
597 public function get_table_prefix($allow_override = false) {
598 global $wpdb;
599 if (is_multisite() && !defined('MULTISITE')) {
600 // In this case (which should only be possible on installs upgraded from pre WP 3.0 WPMU), $wpdb->get_blog_prefix() cannot be made to return the right thing. $wpdb->base_prefix is not explicitly marked as public, so we prefer to use get_blog_prefix if we can, for future compatibility.
601 $prefix = $wpdb->base_prefix;
602 } else {
603 $prefix = $wpdb->get_blog_prefix(0);
604 }
605 return ($allow_override) ? apply_filters('updraftplus_get_table_prefix', $prefix) : $prefix;
606 }
607
608 public function siteid() {
609 $sid = get_site_option('updraftplus-addons_siteid');
610 if (!is_string($sid) || empty($sid)) {
611 $sid = md5(rand().microtime(true).home_url());
612 update_site_option('updraftplus-addons_siteid', $sid);
613 }
614 return $sid;
615 }
616
617 public function show_admin_warning_unreadablelog() {
618 global $updraftplus_admin;
619 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('The log file could not be read.', 'updraftplus'));
620 }
621
622 public function show_admin_warning_nolog() {
623 global $updraftplus_admin;
624 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('No log files were found.', 'updraftplus'));
625 }
626
627 public function show_admin_warning_unreadablefile() {
628 global $updraftplus_admin;
629 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('The given file was not found, or could not be read.', 'updraftplus'));
630 }
631
632 /**
633 * Runs upon the WP action plugins_loaded
634 */
635 public function plugins_loaded() {
636
637 // Tell WordPress where to find the translations
638 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages/');
639
640 // The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
641 if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && 'updraftplus' == $_GET['page'] )) {
642 remove_action('init', 'ganalyticator_stats_init');
643 // Appointments+ does the same; but provides a cleaner way to disable it
644 @define('APP_GCAL_DISABLE', true);
645 }
646
647 add_filter('updraftplus_remotecontrol_command_classes', array($this, 'updraftplus_remotecontrol_command_classes'));
648 add_action('updraftcentral_command_class_wanted', array($this, 'updraftcentral_command_class_wanted'));
649 add_action('updraftcentral_listener_pre_udrpc_action', array($this, 'updraftcentral_listener_pre_udrpc_action'));
650 add_action('updraftcentral_listener_post_udrpc_action', array($this, 'updraftcentral_listener_post_udrpc_action'));
651
652 if (file_exists(UPDRAFTPLUS_DIR.'/central/bootstrap.php')) {
653 include_once(UPDRAFTPLUS_DIR.'/central/bootstrap.php');
654 }
655
656 $load_classes = array();
657
658 if (defined('UPDRAFTPLUS_THIS_IS_CLONE')) {
659 $load_classes['UpdraftPlus_Temporary_Clone_Dash_Notice'] = 'includes/updraftclone/temporary-clone-dash-notice.php';
660 $load_classes['UpdraftPlus_Temporary_Clone_User_Notice'] = 'includes/updraftclone/temporary-clone-user-notice.php';
661 $load_classes['UpdraftPlus_Temporary_Clone_Restore'] = 'includes/updraftclone/temporary-clone-restore.php';
662 $load_classes['UpdraftPlus_Temporary_Clone_Auto_Login'] = 'includes/updraftclone/temporary-clone-auto-login.php';
663 $load_classes['UpdraftPlus_Temporary_Clone_Status'] = 'includes/updraftclone/temporary-clone-status.php';
664 }
665
666 foreach ($load_classes as $class => $relative_path) {
667 if (!class_exists($class)) include_once(UPDRAFTPLUS_DIR.'/'.$relative_path);
668 }
669
670 }
671
672 /**
673 * Get the character set for the current database connection
674 *
675 * @uses WPDB::determine_charset() - exists on WP 4.6+
676 *
677 * @param Object|Null $wpdb - WPDB object; if none passed, then use the global one
678 *
679 * @return String
680 */
681 public function get_connection_charset($wpdb = null) {
682 if (null === $wpdb) {
683 global $wpdb;
684 }
685
686 $charset = (defined('DB_CHARSET') && DB_CHARSET) ? DB_CHARSET : 'utf8mb4';
687
688 if (method_exists($wpdb, 'determine_charset')) {
689 $charset_collate = $wpdb->determine_charset($charset, '');
690 if (!empty($charset_collate['charset'])) $charset = $charset_collate['charset'];
691 }
692
693 return $charset;
694 }
695
696 /**
697 * Runs upon the action updraftcentral_listener_pre_udrpc_action
698 */
699 public function updraftcentral_listener_pre_udrpc_action() {
700 $this->register_wp_http_option_hooks();
701 }
702
703 /**
704 * Runs upon the action updraftcentral_listener_post_udrpc_action
705 */
706 public function updraftcentral_listener_post_udrpc_action() {
707 $this->register_wp_http_option_hooks(false);
708 }
709
710
711 /**
712 * Register our class. WP filter updraftplus_remotecontrol_command_classes.
713 *
714 * @param Array $command_classes sends across the command class
715 *
716 * @return Array - filtered value
717 */
718 public function updraftplus_remotecontrol_command_classes($command_classes) {
719 if (is_array($command_classes)) $command_classes['updraftplus'] = 'UpdraftCentral_UpdraftPlus_Commands';
720 if (is_array($command_classes)) $command_classes['updraftvault'] = 'UpdraftCentral_UpdraftVault_Commands';
721 return $command_classes;
722 }
723
724 /**
725 * Load the class when required
726 *
727 * @param string $command_php_class Sends across the php class type
728 */
729 public function updraftcentral_command_class_wanted($command_php_class) {
730 if ('UpdraftCentral_UpdraftPlus_Commands' == $command_php_class) {
731 include_once(UPDRAFTPLUS_DIR.'/includes/class-updraftcentral-updraftplus-commands.php');
732 } elseif ('UpdraftCentral_UpdraftVault_Commands' == $command_php_class) {
733 include_once(UPDRAFTPLUS_DIR.'/includes/updraftvault.php');
734 }
735 }
736
737 /**
738 * This function allows you to manually set the nonce and timestamp for the current backup job. If none are provided then it will create new ones.
739 *
740 * @param boolean|string $nonce - the nonce you want to set
741 * @param boolean|string $timestamp - the timestamp you want to set
742 *
743 * @return string - returns the backup nonce that has been set
744 */
745 public function backup_time_nonce($nonce = false, $timestamp = false) {
746 $this->job_time_ms = microtime(true);
747 if (false === $timestamp) $timestamp = time();
748 if (false === $nonce) $nonce = substr(md5(time().rand()), 20);
749 $this->backup_time = $timestamp;
750 $this->file_nonce = apply_filters('updraftplus_incremental_backup_file_nonce', $nonce);
751 $this->nonce = $nonce;
752 return $nonce;
753 }
754
755 /**
756 * Get the WordPress version
757 *
758 * @return String - the version
759 */
760 public function get_wordpress_version() {
761 static $got_wp_version = false;
762 if (!$got_wp_version) {
763 global $wp_version;
764 @include(ABSPATH.WPINC.'/version.php');
765 $got_wp_version = $wp_version;
766 }
767 return $got_wp_version;
768 }
769
770 /**
771 * Opens the log file, writes a standardised header, and stores the resulting name and handle in the class variables logfile_name/logfile_handle/opened_log_time (and possibly backup_is_already_complete)
772 *
773 * @param string $nonce - Used in the log file name to distinguish it from other log files. Should be the job nonce.
774 * @returns void
775 */
776 public function logfile_open($nonce) {
777
778 $updraft_dir = $this->backups_dir_location();
779 $this->logfile_name = $updraft_dir."/log.$nonce.txt";
780
781 if (file_exists($this->logfile_name)) {
782 $seek_to = max((filesize($this->logfile_name) - 340), 1);
783 $handle = fopen($this->logfile_name, 'r');
784 if (is_resource($handle)) {
785 // Returns 0 on success
786 if (0 === @fseek($handle, $seek_to)) {
787 $bytes_back = filesize($this->logfile_name) - $seek_to;
788 // Return to the end of the file
789 $read_recent = fread($handle, $bytes_back);
790 // Move to end of file - ought to be redundant
791 if (false !== strpos($read_recent, ') The backup apparently succeeded') && false !== strpos($read_recent, 'and is now complete')) {
792 $this->backup_is_already_complete = true;
793 }
794 }
795 fclose($handle);
796 }
797 }
798
799 $this->logfile_handle = fopen($this->logfile_name, 'a');
800
801 $this->opened_log_time = microtime(true);
802
803 $this->write_log_header(array($this, 'log'));
804
805 }
806
807 /**
808 * Writes a standardised header to the log file, using the specified logging function, which needs to be compatible with (or to be) UpdraftPlus::log()
809 *
810 * @param callable $logging_function
811 */
812 public function write_log_header($logging_function) {
813
814 global $wpdb;
815
816 $updraft_dir = $this->backups_dir_location();
817
818 call_user_func($logging_function, 'Opened log file at time: '.date('r').' on '.network_site_url());
819
820 $wp_version = $this->get_wordpress_version();
821 $mysql_version = $wpdb->get_var('SELECT VERSION()');
822 if ('' == $mysql_version) $mysql_version = $wpdb->db_version();
823 $safe_mode = $this->detect_safe_mode();
824
825 $memory_limit = ini_get('memory_limit');
826 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
827 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
828
829 // Attempt to raise limit to avoid false positives
830 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
831 $max_execution_time = (int) @ini_get("max_execution_time");
832
833 $logline = "UpdraftPlus WordPress backup plugin (https://updraftplus.com): ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".PHP_SAPI.", ".@php_uname().") MySQL: $mysql_version WPLANG: ".get_locale()." Server: ".$_SERVER["SERVER_SOFTWARE"]." safe_mode: $safe_mode max_execution_time: $max_execution_time memory_limit: $memory_limit (used: ${memory_usage}M | ${memory_usage2}M) multisite: ".(is_multisite() ? 'Y' : 'N')." openssl: ".(defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : 'N')." mcrypt: ".(function_exists('mcrypt_encrypt') ? 'Y' : 'N')." LANG: ".getenv('LANG')." ZipArchive::addFile: ";
834
835 // method_exists causes some faulty PHP installations to segfault, leading to support requests
836 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
837 $logline .= 'Y';
838 } else {
839 $logline .= (class_exists('ZipArchive') && method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
840 }
841
842 if (0 === $this->current_resumption) {
843 $memlim = $this->memory_check_current();
844 if ($memlim<65 && $memlim>0) {
845 $this->log(sprintf(__('The amount of memory (RAM) allowed for PHP is very low (%s Mb) - you should increase it to avoid failures due to insufficient memory (consult your web hosting company for more help)', 'updraftplus'), round($memlim, 1)), 'warning', 'lowram');
846 }
847 if ($max_execution_time>0 && $max_execution_time<20) {
848 call_user_func($logging_function, sprintf(__('The amount of time allowed for WordPress plugins to run is very low (%s seconds) - you should increase it to avoid backup failures due to time-outs (consult your web hosting company for more help - it is the max_execution_time PHP setting; the recommended value is %s seconds or more)', 'updraftplus'), $max_execution_time, 90), 'warning', 'lowmaxexecutiontime');
849 }
850
851 }
852
853 call_user_func($logging_function, $logline);
854
855 $hosting_bytes_free = $this->get_hosting_disk_quota_free();
856 if (is_array($hosting_bytes_free)) {
857 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
858 $quota_free = ' / '.sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
859 if ($hosting_bytes_free[3] < 1048576*50) {
860 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
861 call_user_func($logging_function, sprintf(__('Your free space in your hosting account is very low - only %s Mb remain', 'updraftplus'), $quota_free_mb), 'warning', 'lowaccountspace'.$quota_free_mb);
862 }
863 } else {
864 $quota_free = '';
865 }
866
867 $disk_free_space = @disk_free_space($updraft_dir);
868 // == rather than === here is deliberate; support experience shows that a result of (int)0 is not reliable. i.e. 0 can be returned when the real result should be false.
869 if (false == $disk_free_space) {
870 call_user_func($logging_function, "Free space on disk containing Updraft's temporary directory: Unknown".$quota_free);
871 } else {
872 call_user_func($logging_function, "Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576, 1)." MB".$quota_free);
873 $disk_free_mb = round($disk_free_space/1048576, 1);
874 if ($disk_free_space < 50*1048576) call_user_func($logging_function, sprintf(__('Your free disk space is very low - only %s Mb remain', 'updraftplus'), round($disk_free_space/1048576, 1)), 'warning', 'lowdiskspace'.$disk_free_mb);
875 }
876
877 }
878
879 /**
880 * Logs the given line, adding (relative) time stamp and newline
881 * Note these subtleties of log handling:
882 * - Messages at level 'error' are not logged to file - it is assumed that a separate call to log() at another level will take place. This is because at level 'error', messages are translated; whereas the log file is for developers who may not know the translated language. Messages at level 'error' are for the user.
883 * - Messages at level 'error' do not persist through the job (they are only saved with save_backup_to_history(), and never restored from there - so only the final save_backup_to_history() errors
884 * persist); we presume that either a) they will be cleared on the next attempt, or b) they will occur again on the final attempt (at which point they will go to the user). But...
885 * - messages at level 'warning' persist. These are conditions that are unlikely to be cleared, not-fatal, but the user should be informed about. The $uniq_id field (which should not be numeric) can then be used for warnings that should only be logged once
886 * $skip_dblog = true is suitable when there's a risk of excessive logging, and the information is not important for the user to see in the browser on the settings page
887 * The uniq_id field is also used with PHP event detection - it is set then to 'php_event' - which is useful for anything hooking the action to detect
888 *
889 * @param String $how_many_bytes_needed - how many bytes need to be available
890 * @return Boolean - whether the needed number of bytes is available
891 */
892 public function verify_free_memory($how_many_bytes_needed) {
893 // This returns in MB
894 $memory_limit = $this->memory_check_current();
895 if (!is_numeric($memory_limit)) return false;
896 $memory_limit = $memory_limit * 1048576;
897 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
898 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
899 if ($memory_limit - $memory_usage > $how_many_bytes_needed && $memory_limit - $memory_usage2 > $how_many_bytes_needed) return true;
900 return false;
901 }
902
903 /**
904 * Log
905 *
906 * @param string $line the log line
907 * @param string $level the log level: notice, warning, error. If suffixed with a hypen and a destination, then the default destination is changed too.
908 * @param boolean $uniq_id each of these will only be logged once
909 * @param boolean $skip_dblog if true, then do not write to the database
910 * @return null
911 */
912 public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
913
914 $destination = 'default';
915 if (preg_match('/^([a-z]+)-([a-z]+)$/', $level, $matches)) {
916 $level = $matches[1];
917 $destination = $matches[2];
918 }
919
920 if ('error' == $level || 'warning' == $level) {
921 if ('error' == $level && 0 == $this->error_count()) $this->log('An error condition has occurred for the first time during this job');
922 if ($uniq_id) {
923 $this->errors[$uniq_id] = array('level' => $level, 'message' => $line);
924 } else {
925 $this->errors[] = array('level' => $level, 'message' => $line);
926 }
927 // Errors are logged separately
928 if ('error' == $level) return;
929 // It's a warning
930 $warnings = $this->jobdata_get('warnings');
931 if (!is_array($warnings)) $warnings = array();
932 if ($uniq_id) {
933 $warnings[$uniq_id] = $line;
934 } else {
935 $warnings[] = $line;
936 }
937 $this->jobdata_set('warnings', $warnings);
938 }
939
940 if (false === ($line = apply_filters('updraftplus_logline', $line, $this->nonce, $level, $uniq_id, $destination))) return;
941
942 if ($this->logfile_handle) {
943 // Record log file times relative to the backup start, if possible
944 $rtime = (!empty($this->job_time_ms)) ? microtime(true)-$this->job_time_ms : microtime(true)-$this->opened_log_time;
945 fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") ".(('notice' != $level) ? '['.ucfirst($level).'] ' : '').$line."\n");
946 }
947
948 switch ($this->jobdata_get('job_type')) {
949 case 'download':
950 // Download messages are keyed on the job (since they could be running several), and type
951 // The values of the POST array were checked before
952 $findex = empty($_POST['findex']) ? 0 : $_POST['findex'];
953
954 if (!empty($_POST['timestamp']) && !empty($_POST['type'])) $this->jobdata_set('dlmessage_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line);
955 break;
956
957 case 'restore':
958 // if ('debug' != $level) echo $line."\n";
959 break;
960
961 default:
962 if (!$skip_dblog && 'debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")", false);
963 break;
964 }
965
966 if (defined('UPDRAFTPLUS_CONSOLELOG') && UPDRAFTPLUS_CONSOLELOG) echo $line."\n";
967 if (defined('UPDRAFTPLUS_BROWSERLOG') && UPDRAFTPLUS_BROWSERLOG) echo htmlentities($line)."<br>\n";
968 }
969
970 /**
971 * Remove any logged warnings with the specified identifier. (The use case for this is that you can warn of something that may be about to happen (with a probably crash if it does), and then remove the warning if it did not happen).
972 *
973 * @see self::log()
974 *
975 * @param String $uniq_id - the identifier, previously passed to self::log()
976 */
977 public function log_remove_warning($uniq_id) {
978 $warnings = $this->jobdata_get('warnings');
979 if (!is_array($warnings)) $warnings = array();
980 unset($warnings[$uniq_id]);
981 $this->jobdata_set('warnings', $warnings);
982 unset($this->errors[$uniq_id]);
983 }
984
985 /**
986 * For efficiency, you can also feed false or a string into this function
987 *
988 * @param Boolean|String|WP_Error $err - the errors
989 * @param Boolean $echo - whether to echo() the error(s)
990 * @param Boolean $logerror - whether to pass errors to UpdraftPlus::log()
991 * @return Boolean - returns false for convenience
992 */
993 public function log_wp_error($err, $echo = false, $logerror = false) {
994 if (false === $err) return false;
995 if (is_string($err)) {
996 $this->log("Error message: $err");
997 if ($echo) $this->log(sprintf(__('Error: %s', 'updraftplus'), $err), 'notice-warning');
998 if ($logerror) $this->log($err, 'error');
999 return false;
1000 }
1001 foreach ($err->get_error_messages() as $msg) {
1002 $this->log("Error message: $msg");
1003 if ($echo) $this->log(sprintf(__('Error: %s', 'updraftplus'), $msg), 'notice-warning');
1004 if ($logerror) $this->log($msg, 'error');
1005 }
1006 $codes = $err->get_error_codes();
1007 if (is_array($codes)) {
1008 foreach ($codes as $code) {
1009 $data = $err->get_error_data($code);
1010 if (!empty($data)) {
1011 $ll = (is_string($data)) ? $data : serialize($data);
1012 $this->log("Error data (".$code."): ".$ll);
1013 }
1014 }
1015 }
1016 // Returns false so that callers can return with false more efficiently if they wish
1017 return false;
1018 }
1019
1020 /**
1021 * Get the maximum packet size on the WPDB MySQL connection, in bytes, after (optionally) attempting to raise it to 32MB if it appeared to be lower.
1022 * A default value equal to 1MB is returned if the true value could not be found - it has been found reasonable to assume that at least this is available.
1023 *
1024 * @param Boolean $first_raise
1025 * @param Boolean $log_it
1026 *
1027 * @return Integer
1028 */
1029 public function max_packet_size($first_raise = true, $log_it = true) {
1030 global $wpdb;
1031 $mp = (int) $wpdb->get_var("SELECT @@session.max_allowed_packet");
1032 // Default to 1MB
1033 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
1034 // 32MB
1035 if ($first_raise && $mp < 33554432) {
1036 $save = $wpdb->show_errors(false);
1037 $req = @$wpdb->query("SET GLOBAL max_allowed_packet=33554432");
1038 $wpdb->show_errors($save);
1039 if (!$req) $this->log("Tried to raise max_allowed_packet from ".round($mp/1048576, 1)." MB to 32 MB, but failed (".$wpdb->last_error.", ".serialize($req).")");
1040 $mp = (int) $wpdb->get_var("SELECT @@session.max_allowed_packet");
1041 // Default to 1MB
1042 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
1043 }
1044 if ($log_it) $this->log("Max packet size: ".round($mp/1048576, 1)." MB");
1045 return $mp;
1046 }
1047
1048 /**
1049 * Q. Why is this abstracted into a separate function? A. To allow poedit and other parsers to pick up the need to translate strings passed to it (and not pick up all of those passed to log()).
1050 * 1st argument = the line to be logged (obligatory)
1051 * Further arguments = parameters for sprintf()
1052 *
1053 * @return null
1054 */
1055 public function log_e() {
1056 $args = func_get_args();
1057 // Get first argument
1058 $pre_line = array_shift($args);
1059 // Log it whilst still in English
1060 if (is_wp_error($pre_line)) {
1061 $this->log_wp_error($pre_line);
1062 } else {
1063 // Now run (v)sprintf on it, using any remaining arguments. vsprintf = sprintf but takes an array instead of individual arguments
1064 $this->log(vsprintf($pre_line, $args));
1065 // This is slightly hackish, in that we have no way to use a different level or destination. In that case, the caller should instead call log() twice with different parameters, instead of using this convenience function.
1066 $this->log(vsprintf($pre_line, $args), 'notice-restore');
1067 }
1068 }
1069
1070 /**
1071 * This function is used by cloud methods to provide standardised logging, but more importantly to help us detect that meaningful activity took place during a resumption run, so that we can schedule further resumptions if it is worthwhile
1072 *
1073 * @param Number $percent - the amount of the file uploaded
1074 * @param String $extra - anything extra to include in the log message
1075 * @param Boolean $file_path - the full path to the file being uploaded
1076 * @param Boolean $log_it - whether to pass the message to UpdraftPlus::log()
1077 * @return Void
1078 */
1079 public function record_uploaded_chunk($percent, $extra = '', $file_path = false, $log_it = true) {
1080
1081 // Touch the original file, which helps prevent overlapping runs
1082 if ($file_path) touch($file_path);
1083
1084 // What this means in effect is that at least one of the files touched during the run must reach this percentage (so lapping round from 100 is OK)
1085 if ($percent > 0.7 * ($this->current_resumption - max($this->jobdata_get('uploaded_lastreset'), 9))) UpdraftPlus_Job_Scheduler::something_useful_happened();
1086
1087 // Log it
1088 global $updraftplus_backup;
1089 $log = (!empty($updraftplus_backup->current_service)) ? ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded" : '';
1090 if ($log && $log_it) $this->log($log.(($extra) ? " ($extra)" : ''));
1091 // If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
1092 // Our definition of meaningful is that we must maintain an overall average of at least 0.7% per run, after allowing 9 runs for everything else to get going
1093 // i.e. Max 100/.7 + 9 = 150 runs = 760 minutes = 12 hrs 40, if spaced at 5 minute intervals. However, our algorithm now decreases the intervals if it can, so this should not really come into play
1094 // If they get 2 minutes on each run, and the file is 1GB, then that equals 10.2MB/120s = minimum 59KB/s upload speed required
1095
1096 $upload_status = $this->jobdata_get('uploading_substatus');
1097 if (is_array($upload_status)) {
1098 $upload_status['p'] = $percent/100;
1099 $this->jobdata_set('uploading_substatus', $upload_status);
1100 }
1101
1102 }
1103
1104 /**
1105 * Method for helping remote storage methods to upload files in chunks without needing to duplicate all the overhead
1106 *
1107 * @param object $caller the object to call back to do the actual network API calls; needs to have a chunked_upload() method.
1108 * @param string $file the full path to the file
1109 * @param string $cloudpath this is passed back to the callback function; within this function, it is used only for logging
1110 * @param string $logname the prefix used on log lines. Also passed back to the callback function.
1111 * @param integer $chunk_size the size, in bytes, of each upload chunk
1112 * @param integer $uploaded_size how many bytes have already been uploaded. This is passed back to the callback function; within this method, it is only used for logging.
1113 * @param boolean $singletons when the file, given the chunk size, would only have one chunk, should that be uploaded (true), or instead should 1 be returned (false) ?
1114 * @return boolean
1115 */
1116 public function chunked_upload($caller, $file, $cloudpath, $logname, $chunk_size, $uploaded_size, $singletons = false) {
1117
1118 $fullpath = $this->backups_dir_location().'/'.$file;
1119 $orig_file_size = filesize($fullpath);
1120
1121 if ($uploaded_size >= $orig_file_size && !method_exists($caller, 'chunked_upload_finish')) return true;
1122
1123 $chunks = floor($orig_file_size / $chunk_size);
1124 // There will be a remnant unless the file size was exactly on a chunk boundary
1125 if ($orig_file_size % $chunk_size > 0) $chunks++;
1126
1127 $this->log("$logname upload: $file (chunks: $chunks, of size: $chunk_size) -> $cloudpath ($uploaded_size)");
1128
1129 if (0 == $chunks) {
1130 return 1;
1131 } elseif ($chunks < 2 && !$singletons) {
1132 return 1;
1133 }
1134
1135 // We have multiple chunks
1136 if ($uploaded_size < $orig_file_size) {
1137
1138 if (false == ($fp = @fopen($fullpath, 'rb'))) {
1139 $this->log("$logname: failed to open file: $fullpath");
1140 $this->log("$file: ".sprintf(__('%s Error: Failed to open local file', 'updraftplus'), $logname), 'error');
1141 return false;
1142 }
1143
1144 $upload_start = 0;
1145 $upload_end = -1;
1146 $chunk_index = 1;
1147 // The file size minus one equals the byte offset of the final byte
1148 $upload_end = min($chunk_size - 1, $orig_file_size - 1);
1149 $errors_on_this_chunk = 0;
1150
1151 while ($upload_start < $orig_file_size) {
1152
1153 // Don't forget the +1; otherwise the last byte is omitted
1154 $upload_size = $upload_end - $upload_start + 1;
1155
1156 if ($upload_start) fseek($fp, $upload_start);
1157
1158 /*
1159 * Valid return values for $uploaded are many, as the possibilities have grown over time.
1160 * This could be cleaned up; but, it works, and it's not hugely complex.
1161 *
1162 * WP_Error : an error occured. The only permissible codes are: reduce_chunk_size (only on the first chunk), try_again
1163 * (bool)true : What was requested was done
1164 * (int)1 : What was requested was done, but do not log anything
1165 * (bool)false : There was an error
1166 * (Object) : Properties:
1167 * (bool)log: (bool) - if absent, defaults to true
1168 * (int)new_chunk_size: advisory amount for the chunk size for future chunks
1169 * NOT IMPLEMENTED: (int)bytes_uploaded: Actual number of bytes uploaded (needs to be positive - o/w, should return an error instead)
1170 * N.B. Consumers should consult $fp and $upload_start to get data; they should not re-calculate from $chunk_index, which is not an indicator of file position.
1171 */
1172 $uploaded = $caller->chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $orig_file_size);
1173
1174 // Try again? (Just once - added in 1.12.6 (can make more sophisticated if there is a need))
1175 if (is_wp_error($uploaded) && 'try_again' == $uploaded->get_error_code()) {
1176 // Arbitrary wait
1177 sleep(3);
1178 $this->log("Re-trying after wait (to allow apparent inconsistency to clear)");
1179 $uploaded = $caller->chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $orig_file_size);
1180 }
1181
1182 // This is the only other supported case of a WP_Error - otherwise, a boolean must be returned
1183 // Note that this is only allowed on the first chunk. The caller is responsible to remember its chunk size if it uses this facility.
1184 if (1 == $chunk_index && is_wp_error($uploaded) && 'reduce_chunk_size' == $uploaded->get_error_code() && false != ($new_chunk_size = $uploaded->get_error_data()) && is_numeric($new_chunk_size)) {
1185 $this->log("Re-trying with new chunk size: ".$new_chunk_size);
1186 return $this->chunked_upload($caller, $file, $cloudpath, $logname, $new_chunk_size, $uploaded_size, $singletons);
1187 }
1188
1189 $uploaded_amount = $chunk_size;
1190
1191 /*
1192 // Not using this approach for now. Instead, going to allow the consumers to increase the next chunk size
1193 if (is_object($uploaded) && isset($uploaded->bytes_uploaded)) {
1194 if (!$uploaded->bytes_uploaded) {
1195 $uploaded = false;
1196 } else {
1197 $uploaded_amount = $uploaded->bytes_uploaded;
1198 $uploaded = (!isset($uploaded->log) || $uploaded->log) ? true : 1;
1199 }
1200 }
1201 */
1202 if (is_object($uploaded) && isset($uploaded->new_chunk_size)) {
1203 if ($uploaded->new_chunk_size >= 1048576) $new_chunk_size = $uploaded->new_chunk_size;
1204 $uploaded = (!isset($uploaded->log) || $uploaded->log) ? true : 1;
1205 }
1206
1207 // The joys of PHP: is_wp_error() is not false-y.
1208 if ($uploaded && !is_wp_error($uploaded)) {
1209 $perc = round(100*($upload_end + 1)/max($orig_file_size, 1), 1);
1210 // Consumers use a return value of (int)1 (rather than (bool)true) to suppress logging
1211 $log_it = (1 === $uploaded) ? false : true;
1212 $this->record_uploaded_chunk($perc, $chunk_index, $fullpath, $log_it);
1213
1214 // $uploaded_bytes = $upload_end + 1;
1215
1216 // If there was an error, then we re-try the same chunk; we don't move on to the next one. Otherwise, we would need more code to handle potential 'intermediate' failed chunks (in case PHP dies before this method eventually returns false, and thus the intermediate chunk failure never gets detected)
1217 $chunk_index++;
1218 $errors_on_this_chunk = 0;
1219 $upload_start = $upload_end + 1;
1220 $upload_end += isset($new_chunk_size) ? $uploaded_amount + $new_chunk_size - $chunk_size : $uploaded_amount;
1221 $upload_end = min($upload_end, $orig_file_size - 1);
1222
1223 } else {
1224
1225 $errors_on_this_chunk++;
1226
1227 // Either $uploaded is false-y, or is a WP_Error
1228 if (is_wp_error($uploaded)) {
1229 $this->log("$logname: Chunk upload ($chunk_index) failed (".$uploaded->get_error_code().'): '.$uploaded->get_error_message());
1230 } else {
1231 $this->log("$logname: Chunk upload ($chunk_index) failed");
1232 }
1233
1234 if ($errors_on_this_chunk >= 3) {
1235 @fclose($fp);
1236 return false;
1237 }
1238 }
1239
1240 }
1241
1242 @fclose($fp);
1243
1244 }
1245
1246 // All chunks are uploaded - now combine the chunks
1247 $ret = true;
1248
1249 // The action calls here exist to aid debugging
1250 if (method_exists($caller, 'chunked_upload_finish')) {
1251 do_action('updraftplus_pre_chunked_upload_finish', $file, $caller);
1252 $ret = $caller->chunked_upload_finish($file);
1253 if (!$ret) {
1254 $this->log("$logname - failed to re-assemble chunks");
1255 $this->log(sprintf(__('%s error - failed to re-assemble chunks', 'updraftplus'), $logname), 'error');
1256 }
1257 do_action('updraftplus_post_chunked_upload_finish', $file, $caller, $ret);
1258 }
1259
1260 if ($ret) {
1261 // We allow chunked_upload_finish to return (int)1 to indicate that it took care of any logging.
1262 if (true === $ret) $this->log("$logname upload: success");
1263 $ret = true;
1264 // UpdraftPlus_RemoteStorage_Addons_Base calls this itself
1265 if (!is_a($caller, 'UpdraftPlus_RemoteStorage_Addons_Base_v2')) $this->uploaded_file($file);
1266 }
1267
1268 return $ret;
1269
1270 }
1271
1272 /**
1273 * Provides a convenience function allowing remote storage methods to download a file in chunks, without duplicated overhead.
1274 *
1275 * @param string $file - The basename of the file being downloaded
1276 * @param object $method - This remote storage method object needs to have a chunked_download() method to call back
1277 * @param integer $remote_size - The size, in bytes, of the object being downloaded
1278 * @param boolean $manually_break_up - Whether to break the download into multiple network operations (rather than just issuing a GET with a range beginning at the end of the already-downloaded data, and carrying on until it times out)
1279 * @param Mixed $passback - A value to pass back to the callback function
1280 * @param integer $chunk_size - Break up the download into chunks of this number of bytes. Should be set if and only if $manually_break_up is true.
1281 */
1282 public function chunked_download($file, $method, $remote_size, $manually_break_up = false, $passback = null, $chunk_size = 1048576) {
1283
1284 try {
1285
1286 $fullpath = $this->backups_dir_location().'/'.$file;
1287 $start_offset = file_exists($fullpath) ? filesize($fullpath) : 0;
1288
1289 if ($start_offset >= $remote_size) {
1290 $this->log("File is already completely downloaded ($start_offset/$remote_size)");
1291 return true;
1292 }
1293
1294 // Some more remains to download - so let's do it
1295 // N.B. We use ftell(), which precludes us from using open in append-only ('a') mode - see https://php.net/manual/en/function.fopen.php
1296 if (!($fh = fopen($fullpath, 'c'))) {// phpcs:ignore PHPCompatibility.ParameterValues.NewFopenModes.cFound -- Passing "c" as the $mode to fopen() is not supported in PHP 5.2.5 or lower. Found 'c'
1297 $this->log("Error opening local file: $fullpath");
1298 $this->log($file.": ".__("Error", 'updraftplus').": ".__('Error opening local file: Failed to download', 'updraftplus'), 'error');
1299 return false;
1300 }
1301
1302 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
1303
1304 // This only affects logging
1305 $expected_bytes_delivered_so_far = true;
1306
1307 while ($start_offset < $remote_size) {
1308 $headers = array();
1309 // If resuming, then move to the end of the file
1310
1311 $requested_bytes = $last_byte-$start_offset;
1312
1313 if ($expected_bytes_delivered_so_far) {
1314 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next $requested_bytes bytes");
1315 } else {
1316 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next chunk (${start_offset}-)");
1317 }
1318
1319 if ($start_offset > 0 || $last_byte<$remote_size) {
1320 fseek($fh, $start_offset);
1321 // N.B. Don't alter this format without checking what relies upon it
1322 $last_byte_start = $last_byte - 1;
1323 $headers['Range'] = "bytes=$start_offset-$last_byte_start";
1324 }
1325
1326 /*
1327 * The most common method is for the remote storage module to return a string with the results in it. In that case, the final $fh parameter is unused. However, since not all SDKs have that option conveniently, it is also possible to use the file handle and write directly to that; in that case, the method can either return the number of bytes written, or (boolean)true to infer it from the new file *pointer*.
1328 * The method is free to write/return as much data as it pleases.
1329 */
1330 $ret = $method->chunked_download($file, $headers, $passback, $fh);
1331 if (true === $ret) {
1332 clearstatcache();
1333 // Some SDKs (including AWS/S3) close the resource
1334 // N.B. We use ftell(), which precludes us from using open in append-only ('a') mode - see https://php.net/manual/en/function.fopen.php
1335 if (is_resource($fh)) {
1336 $ret = ftell($fh);
1337 } else {
1338 $ret = filesize($fullpath);
1339 // fseek returns - on success
1340 if (false == ($fh = fopen($fullpath, 'c')) || 0 !== fseek($fh, $ret)) {// phpcs:ignore PHPCompatibility.ParameterValues.NewFopenModes.cFound -- Passing "c" as the $mode to fopen() is not supported in PHP 5.2.5 or lower. Found 'c'
1341 $this->log("Error opening local file: $fullpath");
1342 $this->log($file.": ".__("Error", 'updraftplus').": ".__('Error opening local file: Failed to download', 'updraftplus'), 'error');
1343 return false;
1344 }
1345 }
1346 if (is_integer($ret)) $ret -= $start_offset;
1347 }
1348
1349 // Note that this covers a false code returned either by chunked_download() or by ftell.
1350 if (false === $ret) return false;
1351
1352 $returned_bytes = is_integer($ret) ? $ret : strlen($ret);
1353
1354 if ($returned_bytes > $requested_bytes || $returned_bytes < $requested_bytes - 1) $expected_bytes_delivered_so_far = false;
1355
1356 if (!is_integer($ret) && !fwrite($fh, $ret)) throw new Exception('Write failure (start offset: '.$start_offset.', bytes: '.strlen($ret).'; requested: '.$requested_bytes.')');
1357
1358 clearstatcache();
1359 $start_offset = ftell($fh);
1360 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
1361
1362 }
1363
1364 } catch (Exception $e) {
1365 $this->log('Error ('.get_class($e).') - failed to download the file ('.$e->getCode().', '.$e->getMessage().', line '.$e->getLine().' in '.$e->getFile().')');
1366 $this->log("$file: ".__('Error - failed to download the file', 'updraftplus').' ('.$e->getCode().', '.$e->getMessage().')', 'error');
1367 return false;
1368 }
1369
1370 fclose($fh);
1371
1372 return true;
1373 }
1374
1375 /**
1376 * Detect if safe_mode is on. N.B. This is abolished from PHP 7.0
1377 *
1378 * @return Integer - 1 or 0
1379 */
1380 public function detect_safe_mode() {
1381 // @codingStandardsIgnoreLine
1382 return (@ini_get('safe_mode') && 'off' != strtolower(@ini_get('safe_mode'))) ? 1 : 0;
1383 }
1384
1385 /**
1386 * Find, if possible, a working mysqldump executable
1387 *
1388 * @param Boolean $logit - whether to log the workings or not
1389 * @param Boolean $cacheit - whether to cache the results for subsequent queries or not
1390 *
1391 * @return String|Boolean - either a path to an executable, or false for failure
1392 */
1393 public function find_working_sqldump($logit = true, $cacheit = true) {
1394
1395 // The hosting provider may have explicitly disabled the popen or proc_open functions
1396 if ($this->detect_safe_mode() || !function_exists('popen') || !function_exists('escapeshellarg')) {
1397 if ($cacheit) $this->jobdata_set('binsqldump', false);
1398 return false;
1399 }
1400 $existing = $this->jobdata_get('binsqldump', null);
1401 // Theoretically, we could have moved machines, due to a migration
1402 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
1403
1404 $updraft_dir = $this->backups_dir_location();
1405 global $wpdb;
1406 $table_name = $wpdb->get_blog_prefix().'options';
1407 $tmp_file = md5(time().rand()).".sqltest.tmp";
1408 $pfile = md5(time().rand()).'.tmp';
1409 file_put_contents($updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".DB_PASSWORD."\n");
1410
1411 $result = false;
1412 foreach (explode(',', UPDRAFTPLUS_MYSQLDUMP_EXECUTABLE) as $potsql) {
1413
1414 if (!@is_executable($potsql)) continue;
1415
1416 if ($logit) $this->log("Testing potential mysqldump binary: $potsql");
1417
1418 if ('win' == strtolower(substr(PHP_OS, 0, 3))) {
1419 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $updraft_dir))." & ";
1420 $siteurl = "'siteurl'";
1421 if (false !== strpos($potsql, ' ')) $potsql = '"'.$potsql.'"';
1422 } else {
1423 $exec = "cd ".escapeshellarg($updraft_dir)."; ";
1424 $siteurl = "\\'siteurl\\'";
1425 if (false !== strpos($potsql, ' ')) $potsql = "'$potsql'";
1426 }
1427
1428 $exec .= "$potsql --defaults-file=$pfile --max_allowed_packet=1M --quote-names --add-drop-table --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --where=option_name=$siteurl --user=".escapeshellarg(DB_USER)." --host=".escapeshellarg(DB_HOST)." ".DB_NAME." ".escapeshellarg($table_name)."";
1429
1430 $handle = popen($exec, "r");
1431 if ($handle) {
1432 if (!feof($handle)) {
1433 $output = fread($handle, 8192);
1434 if ($output && $logit) {
1435 $log_output = (strlen($output) > 512) ? substr($output, 0, 512).' (truncated - '.strlen($output).' bytes total)' : $output;
1436 $this->log("Output: ".str_replace("\n", '\\n', trim($log_output)));
1437 }
1438 } else {
1439 $output = '';
1440 }
1441 $ret = pclose($handle);
1442 if (0 != $ret) {
1443 if ($logit) {
1444 $this->log("Binary mysqldump: error (code: $ret)");
1445 }
1446 } else {
1447 // $dumped = file_get_contents($updraft_dir.'/'.$tmp_file, false, null, 0, 4096);
1448 if (stripos($output, 'insert into') !== false) {
1449 if ($logit) $this->log("Working binary mysqldump found: $potsql");
1450 $result = $potsql;
1451 break;
1452 }
1453 }
1454 } else {
1455 if ($logit) $this->log("Error: popen failed");
1456 }
1457 }
1458
1459 @unlink($updraft_dir.'/'.$pfile);
1460 @unlink($updraft_dir.'/'.$tmp_file);
1461
1462 if ($cacheit) $this->jobdata_set('binsqldump', $result);
1463
1464 return $result;
1465 }
1466
1467 /**
1468 * This function will work out which zip object we want to use and return it's name
1469 *
1470 * @return string - the name of the zip object we want to use
1471 */
1472 public function get_zip_object_name() {
1473
1474 if (!class_exists('UpdraftPlus_BinZip')) include_once(UPDRAFTPLUS_DIR . '/includes/class-zip.php');
1475
1476 $zip_object = 'UpdraftPlus_ZipArchive';
1477
1478 // In tests, PclZip was found to be 25% slower than ZipArchive
1479 if (((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
1480 $zip_object = 'UpdraftPlus_PclZip';
1481 }
1482
1483 return $zip_object;
1484 }
1485
1486 /**
1487 * We require -@ and -u -r to work - which is the usual Linux binzip
1488 *
1489 * @param Boolean $logit - whether to record the results with UpdraftPlus::log()
1490 * @param Boolean $cacheit - whether to cache the results as job data
1491 * @return String|Boolean - the path to a working zip binary, or false
1492 */
1493 public function find_working_bin_zip($logit = true, $cacheit = true) {
1494 if ($this->detect_safe_mode()) return false;
1495 // The hosting provider may have explicitly disabled the popen or proc_open functions
1496 if (!function_exists('popen') || !function_exists('proc_open') || !function_exists('escapeshellarg')) {
1497 if ($cacheit) $this->jobdata_set('binzip', false);
1498 return false;
1499 }
1500
1501 $existing = $this->jobdata_get('binzip', null);
1502 // Theoretically, we could have moved machines, due to a migration
1503 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
1504
1505 $updraft_dir = $this->backups_dir_location();
1506 foreach (explode(',', UPDRAFTPLUS_ZIP_EXECUTABLE) as $potzip) {
1507 if (!@is_executable($potzip)) continue;
1508 if ($logit) $this->log("Testing: $potzip");
1509
1510 // Test it, see if it is compatible with Info-ZIP
1511 // If you have another kind of zip, then feel free to tell me about it
1512 @mkdir($updraft_dir.'/binziptest/subdir1/subdir2', 0777, true);
1513
1514 if (!file_exists($updraft_dir.'/binziptest/subdir1/subdir2')) return false;
1515
1516 file_put_contents($updraft_dir.'/binziptest/subdir1/subdir2/test.html', '<html><body><a href="https://updraftplus.com">UpdraftPlus is a great backup and restoration plugin for WordPress.</a></body></html>');
1517 @unlink($updraft_dir.'/binziptest/test.zip');
1518 if (is_file($updraft_dir.'/binziptest/subdir1/subdir2/test.html')) {
1519
1520 $exec = "cd ".escapeshellarg($updraft_dir)."; $potzip";
1521 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1522 $exec .= " -v -u -r binziptest/test.zip binziptest/subdir1";
1523
1524 $all_ok=true;
1525 $handle = popen($exec, "r");
1526 if ($handle) {
1527 while (!feof($handle)) {
1528 $w = fgets($handle);
1529 if ($w && $logit) $this->log("Output: ".trim($w));
1530 }
1531 $ret = pclose($handle);
1532 if (0 != $ret) {
1533 if ($logit) $this->log("Binary zip: error (code: $ret)");
1534 $all_ok = false;
1535 }
1536 } else {
1537 if ($logit) $this->log("Error: popen failed");
1538 $all_ok = false;
1539 }
1540
1541 // Now test -@
1542 if (true == $all_ok) {
1543 file_put_contents($updraft_dir.'/binziptest/subdir1/subdir2/test2.html', '<html><body><a href="https://updraftplus.com">UpdraftPlus is a really great backup and restoration plugin for WordPress.</a></body></html>');
1544
1545 $exec = $potzip;
1546 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1547 $exec .= " -v -@ binziptest/test.zip";
1548
1549 $all_ok = true;
1550
1551 $descriptorspec = array(
1552 0 => array('pipe', 'r'),
1553 1 => array('pipe', 'w'),
1554 2 => array('pipe', 'w')
1555 );
1556 $handle = proc_open($exec, $descriptorspec, $pipes, $updraft_dir);
1557 if (is_resource($handle)) {
1558 if (!fwrite($pipes[0], "binziptest/subdir1/subdir2/test2.html\n")) {
1559 @fclose($pipes[0]);
1560 @fclose($pipes[1]);
1561 @fclose($pipes[2]);
1562 $all_ok = false;
1563 } else {
1564 fclose($pipes[0]);
1565 while (!feof($pipes[1])) {
1566 $w = fgets($pipes[1]);
1567 if ($w && $logit) $this->log("Output: ".trim($w));
1568 }
1569 fclose($pipes[1]);
1570
1571 while (!feof($pipes[2])) {
1572 $last_error = fgets($pipes[2]);
1573 if (!empty($last_error) && $logit) $this->log("Stderr output: ".trim($w));
1574 }
1575 fclose($pipes[2]);
1576
1577 $ret = proc_close($handle);
1578 if (0 != $ret) {
1579 if ($logit) $this->log("Binary zip: error (code: $ret)");
1580 $all_ok = false;
1581 }
1582
1583 }
1584
1585 } else {
1586 if ($logit) $this->log("Error: proc_open failed");
1587 $all_ok = false;
1588 }
1589
1590 }
1591
1592 // Do we now actually have a working zip? Need to test the created object using PclZip
1593 // If it passes, then remove dirs and then return $potzip;
1594 $found_first = false;
1595 $found_second = false;
1596 if ($all_ok && file_exists($updraft_dir.'/binziptest/test.zip')) {
1597 if (function_exists('gzopen')) {
1598 if (!class_exists('PclZip')) include_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
1599 $zip = new PclZip($updraft_dir.'/binziptest/test.zip');
1600 $foundit = 0;
1601 if (($list = $zip->listContent()) != 0) {
1602 foreach ($list as $obj) {
1603 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test.html' == $obj['stored_filename'] && 131 == $obj['size']) $found_first=true;
1604 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test2.html' == $obj['stored_filename'] && 138 == $obj['size']) $found_second=true;
1605 }
1606 }
1607 } else {
1608 // PclZip will die() if gzopen is not found
1609 // Obviously, this is a kludge - we assume it's working. We could, of course, just return false - but since we already know now that PclZip can't work, that only leaves ZipArchive
1610 $this->log("gzopen function not found; PclZip cannot be invoked; will assume that binary zip works if we have a non-zero file");
1611 if (filesize($updraft_dir.'/binziptest/test.zip') > 0) {
1612 $found_first = true;
1613 $found_second = true;
1614 }
1615 }
1616 }
1617 $this->remove_binzip_test_files($updraft_dir);
1618 if ($found_first && $found_second) {
1619 if ($logit) $this->log("Working binary zip found: $potzip");
1620 if ($cacheit) $this->jobdata_set('binzip', $potzip);
1621 return $potzip;
1622 }
1623
1624 }
1625 $this->remove_binzip_test_files($updraft_dir);
1626 }
1627 if ($cacheit) $this->jobdata_set('binzip', false);
1628 return false;
1629 }
1630
1631 /**
1632 * Remove potentially existing test files after binzip testing
1633 *
1634 * @param String $updraft_dir - directory to find the files in
1635 */
1636 private function remove_binzip_test_files($updraft_dir) {
1637 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test.html');
1638 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test2.html');
1639 @rmdir($updraft_dir.'/binziptest/subdir1/subdir2');
1640 @rmdir($updraft_dir.'/binziptest/subdir1');
1641 @unlink($updraft_dir.'/binziptest/test.zip');
1642 @rmdir($updraft_dir.'/binziptest');
1643 }
1644
1645 public function option_filter_get($which) {
1646 global $wpdb;
1647 $row = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $which));
1648 // Has to be get_row instead of get_var because of funkiness with 0, false, null values
1649 return (is_object($row)) ? $row->option_value : false;
1650 }
1651
1652 /**
1653 * Indicate which checksums to take for backup files. Abstracted for extensibilty and future changes.
1654 *
1655 * @returns array - a list of hashing algorithms, as understood by PHP's hash() function
1656 */
1657 public function which_checksums() {
1658 return apply_filters('updraftplus_which_checksums', array('sha1', 'sha256'));
1659 }
1660
1661 /**
1662 * Pretty printing of the raw backup information
1663 *
1664 * @param String $description
1665 * @param Array $history
1666 * @param String $entity
1667 * @param Array $checksums
1668 * @param Array $jobdata
1669 * @param Boolean $smaller
1670 * @return String
1671 */
1672 public function printfile($description, $history, $entity, $checksums, $jobdata, $smaller = false) {
1673
1674 if (empty($history[$entity])) return;
1675
1676 // PHP 7.2+ throws a warning if you try to count() a string
1677 $how_many = is_string($history[$entity]) ? 1 : count($history[$entity]);
1678
1679 if ($smaller) {
1680 $pfiles = "<strong>".$description." (".sprintf(__('files: %s', 'updraftplus'), $how_many).")</strong><br>\n";
1681 } else {
1682 $pfiles = "<h3>".$description." (".sprintf(__('files: %s', 'updraftplus'), $how_many).")</h3>\n\n";
1683 }
1684
1685 $is_incremental = (!empty($jobdata) && !empty($jobdata['job_type']) && 'incremental' == $jobdata['job_type'] && 'db' != substr($entity, 0, 2)) ? true : false;
1686
1687 if ($is_incremental) {
1688 $backup_timestamp = $jobdata['backup_time'];
1689 $backup_history = UpdraftPlus_Backup_History::get_history($backup_timestamp);
1690 $pfiles .= "<dl>";
1691 foreach ($backup_history['incremental_sets'] as $timestamp => $backup) {
1692 if (isset($backup[$entity])) {
1693 $pfiles .= "<dt>".get_date_from_gmt(gmdate('Y-m-d H:i:s', (int) $timestamp), 'M d, Y G:i')."\n</dt>\n";
1694 foreach ($backup[$entity] as $ind => $file) {
1695 $pfiles .= "<dd>".$this->get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind)."\n</dd>\n";
1696 }
1697 }
1698 }
1699 $pfiles .= "</dl>\n";
1700 } else {
1701
1702 $pfiles .= "<ul>";
1703 $files = $history[$entity];
1704 if (is_string($files)) $files = array($files);
1705
1706 foreach ($files as $ind => $file) {
1707 $pfiles .= "<li>".$this->get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind)."\n</li>\n";
1708 }
1709 $pfiles .= "</ul>\n";
1710 }
1711
1712 return $pfiles;
1713 }
1714
1715 /**
1716 * This function will use the passed in information to prepare a pretty string describing the backup from the raw backup history
1717 *
1718 * @param String $file - the backup file
1719 * @param Array $history - the backup history
1720 * @param String $entity - the backup entity
1721 * @param Array $checksums - checksums for the backup file
1722 * @param Array $jobdata - the jobdata for this backup
1723 * @param integer $ind - the index of the file
1724 *
1725 * @return String - returns the entity output string
1726 */
1727 public function get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind) {
1728 $op = htmlspecialchars($file)."\n";
1729 $skey = $entity.((0 == $ind) ? '' : $ind).'-size';
1730
1731 $meta = '';
1732 if ('db' == substr($entity, 0, 2) && 'db' != $entity) {
1733 $dind = substr($entity, 2);
1734 if (is_array($jobdata) && !empty($jobdata['backup_database']) && is_array($jobdata['backup_database']) && !empty($jobdata['backup_database'][$dind]) && is_array($jobdata['backup_database'][$dind]['dbinfo']) && !empty($jobdata['backup_database'][$dind]['dbinfo']['host'])) {
1735 $dbinfo = $jobdata['backup_database'][$dind]['dbinfo'];
1736 $meta .= sprintf(__('External database (%s)', 'updraftplus'), $dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'])."<br>";
1737 }
1738 }
1739 if (isset($history[$skey])) $meta .= sprintf(__('Size: %s MB', 'updraftplus'), round($history[$skey]/1048576, 1));
1740 $ckey = $entity.$ind;
1741 foreach ($checksums as $ck) {
1742 $ck_plain = false;
1743 if (isset($history['checksums'][$ck][$ckey])) {
1744 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey]);
1745 $ck_plain = true;
1746 }
1747 if (isset($history['checksums'][$ck][$ckey.'.crypt'])) {
1748 if ($ck_plain) $meta .= ' '.__('(when decrypted)');
1749 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey.'.crypt']);
1750 }
1751 }
1752
1753 $fileinfo = apply_filters("updraftplus_fileinfo_$entity", array(), $ind);
1754 if (is_array($fileinfo) && !empty($fileinfo)) {
1755 if (isset($fileinfo['html'])) {
1756 $meta .= $fileinfo['html'];
1757 }
1758 }
1759
1760 // if ($meta) $meta = " ($meta)";
1761 if ($meta) $meta = "<br><em>$meta</em>";
1762
1763 return $op.$meta;
1764 }
1765
1766 /**
1767 * This important function returns a list of file entities that can potentially be backed up (subject to users settings), and optionally further meta-data about them
1768 *
1769 * @param boolean $include_others
1770 * @param boolean $full_info
1771 * @return array
1772 */
1773 public function get_backupable_file_entities($include_others = true, $full_info = false) {
1774
1775 $wp_upload_dir = $this->wp_upload_dir();
1776
1777 if ($full_info) {
1778 $arr = array(
1779 'plugins' => array('path' => untrailingslashit(WP_PLUGIN_DIR), 'description' => __('Plugins', 'updraftplus')),
1780 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes', 'updraftplus')),
1781 'uploads' => array('path' => untrailingslashit($wp_upload_dir['basedir']), 'description' => __('Uploads', 'updraftplus'))
1782 );
1783 } else {
1784 $arr = array(
1785 'plugins' => untrailingslashit(WP_PLUGIN_DIR),
1786 'themes' => WP_CONTENT_DIR.'/themes',
1787 'uploads' => untrailingslashit($wp_upload_dir['basedir'])
1788 );
1789 }
1790
1791 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
1792
1793 // We then add 'others' on to the end
1794 if ($include_others) {
1795 if ($full_info) {
1796 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others', 'updraftplus'));
1797 } else {
1798 $arr['others'] = WP_CONTENT_DIR;
1799 }
1800 }
1801
1802 // Entries that should be added after 'others'
1803 $arr = apply_filters('updraft_backupable_file_entities_final', $arr, $full_info);
1804
1805 return $arr;
1806
1807 }
1808
1809 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
1810 switch ($errno) {
1811 case 1:
1812 $e_type = 'E_ERROR';
1813 break;
1814 case 2:
1815 $e_type = 'E_WARNING';
1816 break;
1817 case 4:
1818 $e_type = 'E_PARSE';
1819 break;
1820 case 8:
1821 $e_type = 'E_NOTICE';
1822 break;
1823 case 16:
1824 $e_type = 'E_CORE_ERROR';
1825 break;
1826 case 32:
1827 $e_type = 'E_CORE_WARNING';
1828 break;
1829 case 64:
1830 $e_type = 'E_COMPILE_ERROR';
1831 break;
1832 case 128:
1833 $e_type = 'E_COMPILE_WARNING';
1834 break;
1835 case 256:
1836 $e_type = 'E_USER_ERROR';
1837 break;
1838 case 512:
1839 $e_type = 'E_USER_WARNING';
1840 break;
1841 case 1024:
1842 $e_type = 'E_USER_NOTICE';
1843 break;
1844 case 2048:
1845 $e_type = 'E_STRICT';
1846 break;
1847 case 4096:
1848 $e_type = 'E_RECOVERABLE_ERROR';
1849 break;
1850 case 8192:
1851 $e_type = 'E_DEPRECATED';
1852 break;
1853 case 16384:
1854 $e_type = 'E_USER_DEPRECATED';
1855 break;
1856 case 30719:
1857 $e_type = 'E_ALL';
1858 break;
1859 default:
1860 $e_type = "E_UNKNOWN ($errno)";
1861 break;
1862 }
1863
1864 if (!is_string($errstr)) $errstr = serialize($errstr);
1865
1866 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
1867
1868 if ('E_DEPRECATED' == $e_type && !empty($this->no_deprecation_warnings)) {
1869 return false;
1870 }
1871
1872 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
1873
1874 }
1875
1876 public function php_error($errno, $errstr, $errfile, $errline) {
1877 if (0 == error_reporting()) return true;
1878 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
1879 if (false !== $logline) $this->log($logline, 'notice', 'php_event');
1880 // Pass it up the chain
1881 return $this->error_reporting_stop_when_logged;
1882 }
1883
1884 /**
1885 * Proceed with a backup; before calling this, at least all the initial job data must be set up
1886 *
1887 * @param Integer $resumption_no - which resumption this is; from 0 upwards
1888 * @param String $bnonce - the backup job identifier
1889 */
1890 public function backup_resume($resumption_no, $bnonce) {
1891
1892 set_error_handler(array($this, 'php_error'), E_ALL & ~E_STRICT);
1893
1894 $this->current_resumption = $resumption_no;
1895
1896 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1897 @ignore_user_abort(true);
1898
1899 $runs_started = array();
1900 $time_now = microtime(true);
1901
1902 UpdraftPlus_Backup_History::always_get_from_db();
1903
1904 // Restore state
1905 $resumption_extralog = '';
1906 $prev_resumption = $resumption_no - 1;
1907 $last_successful_resumption = -1;
1908 $job_type = 'backup';
1909
1910 if (0 == $resumption_no) {
1911 $label = $this->jobdata_get('label');
1912 if ($label) $resumption_extralog = ", label=$label";
1913 } else {
1914 $this->nonce = $bnonce;
1915 $file_nonce = $this->jobdata_get('file_nonce');
1916 $this->file_nonce = $file_nonce ? $file_nonce : $bnonce;
1917 $this->backup_time = $this->jobdata_get('backup_time');
1918 $this->job_time_ms = $this->jobdata_get('job_time_ms');
1919
1920 // Get the warnings before opening the log file, as opening the log file may generate new ones (which then leads to $this->errors having duplicate entries when they are copied over below)
1921 $warnings = $this->jobdata_get('warnings');
1922
1923 $this->logfile_open($this->file_nonce);
1924
1925 // Import existing warnings. The purpose of this is so that when save_backup_to_history() is called, it has a complete set - because job data expires quickly, whilst the warnings of the last backup run need to persist
1926 if (is_array($warnings)) {
1927 foreach ($warnings as $warning) {
1928 $this->errors[] = array('level' => 'warning', 'message' => $warning);
1929 }
1930 }
1931
1932 $runs_started = $this->jobdata_get('runs_started');
1933 if (!is_array($runs_started)) $runs_started =array();
1934 $time_passed = $this->jobdata_get('run_times');
1935 if (!is_array($time_passed)) $time_passed = array();
1936
1937 foreach ($time_passed as $run => $passed) {
1938 if (isset($runs_started[$run]) && $runs_started[$run] + $time_passed[$run] + 30 > $time_now) {
1939 // We don't want to increase the resumption if WP has started two copies of the same resumption off
1940 if ($run && $run == $resumption_no) {
1941 $increase_resumption = false;
1942 $this->log("It looks like WordPress's scheduler has started multiple instances of this resumption");
1943 } else {
1944 $increase_resumption = true;
1945 }
1946 UpdraftPlus_Job_Scheduler::terminate_due_to_activity('check-in', round($time_now, 1), round($runs_started[$run] + $time_passed[$run], 1), $increase_resumption);
1947 }
1948 }
1949
1950 for ($i = 0; $i<=$prev_resumption; $i++) {
1951 if (isset($time_passed[$i])) $last_successful_resumption = $i;
1952 }
1953
1954 if (isset($time_passed[$prev_resumption])) {
1955 $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 1)."s";
1956 } else {
1957 $this->no_checkin_last_time = true;
1958 }
1959
1960 // This is just a simple test to catch restorations of old backup sets where the backup includes a resumption of the backup job
1961 if ($time_now - $this->backup_time > 172800 && true == apply_filters('updraftplus_check_obsolete_backup', true, $time_now, $this)) {
1962
1963 // We have seen cases where the get_site_option() call that self::get_jobdata() relies on returns nothing, even though the data was there in the database. This appears to be sometimes reproducible for the people who get it, but stops being reproducible if they change their backup times - which suggests that they're having failures at times of extreme load. We can attempt to detect this case, and reschedule, instead of aborting.
1964 if (empty($this->backup_time) && empty($this->backup_is_already_complete) && !empty($this->logfile_name) && is_readable($this->logfile_name)) {
1965 $first_log_bit = file_get_contents($this->logfile_name, false, null, 0, 250);
1966 if (preg_match('/\(0\) Opened log file at time: (.*) on /', $first_log_bit, $matches)) {
1967 $first_opened = strtotime($matches[1]);
1968 // The value of 1000 seconds here is somewhat arbitrary; but allows for the problem to occur in ~ the first 15 minutes. In practice, the problem is extremely rare; if this does not catch it, we can tweak the algorithm.
1969 if (time() - $first_opened < 1000) {
1970 $this->log("This backup task (".$this->nonce.") failed to load its job data (possible database server malfunction), but appears to be only recently started: scheduling a fresh resumption in order to try again, and then ending this resumption ($time_now, ".$this->backup_time.") (existing jobdata keys: ".implode(', ', array_keys($this->jobdata)).")");
1971 UpdraftPlus_Job_Scheduler::reschedule(120);
1972 die;
1973 }
1974 }
1975 }
1976
1977 $this->log("This backup task (".$this->nonce.") is either complete or began over 2 days ago: ending ($time_now, ".$this->backup_time.") (existing jobdata keys: ".implode(', ', array_keys($this->jobdata)).")");
1978 die;
1979 }
1980
1981 }
1982
1983 $this->last_successful_resumption = $last_successful_resumption;
1984
1985 $runs_started[$resumption_no] = $time_now;
1986 if (!empty($this->backup_time)) $this->jobdata_set('runs_started', $runs_started);
1987
1988 // Schedule again, to run in 5 minutes again, in case we again fail
1989 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
1990 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 100);
1991
1992 $btime = $this->backup_time;
1993
1994 $job_type = $this->jobdata_get('job_type');
1995
1996 do_action('updraftplus_resume_backup_'.$job_type);
1997
1998 $updraft_dir = $this->backups_dir_location();
1999
2000 $time_ago = time()-$btime;
2001
2002 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, file_nonce=".$this->file_nonce." begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
2003
2004 // This works round a bizarre bug seen in one WP install, where delete_transient and wp_clear_scheduled_hook both took no effect, and upon 'resumption' the entire backup would repeat.
2005 // Argh. In fact, this has limited effect, as apparently (at least on another install seen), the saving of the updated transient via jobdata_set() also took no effect. Still, it does not hurt.
2006 if ($resumption_no >= 1 && 'finished' == $this->jobdata_get('jobstatus')) {
2007 $this->log('Terminate: This backup job is already finished (1).');
2008 die;
2009 } elseif ('clouduploading' != $this->jobdata_get('jobstatus') && 'backup' == $job_type && !empty($this->backup_is_already_complete)) {
2010 $this->jobdata_set('jobstatus', 'finished');
2011 $this->log('Terminate: This backup job is already finished (2).');
2012 die;
2013 }
2014
2015 if ($resumption_no > 0 && isset($runs_started[$prev_resumption])) {
2016 $our_expected_start = $runs_started[$prev_resumption] + $resume_interval;
2017 // If the previous run increased the resumption time, then it is timed from the end of the previous run, not the start
2018 if (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption]>0) $our_expected_start += $time_passed[$prev_resumption];
2019 $our_expected_start = apply_filters('updraftplus_expected_start', $our_expected_start, $job_type);
2020 // More than 12 minutes late?
2021 if ($time_now > $our_expected_start + 720) {
2022 $this->log('Long time past since expected resumption time: approx expected='.round($our_expected_start, 1).", now=".round($time_now, 1).", diff=".round($time_now-$our_expected_start, 1));
2023 $this->log(__('Your website is visited infrequently and UpdraftPlus is not getting the resources it hoped for; please read this page:', 'updraftplus').' https://updraftplus.com/faqs/why-am-i-getting-warnings-about-my-site-not-having-enough-visitors/', 'warning', 'infrequentvisits');
2024 }
2025 }
2026
2027 $this->jobdata_set('current_resumption', $resumption_no);
2028
2029 $first_run = apply_filters('updraftplus_filerun_firstrun', 0);
2030
2031 // We just do this once, as we don't want to be in permanent conflict with the overlap detector
2032 if ($resumption_no >= $first_run + 8 && $resumption_no < $first_run + 15 && $resume_interval >= 300) {
2033
2034 // $time_passed is set earlier
2035 list($max_time, $timings_string, $run_times_known) = UpdraftPlus_Manipulation_Functions::max_time_passed($time_passed, $resumption_no - 1, $first_run);
2036
2037 // Do this on resumption 8, or the first time that we have 6 data points
2038 if (($first_run + 8 == $resumption_no && $run_times_known >= 6) || (6 == $run_times_known && !empty($time_passed[$prev_resumption]))) {
2039 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
2040 // Remember that 30 seconds is used as the 'perhaps something is still running' detection threshold, and that 45 seconds is used as the 'the next resumption is approaching - reschedule!' interval
2041 if ($max_time + 52 < $resume_interval) {
2042 $resume_interval = round($max_time + 52);
2043 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
2044 $this->jobdata_set('resume_interval', $resume_interval);
2045 }
2046 // This next condition was added in response to HS#9174, a case where on one resumption, PHP was allowed to run for >3000 seconds - but other than that, up to 500 seconds. As a result, the resumption interval got stuck at a large value, whilst resumptions were only allowed to run for a much smaller amount.
2047 // This detects whether our last run was less than half the resume interval, but was non-trivial (at least 50 seconds - so, indicating it didn't just error out straight away), but with a resume interval of over 300 seconds. In this case, it is reduced.
2048 } elseif (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption] > 50 && $resume_interval > 300 && $time_passed[$prev_resumption] < $resume_interval/2 && 'clouduploading' == $this->jobdata_get('jobstatus')) {
2049 $resume_interval = round($time_passed[$prev_resumption] + 52);
2050 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time). Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
2051 $this->jobdata_set('resume_interval', $resume_interval);
2052 }
2053
2054 }
2055
2056 // A different argument than before is needed otherwise the event is ignored
2057 $next_resumption = $resumption_no+1;
2058 if ($next_resumption < $first_run + 10) {
2059 if (true === $this->jobdata_get('one_shot')) {
2060 if (true === $this->jobdata_get('reschedule_before_upload') && 1 == $next_resumption) {
2061 $this->log('A resumption will be scheduled for the cloud backup stage');
2062 $schedule_resumption = true;
2063 } else {
2064 $this->log('We are in "one shot" mode - no resumptions will be scheduled');
2065 }
2066 } else {
2067 $schedule_resumption = true;
2068 }
2069 } else {
2070 // We're in over-time - we only reschedule if something useful happened last time (used to be that we waited for it to happen this time - but that meant that temporary errors, e.g. Google 400s on uploads, scuppered it all - we'd do better to have another chance
2071 $useful_checkin = $this->jobdata_get('useful_checkin');
2072 $last_resumption = $resumption_no-1;
2073
2074 if (empty($useful_checkin) || $useful_checkin < $last_resumption) {
2075 $this->log(sprintf('The current run is resumption number %d, and there was nothing useful done on the last run (last useful run: %s) - will not schedule a further attempt until we see something useful happening this time', $resumption_no, $useful_checkin));
2076 } else {
2077 $schedule_resumption = true;
2078 }
2079 }
2080
2081 // Sanity check
2082 if (empty($this->backup_time)) {
2083 $this->log('The backup_time parameter appears to be empty (usually caused by resuming an already-complete backup).');
2084 return false;
2085 }
2086
2087 if (isset($schedule_resumption)) {
2088 $schedule_for = time()+$resume_interval;
2089 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
2090 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
2091 $this->newresumption_scheduled = $schedule_for;
2092 }
2093
2094 $backup_files = $this->jobdata_get('backup_files');
2095
2096 global $updraftplus_backup;
2097 // Bring in all the backup routines
2098 include_once(UPDRAFTPLUS_DIR.'/backup.php');
2099 $updraftplus_backup = new UpdraftPlus_Backup($backup_files, apply_filters('updraftplus_files_altered_since', -1, $job_type));
2100
2101 $undone_files = array();
2102
2103 if ('no' == $backup_files) {
2104 $this->log("This backup run is not intended for files - skipping");
2105 $our_files = array();
2106 } else {
2107 try {
2108 // This should be always called; if there were no files in this run, it returns us an empty array
2109 $backup_array = $updraftplus_backup->resumable_backup_of_files($resumption_no);
2110 // This save, if there was something, is then immediately picked up again
2111 if (is_array($backup_array)) {
2112 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
2113 $this->save_backup_to_history($backup_array);
2114 }
2115
2116 // Switch of variable name is purely vestigial
2117 $our_files = $backup_array;
2118 if (!is_array($our_files)) $our_files = array();
2119 } catch (Exception $e) {
2120 $log_message = 'Exception ('.get_class($e).') occurred during files backup: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2121 error_log($log_message);
2122 // @codingStandardsIgnoreLine
2123 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
2124 $this->log($log_message);
2125 $this->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2126 die();
2127 // @codingStandardsIgnoreLine
2128 } catch (Error $e) {
2129 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2130 error_log($log_message);
2131 // @codingStandardsIgnoreLine
2132 if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary();
2133 $this->log($log_message);
2134 $this->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2135 die();
2136 }
2137
2138 }
2139
2140 $backup_databases = $this->jobdata_get('backup_database');
2141
2142 if (!is_array($backup_databases)) $backup_databases = array('wp' => $backup_databases);
2143
2144 foreach ($backup_databases as $whichdb => $backup_database) {
2145
2146 if (is_array($backup_database)) {
2147 $dbinfo = $backup_database['dbinfo'];
2148 $backup_database = $backup_database['status'];
2149 } else {
2150 $dbinfo = array();
2151 }
2152
2153 $tindex = ('wp' == $whichdb) ? 'db' : 'db'.$whichdb;
2154
2155 if ('begun' == $backup_database || 'finished' == $backup_database || 'encrypted' == $backup_database) {
2156
2157 if ('wp' == $whichdb) {
2158 $db_descrip = 'WordPress DB';
2159 } else {
2160 if (!empty($dbinfo) && is_array($dbinfo) && !empty($dbinfo['host'])) {
2161 $db_descrip = "External DB $whichdb - ".$dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'];
2162 } else {
2163 $db_descrip = "External DB $whichdb - details appear to be missing";
2164 }
2165 }
2166
2167 if ('begun' == $backup_database) {
2168 if ($resumption_no > 0) {
2169 $this->log("Resuming creation of database dump ($db_descrip)");
2170 } else {
2171 $this->log("Beginning creation of database dump ($db_descrip)");
2172 }
2173 } elseif ('encrypted' == $backup_database) {
2174 $this->log("Database dump ($db_descrip): Creation and encryption were completed already");
2175 } else {
2176 $this->log("Database dump ($db_descrip): Creation was completed already");
2177 }
2178
2179 if ('wp' != $whichdb && (empty($dbinfo) || !is_array($dbinfo) || empty($dbinfo['host']))) {
2180 unset($backup_databases[$whichdb]);
2181 $this->jobdata_set('backup_database', $backup_databases);
2182 continue;
2183 }
2184
2185 // Catch fatal errors through try/catch blocks around the database backup
2186 try {
2187 $db_backup = $updraftplus_backup->backup_db($backup_database, $whichdb, $dbinfo);
2188 } catch (Exception $e) {
2189 $log_message = 'Exception ('.get_class($e).') occurred during files backup: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2190 $this->log($log_message);
2191 error_log($log_message);
2192 $this->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2193 die();
2194 // @codingStandardsIgnoreLine
2195 } catch (Error $e) {
2196 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2197 $this->log($log_message);
2198 error_log($log_message);
2199 $this->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2200 die();
2201 }
2202
2203 if (is_array($our_files) && is_string($db_backup)) $our_files[$tindex] = $db_backup;
2204
2205 if ('encrypted' != $backup_database) {
2206 $backup_databases[$whichdb] = array('status' => 'finished', 'dbinfo' => $dbinfo);
2207 $this->jobdata_set('backup_database', $backup_databases);
2208 }
2209 } elseif ('no' == $backup_database) {
2210 $this->log("No database backup ($whichdb) - not part of this run");
2211 } else {
2212 $this->log("Unrecognised data when trying to ascertain if the database ($whichdb) was backed up (".serialize($backup_database).")");
2213 }
2214
2215 // This is done before cloud despatch, because we want a record of what *should* be in the backup. Whether it actually makes it there or not is not yet known.
2216 $this->save_backup_to_history($our_files);
2217
2218 // Potentially encrypt the database if it is not already
2219 if ('no' != $backup_database && isset($our_files[$tindex]) && !preg_match("/\.crypt$/", $our_files[$tindex]) && 'incremental' != $job_type) {
2220 $our_files[$tindex] = $updraftplus_backup->encrypt_file($our_files[$tindex]);
2221 // No need to save backup history now, as it will happen in a few lines time
2222 if (preg_match("/\.crypt$/", $our_files[$tindex])) {
2223 $backup_databases[$whichdb] = array('status' => 'encrypted', 'dbinfo' => $dbinfo);
2224 $this->jobdata_set('backup_database', $backup_databases);
2225 }
2226 }
2227
2228 if ('no' != $backup_database && isset($our_files[$tindex]) && file_exists($updraft_dir.'/'.$our_files[$tindex])) {
2229 $our_files[$tindex.'-size'] = filesize($updraft_dir.'/'.$our_files[$tindex]);
2230 $this->save_backup_to_history($our_files);
2231 }
2232
2233 }
2234
2235 $backupable_entities = $this->get_backupable_file_entities(true);
2236
2237 $checksum_list = $this->which_checksums();
2238
2239 $checksums = array();
2240
2241 foreach ($checksum_list as $checksum) {
2242 $checksums[$checksum] = array();
2243 }
2244
2245 $total_size = 0;
2246
2247 // Queue files for upload
2248 foreach ($our_files as $key => $files) {
2249 // Only continue if the stored info was about a dump
2250 if (!isset($backupable_entities[$key]) && ('db' != substr($key, 0, 2) || '-size' == substr($key, -5, 5))) continue;
2251 if (is_string($files)) $files = array($files);
2252 foreach ($files as $findex => $file) {
2253
2254 $size_key = (0 == $findex) ? $key.'-size' : $key.$findex.'-size';
2255 $total_size = (false === $total_size || !isset($our_files[$size_key]) || !is_numeric($our_files[$size_key])) ? false : $total_size + $our_files[$size_key];
2256
2257 foreach ($checksum_list as $checksum) {
2258
2259 $cksum = $this->jobdata_get($checksum.'-'.$key.$findex);
2260 if ($cksum) $checksums[$checksum][$key.$findex] = $cksum;
2261 $cksum = $this->jobdata_get($checksum.'-'.$key.$findex.'.crypt');
2262 if ($cksum) $checksums[$checksum][$key.$findex.".crypt"] = $cksum;
2263
2264 }
2265
2266 if ($this->is_uploaded($file)) {
2267 $this->log("$file: $key: This file has already been successfully uploaded");
2268 } elseif (is_file($updraft_dir.'/'.$file)) {
2269 if (!in_array($file, $undone_files)) {
2270 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
2271 $undone_files[$key.$findex] = $file;
2272 } else {
2273 $this->log("$file: $key: This file was already queued for upload (this condition should never be seen)");
2274 }
2275 } elseif ('incremental' == $job_type && 'db' == substr($key, 0, 2)) {
2276 // Here we check if this is an incremental backup and if so then mark the db files as already uploaded as db files are not part of incremental file backups
2277 $this->log("$file: $key: This is an incremental backup; this file will be marked as successfully uploaded.");
2278 $this->uploaded_file($file, true);
2279 } else {
2280 $this->log("$file: $key: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem; now marking as uploaded ($updraft_dir/$file)");
2281 $this->uploaded_file($file, true);
2282 }
2283 }
2284 }
2285 $our_files['checksums'] = $checksums;
2286
2287 // Save again (now that we have checksums)
2288 $size_description = (false === $total_size) ? 'Unknown' : UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($total_size);
2289 $this->log("Saving backup history. Total backup size: $size_description");
2290 $this->save_backup_to_history($our_files);
2291 do_action('updraft_final_backup_history', $our_files);
2292
2293 // We finished; so, low memory was not a problem
2294 $this->log_remove_warning('lowram');
2295
2296 if (0 == count($undone_files)) {
2297 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
2298 if (is_array($our_files)) $this->save_last_backup($our_files);
2299 $this->log("There were no more files that needed uploading");
2300 // No email, as the user probably already got one if something else completed the run
2301 $allow_email = false;
2302 if ('begun' == $this->jobdata_get('prune')) {
2303 // Begun, but not finished
2304 $this->log('Restarting backup prune operation');
2305 $updraftplus_backup->do_prune_standalone();
2306 $allow_email = true;
2307 }
2308 $this->backup_finish($next_resumption, true, $allow_email, $resumption_no);
2309 restore_error_handler();
2310 return;
2311 }
2312
2313 $this->error_count_before_cloud_backup = $this->error_count();
2314
2315 // This is intended for one-shot backups, where we do want a resumption if it's only for uploading
2316 if (empty($this->newresumption_scheduled) && 0 == $resumption_no && 0 == $this->error_count_before_cloud_backup && true === $this->jobdata_get('reschedule_before_upload')) {
2317 $this->log("Cloud backup stage reached on one-shot backup: scheduling resumption for the cloud upload");
2318 UpdraftPlus_Job_Scheduler::reschedule(60);
2319 UpdraftPlus_Job_Scheduler::record_still_alive();
2320 }
2321
2322 $this->log("Requesting upload of the files that have not yet been successfully uploaded (".count($undone_files).")");
2323 // Catch fatal errors through try/catch blocks around the upload to remote storage
2324 $updraftplus_backup->cloud_backup($undone_files);
2325
2326 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
2327 if (is_array($our_files)) $this->save_last_backup($our_files);
2328 $this->backup_finish($next_resumption, true, true, $resumption_no);
2329
2330 restore_error_handler();
2331
2332 }
2333
2334 /**
2335 * Get all the job data in a single array
2336 *
2337 * @param String $job_id - the job identifier (nonce) for the job whose data is to be retrieved
2338 *
2339 * @return Array
2340 */
2341 public function jobdata_getarray($job_id) {
2342 return get_site_option('updraft_jobdata_'.$job_id, array());
2343 }
2344
2345 public function jobdata_set_from_array($array) {
2346 $this->jobdata = $array;
2347 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
2348 }
2349
2350 /**
2351 * This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
2352 * You can list the keys/values (keys must be strings) as consecutive/alternating parameters, or send them all in as an array (with no other parameters)
2353 *
2354 * @return null
2355 */
2356 public function jobdata_set_multi() {
2357 if (!is_array($this->jobdata)) $this->jobdata = array();
2358
2359 $args = func_num_args();
2360
2361 // func_get_arg() could not be used in parameter lists prior to PHP 5.3, so, we get it as a variable
2362 if (1 == $args && null !== ($first_arg = func_get_arg(0)) && is_array($first_arg)) {
2363 foreach ($first_arg as $key => $value) {
2364 $this->jobdata[$key] = $value;
2365 }
2366 } else {
2367
2368 for ($i=1; $i<=$args/2; $i++) {
2369 $key = func_get_arg($i*2-2);
2370 $value = func_get_arg($i*2-1);
2371 $this->jobdata[$key] = $value;
2372 }
2373
2374 }
2375 if (!empty($this->nonce)) update_site_option('updraft_jobdata_'.$this->nonce, $this->jobdata);
2376 }
2377
2378 /**
2379 * Set a job-data key/value pair for the current job
2380 *
2381 * @param String $key - the key
2382 * @param Mixed $value - needs to be serializable
2383 *
2384 * @uses update_site_option()
2385 */
2386 public function jobdata_set($key, $value) {
2387 if (empty($this->jobdata)) {
2388 $this->jobdata = empty($this->nonce) ? array() : get_site_option('updraft_jobdata_'.$this->nonce);
2389 if (!is_array($this->jobdata)) $this->jobdata = array();
2390 }
2391 $this->jobdata[$key] = $value;
2392 if ($this->nonce) update_site_option('updraft_jobdata_'.$this->nonce, $this->jobdata);
2393 }
2394
2395 public function jobdata_delete($key) {
2396 if (!is_array($this->jobdata)) {
2397 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce);
2398 if (!is_array($this->jobdata)) $this->jobdata = array();
2399 }
2400 unset($this->jobdata[$key]);
2401 if ($this->nonce) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
2402 }
2403
2404 public function get_job_option($opt) {
2405 // These are meant to be read-only
2406 if (empty($this->jobdata['option_cache']) || !is_array($this->jobdata['option_cache'])) {
2407 if (!is_array($this->jobdata) && $this->nonce) $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
2408 $this->jobdata['option_cache'] = array();
2409 }
2410 return isset($this->jobdata['option_cache'][$opt]) ? $this->jobdata['option_cache'][$opt] : UpdraftPlus_Options::get_updraft_option($opt);
2411 }
2412
2413 public function jobdata_get($key, $default = null) {
2414 if (empty($this->jobdata)) {
2415 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce, array());
2416 if (!is_array($this->jobdata)) return $default;
2417 }
2418 return isset($this->jobdata[$key]) ? $this->jobdata[$key] : $default;
2419 }
2420
2421 /**
2422 * Reset the job data for the currently active job
2423 */
2424 public function jobdata_reset() {
2425 $this->jobdata = null;
2426 }
2427
2428 /**
2429 * Gets an instance of the "UpdraftPlus_Clone" class which will be
2430 * used to login the user to UpdraftPlus.com
2431 *
2432 * @return object
2433 */
2434 public function get_updraftplus_clone() {
2435 if (!class_exists('UpdraftPlus_Clone')) include_once(UPDRAFTPLUS_DIR.'/includes/updraftplus-clone.php');
2436 return new UpdraftPlus_Clone();
2437 }
2438
2439 /**
2440 * This function will add data to the backup options that is needed for the clone backup job
2441 *
2442 * @param array $options - the backup options array
2443 * @param array $request - the extra data we want to add to the backup options
2444 *
2445 * @return array - the backup options array with the extra data added
2446 */
2447 public function updraftplus_clone_backup_options($options, $request) {
2448 if (!is_array($options)) return $options;
2449
2450 if (!empty($request['clone_id']) && !empty($request['secret_token'])) {
2451 $options['clone_id'] = $request['clone_id'];
2452 $options['secret_token'] = $request['secret_token'];
2453 }
2454
2455 if (isset($request['clone_url'])) $options['clone_url'] = $request['clone_url'];
2456 if (isset($request['key'])) $options['key'] = $request['key'];
2457
2458 return $options;
2459 }
2460
2461 /**
2462 * This function will set up the backup job data for when we are starting a clone backup job. It changes the initial jobdata so that UpdraftPlus knows it's a clone job and adds the needed information for to lookup the clone and if we have it the URL and migration key for the clone.
2463 *
2464 * @param array $jobdata - the initial job data that we want to change
2465 * @param array $options - options sent from the front end includes the clone id, secret token and maybe clone url and migration key
2466 *
2467 * @return array - the modified jobdata
2468 */
2469 public function updraftplus_clone_backup_jobdata($jobdata, $options) {
2470 global $updraftplus;
2471
2472 if (!is_array($jobdata)) return $jobdata;
2473
2474 if (!isset($options['clone_id']) && !isset($options['secret_token']) && !isset($options['clone_url']) && !isset($options['key'])) return $jobdata;
2475
2476 $option_cache_key = array_search('option_cache', $jobdata) + 1;
2477 $option_cache = $jobdata[$option_cache_key];
2478 $option_cache['updraft_encryptionphrase'] = '';
2479 $jobdata[$option_cache_key] = $option_cache;
2480
2481 $service_key = array_search('service', $jobdata) + 1;
2482 $jobdata[$service_key] = array('remotesend');
2483
2484 $jobdata[] = 'clone_job';
2485 $jobdata[] = true;
2486 $jobdata[] = 'clone_id';
2487 $jobdata[] = $options['clone_id'];
2488 $jobdata[] = 'secret_token';
2489 $jobdata[] = $options['secret_token'];
2490 $jobdata[] = 'clone_url';
2491 $jobdata[] = $options['clone_url'];
2492 $jobdata[] = 'clone_key';
2493 $jobdata[] = $options['key'];
2494 $jobdata[] = 'remotesend_info';
2495 $jobdata[] = array('url' => $options['clone_url']);
2496
2497 return $jobdata;
2498 }
2499
2500 /**
2501 * This function will update the database backup jobdata and set each entity to finished or encrypted to prevent that entity from being backed up again. This will also return the blog name that the database backup belongs to, just in case it's from another site.
2502 *
2503 * @param array $db_backups - the database backup jobdata
2504 * @param array $backup - the backup history for this backup
2505 *
2506 * @return array - an array that contains the updated database backup jobdata and the blog name
2507 */
2508 public function update_database_jobdata($db_backups, $backup) {
2509
2510 $backup_database_info = array(
2511 'blog_name' => '',
2512 'db_backups' => $db_backups
2513 );
2514
2515 if (!is_array($db_backups)) return $backup_database_info;
2516
2517 /*
2518 We need to tweak the database array here by setting each database entity to finished or encrypted if it's an encrypted archive.
2519 I also grab the backups blog name here ready to be used later, just in case this backup set is from another site.
2520 */
2521 foreach ($db_backups as $key => $db_info) {
2522 $status = 'finished';
2523 $db_index = ('wp' == $key) ? 0 : $key;
2524
2525 if (isset($backup['db'][$db_index])) {
2526 $db_backup_name = $backup['db'][$db_index];
2527
2528 if (preg_match('/^backup_([\-0-9]{15})_(.*)_([0-9a-f]{12})-[\-a-z]+([0-9]+)?+(\.(zip|gz|gz\.crypt))?$/i', $db_backup_name, $matches)) {
2529 $backup_database_info['blog_name'] = $matches[2];
2530 }
2531
2532 if (UpdraftPlus_Encryption::is_file_encrypted($db_backup_name)) $status = 'encrypted';
2533
2534 if (is_array($db_info) && isset($db_info['status'])) {
2535 $db_backups[$key]['status'] = $status;
2536 } else {
2537 $db_backups[$key] = $status;
2538 }
2539 } else {
2540 unset($db_backups[$key]);
2541 }
2542 }
2543
2544 $backup_database_info['db_backups'] = $db_backups;
2545
2546 return $backup_database_info;
2547 }
2548
2549 public function backup_files() {
2550 // Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
2551 $this->boot_backup(true, false);
2552 }
2553
2554 public function backup_database() {
2555 // Note that nothing will happen if the file backup had the same schedule
2556 $this->boot_backup(false, true);
2557 }
2558
2559 public function backup_all($options) {
2560 $skip_cloud = empty($options['nocloud']) ? false : true;
2561 $this->boot_backup(1, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
2562 }
2563
2564 public function backupnow_files($options) {
2565 $skip_cloud = empty($options['nocloud']) ? false : true;
2566 $this->boot_backup(1, 0, false, false, ($skip_cloud) ? 'none' : false, $options);
2567 }
2568
2569 public function backupnow_database($options) {
2570 $skip_cloud = empty($options['nocloud']) ? false : true;
2571 $this->boot_backup(0, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
2572 }
2573
2574 /**
2575 * This function will try and get a lock for the backup, it will return false if it fails to get a lock.
2576 *
2577 * @param boolean $backup_files - boolean to indicate if we want a lock for files
2578 * @param boolean $backup_database - boolean to indicate if we want a lock for the database
2579 *
2580 * @return boolean - boolean to indicate if we got a lock or not
2581 */
2582 public function get_semaphore_lock($backup_files, $backup_database) {
2583
2584 $semaphore = ($backup_files ? 'f' : '') . ($backup_database ? 'd' : '');
2585
2586 if (!class_exists('UpdraftPlus_Semaphore')) include_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
2587
2588 UpdraftPlus_Semaphore::ensure_semaphore_exists($semaphore);
2589
2590 // Are we doing an action called by the WP scheduler? If so, we want to check when that last happened; the point being that the dodgy WP scheduler, when overloaded, can call the event multiple times - and sometimes, it evades the semaphore because it calls a second run after the first has finished, or > 3 minutes (our semaphore lock time) later
2591 // doing_action() was added in WP 3.9
2592 // wp_cron() can be called from the 'init' action
2593
2594 if (function_exists('doing_action') && (doing_action('init') || @constant('DOING_CRON')) && (doing_action('updraft_backup_database') || doing_action('updraft_backup'))) {
2595 $last_scheduled_action_called_at = get_option("updraft_last_scheduled_$semaphore");
2596 // 11 minutes - so, we're assuming that they haven't custom-modified their schedules to run scheduled backups more often than that. If they have, they need also to use the filter to over-ride this check.
2597 $seconds_ago = time() - $last_scheduled_action_called_at;
2598 if ($last_scheduled_action_called_at && $seconds_ago < 660 && apply_filters('updraft_check_repeated_scheduled_backups', true)) {
2599 $this->log(sprintf('Scheduled backup aborted - another backup of this type was apparently invoked by the WordPress scheduler only %d seconds ago - the WordPress scheduler invoking events multiple times usually indicates a very overloaded server (or other plugins that mis-use the scheduler)', $seconds_ago));
2600 return false;
2601 }
2602 }
2603
2604 update_option("updraft_last_scheduled_$semaphore", time());
2605
2606 $this->semaphore = UpdraftPlus_Semaphore::factory();
2607 $this->semaphore->lock_name = $semaphore;
2608
2609 $semaphore_log_message = 'Requesting semaphore lock ('.$semaphore.')';
2610 if (!empty($last_scheduled_action_called_at)) {
2611 $semaphore_log_message .= " (apparently via scheduler: last_scheduled_action_called_at=$last_scheduled_action_called_at, seconds_ago=$seconds_ago)";
2612 } else {
2613 $semaphore_log_message .= " (apparently not via scheduler)";
2614 }
2615
2616 $this->log($semaphore_log_message);
2617 if (!$this->semaphore->lock()) {
2618 $this->log('Failed to gain semaphore lock ('.$semaphore.') - another backup of this type is apparently already active - aborting (if this is wrong - i.e. if the other backup crashed without removing the lock, then another can be started after 3 minutes)');
2619 return false;
2620 }
2621
2622 return true;
2623 }
2624
2625 /**
2626 * This function will check to see if any of the known backups are still running and return true otherwise returns false.
2627 *
2628 * @return boolean|string - returns false if no backup is running or a error code if there is a backup running
2629 */
2630 public function is_backup_running() {
2631
2632 $backup_history = UpdraftPlus_Backup_History::get_history();
2633
2634 foreach ($backup_history as $key => $backup) {
2635 $nonce = $backup['nonce'];
2636
2637 // Check the job is not still running.
2638 $jobdata = $this->jobdata_getarray($nonce);
2639
2640 if (!empty($jobdata) && 'finished' != $jobdata['jobstatus']) {
2641
2642 // Check that there is not a resumption scheduled
2643 if (wp_next_scheduled('updraft_backup_resume')) return "job_resumption_scheduled";
2644
2645 $time_passed = $jobdata['run_times'];
2646
2647 // No runtime found so return
2648 if (!is_array($time_passed)) return "job_scheduled_${nonce}_no_run_times";
2649
2650 // Runtime has been found so make sure last activity is over an hour
2651 $time_passed = end($time_passed);
2652 if (strtotime($time_passed) <= time() - (3600)) continue;
2653
2654 return "job_scheduled_${nonce}_run_time_activity";
2655 }
2656 }
2657
2658 return false;
2659 }
2660
2661 /**
2662 * This function is a filter function which will return the nonce for the incremental backup set we want to add to
2663 *
2664 * @param string $nonce - the backup nonce we want to filter
2665 *
2666 * @return string - the backup nonce
2667 */
2668 public function incremental_backup_file_nonce($nonce) {
2669 if (apply_filters('updraftplus_incremental_addon_installed', false) && !empty($this->file_nonce)) return $this->file_nonce;
2670 return $nonce;
2671 }
2672
2673 /**
2674 * This procedure initiates a backup run
2675 * $backup_files/$backup_database: true/false = yes/no (over-write allowed); 1/0 = yes/no (force)
2676 *
2677 * @param Boolean $backup_files
2678 * @param Boolean $backup_database
2679 * @param Boolean|Array $restrict_files_to_override
2680 * @param Boolean $one_shot
2681 * @param Boolean|Array|String $service
2682 * @param Array $options
2683 * @return Boolean|Void - not currently well specified (though false indicates definite failure)
2684 */
2685 public function boot_backup($backup_files, $backup_database, $restrict_files_to_override = false, $one_shot = false, $service = false, $options = array()) {
2686
2687 @ignore_user_abort(true);
2688 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
2689
2690 if (false === $restrict_files_to_override && isset($options['restrict_files_to_override'])) $restrict_files_to_override = $options['restrict_files_to_override'];
2691 // Generate backup information
2692 $use_nonce = empty($options['use_nonce']) ? false : $options['use_nonce'];
2693 $use_timestamp = empty($options['use_timestamp']) ? false : $options['use_timestamp'];
2694 $this->backup_time_nonce($use_nonce, $use_timestamp);
2695 // The current_resumption is consulted within logfile_open()
2696 $this->current_resumption = 0;
2697 $this->logfile_open($this->file_nonce);
2698
2699 if (!is_file($this->logfile_name)) {
2700 $this->log('Failed to open log file ('.$this->logfile_name.') - you need to check your UpdraftPlus settings (your chosen directory for creating files in is not writable, or you ran out of disk space). Backup aborted.');
2701 $this->log(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.', 'updraftplus'), 'error');
2702 return false;
2703 }
2704
2705 // Some house-cleaning
2706 UpdraftPlus_Filesystem_Functions::clean_temporary_files();
2707
2708 // Log some information that may be helpful
2709 $this->log("Tasks: Backup files: $backup_files (schedule: ".UpdraftPlus_Options::get_updraft_option('updraft_interval', 'unset').") Backup DB: $backup_database (schedule: ".UpdraftPlus_Options::get_updraft_option('updraft_interval_database', 'unset').")");
2710
2711 // The is_bool() check here is confirming that we're allowed to adjust the parameters
2712 if (false === $one_shot && is_bool($backup_database)) {
2713 // If the files and database schedules are the same, and if this the file one, then we rope in database too.
2714 // On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
2715
2716 $files_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval');
2717 $db_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
2718
2719 $sched_log_extra = '';
2720
2721 if ('manual' != $files_schedule) {
2722 if ($files_schedule == $db_schedule || UpdraftPlus_Options::get_updraft_option('updraft_interval_database', 'xyz') == 'xyz') {
2723 $sched_log_extra = 'Combining jobs from identical schedules. ';
2724 $backup_database = (true == $backup_files) ? true : false;
2725 } elseif ($files_schedule && $db_schedule && $files_schedule != $db_schedule) {
2726
2727 // This stored value is the earliest of the two apparently-close jobs
2728 $combine_around = empty($this->combine_jobs_around) ? false : $this->combine_jobs_around;
2729
2730 if (preg_match('/^(cancel:)?(\d+)$/', $combine_around, $matches)) {
2731
2732 $combine_around = $matches[2];
2733
2734 // Re-save the option, since otherwise it will have been reset and not be accessible to the 'other' run
2735 UpdraftPlus_Options::update_updraft_option('updraft_combine_jobs_around', 'cancel:'.$this->combine_jobs_around);
2736
2737 $margin = (defined('UPDRAFTPLUS_COMBINE_MARGIN') && is_numeric(UPDRAFTPLUS_COMBINE_MARGIN)) ? UPDRAFTPLUS_COMBINE_MARGIN : 600;
2738
2739 $time_now = time();
2740
2741 // The margin is doubled, to cope with the lack of predictability in WP's cron system
2742 if ($time_now >= $combine_around && $time_now <= $combine_around + 2*$margin) {
2743
2744 $sched_log_extra = 'Combining jobs from co-inciding events. ';
2745
2746 if ('cancel:' == $matches[1]) {
2747 $backup_database = false;
2748 $backup_files = false;
2749 } else {
2750 // We want them both to happen on whichever run is first (since, afterwards, the updraft_combine_jobs_around option will have been removed when the event is rescheduled).
2751 $backup_database = true;
2752 $backup_files = true;
2753 }
2754
2755 }
2756
2757 }
2758 }
2759 }
2760 $this->log("Processed schedules. ${sched_log_extra}Tasks now: Backup files: $backup_files Backup DB: $backup_database");
2761 }
2762
2763 if (false == apply_filters('updraftplus_boot_backup', true, $backup_files, $backup_database, $one_shot)) {
2764 $this->log("Backup aborted (via filter)");
2765 return false;
2766 }
2767
2768 if (!is_string($service) && !is_array($service)) {
2769 $all_services = UpdraftPlus_Options::get_updraft_option('updraft_service');
2770 if (is_string($all_services)) $all_services = (array) $all_services;
2771 $enabled_storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($all_services);
2772 $service = array_keys($enabled_storage_objects_and_ids);
2773 }
2774 $service = $this->just_one($service);
2775 if (is_string($service)) $service = array($service);
2776 if (!is_array($service)) $service = array('none');
2777
2778 if (!empty($options['extradata']) && !empty($options['extradata']['services']) && preg_match('#remotesend/(\d+)#', $options['extradata']['services'])) {
2779 if (array('none') === $service) $service = array();
2780 $service[] = 'remotesend';
2781 }
2782
2783 $option_cache = array();
2784
2785 foreach ($service as $serv) {
2786 if ('' == $serv || 'none' == $serv) continue;
2787 include_once(UPDRAFTPLUS_DIR.'/methods/'.$serv.'.php');
2788 $cclass = 'UpdraftPlus_BackupModule_'.$serv;
2789 if (!class_exists($cclass)) {
2790 error_log("UpdraftPlus: backup class does not exist: $cclass");
2791 continue;
2792 }
2793 $obj = new $cclass;
2794
2795 if (is_callable(array($obj, 'get_credentials'))) {
2796 $opts = $obj->get_credentials();
2797 if (is_array($opts)) {
2798 foreach ($opts as $opt) $option_cache[$opt] = UpdraftPlus_Options::get_updraft_option($opt);
2799 }
2800 }
2801 }
2802 $option_cache = apply_filters('updraftplus_job_option_cache', $option_cache);
2803
2804 // If nothing to be done, then just finish
2805 if (!$backup_files && !$backup_database) {
2806 $ret = $this->backup_finish(1, false, false, 0);
2807 // Don't keep useless log files
2808 if (!UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') && !empty($this->logfile_name) && file_exists($this->logfile_name)) {
2809 unlink($this->logfile_name);
2810 }
2811 return $ret;
2812 }
2813
2814 if (!$this->get_semaphore_lock($backup_files, $backup_database)) return;
2815
2816 // Allow the resume interval to be more than 300 if last time we know we went beyond that - but never more than 600
2817 if (defined('UPDRAFTPLUS_INITIAL_RESUME_INTERVAL') && is_numeric(UPDRAFTPLUS_INITIAL_RESUME_INTERVAL)) {
2818 $resume_interval = UPDRAFTPLUS_INITIAL_RESUME_INTERVAL;
2819 } else {
2820 $resume_interval = (int) min(max(300, get_site_transient('updraft_initial_resume_interval')), 600);
2821 }
2822 // We delete it because we only want to know about behaviour found during the very last backup run (so, if you move servers then old data is not retained)
2823 delete_site_transient('updraft_initial_resume_interval');
2824
2825 $job_file_entities = array();
2826 if ($backup_files) {
2827 $possible_backups = $this->get_backupable_file_entities(true);
2828 foreach ($possible_backups as $youwhat => $whichdir) {
2829 if ((false === $restrict_files_to_override && UpdraftPlus_Options::get_updraft_option("updraft_include_$youwhat", apply_filters("updraftplus_defaultoption_include_$youwhat", true))) || (is_array($restrict_files_to_override) && in_array($youwhat, $restrict_files_to_override))) {
2830 // The 0 indicates the zip file index
2831 $job_file_entities[$youwhat] = array(
2832 'index' => 0
2833 );
2834 }
2835 }
2836 }
2837
2838 $followups_allowed = (((!$one_shot && defined('DOING_CRON') && DOING_CRON)) || (defined('UPDRAFTPLUS_FOLLOWUPS_ALLOWED') && UPDRAFTPLUS_FOLLOWUPS_ALLOWED));
2839
2840 $split_every = max(intval(UpdraftPlus_Options::get_updraft_option('updraft_split_every', 400)), UPDRAFTPLUS_SPLIT_MIN);
2841
2842 $initial_jobdata = array(
2843 'resume_interval',
2844 $resume_interval,
2845 'job_type',
2846 'backup',
2847 'jobstatus',
2848 'begun',
2849 'backup_time',
2850 $this->backup_time,
2851 'job_time_ms',
2852 $this->job_time_ms,
2853 'service',
2854 $service,
2855 'split_every',
2856 $split_every,
2857 'maxzipbatch',
2858 26214400, // 25MB
2859 'job_file_entities',
2860 $job_file_entities,
2861 'option_cache',
2862 $option_cache,
2863 'uploaded_lastreset',
2864 9,
2865 'one_shot',
2866 $one_shot,
2867 'followsups_allowed',
2868 $followups_allowed,
2869 );
2870
2871 if ($one_shot) update_site_option('updraft_oneshotnonce', $this->nonce);
2872
2873 if ($this->file_nonce && $this->file_nonce != $this->nonce) array_push($initial_jobdata, 'file_nonce', $this->file_nonce);
2874
2875 // 'autobackup' == $options['extradata'] might be set from another plugin so keeping here to keep support
2876 if (!empty($options['extradata']) && (!empty($options['extradata']['autobackup']) || 'autobackup' === $options['extradata'])) array_push($initial_jobdata, 'is_autobackup', true);
2877 // Save what *should* be done, to make it resumable from this point on
2878 if ($backup_database) {
2879 $dbs = apply_filters('updraft_backup_databases', array('wp' => 'begun'));
2880 if (is_array($dbs)) {
2881 foreach ($dbs as $key => $db) {
2882 if ('wp' != $key && (!is_array($db) || empty($db['dbinfo']) || !is_array($db['dbinfo']) || empty($db['dbinfo']['host']))) unset($dbs[$key]);
2883 }
2884 }
2885 } else {
2886 $dbs = 'no';
2887 }
2888
2889 array_push($initial_jobdata, 'backup_database', $dbs);
2890 array_push($initial_jobdata, 'backup_files', (($backup_files) ? 'begun' : 'no'));
2891
2892 if (is_array($options) && !empty($options['label'])) array_push($initial_jobdata, 'label', $options['label']);
2893
2894 if (!empty($options['always_keep'])) array_push($initial_jobdata, 'always_keep', true);
2895
2896 try {
2897 // Use of jobdata_set_multi saves around 200ms
2898 call_user_func_array(array($this, 'jobdata_set_multi'), apply_filters('updraftplus_initial_jobdata', $initial_jobdata, $options, $split_every));
2899 } catch (Exception $e) {
2900 $this->log("Exception when calling jobdata_set_multi: ".$e->getMessage().' ('.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')');
2901 return false;
2902 }
2903
2904 // Everything is set up; now go
2905 $this->backup_resume(0, $this->nonce);
2906
2907 if ($one_shot) delete_site_option('updraft_oneshotnonce');
2908
2909 }
2910
2911 /**
2912 * The purpose of this function is to abstract away historical discrepancies in service lists, by returning in a single, logical form (in particular, no 'none' or '' entries, and always an array)
2913 *
2914 * @param Array|String|Boolean|Null $services - a list of services to canonicalize, or a string indicating a single service. If null is parsed, then the saved settings will be read.
2915 *
2916 * @return Array - an array of service names. All service names will be non-empty strings, and 'none' will not feature. If there are no services, then the array will be empty.
2917 */
2918 public function get_canonical_service_list($services = null) {
2919
2920 if (null === $services) $services = UpdraftPlus_Options::get_updraft_option('updraft_service');
2921
2922 $services = (array) $services;
2923
2924 foreach ($services as $key => $service) {
2925 if ('' === $service || 'none' === $service || false === $service) unset($services[$key]);
2926 }
2927
2928 return $services;
2929 }
2930
2931 private function backup_finish($cancel_event, $do_cleanup, $allow_email, $resumption_no, $force_abort = false) {
2932 global $updraftplus_admin;
2933
2934 if (!empty($this->semaphore)) $this->semaphore->unlock();
2935
2936 $delete_jobdata = false;
2937
2938 $clone_job = $this->jobdata_get('clone_job');
2939
2940 if (!empty($clone_job)) {
2941 $clone_id = $this->jobdata_get('clone_id');
2942 $secret_token = $this->jobdata_get('secret_token');
2943 }
2944
2945 // The valid use of $do_cleanup is to indicate if in fact anything exists to clean up (if no job really started, then there may be nothing)
2946
2947 // In fact, leaving the hook to run (if debug is set) is harmless, as the resume job should only do tasks that were left unfinished, which at this stage is none.
2948 if (0 == $this->error_count() || $force_abort) {
2949 if ($do_cleanup) {
2950 $this->log("There were no errors in the uploads, so the 'resume' event ($cancel_event) is being unscheduled");
2951 // This apparently-worthless setting of metadata before deleting it is for the benefit of a WP install seen where wp_clear_scheduled_hook() and delete_transient() apparently did nothing (probably a faulty cache)
2952 $this->jobdata_set('jobstatus', 'finished');
2953 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
2954 // This should be unnecessary - even if it does resume, all should be detected as finished; but I saw one very strange case where it restarted, and repeated everything; so, this will help
2955 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+1, $this->nonce));
2956 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+2, $this->nonce));
2957 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+3, $this->nonce));
2958 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+4, $this->nonce));
2959 $delete_jobdata = true;
2960 }
2961 } else {
2962 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
2963 $this->jobdata_set('jobstatus', 'resumingforerrors');
2964 // If there were no errors before moving to the upload stage, on the first run, then bring the resumption back very close. Since this is only attempted on the first run, it is really only an efficiency thing for a quicker finish if there was an unexpected networking event. We don't want to do it straight away every time, as it may be that the cloud service is down - and might be up in 5 minutes time. This was added after seeing a case where resumption 0 got to run for 10 hours... and the resumption 7 that should have picked up the uploading of 1 archive that failed never occurred.
2965 if (isset($this->error_count_before_cloud_backup) && 0 === $this->error_count_before_cloud_backup) {
2966 if (0 == $resumption_no) {
2967 UpdraftPlus_Job_Scheduler::reschedule(60);
2968 } else {
2969 // Added 27/Feb/2016 - though the cloud service seems to be down, we still don't want to wait too long
2970 $resume_interval = $this->jobdata_get('resume_interval');
2971
2972 // 15 minutes + 2 for each resumption (a modest back-off)
2973 $max_interval = 900 + $resumption_no * 120;
2974 if ($resume_interval > $max_interval) {
2975 UpdraftPlus_Job_Scheduler::reschedule($max_interval);
2976 }
2977 }
2978 }
2979 }
2980
2981 // Send the results email if appropriate, which means:
2982 // - The caller allowed it (which is not the case in an 'empty' run)
2983 // - And: An email address was set (which must be so in email mode)
2984 // And one of:
2985 // - Debug mode
2986 // - There were no errors (which means we completed and so this is the final run - time for the final report)
2987 // - It was the tenth resumption; everything failed
2988
2989 $send_an_email = false;
2990 // Save the jobdata's state for the reporting - because it might get changed (e.g. incremental backup is scheduled)
2991 $jobdata_as_was = $this->jobdata;
2992
2993 // Make sure that the final status is shown
2994 if ($force_abort) {
2995 $send_an_email = true;
2996 $final_message = __('The backup was aborted by the user', 'updraftplus');
2997 if (!empty($clone_job)) $this->get_updraftplus_clone()->clone_failed_delete(array('clone_id' => $clone_id, 'secret_token' => $secret_token));
2998 } elseif (0 == $this->error_count()) {
2999 $send_an_email = true;
3000 $service = $this->jobdata_get('service');
3001 $remote_sent = (!empty($service) && ((is_array($service) && in_array('remotesend', $service)) || 'remotesend' === $service)) ? true : false;
3002 if (0 == $this->error_count('warning')) {
3003 $final_message = __('The backup apparently succeeded and is now complete', 'updraftplus');
3004 // Ensure it is logged in English. Not hugely important; but helps with a tiny number of really broken setups in which the options cacheing is broken
3005 if ('The backup apparently succeeded and is now complete' != $final_message) {
3006 $this->log('The backup apparently succeeded and is now complete');
3007 }
3008 } else {
3009 $final_message = __('The backup apparently succeeded (with warnings) and is now complete', 'updraftplus');
3010 if ('The backup apparently succeeded (with warnings) and is now complete' != $final_message) {
3011 $this->log('The backup apparently succeeded (with warnings) and is now complete');
3012 }
3013 }
3014 if ($remote_sent && !$force_abort) {
3015 $final_message .= empty($clone_job) ? '. '.__('To complete your migration/clone, you should now log in to the remote site and restore the backup set.', 'updraftplus') : '. '.__('Your clone will now deploy this data to re-create your site.', 'updraftplus');
3016 do_action('updraftplus_remotesend_upload_complete');
3017 }
3018 if ($do_cleanup) $delete_jobdata = apply_filters('updraftplus_backup_complete', $delete_jobdata);
3019 } elseif (false == $this->newresumption_scheduled) {
3020 $send_an_email = true;
3021 $final_message = __('The backup attempt has finished, apparently unsuccessfully', 'updraftplus');
3022 if (!empty($clone_job)) $this->get_updraftplus_clone()->clone_failed_delete(array('clone_id' => $clone_id, 'secret_token' => $secret_token));
3023 } else {
3024 // There are errors, but a resumption will be attempted
3025 $final_message = __('The backup has not finished; a resumption is scheduled', 'updraftplus');
3026 }
3027
3028 // Now over-ride the decision to send an email, if needed
3029 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
3030 $send_an_email = true;
3031 $this->log("An email has been scheduled for this job, because we are in debug mode");
3032 }
3033
3034 $email = UpdraftPlus_Options::get_updraft_option('updraft_email');
3035
3036 // If there's no email address, or the set was empty, that is the final over-ride: don't send
3037 if (!$allow_email) {
3038 $send_an_email = false;
3039 $this->log("No email will be sent - this backup set was empty.");
3040 } elseif (empty($email)) {
3041 $send_an_email = false;
3042 $this->log("No email will/can be sent - the user has not configured an email address.");
3043 }
3044
3045 global $updraftplus_backup;
3046
3047 if ($force_abort) $jobdata_as_was['aborted'] = true;
3048 if ($send_an_email) $updraftplus_backup->send_results_email($final_message, $jobdata_as_was);
3049
3050 // Make sure this is the final message logged (so it remains on the dashboard)
3051 $this->log($final_message);
3052
3053 @fclose($this->logfile_handle);
3054 $this->logfile_handle = null;
3055
3056 // This is left until last for the benefit of the front-end UI, which then gets maximum chance to display the 'finished' status
3057 if ($delete_jobdata) delete_site_option('updraft_jobdata_'.$this->nonce);
3058
3059 }
3060
3061 /**
3062 * This function returns 'true' if mod_rewrite could be detected as unavailable; a 'false' result may mean it just couldn't find out the answer
3063 *
3064 * @param boolean $check_if_in_use_first
3065 * @return boolean
3066 */
3067 public function mod_rewrite_unavailable($check_if_in_use_first = true) {
3068 if (function_exists('apache_get_modules')) {
3069 global $wp_rewrite;
3070 $mods = apache_get_modules();
3071 if ((!$check_if_in_use_first || $wp_rewrite->using_mod_rewrite_permalinks()) && ((in_array('core', $mods) || in_array('http_core', $mods)) && !in_array('mod_rewrite', $mods))) {
3072 return true;
3073 }
3074 }
3075 return false;
3076 }
3077
3078 public function error_count($level = 'error') {
3079 $count = 0;
3080 foreach ($this->errors as $err) {
3081 if (('error' == $level && (is_string($err) || is_wp_error($err))) || (is_array($err) && $level == $err['level'])) {
3082 $count++;
3083 }
3084 }
3085 return $count;
3086 }
3087
3088 public function list_errors() {
3089 echo '<ul style="list-style: disc inside;">';
3090 foreach ($this->errors as $err) {
3091 if (is_wp_error($err)) {
3092 foreach ($err->get_error_messages() as $msg) {
3093 echo '<li>'.htmlspecialchars($msg).'<li>';
3094 }
3095 } elseif (is_array($err) && ('error' == $err['level'] || 'warning' == $err['level'])) {
3096 echo "<li>".htmlspecialchars($err['message'])."</li>";
3097 } elseif (is_string($err)) {
3098 echo "<li>".htmlspecialchars($err)."</li>";
3099 } else {
3100 print "<li>".print_r($err, true)."</li>";
3101 }
3102 }
3103 echo '</ul>';
3104 }
3105
3106 private function save_last_backup($backup_array) {
3107 $success = ($this->error_count() == 0) ? 1 : 0;
3108 $last_backup = apply_filters('updraftplus_save_last_backup', array(
3109 'backup_time' => $this->backup_time,
3110 'backup_array' => $backup_array,
3111 'success' => $success,
3112 'errors' => $this->errors,
3113 'backup_nonce' => $this->nonce
3114 ));
3115 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup, false);
3116 }
3117
3118 /**
3119 * $handle must be either false or a WPDB class (or extension thereof). Other options are not yet fully supported.
3120 *
3121 * @param Resource|Boolean|Object $handle
3122 * @param Boolean $logit - whether to log information about the check
3123 * @param Boolean $reschedule - whether to schedule a resumption if checking fails
3124 * @return Boolean|Integer - whether the check succeeded, or -1 for an unknown result
3125 */
3126 public function check_db_connection($handle = false, $logit = false, $reschedule = false) {
3127
3128 $type = false;
3129 if (false === $handle || is_a($handle, 'wpdb')) {
3130 $type = 'wpdb';
3131 } elseif (is_resource($handle)) {
3132 // Expected: string(10) "mysql link"
3133 $type = get_resource_type($handle);
3134 } elseif (is_object($handle) && is_a($handle, 'mysqli')) {
3135 $type = 'mysqli';
3136 }
3137
3138 if (false === $type) return -1;
3139
3140 $db_connected = -1;
3141
3142 if ('mysql link' == $type || 'mysqli' == $type) {
3143 // @codingStandardsIgnoreLine
3144 if ('mysql link' == $type && @mysql_ping($handle)) return true;
3145 if ('mysqli' == $type && @mysqli_ping($handle)) return true;
3146
3147 // @codingStandardsIgnoreLine
3148 for ($tries = 1; $tries <= 5; $tries++) {
3149 // to do, if ever needed
3150 // if ($this->db_connect(false )) return true;
3151 // sleep(1);
3152 }
3153
3154 } elseif ('wpdb' == $type) {
3155 if (false === $handle || (is_object($handle) && 'wpdb' == get_class($handle))) {
3156 global $wpdb;
3157 $handle = $wpdb;
3158 }
3159 if (method_exists($handle, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
3160 if (!$handle->check_connection(false)) {
3161 if ($logit) $this->log("The database went away, and could not be reconnected to");
3162 // Almost certainly a no-op
3163 if ($reschedule) UpdraftPlus_Job_Scheduler::reschedule(60);
3164 $db_connected = false;
3165 } else {
3166 $db_connected = true;
3167 }
3168 }
3169 }
3170
3171 return $db_connected;
3172
3173 }
3174
3175 /**
3176 * This should be called whenever a file is successfully uploaded
3177 *
3178 * @param String $file - full filepath
3179 * @param Boolean $force - mark as successfully uploaded even if not on the last service
3180 * @return Void
3181 */
3182 public function uploaded_file($file, $force = false) {
3183
3184 global $updraftplus_backup;
3185
3186 $db_connected = $this->check_db_connection(false, true, true);
3187
3188 $service = empty($updraftplus_backup->current_service) ? '' : $updraftplus_backup->current_service;
3189 $instance_id = empty($updraftplus_backup->current_instance) ? '' : $updraftplus_backup->current_instance;
3190 $shash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
3191
3192 if ($force || !empty($updraftplus_backup->last_storage_instance)) {
3193 $this->log("Recording as successfully uploaded: $file");
3194 $new_jobdata = $this->get_uploaded_jobdata_items($file, $service, $instance_id);
3195 } else {
3196 $new_jobdata = array('uploaded_'.$shash => 'yes');
3197 $this->log("Recording as successfully uploaded: $file (".$updraftplus_backup->current_service.", more services to follow)");
3198 }
3199
3200 $upload_status = $this->jobdata_get('uploading_substatus');
3201 if (is_array($upload_status) && isset($upload_status['i'])) {
3202 $upload_status['i']++;
3203 $upload_status['p'] = 0;
3204 $new_jobdata['uploading_substatus'] = $upload_status;
3205 }
3206
3207 $this->jobdata_set_multi($new_jobdata);
3208
3209 // Really, we could do this immediately when we realise the DB has gone away. This is just for the probably-impossible case that a DB write really can still succeed. But, we must abort before calling delete_local(), as the removal of the local file can cause it to be recreated if the DB is out of sync with the fact that it really is already uploaded
3210 if (false === $db_connected) {
3211 UpdraftPlus_Job_Scheduler::record_still_alive();
3212 die;
3213 }
3214
3215 // Delete local files immediately if the option is set
3216 // Where we are only backing up locally, only the "prune" function should do deleting
3217 $service = $this->jobdata_get('service');
3218 if (!empty($updraftplus_backup->last_storage_instance) && ('' !== $service && ((is_array($service) && count($service)>0 && (count($service) > 1 || (array('') !== $service && array('none') !== $service))) || (is_string($service) && 'none' !== $service)))) {
3219 $this->delete_local($file);
3220 }
3221 }
3222
3223 /**
3224 * Gets the jobdata items to be added to mark a file as uploaded
3225 *
3226 * @param String $file - the file (basename)
3227 * @param String $service - service identifier
3228 * @param String $instance_id - instance identifier
3229 *
3230 * @return Array - jobdata items
3231 */
3232 public function get_uploaded_jobdata_items($file, $service = '', $instance_id = '') {
3233 $hash = md5($file);
3234 $shash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
3235 return array(
3236 'uploaded_lastreset' => $this->current_resumption,
3237 'uploaded_'.$hash => 'yes',
3238 'uploaded_'.$shash =>'yes'
3239 );
3240 }
3241
3242 /**
3243 * Return whether a particular file has been uploaded to a particular remote service
3244 *
3245 * @param String $file - the filename (basename)
3246 * @param String $service - the service identifier; or none, to indicate all services
3247 * @param String $instance_id - the instance identifier
3248 *
3249 * @return Boolean - the result
3250 */
3251 public function is_uploaded($file, $service = '', $instance_id = '') {
3252 $hash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
3253 return ('yes' === $this->jobdata_get("uploaded_$hash")) ? true : false;
3254 }
3255
3256 private function delete_local($file) {
3257 $log = "Deleting local file: $file: ";
3258 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local', 1)) {
3259 $fullpath = $this->backups_dir_location().'/'.$file;
3260
3261 // check to make sure it exists before removing
3262 if (realpath($fullpath)) {
3263 $deleted = unlink($fullpath);
3264 $this->log($log.(($deleted) ? 'OK' : 'failed'));
3265 return $deleted;
3266 }
3267 } else {
3268 $this->log($log."skipped: user has unchecked updraft_delete_local option");
3269 }
3270 return true;
3271 }
3272
3273 /**
3274 * For detecting another run, and aborting if one was found
3275 *
3276 * @param String $file - full file path of the file to check
3277 */
3278 public function check_recent_modification($file) {
3279 if (file_exists($file)) {
3280 $time_mod = (int) @filemtime($file);
3281 $time_now = time();
3282 if ($time_mod > 100 && ($time_now - $time_mod) < 30) {
3283 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($file, $time_now, $time_mod);
3284 }
3285 }
3286 }
3287
3288 public function get_exclude($whichone) {
3289 if ('uploads' == $whichone) {
3290 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE));
3291 } elseif ('others' == $whichone) {
3292 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
3293 } else {
3294 $exclude = apply_filters('updraftplus_include_'.$whichone.'_exclude', array());
3295 }
3296 return (empty($exclude) || !is_array($exclude)) ? array() : $exclude;
3297 }
3298
3299 public function wp_upload_dir() {
3300 if (is_multisite()) {
3301 global $current_site;
3302 switch_to_blog($current_site->blog_id);
3303 }
3304
3305 $wp_upload_dir = wp_upload_dir();
3306
3307 if (is_multisite()) restore_current_blog();
3308
3309 return $wp_upload_dir;
3310 }
3311
3312 public function backup_uploads_dirlist($logit = false) {
3313 // Create an array of directories to be skipped
3314 // Make the values into the keys
3315 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
3316 if ($logit) $this->log("Exclusion option setting (uploads): ".$exclude);
3317 $skip = array_flip(preg_split("/,/", $exclude));
3318 $wp_upload_dir = $this->wp_upload_dir();
3319 $uploads_dir = $wp_upload_dir['basedir'];
3320 return $this->compile_folder_list_for_backup($uploads_dir, array(), $skip);
3321 }
3322
3323 public function backup_others_dirlist($logit = false) {
3324 // Create an array of directories to be skipped
3325 // Make the values into the keys
3326 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE);
3327 if ($logit) $this->log("Exclusion option setting (others): ".$exclude);
3328 $skip = array_flip(preg_split("/,/", $exclude));
3329 $file_entities = $this->get_backupable_file_entities(false);
3330
3331 // Keys = directory names to avoid; values = the label for that directory (used only in log files)
3332 // $avoid_these_dirs = array_flip($file_entities);
3333 $avoid_these_dirs = array();
3334 foreach ($file_entities as $type => $dirs) {
3335 if (is_string($dirs)) {
3336 $avoid_these_dirs[$dirs] = $type;
3337 } elseif (is_array($dirs)) {
3338 foreach ($dirs as $dir) {
3339 $avoid_these_dirs[$dir] = $type;
3340 }
3341 }
3342 }
3343 return $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $avoid_these_dirs, $skip);
3344 }
3345
3346 /**
3347 * avoid_these_dirs and skip_these_dirs ultimately do the same thing; but avoid_these_dirs takes full paths whereas skip_these_dirs takes basenames; and they are logged differently (dirs in avoid are potentially dangerous to include; skip is just a user-level preference). They are allowed to overlap.
3348 *
3349 * @param string $backup_from_inside_dir
3350 * @param string $avoid_these_dirs
3351 * @param string $skip_these_dirs
3352 * @return array
3353 */
3354 public function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
3355
3356 // Entries in $skip_these_dirs are allowed to end in *, which means "and anything else as a suffix". It's not a full shell glob, but it covers what is needed to-date.
3357
3358 $dirlist = array();
3359 $added = 0;
3360
3361 $this->log('Looking for candidates to backup in: '.$backup_from_inside_dir);
3362 $updraft_dir = $this->backups_dir_location();
3363
3364 if (is_file($backup_from_inside_dir)) {
3365 array_push($dirlist, $backup_from_inside_dir);
3366 $added++;
3367 $this->log("finding files: $backup_from_inside_dir: adding to list ($added)");
3368 } elseif ($handle = opendir($backup_from_inside_dir)) {
3369
3370 while (false !== ($entry = readdir($handle))) {
3371
3372 if ('.' == $entry || '..' == $entry) continue;
3373
3374 // $candidate: full path; $entry = one-level
3375 $candidate = $backup_from_inside_dir.'/'.$entry;
3376
3377 if (isset($avoid_these_dirs[$candidate])) {
3378 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
3379 } elseif ($candidate == $updraft_dir) {
3380 $this->log("finding files: $entry: skipping: this is the updraft directory");
3381 } elseif (isset($skip_these_dirs[$entry])) {
3382 $this->log("finding files: $entry: skipping: excluded by options");
3383 } else {
3384 $add_to_list = true;
3385 // Now deal with entries in $skip_these_dirs ending in * or starting with *
3386 foreach ($skip_these_dirs as $skip => $sind) {
3387 if ('*' == substr($skip, -1, 1) && '*' == substr($skip, 0, 1) && strlen($skip) > 2) {
3388 if (strpos($entry, substr($skip, 1, strlen($skip-2))) !== false) {
3389 $this->log("finding files: $entry: skipping: excluded by options (glob)");
3390 $add_to_list = false;
3391 }
3392 } elseif ('*' == substr($skip, -1, 1) && strlen($skip) > 1) {
3393 if (substr($entry, 0, strlen($skip)-1) == substr($skip, 0, strlen($skip)-1)) {
3394 $this->log("finding files: $entry: skipping: excluded by options (glob)");
3395 $add_to_list = false;
3396 }
3397 } elseif ('*' == substr($skip, 0, 1) && strlen($skip) > 1) {
3398 if (strlen($entry) >= strlen($skip)-1 && substr($entry, (strlen($skip)-1)*-1) == substr($skip, 1)) {
3399 $this->log("finding files: $entry: skipping: excluded by options (glob)");
3400 $add_to_list = false;
3401 }
3402 }
3403 }
3404 if ($add_to_list) {
3405 array_push($dirlist, $candidate);
3406 $added++;
3407 $skip_dblog = (($added > 50 && 0 != $added % 100) || ($added > 2000 && 0 != $added % 500));
3408 $this->log("finding files: $entry: adding to list ($added)", 'notice', false, $skip_dblog);
3409 }
3410 }
3411 }
3412 @closedir($handle);
3413 } else {
3414 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
3415 $this->log(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir, 'error');
3416 }
3417
3418 return $dirlist;
3419
3420 }
3421
3422 /**
3423 * Save the backup information to the backup history during a running backup (adding information to the currently-running job)
3424 *
3425 * @param Array $backup_array - the backup history
3426 */
3427 private function save_backup_to_history($backup_array) {
3428
3429 if (!is_array($backup_array)) {
3430 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
3431 $this->log(__('Could not save backup history because we have no backup array. Backup probably failed.', 'updraftplus'), 'error');
3432 return;
3433 }
3434
3435 $job_type = $this->jobdata_get('job_type');
3436
3437 $backup_array['nonce'] = $this->file_nonce;
3438 $backup_array['service'] = $this->jobdata_get('service');
3439 $backup_array['service_instance_ids'] = array();
3440 if ('incremental' != $job_type) $backup_array['always_keep'] = $this->jobdata_get('always_keep', false);
3441 $backup_array['files_enumerated_at'] = $this->jobdata_get('files_enumerated_at');
3442
3443 // N.B. Though the saved 'service' option can have various forms (especially if upgrading from (very) old versions), in the jobdata, it is always an array.
3444 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($backup_array['service']);
3445
3446 // N.B. On PHP 5.5+, we'd use array_column()
3447 foreach ($storage_objects_and_ids as $method => $method_information) {
3448 if ('none' == $method || !$method || !$method_information['object']->supports_feature('multi_options')) continue;
3449 $backup_array['service_instance_ids'][$method] = array_keys($method_information['instance_settings']);
3450 }
3451
3452 if ('incremental' != $job_type && '' != ($label = $this->jobdata_get('label', ''))) $backup_array['label'] = $label;
3453 if (!isset($backup_array['created_by_version'])) $backup_array['created_by_version'] = $this->version;
3454 $backup_array['last_saved_by_version'] = $this->version;
3455 $backup_array['is_multisite'] = is_multisite() ? true : false;
3456 $remotesend_info = $this->jobdata_get('remotesend_info');
3457 if (is_array($remotesend_info) && !empty($remotesend_info['url'])) $backup_array['remotesend_url'] = $remotesend_info['url'];
3458 if (false != $this->jobdata_get('is_autobackup', false)) $backup_array['autobackup'] = true;
3459
3460 if (false != ($morefiles_linked_indexes = $this->jobdata_get('morefiles_linked_indexes', false))) $backup_array['morefiles_linked_indexes'] = $morefiles_linked_indexes;
3461 if (false != ($morefiles_more_locations = $this->jobdata_get('morefiles_more_locations', false))) $backup_array['morefiles_more_locations'] = $morefiles_more_locations;
3462
3463 UpdraftPlus_Backup_History::save_backup(apply_filters('updraftplus_save_backup_history_timestamp', $this->backup_time), $backup_array);
3464 }
3465
3466 /**
3467 * If files + db are on different schedules but are scheduled for the same time,
3468 * then combine them $event = (object) array('hook' => $hook, 'timestamp' => $timestamp, 'schedule' => $recurrence, 'args' => $args, 'interval' => $schedules[$recurrence]['interval']);
3469 * See wp_schedule_single_event() and wp_schedule_event() in wp-includes/cron.php
3470 *
3471 * @param Object|Boolean $event - the event being scheduled
3472 * @return Object|Boolean - the filtered value
3473 */
3474 public function schedule_event($event) {
3475
3476 static $scheduled = array();
3477
3478 if (is_object($event) && ('updraft_backup' == $event->hook || 'updraft_backup_database' == $event->hook)) {
3479
3480 // Reset the option - but make sure it is saved first so that we can used it (since this hook may be called just before our actual cron task)
3481 $this->combine_jobs_around = UpdraftPlus_Options::get_updraft_option('updraft_combine_jobs_around');
3482
3483 UpdraftPlus_Options::delete_updraft_option('updraft_combine_jobs_around');
3484
3485 $scheduled[$event->hook] = true;
3486
3487 // This next fragment is wrong: there's only a 'second call' when saving all settings; otherwise, the WP scheduler might just be updating one event. So, there's some inefficieny as the option is wiped and set uselessly at least once when saving settings.
3488 // We only want to take action on the second call (otherwise, our information is out-of-date already)
3489 // If there is no second call, then that's fine - nothing to do
3490 // if (count($scheduled) < 2) {
3491 // return $event;
3492 // }
3493
3494 $backup_scheduled_for = ('updraft_backup' == $event->hook) ? $event->timestamp : wp_next_scheduled('updraft_backup');
3495 $db_scheduled_for = ('updraft_backup_database' == $event->hook) ? $event->timestamp : wp_next_scheduled('updraft_backup_database');
3496
3497 $diff = absint($backup_scheduled_for - $db_scheduled_for);
3498
3499 $margin = (defined('UPDRAFTPLUS_COMBINE_MARGIN') && is_numeric(UPDRAFTPLUS_COMBINE_MARGIN)) ? UPDRAFTPLUS_COMBINE_MARGIN : 600;
3500
3501 if ($backup_scheduled_for && $db_scheduled_for && $diff < $margin) {
3502 // We could change the event parameters; however, this would complicate other code paths (because the WP cron system uses a hash of the parameters as a key, and you must supply the exact parameters to look up events). So, we just set a marker that boot_backup() can pick up on.
3503 UpdraftPlus_Options::update_updraft_option('updraft_combine_jobs_around', min($backup_scheduled_for, $db_scheduled_for));
3504 }
3505
3506 }
3507
3508 return $event;
3509
3510 }
3511
3512 /**
3513 * This function is both the backup scheduler and a filter callback for saving the option. It is called in the register_setting for the updraft_interval, which means when the admin settings are saved it is called.
3514 *
3515 * @param String $interval
3516 * @return String - filtered value
3517 */
3518 public function schedule_backup($interval) {
3519 $previous_time = wp_next_scheduled('updraft_backup');
3520
3521 // Clear schedule so that we don't stack up scheduled backups
3522 wp_clear_scheduled_hook('updraft_backup');
3523 if ('manual' == $interval) {
3524 // Clear increments schedule as the file schedule is manual
3525 wp_clear_scheduled_hook('updraft_backup_increments');
3526 return 'manual';
3527 }
3528 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval');
3529
3530 $valid_schedules = wp_get_schedules();
3531 if (empty($valid_schedules[$interval])) $interval = 'daily';
3532
3533 // Try to avoid changing the time is one was already scheduled. This is fairly conservative - we could do more, e.g. check if a backup already happened today.
3534 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
3535 $first_time = apply_filters('updraftplus_schedule_firsttime_files', $default_time);
3536
3537 wp_schedule_event($first_time, $interval, 'updraft_backup');
3538
3539 return $interval;
3540 }
3541
3542 /**
3543 * This function is both the database backup scheduler and a filter callback for saving the option. It is called in the register_setting for the updraft_interval_database, which means when the admin settings are saved it is called.
3544 *
3545 * @param String $interval
3546 * @return String - filtered value
3547 */
3548 public function schedule_backup_database($interval) {
3549 $previous_time = wp_next_scheduled('updraft_backup_database');
3550
3551 // Clear schedule so that we don't stack up scheduled backups
3552 wp_clear_scheduled_hook('updraft_backup_database');
3553 if ('manual' == $interval) return 'manual';
3554
3555 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
3556
3557 $valid_schedules = wp_get_schedules();
3558 if (empty($valid_schedules[$interval])) $interval = 'daily';
3559
3560 // Try to avoid changing the time is one was already scheduled. This is fairly conservative - we could do more, e.g. check if a backup already happened today.
3561 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
3562
3563 $first_time = apply_filters('updraftplus_schedule_firsttime_db', $default_time);
3564 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
3565
3566 return $interval;
3567 }
3568
3569 /**
3570 * This function is both the increments backup scheduler and a filter callback for saving the option. It is called in the register_setting for the updraft_interval_increments, which means when the admin settings are saved it is called.
3571 *
3572 * @param String $interval
3573 * @return String - filtered value
3574 */
3575 public function schedule_backup_increments($interval) {
3576 $previous_time = wp_next_scheduled('updraft_backup_increments');
3577
3578 // Clear schedule so that we don't stack up scheduled backups
3579 wp_clear_scheduled_hook('updraft_backup_increments');
3580 if ('none' == $interval || empty($interval)) return 'none';
3581 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_increments');
3582
3583 $valid_schedules = wp_get_schedules();
3584 if (empty($valid_schedules[$interval])) $interval = 'daily';
3585
3586 // Try to avoid changing the time is one was already scheduled. This is fairly conservative - we could do more, e.g. check if a backup already happened today.
3587 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
3588 $first_time = apply_filters('updraftplus_schedule_firsttime_increments', $default_time);
3589
3590 wp_schedule_event($first_time, $interval, 'updraft_backup_increments');
3591
3592 return $interval;
3593 }
3594
3595 /**
3596 * Acts as a WordPress options filter
3597 *
3598 * @param Array $options - An array of options
3599 * @param String $option_name - The option name
3600 *
3601 * @return Array - the returned array can either be the set of updated options or a WordPress error array
3602 */
3603 public function storage_options_filter($options, $option_name) {
3604 if ('updraft_' !== substr($option_name, 0, 8)) return $options;
3605 $method = substr($option_name, 8);
3606
3607 $storage = UpdraftPlus_Storage_Methods_Interface::get_storage_object($method);
3608
3609 if (!is_a($storage, 'UpdraftPlus_BackupModule') || !is_callable(array($storage, 'options_filter'))) return $options;
3610
3611 return call_user_func(array($storage, 'options_filter'), $options);
3612 }
3613
3614 /**
3615 * Get the location of UD's internal directory
3616 *
3617 * @param Boolean $allow_cache
3618 * @return String - the directory path. Returns without any trailing slash.
3619 */
3620 public function backups_dir_location($allow_cache = true) {
3621
3622 if ($allow_cache && !empty($this->backup_dir)) return $this->backup_dir;
3623
3624 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
3625 // When newly installing, if someone had (e.g.) wp-content/updraft in their database from a previous, deleted pre-1.7.18 install but had removed the updraft directory before re-installing, without this fix they'd end up with wp-content/wp-content/updraft.
3626 if (preg_match('/^wp-content\/(.*)$/', $updraft_dir, $matches) && ABSPATH.'wp-content' === WP_CONTENT_DIR) {
3627 UpdraftPlus_Options::update_updraft_option('updraft_dir', $matches[1]);
3628 $updraft_dir = WP_CONTENT_DIR.'/'.$matches[1];
3629 }
3630 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
3631 $updraft_dir = ($updraft_dir) ? $updraft_dir : $default_backup_dir;
3632
3633 // Do a test for a relative path
3634 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
3635 // Legacy - file paths stored related to ABSPATH
3636 if (is_dir(ABSPATH.$updraft_dir) && is_file(ABSPATH.$updraft_dir.'/index.html') && is_file(ABSPATH.$updraft_dir.'/.htaccess') && !is_file(ABSPATH.$updraft_dir.'/index.php') && false !== strpos(file_get_contents(ABSPATH.$updraft_dir.'/.htaccess', false, null, 0, 20), 'deny from all')) {
3637 $updraft_dir = ABSPATH.$updraft_dir;
3638 } else {
3639 // File paths stored relative to WP_CONTENT_DIR
3640 $updraft_dir = trailingslashit(WP_CONTENT_DIR).$updraft_dir;
3641 }
3642 }
3643
3644 // Check for the existence of the dir and prevent enumeration
3645 // index.php is for a sanity check - make sure that we're not somewhere unexpected
3646 if ((!is_dir($updraft_dir) || !is_file($updraft_dir.'/index.html') || !is_file($updraft_dir.'/.htaccess')) && !is_file($updraft_dir.'/index.php') || !is_file($updraft_dir.'/web.config')) {
3647 @mkdir($updraft_dir, 0775, true);
3648 @file_put_contents($updraft_dir.'/index.html', "<html><body><a href=\"https://updraftplus.com\" target=\"_blank\">WordPress backups by UpdraftPlus</a></body></html>");
3649 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess', 'deny from all');
3650 if (!is_file($updraft_dir.'/web.config')) @file_put_contents($updraft_dir.'/web.config', "<configuration>\n<system.webServer>\n<authorization>\n<deny users=\"*\" />\n</authorization>\n</system.webServer>\n</configuration>\n");
3651 }
3652
3653 $this->backup_dir = $updraft_dir;
3654
3655 return $updraft_dir;
3656 }
3657
3658 public function spool_file($fullpath, $encryption = '') {
3659 @set_time_limit(900);
3660
3661 if (!file_exists($fullpath) || filesize($fullpath) < 1) {
3662 _e('File not found', 'updraftplus');
3663 return;
3664 }
3665
3666 // Prevent any debug output
3667 // Don't enable this line - it causes 500 HTTP errors in some cases/hosts on some large files, for unknown reason
3668 // @ini_set('display_errors', '0');
3669
3670 $spooled = false;
3671 if (UpdraftPlus_Encryption::is_file_encrypted($fullpath)) {
3672 if (ob_get_level()) {
3673 $flush_max = min(5, (int) ob_get_level());
3674 for ($i=1; $i<=$flush_max; $i++) {
3675 @ob_end_clean();
3676 }
3677 }
3678 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
3679 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
3680 UpdraftPlus_Encryption::spool_crypted_file($fullpath, (string) $encryption);
3681 return;
3682 }
3683
3684 $content_type = UpdraftPlus_Manipulation_Functions::get_mime_type_from_filename($fullpath, false);
3685
3686 include_once(UPDRAFTPLUS_DIR.'/includes/class-partialfileservlet.php');
3687
3688 // Prevent the file being read into memory
3689 if (ob_get_level()) {
3690 $flush_max = min(5, (int) ob_get_level());
3691 for ($i=1; $i<=$flush_max; $i++) {
3692 @ob_end_clean();
3693 }
3694 }
3695 if (ob_get_level()) @ob_end_clean(); // Twice - see HS#6673 - someone at least needed it
3696
3697 if (isset($_SERVER['HTTP_RANGE'])) {
3698 $range_header = trim($_SERVER['HTTP_RANGE']);
3699 } elseif (function_exists('apache_request_headers')) {
3700 foreach (apache_request_headers() as $name => $value) {
3701 if (strtoupper($name) === 'RANGE') {
3702 $range_header = trim($value);
3703 }
3704 }
3705 }
3706
3707 if (empty($range_header)) {
3708 header("Content-Length: ".filesize($fullpath));
3709 header("Content-type: $content_type");
3710 header("Content-Disposition: attachment; filename=\"".basename($fullpath)."\";");
3711 readfile($fullpath);
3712 return;
3713 }
3714
3715 try {
3716 $range_header = UpdraftPlus_RangeHeader::createFromHeaderString($range_header);
3717 $servlet = new UpdraftPlus_PartialFileServlet($range_header);
3718 $servlet->send_file($fullpath, $content_type);
3719 } catch (UpdraftPlus_InvalidRangeHeaderException $e) {
3720 header("HTTP/1.1 400 Bad Request");
3721 error_log("UpdraftPlus: UpdraftPlus_InvalidRangeHeaderException: ".$e->getMessage());
3722 } catch (UpdraftPlus_UnsatisfiableRangeException $e) {
3723 header("HTTP/1.1 416 Range Not Satisfiable");
3724 } catch (UpdraftPlus_NonExistentFileException $e) {
3725 header("HTTP/1.1 404 Not Found");
3726 } catch (UpdraftPlus_UnreadableFileException $e) {
3727 header("HTTP/1.1 500 Internal Server Error");
3728 }
3729
3730 }
3731
3732 public function just_one_email($input, $required = false) {
3733 $x = $this->just_one($input, 'saveemails', (empty($input) && false === $required) ? '' : get_bloginfo('admin_email'));
3734 if (is_array($x)) {
3735 foreach ($x as $ind => $val) {
3736 if (empty($val)) unset($x[$ind]);
3737 }
3738 if (empty($x)) $x = '';
3739 }
3740 return $x;
3741 }
3742
3743 /**
3744 * Filter the values down to just one (subject to being filtered)
3745 *
3746 * @param Array|String $input - input
3747 * @param String $filter - filter suffix to use
3748 * @param Boolean|String $rinput - a 'preferred' value (unless false) if no filtering is done
3749 *
3750 * @return Array|String - output, after filtering
3751 */
3752 public function just_one($input, $filter = 'savestorage', $rinput = false) {
3753 $oinput = $input;
3754 if (false === $rinput) $rinput = is_array($input) ? array_pop($input) : $input;
3755 if (is_string($rinput) && false !== strpos($rinput, ',')) $rinput = substr($rinput, 0, strpos($rinput, ','));
3756 return apply_filters('updraftplus_'.$filter, $rinput, $oinput);
3757 }
3758
3759 /**
3760 * Enqueue the JavaScript and CSS for the select2 library
3761 */
3762 public function enqueue_select2() {
3763 // De-register to defeat any plugins that may have registered incompatible versions (e.g. WooCommerce 2.5 beta1 still has the Select 2 3.5 series)
3764 wp_deregister_script('select2');
3765 wp_deregister_style('select2');
3766 $select2_version = $this->use_unminified_scripts() ? '4.0.3'.'.'.time() : '4.0.3';
3767 $min_or_not = $this->use_unminified_scripts() ? '' : '.min';
3768 wp_enqueue_script('select2', UPDRAFTPLUS_URL."/includes/select2/select2".$min_or_not.".js", array('jquery'), $select2_version);
3769 wp_enqueue_style('select2', UPDRAFTPLUS_URL."/includes/select2/select2".$min_or_not.".css", array(), $select2_version);
3770 }
3771
3772 public function memory_check_current($memory_limit = false) {
3773 // Returns in megabytes
3774 if (false == $memory_limit) $memory_limit = ini_get('memory_limit');
3775 $memory_limit = rtrim($memory_limit);
3776 $memory_unit = $memory_limit[strlen($memory_limit)-1];
3777 if (0 == (int) $memory_unit && '0' !== $memory_unit) {
3778 $memory_limit = substr($memory_limit, 0, strlen($memory_limit)-1);
3779 } else {
3780 $memory_unit = '';
3781 }
3782 switch ($memory_unit) {
3783 case '':
3784 $memory_limit = floor($memory_limit/1048576);
3785 break;
3786 case 'K':
3787 case 'k':
3788 $memory_limit = floor($memory_limit/1024);
3789 break;
3790 case 'G':
3791 $memory_limit = $memory_limit*1024;
3792 break;
3793 case 'M':
3794 // assumed size, no change needed
3795 break;
3796 }
3797 return $memory_limit;
3798 }
3799
3800 public function memory_check($memory, $check_using = false) {
3801 $memory_limit = $this->memory_check_current($check_using);
3802 return ($memory_limit >= $memory) ? true : false;
3803 }
3804
3805 /**
3806 * Get the UpdraftPlus RSS feed
3807 *
3808 * @uses fetch_feed()
3809 *
3810 * @return WP_Error|SimplePie WP_Error object on failure or SimplePie object on success
3811 */
3812 public function get_updraftplus_rssfeed() {
3813 if (!function_exists('fetch_feed')) include(ABSPATH.WPINC.'/feed.php');
3814 return fetch_feed('http://feeds.feedburner.com/updraftplus/');
3815 }
3816
3817 /**
3818 * Sets up the nonce, basic job data, opens a log file for a new restore job, and makes sure that the Updraft_Restorer class is available
3819 */
3820 public function initiate_restore_job() {
3821 $this->backup_time_nonce();
3822 $this->jobdata_set('job_type', 'restore');
3823 $this->jobdata_set('job_time_ms', $this->job_time_ms);
3824 $this->logfile_open($this->nonce);
3825 if (!class_exists('Updraft_Restorer')) include_once(UPDRAFTPLUS_DIR.'/restorer.php');
3826 }
3827
3828 /**
3829 * Analyse a database file and return information about it
3830 *
3831 * @param Integer $timestamp - the database time in the backup history
3832 * @param Array $res - accompanying data. The key 'updraft_encryptionphrase' will be used for decryption if relevant.
3833 * @param Boolean|String $db_file - the path to the file to analyse; if not specified (false), then it will be obtained from the backup history
3834 * @param Boolean $header_only - whether or not to stop analysis once the header ends
3835 *
3836 * @return Array - containing arrays for the resulting messages, warnings, errors and meta information
3837 */
3838 public function analyse_db_file($timestamp, $res, $db_file = false, $header_only = false) {
3839
3840 $mess = array();
3841 $warn = array();
3842 $err = array();
3843 $info = array();
3844 $wp_version = $this->get_wordpress_version();
3845 global $wpdb;
3846
3847 $updraft_dir = $this->backups_dir_location();
3848
3849 if (false === $db_file) {
3850 // This attempts to raise the maximum packet size. This can't be done within the session, only globally. Therefore, it has to be done before the session starts; in our case, during the pre-analysis.
3851 $this->max_packet_size();
3852
3853 $backup = UpdraftPlus_Backup_History::get_history($timestamp);
3854 if (!isset($backup['nonce']) || !isset($backup['db'])) return array($mess, $warn, $err, $info);
3855
3856 $db_file = is_string($backup['db']) ? $updraft_dir.'/'.$backup['db'] : $updraft_dir.'/'.$backup['db'][0];
3857 }
3858
3859 if (!is_readable($db_file)) return array($mess, $warn, $err, $info);
3860
3861 // Encrypted - decrypt it
3862 if (UpdraftPlus_Encryption::is_file_encrypted($db_file)) {
3863
3864 $encryption = empty($res['updraft_encryptionphrase']) ? UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase') : $res['updraft_encryptionphrase'];
3865
3866 if (!$encryption) {
3867 if (class_exists('UpdraftPlus_Addon_MoreDatabase')) {
3868 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted, but you have no encryption key entered.', 'updraftplus'));
3869 } else {
3870 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted.', 'updraftplus'));
3871 }
3872 return array($mess, $warn, $err, $info);
3873 }
3874
3875 $decrypted_file = UpdraftPlus_Encryption::decrypt($db_file, $encryption);
3876
3877 if (is_array($decrypted_file)) {
3878 $db_file = $decrypted_file['fullpath'];
3879 } else {
3880 $err[] = __('Decryption failed. The most likely cause is that you used the wrong key.', 'updraftplus');
3881 return array($mess, $warn, $err, $info);
3882 }
3883 }
3884
3885 // Even the empty schema when gzipped comes to 1565 bytes; a blank WP 3.6 install at 5158. But we go low, in case someone wants to share single tables.
3886 if (filesize($db_file) < 1000) {
3887 $err[] = sprintf(__('The database is too small to be a valid WordPress database (size: %s Kb).', 'updraftplus'), round(filesize($db_file)/1024, 1));
3888 return array($mess, $warn, $err, $info);
3889 }
3890
3891 $is_plain = ('.gz' == substr($db_file, -3, 3)) ? false : true;
3892
3893 $dbhandle = $is_plain ? fopen($db_file, 'r') : UpdraftPlus_Filesystem_Functions::gzopen_for_read($db_file, $warn, $err);
3894 if (!is_resource($dbhandle)) {
3895 $err[] = __('Failed to open database file.', 'updraftplus');
3896 return array($mess, $warn, $err, $info);
3897 }
3898
3899 $info['timestamp'] = $timestamp;
3900
3901 // Analyse the file, print the results.
3902
3903 $line = 0;
3904 $old_siteurl = '';
3905 $old_home = '';
3906 $old_table_prefix = '';
3907 $old_siteinfo = array();
3908 $gathering_siteinfo = true;
3909 $old_wp_version = '';
3910 $old_php_version = '';
3911
3912 $tables_found = array();
3913 $db_charsets_found = array();
3914
3915 // TODO: If the backup is the right size/checksum, then we could restore the $line <= 100 in the 'while' condition and not bother scanning the whole thing? Or better: sort the core tables to be first so that this usually terminates early
3916
3917 $wanted_tables = array('terms', 'term_taxonomy', 'term_relationships', 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', 'users', 'usermeta');
3918
3919 $migration_warning = false;
3920 $processing_create = false;
3921 $db_version = $wpdb->db_version();
3922
3923 // Don't set too high - we want a timely response returned to the browser
3924 // Until April 2015, this was always 90. But we've seen a few people with ~1GB databases (uncompressed), and 90s is not enough. Note that we don't bother checking here if it's compressed - having a too-large timeout when unexpected is harmless, as it won't be hit. On very large dbs, they're expecting it to take a while.
3925 // "120 or 240" is a first attempt at something more useful than just fixed at 90 - but should be sufficient (as 90 was for everyone without ~1GB databases)
3926 $default_dbscan_timeout = (filesize($db_file) < 31457280) ? 120 : 240;
3927 $dbscan_timeout = (defined('UPDRAFTPLUS_DBSCAN_TIMEOUT') && is_numeric(UPDRAFTPLUS_DBSCAN_TIMEOUT)) ? UPDRAFTPLUS_DBSCAN_TIMEOUT : $default_dbscan_timeout;
3928 @set_time_limit($dbscan_timeout);
3929
3930 // We limit the time that we spend scanning the file for character sets
3931 $db_charset_collate_scan_timeout = (defined('UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT') && is_numeric(UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT)) ? UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT : 10;
3932 $charset_scan_start_time = microtime(true);
3933 $db_supported_character_sets_res = $GLOBALS['wpdb']->get_results('SHOW CHARACTER SET', OBJECT_K);
3934 $db_supported_character_sets = (null !== $db_supported_character_sets_res) ? $db_supported_character_sets_res : array();
3935 $db_charsets_found = array();
3936 $db_supported_collations_res = $GLOBALS['wpdb']->get_results('SHOW COLLATION', OBJECT_K);
3937 $db_supported_collations = (null !== $db_supported_collations_res) ? $db_supported_collations_res : array();
3938 $db_charsets_found = array();
3939 $db_collates_found = array();
3940 $db_supported_charset_related_to_unsupported_collation = false;
3941 $db_supported_charsets_related_to_unsupported_collations = array();
3942 while ((($is_plain && !feof($dbhandle)) || (!$is_plain && !gzeof($dbhandle))) && ($line<100 || (!$header_only && count($wanted_tables)>0) || ((microtime(true) - $charset_scan_start_time) < $db_charset_collate_scan_timeout && !empty($db_supported_character_sets)))) {
3943 $line++;
3944 // Up to 1MB
3945 $buffer = ($is_plain) ? rtrim(fgets($dbhandle, 1048576)) : rtrim(gzgets($dbhandle, 1048576));
3946 // Comments are what we are interested in
3947 if (substr($buffer, 0, 1) == '#') {
3948 $processing_create = false;
3949 if ('' == $old_siteurl && preg_match('/^\# Backup of: (http(.*))$/', $buffer, $matches)) {
3950 $old_siteurl = untrailingslashit($matches[1]);
3951 $mess[] = __('Backup of:', 'updraftplus').' '.htmlspecialchars($old_siteurl).((!empty($old_wp_version)) ? ' '.sprintf(__('(version: %s)', 'updraftplus'), $old_wp_version) : '');
3952 // Check for should-be migration
3953 if (untrailingslashit(site_url()) != $old_siteurl) {
3954 if (!$migration_warning) {
3955 $migration_warning = true;
3956 $info['migration'] = true;
3957 // && !class_exists('UpdraftPlus_Addons_Migrator')
3958 if (UpdraftPlus_Manipulation_Functions::normalise_url($old_siteurl) == UpdraftPlus_Manipulation_Functions::normalise_url(site_url())) {
3959 // Same site migration with only http/https difference
3960 $info['same_url'] = false;
3961 $old_siteurl_parsed = parse_url($old_siteurl);
3962 $actual_siteurl_parsed = parse_url(site_url());
3963 if ((stripos($old_siteurl_parsed['host'], 'www.') === 0 && stripos($actual_siteurl_parsed['host'], 'www.') !== 0) || (stripos($old_siteurl_parsed['host'], 'www.') !== 0 && stripos($actual_siteurl_parsed['host'], 'www.') === 0)) {
3964 $powarn = sprintf(__('The website address in the backup set (%s) is slightly different from that of the site now (%s). This is not expected to be a problem for restoring the site, as long as visits to the former address still reach the site.', 'updraftplus'), $old_siteurl, site_url()).' ';
3965 } else {
3966 $powarn = '';
3967 }
3968 if (('https' == $old_siteurl_parsed['scheme'] && 'http' == $actual_siteurl_parsed['scheme']) || ('http' == $old_siteurl_parsed['scheme'] && 'https' == $actual_siteurl_parsed['scheme'])) {
3969 $powarn .= sprintf(__('This backup set is of this site, but at the time of the backup you were using %s, whereas the site now uses %s.', 'updraftplus'), $old_siteurl_parsed['scheme'], $actual_siteurl_parsed['scheme']);
3970 if ('https' == $old_siteurl_parsed['scheme']) {
3971 $powarn .= ' '.apply_filters('updraftplus_https_to_http_additional_warning', sprintf(__('This restoration will work if you still have an SSL certificate (i.e. can use https) to access the site. Otherwise, you will want to use %s to search/replace the site address so that the site can be visited without https.', 'updraftplus'), '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.__('the migrator add-on', 'updraftplus').'</a>'));
3972 } else {
3973 $powarn .= ' '.apply_filters('updraftplus_http_to_https_additional_warning', sprintf(__('As long as your web hosting allows http (i.e. non-SSL access) or will forward requests to https (which is almost always the case), this is no problem. If that is not yet set up, then you should set it up, or use %s so that the non-https links are automatically replaced.', 'updraftplus'), apply_filters('updraftplus_migrator_addon_link', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.__('the migrator add-on', 'updraftplus').'</a>')));
3974 }
3975 } else {
3976 $powarn .= apply_filters('updraftplus_dbscan_urlchange_www_append_warning', '');
3977 }
3978 $warn[] = $powarn;
3979 } else {
3980 // For completely different site migration
3981 $info['same_url'] = false;
3982 $warn[] = apply_filters('updraftplus_dbscan_urlchange', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.sprintf(__('This backup set is from a different site (%s) - this is not a restoration, but a migration. You need the Migrator add-on in order to make this work.', 'updraftplus'), htmlspecialchars($old_siteurl.' / '.untrailingslashit(site_url()))).'</a>', $old_siteurl, $res);
3983 }
3984 if (!class_exists('UpdraftPlus_Addons_Migrator')) {
3985 $warn[] .= '<strong><a href="'.apply_filters('updraftplus_com_link', "https://updraftplus.com/faqs/tell-me-more-about-the-search-and-replace-site-location-in-the-database-option/").'" target="_blank">'.__('You can search and replace your database (for migrating a website to a new location/URL) with the Migrator add-on - follow this link for more information', 'updraftplus').'</a></strong>';
3986 }
3987 }
3988
3989 if ($this->mod_rewrite_unavailable(false)) {
3990 $warn[] = sprintf(__('You are using the %s webserver, but do not seem to have the %s module loaded.', 'updraftplus'), 'Apache', 'mod_rewrite').' '.sprintf(__('You should enable %s to make any pretty permalinks (e.g. %s) work', 'updraftplus'), 'mod_rewrite', 'http://example.com/my-page/');
3991 }
3992
3993 } else {
3994 // For exactly same URL site restoration
3995 $info['same_url'] = true;
3996 }
3997 } elseif ('' == $old_home && preg_match('/^\# Home URL: (http(.*))$/', $buffer, $matches)) {
3998 $old_home = untrailingslashit($matches[1]);
3999 // Check for should-be migration
4000 if (!$migration_warning && UpdraftPlus_Manipulation_Functions::normalise_url(home_url()) != UpdraftPlus_Manipulation_Functions::normalise_url($old_home)) {
4001 $migration_warning = true;
4002 $powarn = apply_filters('updraftplus_dbscan_urlchange', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.sprintf(__('This backup set is from a different site (%s) - this is not a restoration, but a migration. You need the Migrator add-on in order to make this work.', 'updraftplus'), htmlspecialchars($old_home.' / '.home_url())).'</a>', $old_home, $res);
4003 if (!empty($powarn)) $warn[] = $powarn;
4004 }
4005 } elseif (!isset($info['created_by_version']) && preg_match('/^\# Created by UpdraftPlus version ([\d\.]+)/', $buffer, $matches)) {
4006 $info['created_by_version'] = trim($matches[1]);
4007 } elseif ('' == $old_wp_version && preg_match('/^\# WordPress Version: ([0-9]+(\.[0-9]+)+)(-[-a-z0-9]+,)?(.*)$/', $buffer, $matches)) {
4008 $old_wp_version = $matches[1];
4009 if (!empty($matches[3])) $old_wp_version .= substr($matches[3], 0, strlen($matches[3])-1);
4010 if (version_compare($old_wp_version, $wp_version, '>')) {
4011 // $mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
4012 $warn[] = sprintf(__('You are importing from a newer version of WordPress (%s) into an older one (%s). There are no guarantees that WordPress can handle this.', 'updraftplus'), $old_wp_version, $wp_version);
4013 }
4014 if (preg_match('/running on PHP ([0-9]+\.[0-9]+)(\s|\.)/', $matches[4], $nmatches) && preg_match('/^([0-9]+\.[0-9]+)(\s|\.)/', PHP_VERSION, $cmatches)) {
4015 $old_php_version = $nmatches[1];
4016 $current_php_version = $cmatches[1];
4017 if (version_compare($old_php_version, $current_php_version, '>')) {
4018 // $mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
4019 $warn[] = sprintf(__('The site in this backup was running on a webserver with version %s of %s. ', 'updraftplus'), $old_php_version, 'PHP').' '.sprintf(__('This is significantly newer than the server which you are now restoring onto (version %s).', 'updraftplus'), PHP_VERSION).' '.sprintf(__('You should only proceed if you cannot update the current server and are confident (or willing to risk) that your plugins/themes/etc. are compatible with the older %s version.', 'updraftplus'), 'PHP').' '.sprintf(__('Any support requests to do with %s should be raised with your web hosting company.', 'updraftplus'), 'PHP');
4020 }
4021 }
4022 } elseif ('' == $old_table_prefix && (preg_match('/^\# Table prefix: (\S+)$/', $buffer, $matches) || preg_match('/^-- Table prefix: (\S+)$/i', $buffer, $matches))) {
4023 $old_table_prefix = $matches[1];
4024 // echo '<strong>'.__('Old table prefix:', 'updraftplus').'</strong> '.htmlspecialchars($old_table_prefix).'<br>';
4025 } elseif (empty($info['label']) && preg_match('/^\# Label: (.*)$/', $buffer, $matches)) {
4026 $info['label'] = $matches[1];
4027 $mess[] = __('Backup label:', 'updraftplus').' '.htmlspecialchars($info['label']);
4028 } elseif ($gathering_siteinfo && preg_match('/^\# Site info: (\S+)$/', $buffer, $matches)) {
4029 if ('end' == $matches[1]) {
4030 $gathering_siteinfo = false;
4031 // Sanity checks
4032 if (isset($old_siteinfo['multisite']) && !$old_siteinfo['multisite'] && is_multisite()) {
4033 // Just need to check that you're crazy
4034 // if (!defined('UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE') || !UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE) {
4035 // $err[] = sprintf(__('Error: %s', 'updraftplus'), __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus'));
4036 // return array($mess, $warn, $err, $info);
4037 // } else {
4038 $warn[] = __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus').' '.__('It will be imported as a new site.', 'updraftplus').' <a href="https://updraftplus.com/information-on-importing-a-single-site-wordpress-backup-into-a-wordpress-network-i-e-multisite/" target="_blank">'.__('Please read this link for important information on this process.', 'updraftplus').'</a>';
4039 // }
4040 // Got the needed code?
4041 if (!class_exists('UpdraftPlusAddOn_MultiSite') || !class_exists('UpdraftPlus_Addons_Migrator')) {
4042 $err[] = sprintf(__('Error: %s', 'updraftplus'), sprintf(__('To import an ordinary WordPress site into a multisite installation requires %s.', 'updraftplus'), 'UpdraftPlus Premium'));
4043 return array($mess, $warn, $err, $info);
4044 }
4045 } elseif (isset($old_siteinfo['multisite']) && $old_siteinfo['multisite'] && !is_multisite()) {
4046 $warn[] = __('Warning:', 'updraftplus').' '.__('Your backup is of a WordPress multisite install; but this site is not. Only the first site of the network will be accessible.', 'updraftplus').' <a href="https://codex.wordpress.org/Create_A_Network" target="_blank">'.__('If you want to restore a multisite backup, you should first set up your WordPress installation as a multisite.', 'updraftplus').'</a>';
4047 }
4048 } elseif (preg_match('/^([^=]+)=(.*)$/', $matches[1], $kvmatches)) {
4049 $key = $kvmatches[1];
4050 $val = $kvmatches[2];
4051 if ('multisite' == $key) {
4052 $info['multisite'] = $val ? true : false;
4053 if ($val) $mess[] = '<strong>'.__('Site information:', 'updraftplus').'</strong> '.'backup is of a WordPress Network';
4054 }
4055 $old_siteinfo[$key] = $val;
4056 }
4057 } elseif (preg_match('/^\# Skipped tables: (.*)$/', $buffer, $matches)) {
4058 $skipped_tables = explode(',', $matches[1]);
4059 }
4060
4061 } elseif (preg_match('#^\s*/\*\!40\d+ SET NAMES (.*)\*\/#i', $buffer, $smatches)) {
4062 $db_charsets_found[] = rtrim($smatches[1]);
4063 } elseif (preg_match('/^\s*create table \`?([^\`\(]*)\`?\s*\(/i', $buffer, $matches)) {
4064 $table = $matches[1];
4065 $tables_found[] = $table;
4066 if ($old_table_prefix) {
4067 // Remove prefix
4068 $table = UpdraftPlus_Manipulation_Functions::str_replace_once($old_table_prefix, '', $table);
4069 if (in_array($table, $wanted_tables)) {
4070 $wanted_tables = array_diff($wanted_tables, array($table));
4071 }
4072 }
4073 if (';' != substr($buffer, -1, 1)) {
4074 $processing_create = true;
4075 $db_supported_charset_related_to_unsupported_collation = true;
4076 }
4077 } elseif ($processing_create) {
4078 if (!empty($db_supported_collations)) {
4079 if (preg_match('/ COLLATE=([^\s;]+)/i', $buffer, $collate_match)) {
4080 $db_collates_found[] = $collate_match[1];
4081 if (!isset($db_supported_collations[$collate_match[1]])) {
4082 $db_supported_charset_related_to_unsupported_collation = true;
4083 }
4084 }
4085 if (preg_match('/ COLLATE ([a-zA-Z0-9._-]+),/i', $buffer, $collate_match)) {
4086 $db_collates_found[] = $collate_match[1];
4087 if (!isset($db_supported_collations[$collate_match[1]])) {
4088 $db_supported_charset_related_to_unsupported_collation = true;
4089 }
4090 }
4091 if (preg_match('/ COLLATE ([a-zA-Z0-9._-]+) /i', $buffer, $collate_match)) {
4092 $db_collates_found[] = $collate_match[1];
4093 if (!isset($db_supported_collations[$collate_match[1]])) {
4094 $db_supported_charset_related_to_unsupported_collation = true;
4095 }
4096 }
4097 }
4098 if (!empty($db_supported_character_sets)) {
4099 if (preg_match('/ CHARSET=([^\s;]+)/i', $buffer, $charset_match)) {
4100 $db_charsets_found[] = $charset_match[1];
4101 if ($db_supported_charset_related_to_unsupported_collation && !in_array($charset_match[1], $db_supported_charsets_related_to_unsupported_collations)) {
4102 $db_supported_charsets_related_to_unsupported_collations[] = $charset_match[1];
4103 }
4104 }
4105 }
4106 if (';' == substr($buffer, -1, 1)) {
4107 $processing_create = false;
4108 $db_supported_charset_related_to_unsupported_collation = false;
4109 }
4110 static $mysql_version_warned = false;
4111 if (!$mysql_version_warned && version_compare($db_version, '5.2.0', '<') && preg_match('/(CHARSET|COLLATE)[= ]utf8mb4/', $buffer)) {
4112 $mysql_version_warned = true;
4113 $err[] = sprintf(__('Error: %s', 'updraftplus'), sprintf(__('The database backup uses MySQL features not available in the old MySQL version (%s) that this site is running on.', 'updraftplus'), $db_version).' '.__('You must upgrade MySQL to be able to use this database.', 'updraftplus'));
4114 }
4115 }
4116 }
4117 if ($is_plain) {
4118 @fclose($dbhandle);
4119 } else {
4120 @gzclose($dbhandle);
4121 }
4122 if (!empty($db_supported_character_sets)) {
4123 $db_charsets_found_unique = array_unique($db_charsets_found);
4124 $db_unsupported_charset = array();
4125 $db_charset_forbidden = false;
4126 foreach ($db_charsets_found_unique as $db_charset) {
4127 if (!isset($db_supported_character_sets[$db_charset])) {
4128 $db_unsupported_charset[] = $db_charset;
4129 $db_charset_forbidden = true;
4130 }
4131 }
4132 if ($db_charset_forbidden) {
4133 $db_unsupported_charset_unique = array_unique($db_unsupported_charset);
4134 $warn[] = sprintf(_n("The database server that this WordPress site is running on doesn't support the character set (%s) which you are trying to import.", "The database server that this WordPress site is running on doesn't support the character sets (%s) which you are trying to import.", count($db_unsupported_charset_unique), 'updraftplus'), implode(', ', $db_unsupported_charset_unique)).' '.__('You can choose another suitable character set instead and continue with the restoration at your own risk.', 'updraftplus').' <a target="_blank" href="https://updraftplus.com/faqs/implications-changing-tables-character-set/" target="_blank">'.__('Go here for more information.', 'updraftplus').'</a>'.' <a target="_blank" href="https://updraftplus.com/faqs/implications-changing-tables-character-set/" target="_blank">'.__('Go here for more information.', 'updraftplus').'</a>';
4135 $db_supported_character_sets = array_keys($db_supported_character_sets);
4136 $similar_type_charset = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_charset_unique, $db_supported_character_sets, true);
4137 if (empty($similar_type_charset)) {
4138 $row = $GLOBALS['wpdb']->get_row('show variables like "character_set_database"');
4139 $similar_type_charset = (null !== $row) ? $row->Value : '';
4140 }
4141 if (empty($similar_type_charset) && !empty($db_supported_character_sets[0])) {
4142 $similar_type_charset = $db_supported_character_sets[0];
4143 }
4144 $charset_select_html = '<label>'.__('Your chosen character set to use instead:', 'updraftplus').'</label> ';
4145 $charset_select_html .= '<select name="updraft_restorer_charset" id="updraft_restorer_charset">';
4146 if (is_array($db_supported_character_sets)) {
4147 foreach ($db_supported_character_sets as $character_set) {
4148 $charset_select_html .= '<option value="'.esc_attr($character_set).'" '.selected($character_set, $similar_type_charset, false).'>'.esc_html($character_set).'</option>';
4149 }
4150 }
4151 $charset_select_html .= '</select>';
4152 if (empty($info['addui'])) $info['addui'] = '';
4153 $info['addui'] .= $charset_select_html;
4154 }
4155 }
4156 if (!empty($db_supported_collations)) {
4157 $db_collates_found_unique = array_unique($db_collates_found);
4158 $db_unsupported_collate = array();
4159 $db_collate_forbidden = false;
4160 foreach ($db_collates_found_unique as $db_collate) {
4161 if (!isset($db_supported_collations[$db_collate])) {
4162 $db_unsupported_collate[] = $db_collate;
4163 $db_collate_forbidden = true;
4164 }
4165 }
4166 if ($db_collate_forbidden) {
4167 $db_unsupported_collate_unique = array_unique($db_unsupported_collate);
4168 $warn[] = sprintf(_n("The database server that this WordPress site is running on doesn't support the collation (%s) used in the database which you are trying to import.", "The database server that this WordPress site is running on doesn't support multiple collations (%s) used in the database which you are trying to import.", count($db_unsupported_collate_unique), 'updraftplus'), implode(', ', $db_unsupported_collate_unique)).' '.__('You can choose another suitable collation instead and continue with the restoration (at your own risk).', 'updraftplus');
4169 $similar_type_collate = '';
4170 if ($db_charset_forbidden && !empty($similar_type_charset)) {
4171 $similar_type_collate = $this->get_similar_collate_related_to_charset($db_supported_collations, $db_unsupported_collate_unique, $similar_type_charset);
4172 }
4173 if (empty($similar_type_collate) && !empty($db_supported_charsets_related_to_unsupported_collations)) {
4174 $db_supported_collations_related_to_charset = array();
4175 foreach ($db_supported_collations as $db_supported_collation => $db_supported_collations_info_obj) {
4176 if (isset($db_supported_collations_info_obj->Charset) && in_array($db_supported_collations_info_obj->Charset, $db_supported_charsets_related_to_unsupported_collations)) {
4177 $db_supported_collations_related_to_charset[] = $db_supported_collation;
4178 }
4179 }
4180 if (!empty($db_supported_collations_related_to_charset)) {
4181 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, $db_supported_collations_related_to_charset, false);
4182 }
4183 }
4184 if (empty($similar_type_collate)) {
4185 $similar_type_collate = $this->get_similar_collate_based_on_ocuurence_count($db_collates_found, $db_supported_collations, $db_supported_charsets_related_to_unsupported_collations);
4186 }
4187 if (empty($similar_type_collate)) {
4188 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, array_keys($db_supported_collations), false);
4189 }
4190
4191 $collate_select_html = '<label>'.__('Your chosen replacement collation', 'updraftplus').':</label>';
4192 $collate_select_html .= '<select name="updraft_restorer_collate" id="updraft_restorer_collate">';
4193 $db_charsets_found_unique = array_unique($db_charsets_found);
4194 foreach ($db_supported_collations as $collate => $collate_info_obj) {
4195 $option_other_attr = array();
4196 if ($db_charset_forbidden && isset($collate_info_obj->Charset)) {
4197 $option_other_attr[] = 'data-charset='.esc_attr($collate_info_obj->Charset);
4198 if ($similar_type_charset != $collate_info_obj->Charset) {
4199 $option_other_attr[] = 'style="display:none;"';
4200 }
4201 } else {
4202 if (1 == count($db_charsets_found_unique)) {
4203 if (!in_array($collate_info_obj->Charset, $db_charsets_found_unique)) {
4204 $option_other_attr[] = 'style="display:none;"';
4205 }
4206 } else {
4207 $option_other_attr[] = 'style="display:none;"';
4208 }
4209 }
4210 $collate_select_html .= '<option value="'.esc_attr($collate).'" '.selected($collate, $similar_type_collate, $echo = false).' '.implode(' ', $option_other_attr).'>'.esc_html($collate).'</option>';
4211 }
4212
4213 if (count($db_charsets_found_unique) > 1 && !$db_charset_forbidden) {
4214 $collate_select_html .= '<option value="choose_a_default_for_each_table" selected="selected">'.__('Choose a default for each table', 'updraftplus').'</option>';
4215 }
4216 $collate_select_html .= '</select>';
4217
4218 $info['addui'] = empty($info['addui']) ? $collate_select_html : $info['addui'].'<br>'.$collate_select_html;
4219
4220 if ($db_charset_forbidden) {
4221 $collate_change_on_charset_selection_data = array(
4222 'db_supported_collations' => $db_supported_collations,
4223 'db_unsupported_collate_unique' => $db_unsupported_collate_unique,
4224 'db_collates_found' => $db_collates_found,
4225 );
4226 $info['addui'] .= '<input type="hidden" name="collate_change_on_charset_selection_data" id="collate_change_on_charset_selection_data" value="'.esc_attr(json_encode($collate_change_on_charset_selection_data)).'">';
4227 }
4228 }
4229 }
4230 /* $blog_tables = "CREATE TABLE $wpdb->terms (
4231 CREATE TABLE $wpdb->term_taxonomy (
4232 CREATE TABLE $wpdb->term_relationships (
4233 CREATE TABLE $wpdb->commentmeta (
4234 CREATE TABLE $wpdb->comments (
4235 CREATE TABLE $wpdb->links (
4236 CREATE TABLE $wpdb->options (
4237 CREATE TABLE $wpdb->postmeta (
4238 CREATE TABLE $wpdb->posts (
4239 $users_single_table = "CREATE TABLE $wpdb->users (
4240 $users_multi_table = "CREATE TABLE $wpdb->users (
4241 $usermeta_table = "CREATE TABLE $wpdb->usermeta (
4242 $ms_global_tables = "CREATE TABLE $wpdb->blogs (
4243 CREATE TABLE $wpdb->blog_versions (
4244 CREATE TABLE $wpdb->registration_log (
4245 CREATE TABLE $wpdb->site (
4246 CREATE TABLE $wpdb->sitemeta (
4247 CREATE TABLE $wpdb->signups (
4248 */
4249 if (!isset($skipped_tables)) $skipped_tables = array();
4250 $missing_tables = array();
4251 if ($old_table_prefix) {
4252 if (!$header_only) {
4253 foreach ($wanted_tables as $table) {
4254 if (!in_array($old_table_prefix.$table, $tables_found)) {
4255 $missing_tables[] = $table;
4256 }
4257 }
4258
4259 foreach ($missing_tables as $key => $value) {
4260 if (in_array($old_table_prefix.$value, $skipped_tables)) {
4261 unset($missing_tables[$key]);
4262 }
4263 }
4264
4265 if (count($missing_tables)>0) {
4266 $warn[] = sprintf(__('This database backup is missing core WordPress tables: %s', 'updraftplus'), implode(', ', $missing_tables));
4267 }
4268 if (count($skipped_tables)>0) {
4269 $warn[] = sprintf(__('This database backup has the following WordPress tables excluded: %s', 'updraftplus'), implode(', ', $skipped_tables));
4270 }
4271 }
4272 } else {
4273 if (empty($backup['meta_foreign'])) {
4274 $warn[] = __('UpdraftPlus was unable to find the table prefix when scanning the database backup.', 'updraftplus');
4275 }
4276 }
4277
4278 // //need to make sure that we reset the file back to .crypt before clean temp files
4279 // $db_file = $decrypted_file['fullpath'].'.crypt';
4280 // unlink($decrypted_file['fullpath']);
4281
4282 return array($mess, $warn, $err, $info);
4283 }
4284
4285 /**
4286 * Get the current outgoing IP address. Use this wisely; of course, it's not guaranteed to always be the same.
4287 *
4288 * @return String|Boolean - returns false upon failure
4289 */
4290 public function get_outgoing_ip_address() {
4291 $ip_lookup = wp_remote_get('https://ipvigilante.com/json', array('timeout' => 6));
4292 if (200 == wp_remote_retrieve_response_code($ip_lookup)) {
4293 $info = json_decode(wp_remote_retrieve_body($ip_lookup), true);
4294 if (!empty($info['status']) && !empty($info['data']) && 'success' === $info['status']);
4295 if (!empty($info['data']['ipv4'])) return $info['data']['ipv4'];
4296 if (!empty($info['data']['ipv6'])) return $info['data']['ipv6'];
4297 }
4298 return false;
4299 }
4300
4301 /**
4302 * Get default substitute similar collate related to charset
4303 *
4304 * @param array $db_supported_collations Supported collations. It should contain result of 'SHOW COLLATION' query
4305 * @param array $db_unsupported_collate_unique Unsupported unique collates collection
4306 * @param string $similar_type_charset Charset for which need to get default collate substitution
4307 * @return string $similar_type_collate default substitute collate which is best suitable or blank string
4308 */
4309 public function get_similar_collate_related_to_charset($db_supported_collations, $db_unsupported_collate_unique, $similar_type_charset) {
4310 $similar_type_collate = '';
4311 $db_supported_collations_related_to_charset = array();
4312 foreach ($db_supported_collations as $db_supported_collation => $db_supported_collations_info_obj) {
4313 if (isset($db_supported_collations_info_obj->Charset) && $db_supported_collations_info_obj->Charset == $similar_type_charset) {
4314 $db_supported_collations_related_to_charset[] = $db_supported_collation;
4315 }
4316 }
4317 if (!empty($db_supported_collations_related_to_charset)) {
4318 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, $db_supported_collations_related_to_charset, false);
4319 }
4320 return $similar_type_collate;
4321 }
4322
4323 /**
4324 * Get default substitute similar collate based on existing supported collates count in database backup file
4325 *
4326 * @param array $db_collates_found All collates which have found in database backup file regardless whether they are supported or unsupported
4327 * @param array $db_supported_collations Supported collations. It should contain result of 'SHOW COLLATION' query
4328 * @param array $db_supported_charsets_related_to_unsupported_collations All charset which are related to unsupported collation
4329 *
4330 * @return string $similar_type_collate default substitute collate which is best suitable or blank string
4331 */
4332 public function get_similar_collate_based_on_ocuurence_count($db_collates_found, $db_supported_collations, $db_supported_charsets_related_to_unsupported_collations) {
4333 $similar_type_collate = '';
4334 $db_supported_collates_found_with_occurrence = array();
4335 foreach ($db_collates_found as $db_collate_found) {
4336 if (isset($db_supported_collations[$db_collate_found])) {
4337 if (isset($db_supported_collates_found_with_occurrence[$db_collate_found])) {
4338 $db_supported_collates_found_with_occurrence[$db_collate_found] = intval($db_supported_collates_found_with_occurrence[$db_collate_found]) + 1;
4339 } else {
4340 $db_supported_collates_found_with_occurrence[$db_collate_found] = 1;
4341 }
4342 }
4343 }
4344 if (!empty($db_supported_collates_found_with_occurrence)) {
4345 arsort($db_supported_collates_found_with_occurrence);
4346 if (!empty($db_supported_charsets_related_to_unsupported_collations)) {
4347 foreach ($db_supported_collates_found_with_occurrence as $db_supported_collate_with_occurrence => $occurrence_count) {
4348 if (isset($db_supported_collations[$db_supported_collate_with_occurrence]) && isset($db_supported_collations[$db_supported_collate_with_occurrence]->Charset) && in_array($db_supported_collations[$db_supported_collate_with_occurrence]->Charset, $db_supported_charsets_related_to_unsupported_collations)) {
4349 $similar_type_collate = $db_supported_collate_with_occurrence;
4350 break;
4351 }
4352 }
4353 } else {
4354 $similar_type_collate = array_search(max($db_supported_collates_found_with_occurrence), $db_supported_collates_found_with_occurrence);
4355 }
4356 }
4357 return $similar_type_collate;
4358 }
4359
4360 /**
4361 * Retrieves current clean url for anchor link where href attribute value is not url (for ex. #div) or empty
4362 *
4363 * @return String - current clean url
4364 */
4365 public static function get_current_clean_url() {
4366
4367 // Within an UpdraftCentral context, there should be no prefix on the anchor link
4368 if (defined('UPDRAFTCENTRAL_COMMAND') && UPDRAFTCENTRAL_COMMAND) return '';
4369
4370 if (defined('DOING_AJAX') && DOING_AJAX) {
4371 $current_url = $_SERVER["HTTP_REFERER"];
4372 } else {
4373 $url_prefix = is_ssl() ? 'https' : 'http';
4374 $current_url = $url_prefix."://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
4375 }
4376 $remove_query_args = array('state', 'action', 'oauth_verifier', 'nonce', 'updraftplus_instance', 'access_token', 'user_id', 'updraftplus_googledriveauth');
4377
4378 return UpdraftPlus_Manipulation_Functions::wp_unslash(remove_query_arg($remove_query_args, $current_url));
4379 }
4380
4381 /**
4382 * TODO: Remove legacy storage setting keys from here
4383 * These are used in 4 places (Feb 2016 - of course, you should re-scan the code to check if relying on this): showing current settings on the debug modal, wiping all current settings, getting a settings bundle to restore when migrating, and for relevant keys in POST-ed data when saving settings over AJAX
4384 *
4385 * @return Array - the list of keys
4386 */
4387 public function get_settings_keys() {
4388 // N.B. updraft_backup_history is not included here, as we don't want that wiped
4389 return array(
4390 'updraft_autobackup_default',
4391 'updraft_dropbox',
4392 'updraft_googledrive',
4393 'updraftplus_tmp_googledrive_access_token',
4394 'updraftplus_dismissedautobackup',
4395 'dismissed_general_notices_until',
4396 'dismissed_season_notices_until',
4397 'updraftplus_dismissedexpiry',
4398 'updraftplus_dismisseddashnotice',
4399 'updraft_interval',
4400 'updraft_interval_increments',
4401 'updraft_interval_database',
4402 'updraft_retain',
4403 'updraft_retain_db',
4404 'updraft_encryptionphrase',
4405 'updraft_service',
4406 'updraft_googledrive_clientid',
4407 'updraft_googledrive_secret',
4408 'updraft_googledrive_remotepath',
4409 'updraft_ftp',
4410 'updraft_backblaze',
4411 'updraft_server_address',
4412 'updraft_dir',
4413 'updraft_email',
4414 'updraft_delete_local',
4415 'updraft_debug_mode',
4416 'updraft_include_plugins',
4417 'updraft_include_themes',
4418 'updraft_include_uploads',
4419 'updraft_include_others',
4420 'updraft_include_wpcore',
4421 'updraft_include_wpcore_exclude',
4422 'updraft_include_more',
4423 'updraft_include_blogs',
4424 'updraft_include_mu-plugins',
4425 'updraft_auto_updates',
4426 'updraft_include_others_exclude',
4427 'updraft_include_uploads_exclude',
4428 'updraft_lastmessage',
4429 'updraft_googledrive_token',
4430 'updraft_dropboxtk_request_token',
4431 'updraft_dropboxtk_access_token',
4432 'updraft_adminlocking',
4433 'updraft_updraftvault',
4434 'updraft_remotesites',
4435 'updraft_migrator_localkeys',
4436 'updraft_central_localkeys',
4437 'updraft_retain_extrarules',
4438 'updraft_googlecloud',
4439 'updraft_include_more_path',
4440 'updraft_split_every',
4441 'updraft_ssl_nossl',
4442 'updraft_backupdb_nonwp',
4443 'updraft_extradbs',
4444 'updraft_combine_jobs_around',
4445 'updraft_last_backup',
4446 'updraft_starttime_files',
4447 'updraft_starttime_db',
4448 'updraft_startday_db',
4449 'updraft_startday_files',
4450 'updraft_sftp',
4451 'updraft_s3',
4452 'updraft_s3generic',
4453 'updraft_dreamhost',
4454 'updraft_s3generic_login',
4455 'updraft_s3generic_pass',
4456 'updraft_s3generic_remote_path',
4457 'updraft_s3generic_endpoint',
4458 'updraft_webdav',
4459 'updraft_openstack',
4460 'updraft_onedrive',
4461 'updraft_azure',
4462 'updraft_cloudfiles',
4463 'updraft_cloudfiles_user',
4464 'updraft_cloudfiles_apikey',
4465 'updraft_cloudfiles_path',
4466 'updraft_cloudfiles_authurl',
4467 'updraft_ssl_useservercerts',
4468 'updraft_ssl_disableverify',
4469 'updraft_s3_login',
4470 'updraft_s3_pass',
4471 'updraft_s3_remote_path',
4472 'updraft_dreamobjects_login',
4473 'updraft_dreamobjects_pass',
4474 'updraft_dreamobjects_remote_path',
4475 'updraft_dreamobjects',
4476 'updraft_report_warningsonly',
4477 'updraft_report_wholebackup',
4478 'updraft_report_dbbackup',
4479 'updraft_log_syslog',
4480 'updraft_extradatabases',
4481 'updraftplus_tour_cancelled_on',
4482 'updraftplus_version',
4483 );
4484 }
4485
4486 /**
4487 * A function that works through the array passed to it and gets a list of all the tables from that database and puts the information in an array ready to be parsed and output to html.
4488 *
4489 * @param Array $dbsinfo an array that contains information about each database, the default 'wp' array is just an empty array, but other entries can be added so that this method can get tables from other databases the array structure for this would be array('wp' => array(), 'TestDB' => array('host' => '', 'user' => '', 'pass' => '', 'name' => '', 'prefix' => ''))
4490 * note that the extra tables array key must match the database name in the array note that the extra tables array key must match the database name in the array
4491 * @return Array - databases and their table names
4492 */
4493 public function get_database_tables($dbsinfo = array('wp' => array())) {
4494
4495 global $wpdb;
4496
4497 if (!class_exists('UpdraftPlus_Database_Utility')) include_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
4498
4499 $dbhandle = '';
4500 $db_tables_array = array();
4501
4502 foreach ($dbsinfo as $key => $value) {
4503 if ('wp' == $key) {
4504 // The table prefix after being filtered - i.e. what filters what we'll actually backup
4505 $table_prefix = $this->get_table_prefix(true);
4506 // The unfiltered table prefix - i.e. the real prefix that things are relative to
4507 $table_prefix_raw = $this->get_table_prefix(false);
4508 $dbinfo['host'] = DB_HOST;
4509 $dbinfo['name'] = DB_NAME;
4510 $dbinfo['user'] = DB_USER;
4511 $dbinfo['pass'] = DB_PASSWORD;
4512 $dbhandle = $wpdb;
4513 } else {
4514 $dbhandle = new UpdraftPlus_WPDB_OtherDB_Utility($dbsinfo[$key]['user'], $dbsinfo[$key]['pass'], $dbsinfo[$key]['name'], $dbsinfo[$key]['host']);
4515 if (!empty($dbhandle->error)) {
4516 return $this->log_wp_error($dbhandle->error);
4517 }
4518 $table_prefix = $dbsinfo[$key]['prefix'];
4519 $table_prefix_raw = $dbsinfo[$key]['prefix'];
4520 }
4521
4522 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
4523 $all_tables = $dbhandle->get_results("SHOW FULL TABLES", ARRAY_N);
4524
4525 if (empty($all_tables) && !empty($dbhandle->last_error)) {
4526 $all_tables = $dbhandle->get_results("SHOW TABLES", ARRAY_N);
4527 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
4528 } else {
4529 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
4530 }
4531
4532 // If this is not the WP database, then we do not consider it a fatal error if there are no tables
4533 if ('wp' == $key && 0 == count($all_tables)) {
4534 return $this->log_wp_error("No tables found in wp database.");
4535 die;
4536 }
4537
4538 // Put the options table first
4539 $updraftplus_database_utility = new UpdraftPlus_Database_Utility($key, $table_prefix_raw, $dbhandle);
4540 usort($all_tables, array($updraftplus_database_utility, 'backup_db_sorttables'));
4541
4542 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
4543 $db_tables_array[$key] = $all_table_names;
4544 }
4545
4546 return $db_tables_array;
4547 }
4548
4549 /**
4550 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
4551 *
4552 * @param Array $a - the array
4553 *
4554 * @return Array - with keys 'name' and 'type'
4555 */
4556 private function cb_get_name_base_type($a) {
4557 return array('name' => $a[0], 'type' => 'BASE TABLE');
4558 }
4559
4560 /**
4561 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
4562 *
4563 * @param Array $a - the array
4564 *
4565 * @return Array - keys are 'name' and 'type'
4566 */
4567 private function cb_get_name_type($a) {
4568 return array('name' => $a[0], 'type' => $a[1]);
4569 }
4570
4571 /**
4572 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
4573 *
4574 * @param Array $a - the array
4575 *
4576 * @return Mixed - the value with key (string)'name'
4577 */
4578 private function cb_get_name($a) {
4579 return $a['name'];
4580 }
4581
4582 /**
4583 * Retrieves the appropriate URL for the given target page
4584 *
4585 * @internal
4586 * @param String $which_page The target page
4587 * @return String - The requested URL for a given page
4588 */
4589 public function get_url($which_page = false) {
4590 switch ($which_page) {
4591 case 'my-account':
4592 return apply_filters('updraftplus_com_myaccount', 'https://updraftplus.com/my-account/');
4593 break;
4594 case 'shop':
4595 return apply_filters('updraftplus_com_shop', 'https://updraftplus.com/shop/');
4596 break;
4597 case 'premium':
4598 return apply_filters('updraftplus_com_premium', 'https://updraftplus.com/shop/updraftplus-premium/');
4599 break;
4600 case 'buy-tokens':
4601 return apply_filters('updraftplus_com_updraftclone_tokens', 'https://updraftplus.com/shop/updraftclone-tokens/');
4602 break;
4603 case 'lost-password':
4604 return apply_filters('updraftplus_com_myaccount_lostpassword', 'https://updraftplus.com/my-account/lost-password/');
4605 break;
4606 case 'mothership':
4607 return apply_filters('updraftplus_com_mothership', 'https://updraftplus.com/plugin-info');
4608 break;
4609 default:
4610 return 'URL not found ('.$which_page.')';
4611 }
4612 }
4613
4614 /**
4615 * Get log message for permission failure
4616 *
4617 * @param string $path full path of file or folder
4618 * @param string $log_message_prefix action which is performed to path
4619 * @param string $directory_prefix_in_log_message Directory Prefix. It should be either "Parent" or "Destination"
4620 * @return string|boolean log message (HTML). If posix function doesn't exist, It returns false
4621 */
4622 public function log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message = 'Parent') {
4623 if ($this->do_posix_functions_exist()) {
4624 $stat_data = stat($path);
4625 $log_message = $log_message_prefix.': Failed. ';
4626 $log_message .= $directory_prefix_in_log_message.' Directory UID='.$stat_data['uid'].', GID='.$stat_data['gid'].'. ';
4627 $log_message .= $this->get_log_message_for_current_uid_and_gid();
4628 return $log_message;
4629 } else {
4630 return false;
4631 }
4632 }
4633
4634 /**
4635 * Get log message for current uid and gid
4636 *
4637 * @return String log message of current process (HTML)
4638 */
4639 private function get_log_message_for_current_uid_and_gid() {
4640 $log_message = 'Effective/real user IDs of the current process: '.posix_geteuid().'/'.posix_getuid().'. ';
4641 $log_message .= 'Effective/real group IDs of the current process: '.posix_getegid().'/'.posix_getgid().'. ';
4642 return $log_message;
4643 }
4644
4645 /**
4646 * Checks whether POSIX functions exists or not
4647 *
4648 * @return boolean true if POSIX functions exists or not
4649 */
4650 private function do_posix_functions_exist() {
4651 return function_exists('posix_geteuid') && function_exists('posix_getuid') && function_exists('posix_getegid') && function_exists('posix_getgid');
4652 }
4653
4654 /**
4655 * Checks whether debug mode is on or not. If it is on then unminified script will be used.
4656 *
4657 * @return boolean true indicate use the unminified script
4658 */
4659 public function use_unminified_scripts() {
4660 return UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') || (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG);
4661 }
4662 }
4663