PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.13.9
UpdraftPlus: WP Backup & Migration Plugin v1.13.9
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.13.9, at class-updraftplus.php

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