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

6,174 lines 278.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 $file_nonce;
36
37 public $logfile_name = "";
38
39 public $logfile_handle = false;
40
41 public $backup_time;
42
43 public $job_time_ms;
44
45 public $opened_log_time;
46
47 private $backup_dir;
48
49 private $jobdata;
50
51 public $something_useful_happened = false;
52
53 public $have_addons = false;
54
55 // Used to schedule resumption attempts beyond the tenth, if needed
56 public $current_resumption;
57
58 public $last_successful_resumption;
59
60 public $newresumption_scheduled = false;
61
62 public $resumption_scheduled_for_cleanup = false;
63
64 public $cpanel_quota_readable = false;
65
66 public $error_reporting_stop_when_logged = false;
67
68 private $combine_jobs_around;
69
70 // Used for reporting
71 private $attachments;
72
73 private $remotestorage_extrainfo = array();
74
75 public $no_checkin_last_time;
76
77 private $removed_autoloaders = array();
78
79 private $no_deprecation_warnings = false;
80
81 private $backup_is_already_complete = false;
82
83 private $error_count_before_cloud_backup = 0;
84
85 private $semaphore;
86
87 private $backup_semaphore;
88
89 /**
90 * Class constructor
91 */
92 public function __construct() {
93 global $pagenow;
94 // Initialisation actions - takes place on plugin load
95
96 if ($fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
97 $file_data = fread($fp, 1024);
98 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
99 $this->version = $matches[1];
100 }
101 fclose($fp);
102 }
103
104 $load_classes = array(
105 'UpdraftPlus_Backup_History' => 'includes/class-backup-history.php',
106 'UpdraftPlus_Encryption' => 'includes/class-updraftplus-encryption.php',
107 'UpdraftPlus_Manipulation_Functions' => 'includes/class-manipulation-functions.php',
108 'UpdraftPlus_Filesystem_Functions' => 'includes/class-filesystem-functions.php',
109 'UpdraftPlus_Storage_Methods_Interface' => 'includes/class-storage-methods-interface.php',
110 'UpdraftPlus_Job_Scheduler' => 'includes/class-job-scheduler.php',
111 'UpdraftPlus_HTTP_Error_Descriptions' => 'includes/class-http-error-descriptions.php',
112 );
113
114 foreach ($load_classes as $class => $relative_path) {
115 if (!class_exists($class)) include_once(UPDRAFTPLUS_DIR.'/'.$relative_path);
116 }
117
118 // Create admin page
119 add_action('init', array($this, 'handle_url_actions'));
120 add_action('init', array($this, 'updraftplus_single_site_maintenance_init'));
121 // Run earlier than default - hence earlier than other components
122 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
123 add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
124 // Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
125 add_action('admin_init', array($this, 'admin_menu'), 9);
126 add_action('admin_init', array($this, 'wordpress_55_updates_potential_migration'));
127
128 // The two actions which we schedule upon
129 add_action('updraft_backup', array($this, 'backup_files'));
130 add_action('updraft_backup_database', array($this, 'backup_database'));
131
132 // The three actions that can be called from "Backup Now"
133 add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
134 add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
135 add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
136
137 // backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
138 add_action('updraft_backup_all', array($this, 'backup_all'));
139
140 // This is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
141 add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
142
143 // If files + db are on different schedules but are scheduled for the same time, then combine them
144 add_filter('schedule_event', array($this, 'schedule_event'));
145
146 add_action('plugins_loaded', array($this, 'plugins_loaded'));
147
148 // Since the WordPress version 5.5, we are no longer forcing an auto update by hooking the auto_update_plugin filter because WordPress does something different to its auto-update interface
149 if (version_compare($this->get_wordpress_version(), '5.5', '<')) {
150 // Auto update plugin
151 add_filter('auto_update_plugin', array($this, 'maybe_auto_update_plugin'), 20, 2);
152 }
153
154 // Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
155 add_filter('itsec_has_external_backup', '__return_true', 999);
156 add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
157 add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
158
159 add_action('updraft_report_remotestorage_extrainfo', array($this, 'report_remotestorage_extrainfo'), 10, 3);
160
161 // Prevent people using WP < 5.5 upgrading from being baffled by WP's obscure error message. See: https://core.trac.wordpress.org/ticket/27196
162
163 if (version_compare($this->get_wordpress_version(), '5.4.99999999', '<')) {
164 add_filter('upgrader_source_selection', array($this, 'upgrader_source_selection'), 10, 4);
165 }
166
167 // register_deactivation_hook(__FILE__, array($this, 'deactivation'));
168 if (!empty($_POST) && !empty($_GET['udm_action']) && 'vault_disconnect' == $_GET['udm_action'] && !empty($_POST['udrpc_message']) && !empty($_POST['reset_hash'])) {
169 add_action('wp_loaded', array($this, 'wp_loaded_vault_disconnect'), 1);
170 }
171
172 // Remove the notice on the Updates page that confuses users who already have backups installed
173 if ('update-core.php' == $pagenow) {
174 // added filter here instead of admin.php because the jetpack_just_in_time_msgs filter applied in init hook
175 add_filter('jetpack_just_in_time_msgs', '__return_false', 20);
176 }
177
178 // Cron to clean temporary files even in the absence of a new backup job beginning
179 add_action('updraftplus_clean_temporary_files', 'UpdraftPlus_Filesystem_Functions::clean_temporary_files', 10);
180
181 if (!wp_next_scheduled('updraftplus_clean_temporary_files')) {
182 wp_schedule_event(time(), 'twicedaily', 'updraftplus_clean_temporary_files');
183 }
184 }
185
186 /**
187 * Enables automatic updates for the plugin.
188 *
189 * @access public
190 * @see __construct
191 * @internal uses auto_update_plugin filter
192 *
193 * @param Bool $update Whether the item has automatic updates enabled
194 * @param Object $item Object holding the asset to be updated
195 * @return bool True of automatic updates enabled, false if not
196 */
197 public function maybe_auto_update_plugin($update, $item) {
198 if (!isset($item->plugin) || basename(UPDRAFTPLUS_DIR).'/updraftplus.php' !== $item->plugin) return $update;
199 $option_auto_update_settings = (array) get_site_option('auto_update_plugins', array());
200 return in_array($item->plugin, $option_auto_update_settings, true);
201 }
202
203 /**
204 * Called by the WP action updraft_report_remotestorage_extrainfo
205 *
206 * @param String $service
207 * @param String $info_html - the HTML version of the extra info
208 * @param String $info_plain - the plain text version of the extra info
209 */
210 public function report_remotestorage_extrainfo($service, $info_html, $info_plain) {
211 $this->remotestorage_extrainfo[$service] = array('pretty' => $info_html, 'plain' => $info_plain);
212 }
213
214 /**
215 * WP filter upgrader_source_selection. We use it to tweak the error message shown when an install of a new version is prevented by the existence of an existing version (i.e. us!), to give the user some actual useful information instead of WP's default.
216 *
217 * @param String $source File source location.
218 * @param String $remote_source Remote file source location.
219 * @param WP_Upgrader $upgrader_object WP_Upgrader instance.
220 * @param Array $hook_extra Extra arguments passed to hooked filters.
221 *
222 * @return String - filtered value
223 */
224 public function upgrader_source_selection($source, $remote_source, $upgrader_object, $hook_extra = array()) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Filter use
225
226 static $been_here_already = false;
227
228 if ($been_here_already || !is_array($hook_extra) || empty($hook_extra['type']) || 'plugin' !== $hook_extra['type'] || empty($hook_extra['action']) || 'install' !== $hook_extra['action'] || empty($source) || 'updraftplus' !== basename(untrailingslashit($source)) || !class_exists('ReflectionObject')) return $source;
229
230 $been_here_already = true;
231
232 $reflect = new ReflectionObject($upgrader_object);
233
234 $properties = $reflect->getProperty('strings');
235
236 if (!$properties->isPublic() || !is_array($upgrader_object->strings) || empty($upgrader_object->strings['folder_exists'])) return $source;
237
238 $upgrader_object->strings['folder_exists'] .= ' '.__('A version of UpdraftPlus is already installed. WordPress will only allow you to install your new version after first de-installing the existing one. That is safe - all your settings and backups will be retained. So, go to the "Plugins" page, de-activate and de-install UpdraftPlus, and then try again.', 'updraftplus');
239
240 return $source;
241
242 }
243
244 /**
245 * WordPress filter itsec_scheduled_external_backup - from iThemes Security
246 *
247 * @return Boolean - filtered value
248 */
249 public function itsec_scheduled_external_backup() {
250 return wp_next_scheduled('updraft_backup') ? true : false;
251 }
252
253 /**
254 * WordPress filter itsec_external_backup_link - from iThemes security
255 *
256 * @return String - filtered value
257 */
258 public function itsec_external_backup_link() {
259 return UpdraftPlus_Options::admin_page_url().'?page=updraftplus';
260 }
261
262 /**
263 * This method will disconnect UpdraftVault accounts.
264 *
265 * @return Array - returns the saved options if an error is encountered.
266 */
267 public function wp_loaded_vault_disconnect() {
268 $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('updraftvault');
269
270 if (is_wp_error($opts)) {
271 if ('recursion' !== $opts->get_error_code()) {
272 $msg = "UpdraftVault (".$opts->get_error_code()."): ".$opts->get_error_message();
273 $this->log($msg);
274 error_log("UpdraftPlus: $msg");
275 }
276 // The saved options had a problem; so, return the new ones
277 return $opts;
278 } elseif (!empty($opts['settings'])) {
279
280 foreach ($opts['settings'] as $storage_options) {
281 if (!empty($storage_options['token']) && $storage_options['token']) {
282 $site_id = $this->siteid();
283 $hash = hash('sha256', $site_id.':::'.$storage_options['token']);
284 if ($hash == $_POST['reset_hash']) {
285 $this->log('This site has been remotely disconnected from UpdraftPlus Vault');
286 include_once(UPDRAFTPLUS_DIR.'/methods/updraftvault.php');
287 $vault = new UpdraftPlus_BackupModule_updraftvault();
288 $vault->ajax_vault_disconnect();
289 // Die, as the vault method has already sent output
290 die;
291 } else {
292 $this->log('An invalid request was received to disconnect this site from UpdraftPlus Vault');
293 }
294 }
295 echo json_encode(array('disconnected' => 0));
296 }
297 }
298 die;
299 }
300
301 /**
302 * Gets an RPC object, and sets some defaults on it that we always want
303 *
304 * @param string $indicator_name indicator name
305 * @return array
306 */
307 public function get_udrpc($indicator_name = 'migrator.updraftplus.com') {
308 if (!class_exists('UpdraftPlus_Remote_Communications')) include_once(apply_filters('updraftplus_class_udrpc_path', UPDRAFTPLUS_DIR.'/vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc.php', $this->version));
309 $ud_rpc = new UpdraftPlus_Remote_Communications($indicator_name);
310 $ud_rpc->set_can_generate(true);
311 return $ud_rpc;
312 }
313
314 /**
315 * Ensure that the indicated phpseclib classes are available
316 *
317 * @param String|Array $classes - a class, or list of classes. There used to be a second parameter with paths to include; but this is now inferred from $classes; and there's no backwards compatibility problem because sending more parameters than are used is acceptable in PHP.
318 *
319 * @return Boolean|WP_Error
320 */
321 public function ensure_phpseclib($classes = array()) {
322
323 $classes = (array) $classes;
324
325 $this->no_deprecation_warnings_on_php7();
326
327 $any_missing = false;
328
329 foreach ($classes as $cl) {
330 if (!class_exists($cl)) $any_missing = true;
331 }
332
333 if (!$any_missing) return true;
334
335 $ret = true;
336
337 // From phpseclib/phpseclib/phpseclib/bootstrap.php - we nullify it there, but log here instead
338 if (extension_loaded('mbstring')) {
339 // 2 - MB_OVERLOAD_STRING
340 // @codingStandardsIgnoreLine
341 if (ini_get('mbstring.func_overload') & 2) {
342 // We go on to try anyway, in case the caller wasn't using an affected part of phpseclib
343 // @codingStandardsIgnoreLine
344 $ret = new WP_Error('mbstring_func_overload', 'Overloading of string functions using mbstring.func_overload is not supported by phpseclib.');
345 }
346 }
347
348 $phpseclib_dir = UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib';
349 if (false === strpos(get_include_path(), $phpseclib_dir)) set_include_path(get_include_path().PATH_SEPARATOR.$phpseclib_dir);
350 foreach ($classes as $cl) {
351 $path = str_replace('_', '/', $cl);
352 if (!class_exists($cl)) include_once($phpseclib_dir.'/'.$path.'.php');
353 }
354
355 return $ret;
356 }
357
358 /**
359 * Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
360 */
361 private function no_deprecation_warnings_on_php7() {
362 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
363 // 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).
364 // @codingStandardsIgnoreLine
365 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
366 $old_level = error_reporting();
367 // @codingStandardsIgnoreLine
368 $new_level = $old_level & ~E_DEPRECATED;
369 if ($old_level != $new_level) error_reporting($new_level);
370 $this->no_deprecation_warnings = true;
371 }
372 }
373
374 /**
375 * Attempt to close the connection to the browser, optionally with some output sent first, whilst continuing execution
376 *
377 * @param String $txt - output to send
378 */
379 public function close_browser_connection($txt = '') {
380 // Close browser connection so that it can resume AJAX polling
381 header('Content-Length: '.(empty($txt) ? '0' : 4+strlen($txt)));
382 header('Connection: close');
383 header('Content-Encoding: none');
384 if (function_exists('session_id') && session_id()) session_write_close();
385 echo "\r\n\r\n";
386 echo $txt;
387 // These two added - 19-Feb-15 - started being required on local dev machine, for unknown reason (probably some plugin that started an output buffer).
388 $ob_level = ob_get_level();
389 while ($ob_level > 0) {
390 ob_end_flush();
391 $ob_level--;
392 }
393 flush();
394 if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
395 }
396
397 /**
398 * Returns the number of bytes free, if it can be detected; otherwise, false
399 * Presently, we only detect CPanel. If you know of others, then feel free to contribute!
400 */
401 public function get_hosting_disk_quota_free() {
402 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;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
403
404 $perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
405
406 $exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
407
408 $handle = function_exists('popen') ? @popen($exec, 'r') : false; // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
409 if (!is_resource($handle)) return false;
410
411 $found = false;
412 $lines = 0;
413 while (false === $found && !feof($handle) && $lines<100) {
414 $lines++;
415 $w = fgets($handle);
416 // Used, limit, remain
417 if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) {
418 $found = true;
419 }
420 }
421 $ret = pclose($handle);
422 // The manual page for pclose() claims that only -1 indicates an error, but this is untrue
423 if (false === $found || 0 != $ret) return false;
424
425 if ((int) $matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
426
427 $this->cpanel_quota_readable = true;
428
429 return $matches;
430 }
431
432 /**
433 * Fetch information about the most recently modified log file
434 *
435 * @return Array - lists the modification time, the full path to the log file, and the log's nonce (ID)
436 */
437 public function last_modified_log() {
438 $updraft_dir = $this->backups_dir_location();
439
440 $log_file = '';
441 $mod_time = false;
442 $nonce = '';
443
444 if ($handle = @opendir($updraft_dir)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
445 while (false !== ($entry = readdir($handle))) {
446 // The latter match is for files created internally by zipArchive::addFile
447 if (preg_match('/^log\.([a-z0-9]+)\.txt$/i', $entry, $matches)) {
448 $mtime = filemtime($updraft_dir.'/'.$entry);
449 if ($mtime > $mod_time) {
450 $mod_time = $mtime;
451 $log_file = $updraft_dir.'/'.$entry;
452 $nonce = $matches[1];
453 }
454 }
455 }
456 @closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
457 }
458
459 return array($mod_time, $log_file, $nonce);
460 }
461
462 /**
463 * This function may get called multiple times, so write accordingly
464 */
465 public function admin_menu() {
466 // We are in the admin area: now load all that code
467 global $updraftplus_admin;
468 if (empty($updraftplus_admin)) include_once(UPDRAFTPLUS_DIR.'/admin.php');
469
470 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && 'updraftplus' == $_GET['page'] && 'downloadlatestmodlog' == $_GET['action'] && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
471
472 list($mod_time, $log_file, $nonce) = $this->last_modified_log();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
473
474 if ($mod_time >0) {
475 if (is_readable($log_file)) {
476 header('Content-type: text/plain');
477 readfile($log_file);
478 exit;
479 } else {
480 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablelog'));
481 }
482 } else {
483 add_action('all_admin_notices', array($this, 'show_admin_warning_nolog'));
484 }
485 }
486
487 }
488
489 /**
490 * WP action http_api_curl
491 *
492 * @param Resource $handle A curl handle returned by curl_init()
493 *
494 * @return the handle (having potentially had some options set upon it)
495 */
496 public function http_api_curl($handle) {
497 if (defined('UPDRAFTPLUS_IPV4_ONLY') && UPDRAFTPLUS_IPV4_ONLY) {
498 curl_setopt($handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
499 }
500 return $handle;
501 }
502
503 /**
504 * Used as a central location (to avoid repetition) to register or de-register hooks into the WP HTTP API
505 *
506 * @param Boolean $register - true to register, false to de-register
507 */
508 public function register_wp_http_option_hooks($register = true) {
509 if ($register) {
510 add_filter('http_request_args', array($this, 'modify_http_options'));
511 add_action('http_api_curl', array($this, 'http_api_curl'));
512 } else {
513 remove_filter('http_request_args', array($this, 'modify_http_options'));
514 remove_action('http_api_curl', array($this, 'http_api_curl'));
515 }
516 }
517
518 /**
519 * Used as a WordPress options filter (http_request_args)
520 *
521 * @param Array $opts - existing options
522 *
523 * @return Array - modified options
524 */
525 public function modify_http_options($opts) {
526
527 if (!is_array($opts)) return $opts;
528
529 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) $opts['sslcertificates'] = UPDRAFTPLUS_DIR.'/includes/cacert.pem';
530
531 $opts['sslverify'] = UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify') ? false : true;
532
533 return $opts;
534
535 }
536
537 /**
538 * Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?action=updraftmethod-googledrive-auth&page=updraftplus
539 * 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.
540 * Also handle action=downloadlog
541 *
542 * @return Void - may not necessarily return at all, depending on the action
543 */
544 public function handle_url_actions() {
545
546 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
547 // Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
548 if (isset($_SERVER['REQUEST_METHOD']) && ('GET' == $_SERVER['REQUEST_METHOD'] || 'POST' == $_SERVER['REQUEST_METHOD']) && isset($_GET['action'])) {
549 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php') && UpdraftPlus_Options::user_can_manage()) {
550 $_GET['page'] = 'updraftplus';
551 $_REQUEST['page'] = 'updraftplus';
552 $method = $matches[1];
553 $call_method = "action_".$matches[2];
554 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($method));
555
556 $instance_id = isset($_GET['updraftplus_instance']) ? $_GET['updraftplus_instance'] : '';
557
558 if ("POST" == $_SERVER['REQUEST_METHOD'] && isset($_POST['state'])) {
559 $state = urldecode($_POST['state']);
560 } elseif (isset($_GET['state'])) {
561 $state = $_GET['state'];
562 }
563
564 // If we don't have an instance_id but the state is set then we are coming back to finish the auth and should extract the instance_id from the state
565 if ('' == $instance_id && isset($state) && false !== strpos($state, ':')) {
566 $parts = explode(':', $state);
567 $instance_id = $parts[1];
568 }
569
570 if (isset($storage_objects_and_ids[$method]['instance_settings'][$instance_id])) {
571 $opts = $storage_objects_and_ids[$method]['instance_settings'][$instance_id];
572 $backup_obj = $storage_objects_and_ids[$method]['object'];
573 $backup_obj->set_options($opts, false, $instance_id);
574 } else {
575 include_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
576 $call_class = "UpdraftPlus_BackupModule_".$method;
577 $backup_obj = new $call_class;
578 }
579
580 $this->register_wp_http_option_hooks();
581
582 try {
583 if (method_exists($backup_obj, $call_method)) {
584 call_user_func(array($backup_obj, $call_method));
585 }
586 } catch (Exception $e) {
587 $this->log(sprintf(__("%s error: %s", 'updraftplus'), $method, $e->getMessage().' ('.$e->getCode().')', 'error'));
588 }
589 $this->register_wp_http_option_hooks(false);
590 } 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()) {
591 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
592 $updraft_dir = $this->backups_dir_location();
593 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
594 if (is_readable($log_file)) {
595 header('Content-type: text/plain');
596 if (!empty($_GET['force_download'])) header('Content-Disposition: attachment; filename="'.basename($log_file).'"');
597 readfile($log_file);
598 exit;
599 } else {
600 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablelog'));
601 }
602 } 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()) {
603 // 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
604 $updraft_dir = $this->backups_dir_location();
605 $file = $_GET['updraftplus_file'];
606 $spool_file = $updraft_dir.'/'.basename($file);
607 if (is_readable($spool_file)) {
608 $dkey = isset($_GET['decrypt_key']) ? stripslashes($_GET['decrypt_key']) : '';
609 $this->spool_file($spool_file, $dkey);
610 exit;
611 } else {
612 add_action('all_admin_notices', array($this, 'show_admin_warning_unreadablefile'));
613 }
614 } elseif ('updraftplus_spool_file' == $_GET['action'] && !empty($_GET['what']) && !empty($_GET['backup_timestamp']) && is_numeric($_GET['backup_timestamp']) && UpdraftPlus_Options::user_can_manage()) {
615 // At some point, it may be worth merging this with the previous section
616 $updraft_dir = $this->backups_dir_location();
617
618 $findex = isset($_GET['findex']) ? (int) $_GET['findex'] : 0;
619 $backup_timestamp = $_GET['backup_timestamp'];
620 $what = $_GET['what'];
621
622 $backup_set = UpdraftPlus_Backup_History::get_history($backup_timestamp);
623
624 $filename = null;
625 if (!empty($backup_set)) {
626 if ('db' != substr($what, 0, 2)) {
627 $backupable_entities = $this->get_backupable_file_entities();
628 if (!isset($backupable_entities[$what])) $filename = false;
629 }
630 if (false !== $filename && isset($backup_set[$what])) {
631 if (is_string($backup_set[$what]) && 0 == $findex) {
632 $filename = $backup_set[$what];
633 } elseif (isset($backup_set[$what][$findex])) {
634 $filename = $backup_set[$what][$findex];
635 }
636 }
637 }
638 if (empty($filename) || !is_readable($updraft_dir.'/'.basename($filename))) {
639 echo json_encode(array('result' => __('UpdraftPlus notice:', 'updraftplus').' '.__('The given file was not found, or could not be read.', 'updraftplus')));
640 exit;
641 }
642
643 $dkey = isset($_GET['decrypt_key']) ? stripslashes($_GET['decrypt_key']) : "";
644
645 $this->spool_file($updraft_dir.'/'.basename($filename), $dkey);
646 exit;
647
648 }
649 }
650 }
651
652 /**
653 * This function will check if this is a multisite and if our maintenance mode file is present if so return a service unavailable
654 *
655 * @return void
656 */
657 public function updraftplus_single_site_maintenance_init() {
658
659 if (!is_multisite()) return;
660
661 $wp_upload_dir = wp_upload_dir();
662 $subsite_dir = $wp_upload_dir['basedir'].'/';
663
664 if (!file_exists($subsite_dir.'.maintenance')) return;
665
666 $timestamp = file_get_contents($subsite_dir.'.maintenance');
667 $time = time();
668
669 if ($time - $timestamp > 3600) {
670 unlink($subsite_dir.'.maintenance');
671 return;
672 }
673
674 wp_die('<h1>'.__('Under Maintenance', 'updraftplus') .'</h1><p>'.__('Briefly unavailable for scheduled maintenance. Check back in a minute.', 'updraftplus').'</p>');
675 }
676
677 /**
678 * Get the installation's base table prefix, optionally allowing the result to be filtered
679 *
680 * @param Boolean $allow_override - allow the result to be filtered
681 *
682 * @return String
683 */
684 public function get_table_prefix($allow_override = false) {
685 global $wpdb;
686 if (is_multisite() && !defined('MULTISITE')) {
687 // 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.
688 $prefix = $wpdb->base_prefix;
689 } else {
690 $prefix = $wpdb->get_blog_prefix(0);
691 }
692 return $allow_override ? apply_filters('updraftplus_get_table_prefix', $prefix) : $prefix;
693 }
694
695 /**
696 * Get the site's identifier
697 *
698 * @return String
699 */
700 public function siteid() {
701 $sid = get_site_option('updraftplus-addons_siteid');
702 if (!is_string($sid) || empty($sid)) {
703 $sid = md5(rand().microtime(true).home_url());
704 update_site_option('updraftplus-addons_siteid', $sid);
705 }
706 return $sid;
707 }
708
709 public function show_admin_warning_unreadablelog() {
710 global $updraftplus_admin;
711 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('The log file could not be read.', 'updraftplus'));
712 }
713
714 public function show_admin_warning_nolog() {
715 global $updraftplus_admin;
716 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('No log files were found.', 'updraftplus'));
717 }
718
719 public function show_admin_warning_unreadablefile() {
720 global $updraftplus_admin;
721 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:', 'updraftplus').'</strong> '.__('The given file was not found, or could not be read.', 'updraftplus'));
722 }
723
724 /**
725 * Runs upon the WP action plugins_loaded
726 */
727 public function plugins_loaded() {
728
729 // Tell WordPress where to find the translations
730 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages/');
731
732 // The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
733 if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && 'updraftplus' == $_GET['page'] )) {
734 remove_action('init', 'ganalyticator_stats_init');
735 // Appointments+ does the same; but provides a cleaner way to disable it
736 @define('APP_GCAL_DISABLE', true);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
737 }
738
739 add_filter('updraftcentral_remotecontrol_command_classes', array($this, 'updraftcentral_remotecontrol_command_classes'));
740 add_action('updraftcentral_command_class_wanted', array($this, 'updraftcentral_command_class_wanted'));
741 add_action('updraftcentral_listener_pre_udrpc_action', array($this, 'updraftcentral_listener_pre_udrpc_action'));
742 add_action('updraftcentral_listener_post_udrpc_action', array($this, 'updraftcentral_listener_post_udrpc_action'));
743
744 add_filter('updraftcentral_host_plugins', array($this, 'attach_updraftcentral_host'));
745 if (file_exists(UPDRAFTPLUS_DIR.'/central/factory.php')) include_once(UPDRAFTPLUS_DIR.'/central/factory.php');
746
747 $load_classes = array();
748
749 if (defined('UPDRAFTPLUS_THIS_IS_CLONE')) {
750 $load_classes['UpdraftPlus_Temporary_Clone_Dash_Notice'] = 'includes/updraftclone/temporary-clone-dash-notice.php';
751 $load_classes['UpdraftPlus_Temporary_Clone_User_Notice'] = 'includes/updraftclone/temporary-clone-user-notice.php';
752 $load_classes['UpdraftPlus_Temporary_Clone_Restore'] = 'includes/updraftclone/temporary-clone-restore.php';
753 $load_classes['UpdraftPlus_Temporary_Clone_Auto_Login'] = 'includes/updraftclone/temporary-clone-auto-login.php';
754 $load_classes['UpdraftPlus_Temporary_Clone_Status'] = 'includes/updraftclone/temporary-clone-status.php';
755 }
756
757 foreach ($load_classes as $class => $relative_path) {
758 if (!class_exists($class)) include_once(UPDRAFTPLUS_DIR.'/'.$relative_path);
759 }
760
761 }
762
763 /**
764 * Attach this updraftplus plugin as host of the UpdraftCentral libraries
765 * (e.g. "central" folder)
766 *
767 * @param array $hosts List of plugins having the "central" library integrated into them
768 *
769 * @return array
770 */
771 public function attach_updraftcentral_host($hosts) {
772 $hosts[] = 'updraftplus';
773 return $hosts;
774 }
775
776 /**
777 * Get the character set for the current database connection
778 *
779 * @uses WPDB::determine_charset() - exists on WP 4.6+
780 *
781 * @param Object|Null $wpdb - WPDB object; if none passed, then use the global one
782 *
783 * @return String
784 */
785 public function get_connection_charset($wpdb = null) {
786 if (null === $wpdb) {
787 global $wpdb;
788 }
789
790 $charset = (defined('DB_CHARSET') && DB_CHARSET) ? DB_CHARSET : 'utf8mb4';
791
792 if (method_exists($wpdb, 'determine_charset')) {
793 $charset_collate = $wpdb->determine_charset($charset, '');
794 if (!empty($charset_collate['charset'])) $charset = $charset_collate['charset'];
795 }
796
797 return $charset;
798 }
799
800 /**
801 * Runs upon the action updraftcentral_listener_pre_udrpc_action
802 */
803 public function updraftcentral_listener_pre_udrpc_action() {
804 $this->register_wp_http_option_hooks();
805 }
806
807 /**
808 * Runs upon the action updraftcentral_listener_post_udrpc_action
809 */
810 public function updraftcentral_listener_post_udrpc_action() {
811 $this->register_wp_http_option_hooks(false);
812 }
813
814 /**
815 * Register our class. WP filter updraftcentral_remotecontrol_command_classes.
816 *
817 * @param Array $command_classes sends across the command class
818 *
819 * @return Array - filtered value
820 */
821 public function updraftcentral_remotecontrol_command_classes($command_classes) {
822 if (is_array($command_classes)) $command_classes['updraftplus'] = 'UpdraftCentral_UpdraftPlus_Commands';
823 if (is_array($command_classes)) $command_classes['updraftvault'] = 'UpdraftCentral_UpdraftVault_Commands';
824 return $command_classes;
825 }
826
827 /**
828 * Load the class when required
829 *
830 * @param string $command_php_class Sends across the php class type
831 */
832 public function updraftcentral_command_class_wanted($command_php_class) {
833 if ('UpdraftCentral_UpdraftPlus_Commands' == $command_php_class) {
834 include_once(UPDRAFTPLUS_DIR.'/includes/class-updraftcentral-updraftplus-commands.php');
835 } elseif ('UpdraftCentral_UpdraftVault_Commands' == $command_php_class) {
836 include_once(UPDRAFTPLUS_DIR.'/includes/updraftvault.php');
837 }
838 }
839
840 /**
841 * This function allows you to manually set the nonce and timestamp for the current backup job. If none are provided then it will create new ones.
842 *
843 * @param Boolean|string $nonce - the nonce you want to set
844 * @param Boolean|string $timestamp - the timestamp you want to set
845 *
846 * @return string - returns the backup nonce that has been set
847 */
848 public function backup_time_nonce($nonce = false, $timestamp = false) {
849 $this->job_time_ms = microtime(true);
850 if (false === $timestamp) $timestamp = time();
851 if (false === $nonce) $nonce = substr(md5(time().rand()), 20);
852 $this->backup_time = $timestamp;
853 $this->file_nonce = apply_filters('updraftplus_incremental_backup_file_nonce', $nonce);
854 $this->nonce = $nonce;
855 return $nonce;
856 }
857
858 /**
859 * Get the WordPress version
860 *
861 * @return String - the version
862 */
863 public function get_wordpress_version() {
864 static $got_wp_version = false;
865 if (!$got_wp_version) {
866 global $wp_version;
867 @include(ABSPATH.WPINC.'/version.php');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
868 $got_wp_version = $wp_version;
869 }
870 return $got_wp_version;
871 }
872
873 /**
874 * Get the UpdraftPlus version and convert it to the correct format to be used in filenames
875 *
876 * @return String - the file version number
877 */
878 public function get_updraftplus_file_version() {
879
880 if ($this->use_unminified_scripts()) return '';
881
882 $version_parts = explode('.', $this->version);
883 $version_parts = array_slice($version_parts, 0, 3);
884 $version = implode('.', $version_parts);
885
886 return '-'.str_replace('.', '-', $version).'.min';
887 }
888
889 /**
890 * 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)
891 *
892 * @param String $nonce - Used in the log file name to distinguish it from other log files. Should be the job nonce.
893 * @returns void
894 */
895 public function logfile_open($nonce) {
896
897 $this->logfile_name = $this->get_logfile_name($nonce);
898
899 $this->backup_is_already_complete = $this->found_backup_complete_in_logfile($nonce, false);
900
901 $this->logfile_handle = fopen($this->logfile_name, 'a');
902
903 $this->opened_log_time = microtime(true);
904
905 $this->write_log_header(array($this, 'log'));
906
907 }
908
909 /**
910 * Opens the log file, and finds if backup_is_already_complete
911 *
912 * @param String $nonce - Used in the log file name to distinguish it from other log files. Should be the job nonce.
913 * @param Boolean $use_existing_result - Whether to use any existing result or not
914 *
915 * @return boolean - returns true if the backup is complete otherwise returns false
916 */
917 public function found_backup_complete_in_logfile($nonce, $use_existing_result = true) {
918
919 static $checked_files = array();
920
921 if (isset($checked_files[$nonce]) && $use_existing_result) return $checked_files[$nonce];
922 $logfile_name = $this->get_logfile_name($nonce);
923
924 if (!file_exists($logfile_name)) return false;
925
926 $backup_is_already_complete = false;
927
928 $seek_to = max((filesize($logfile_name) - 340), 1);
929 $handle = fopen($logfile_name, 'r');
930 if (is_resource($handle)) {
931 // Returns 0 on success
932 if (0 === @fseek($handle, $seek_to)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
933 $bytes_back = filesize($logfile_name) - $seek_to;
934 // Return to the end of the file
935 $read_recent = fread($handle, $bytes_back);
936 // Move to end of file - ought to be redundant
937 if ((false !== strpos($read_recent, ') The backup apparently succeeded') || false !== strpos($read_recent, ') The backup succeeded')) && false !== strpos($read_recent, 'and is now complete')) {
938 $backup_is_already_complete = true;
939 }
940 }
941 fclose($handle);
942 }
943
944 $checked_files[$nonce] = $backup_is_already_complete;
945
946 return $backup_is_already_complete;
947 }
948
949 /**
950 * Returns the logfile name for a given job
951 *
952 * @param String $nonce - Used in the log file name to distinguish it from other log files. Should be the job nonce.
953 * @return string
954 */
955 public function get_logfile_name($nonce) {
956 $updraft_dir = $this->backups_dir_location();
957 return $updraft_dir."/log.$nonce.txt";
958 }
959
960 /**
961 * Writes a standardised header to the log file, using the specified logging function, which needs to be compatible with (or to be) UpdraftPlus::log()
962 *
963 * @param callable $logging_function
964 */
965 public function write_log_header($logging_function) {
966
967 global $wpdb;
968
969 $updraft_dir = $this->backups_dir_location();
970
971 call_user_func($logging_function, 'Opened log file at time: '.date('r').' on '.network_site_url());
972
973 $wp_version = $this->get_wordpress_version();
974 $mysql_version = $wpdb->get_var('SELECT VERSION()');
975 if ('' == $mysql_version) $mysql_version = $wpdb->db_version();
976 $safe_mode = $this->detect_safe_mode();
977
978 $memory_limit = ini_get('memory_limit');
979 $memory_usage = round(@memory_get_usage(false)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
980 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
981
982 // Attempt to raise limit to avoid false positives
983 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
984 $max_execution_time = (int) @ini_get("max_execution_time");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
985
986 $mp = (int) $wpdb->get_var("SELECT @@session.max_allowed_packet");
987
988 $logline = "UpdraftPlus WordPress backup plugin (https://updraftplus.com): ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".PHP_SAPI.", ".(function_exists('php_uname') ? @php_uname() : PHP_OS).") MySQL: $mysql_version (max packet size=$mp) 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() ? (is_subdomain_install() ? 'Y (sub-domain)' : 'Y (sub-folder)') : 'N')." openssl: ".(defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : 'N')." mcrypt: ".(function_exists('mcrypt_encrypt') ? 'Y' : 'N')." LANG: ".getenv('LANG')." ZipArchive::addFile: ";// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
989
990 // method_exists causes some faulty PHP installations to segfault, leading to support requests
991 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
992 $logline .= 'Y';
993 } else {
994 $logline .= (class_exists('ZipArchive') && method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
995 }
996
997 if (0 === $this->current_resumption) {
998 $memlim = $this->memory_check_current();
999 if ($memlim<65 && $memlim>0) {
1000 $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');
1001 }
1002 if ($max_execution_time>0 && $max_execution_time<20) {
1003 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');
1004 }
1005
1006 }
1007
1008 call_user_func($logging_function, $logline);
1009
1010 $hosting_bytes_free = $this->get_hosting_disk_quota_free();
1011 if (is_array($hosting_bytes_free)) {
1012 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
1013 $quota_free = ' / '.sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
1014 if ($hosting_bytes_free[3] < 1048576*50) {
1015 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
1016 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);
1017 }
1018 } else {
1019 $quota_free = '';
1020 }
1021
1022 $disk_free_space = function_exists('disk_free_space') ? @disk_free_space($updraft_dir) : false;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1023 // == 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.
1024 if (false == $disk_free_space) {
1025 call_user_func($logging_function, "Free space on disk containing Updraft's temporary directory: Unknown".$quota_free);
1026 } else {
1027 call_user_func($logging_function, "Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576, 1)." MB".$quota_free);
1028 $disk_free_mb = round($disk_free_space/1048576, 1);
1029 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);
1030 }
1031
1032 }
1033
1034 /**
1035 * This function will read the next chunk from the log file and return it's contents and last read byte position
1036 *
1037 * @param String $nonce - the UpdraftPlus file nonce
1038 *
1039 * @return array - an empty array if there is no log file or an array with log file contents and last read byte position
1040 */
1041 public function get_last_log_chunk($nonce) {
1042
1043 $this->logfile_name = $this->get_logfile_name($nonce);
1044
1045 if (file_exists($this->logfile_name)) {
1046 $contents = '';
1047 $seek_to = max(0, $this->jobdata_get('clone_first_byte', 0));
1048 $first_byte = $seek_to;
1049 $handle = fopen($this->logfile_name, 'r');
1050 if (is_resource($handle)) {
1051 // Returns 0 on success
1052 if (0 === @fseek($handle, $seek_to)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1053 while (strlen($contents) < 1048576 && ($buffer = fgets($handle, 262144)) !== false) {
1054 $contents .= $buffer;
1055 $seek_to += 262144;
1056 }
1057 $this->jobdata_set('clone_first_byte', $seek_to);
1058 }
1059 fclose($handle);
1060 }
1061 return array('log_contents' => $contents, 'first_byte' => $first_byte);
1062 }
1063 return array();
1064 }
1065
1066 /**
1067 *
1068 * Verifies that the indicated amount of memory is available
1069 *
1070 * @param Integer $how_many_bytes_needed - how many bytes need to be available
1071 *
1072 * @return Boolean - whether the needed number of bytes is available
1073 */
1074 public function verify_free_memory($how_many_bytes_needed) {
1075 // This returns in MB
1076 $memory_limit = $this->memory_check_current();
1077 if (!is_numeric($memory_limit)) return false;
1078 $memory_limit = $memory_limit * 1048576;
1079 $memory_usage = round(@memory_get_usage(false), 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1080 $memory_usage2 = round(@memory_get_usage(true), 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1081 if ($memory_limit - $memory_usage > $how_many_bytes_needed && $memory_limit - $memory_usage2 > $how_many_bytes_needed) return true;
1082 return false;
1083 }
1084
1085 /**
1086 * Logs the given line, adding (relative) time stamp and newline
1087 * Note these subtleties of log handling:
1088 * - 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.
1089 * - 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
1090 * 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...
1091 * - 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
1092 * $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
1093 * 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
1094 *
1095 * @param string $line the log line
1096 * @param string $level the log level: notice, warning, error. If suffixed with a hyphen and a destination, then the default destination is changed too.
1097 * @param boolean $uniq_id each of these will only be logged once
1098 * @param boolean $skip_dblog if true, then do not write to the database
1099 * @return null
1100 */
1101 public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
1102
1103 $destination = 'default';
1104 if (preg_match('/^([a-z]+)-([a-z]+)$/', $level, $matches)) {
1105 $level = $matches[1];
1106 $destination = $matches[2];
1107 }
1108
1109 if ('error' == $level || 'warning' == $level) {
1110 if ('error' == $level && 0 == $this->error_count()) $this->log('An error condition has occurred for the first time during this job');
1111 if ($uniq_id) {
1112 $this->errors[$uniq_id] = array('level' => $level, 'message' => $line);
1113 } else {
1114 $this->errors[] = array('level' => $level, 'message' => $line);
1115 }
1116 // Errors are logged separately
1117 if ('error' == $level) return;
1118 // It's a warning
1119 $warnings = $this->jobdata_get('warnings');
1120 if (!is_array($warnings)) $warnings = array();
1121 if ($uniq_id) {
1122 $warnings[$uniq_id] = $line;
1123 } else {
1124 $warnings[] = $line;
1125 }
1126 $this->jobdata_set('warnings', $warnings);
1127 }
1128
1129 if (false === ($line = apply_filters('updraftplus_logline', $line, $this->nonce, $level, $uniq_id, $destination))) return;
1130
1131 if ($this->logfile_handle) {
1132 // Record log file times relative to the backup start, if possible
1133 $rtime = (!empty($this->job_time_ms)) ? microtime(true)-$this->job_time_ms : microtime(true)-$this->opened_log_time;
1134 fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") ".(('notice' != $level) ? '['.ucfirst($level).'] ' : '').$line."\n");
1135 }
1136
1137 switch ($this->jobdata_get('job_type')) {
1138 case 'download':
1139 // Download messages are keyed on the job (since they could be running several), and type
1140 // The values of the POST array were checked before
1141 $findex = empty($_POST['findex']) ? 0 : $_POST['findex'];
1142
1143 if (!empty($_POST['timestamp']) && !empty($_POST['type'])) $this->jobdata_set('dlmessage_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line);
1144 break;
1145
1146 case 'restore':
1147 // if ('debug' != $level) echo $line."\n";
1148 break;
1149
1150 default:
1151 if (!$skip_dblog && 'debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")", false);
1152 break;
1153 }
1154
1155 if (defined('UPDRAFTPLUS_CONSOLELOG') && UPDRAFTPLUS_CONSOLELOG) echo $line."\n";
1156 if (defined('UPDRAFTPLUS_BROWSERLOG') && UPDRAFTPLUS_BROWSERLOG) echo htmlentities($line)."<br>\n";
1157 }
1158
1159 /**
1160 * Remove any logged warnings with the specified identifier. (The use case for this is that you can warn of something that may be about to happen (with a probably crash if it does), and then remove the warning if it did not happen).
1161 *
1162 * @see self::log()
1163 *
1164 * @param String $uniq_id - the identifier, previously passed to self::log()
1165 */
1166 public function log_remove_warning($uniq_id) {
1167 $warnings = $this->jobdata_get('warnings');
1168 if (!is_array($warnings)) $warnings = array();
1169 // Avoid an unnecessary database write if nothing changed
1170 if (isset($warnings[$uniq_id])) {
1171 unset($warnings[$uniq_id]);
1172 $this->jobdata_set('warnings', $warnings);
1173 }
1174 unset($this->errors[$uniq_id]);
1175 }
1176
1177 /**
1178 * Indicate whether or not a warning is logged with a specific identifier
1179 *
1180 * @see self::log()
1181 *
1182 * @param String $uniq_id - the identifier, previously passed to self::log()
1183 *
1184 * @return Boolean
1185 */
1186 public function warning_exists($uniq_id) {
1187 $warnings = $this->jobdata_get('warnings');
1188 return !empty($warnings[$uniq_id]);
1189 }
1190
1191 /**
1192 * For efficiency, you can also feed false or a string into this function
1193 *
1194 * @param Boolean|String|WP_Error $err - the errors
1195 * @param Boolean $echo - whether to echo() the error(s)
1196 * @param Boolean $logerror - whether to pass errors to UpdraftPlus::log()
1197 * @return Boolean - returns false for convenience
1198 */
1199 public function log_wp_error($err, $echo = false, $logerror = false) {
1200 if (false === $err) return false;
1201 if (is_string($err)) {
1202 $this->log("Error message: $err");
1203 if ($echo) $this->log(sprintf(__('Error: %s', 'updraftplus'), $err), 'notice-warning');
1204 if ($logerror) $this->log($err, 'error');
1205 return false;
1206 }
1207 foreach ($err->get_error_messages() as $msg) {
1208 $this->log("Error message: $msg");
1209 if ($echo) $this->log(sprintf(__('Error: %s', 'updraftplus'), $msg), 'notice-warning');
1210 if ($logerror) $this->log($msg, 'error');
1211 }
1212 $codes = $err->get_error_codes();
1213 if (is_array($codes)) {
1214 foreach ($codes as $code) {
1215 $data = $err->get_error_data($code);
1216 if (!empty($data)) {
1217 $ll = (is_string($data)) ? $data : serialize($data);
1218 $this->log("Error data (".$code."): ".$ll);
1219 }
1220 }
1221 }
1222 // Returns false so that callers can return with false more efficiently if they wish
1223 return false;
1224 }
1225
1226 /**
1227 * This function will construct the restore information log line using the passed in parameters and then log the line using $this->log();
1228 *
1229 * @param array $restore_information - an array of restore information
1230 *
1231 * @return void
1232 */
1233 public function log_restore_update($restore_information) {
1234 $this->log("RINFO:".json_encode($restore_information), 'notice-progress');
1235 }
1236
1237 /**
1238 * Outputs data to the browser.
1239 * Will also fill the buffer on nginx systems after a specified amount of time.
1240 *
1241 * @param String $line The text to output
1242 * @return void
1243 */
1244 public function output_to_browser($line) {
1245 echo $line;
1246 if (false === stripos($_SERVER['SERVER_SOFTWARE'], 'nginx')) return;
1247 static $strcount = 0;
1248 static $time = 0;
1249 $buffer_size = 65536; // The default NGINX config uses a buffer size of 32 or 64k, depending on the system. So we use 64K.
1250 if (0 == $time) $time = time();
1251 $strcount += strlen($line);
1252 if ((time() - $time) >= 8) {
1253 // if the string count is > the buffer size, we reset, as it's likely the string was already sent.
1254 if ($strcount > $buffer_size) {
1255 $time = time();
1256 $strcount = $strcount - $buffer_size;
1257 return;
1258 }
1259 echo str_repeat(" ", ($buffer_size-$strcount));
1260 // reset values
1261 $time = time();
1262 $strcount = 0;
1263 }
1264 }
1265 /**
1266 * Get the maximum packet size on the WPDB MySQL connection, in bytes, after (optionally) attempting to raise it to 32MB if it appeared to be lower.
1267 * A default value equal to 1MB is returned if the true value could not be found - it has been found reasonable to assume that at least this is available.
1268 *
1269 * @param Boolean $first_raise
1270 * @param Boolean $log_it
1271 *
1272 * @return Integer
1273 */
1274 public function max_packet_size($first_raise = true, $log_it = true) {
1275 global $wpdb;
1276 $mp = (int) $wpdb->get_var("SELECT @@session.max_allowed_packet");
1277 // Default to 1MB
1278 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
1279 // 32MB
1280 if ($first_raise && $mp < 33554432) {
1281 $save = $wpdb->show_errors(false);
1282 $req = @$wpdb->query("SET GLOBAL max_allowed_packet=33554432");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1283 $wpdb->show_errors($save);
1284 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).")");
1285 $mp = (int) $wpdb->get_var("SELECT @@session.max_allowed_packet");
1286 // Default to 1MB
1287 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
1288 }
1289 if ($log_it) $this->log("Max packet size: ".round($mp/1048576, 1)." MB");
1290 return $mp;
1291 }
1292
1293 /**
1294 * 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()).
1295 * 1st argument = the line to be logged (obligatory)
1296 * Further arguments = parameters for sprintf()
1297 *
1298 * @return null
1299 */
1300 public function log_e() {
1301 $args = func_get_args();
1302 // Get first argument
1303 $pre_line = array_shift($args);
1304 // Log it whilst still in English
1305 if (is_wp_error($pre_line)) {
1306 $this->log_wp_error($pre_line);
1307 } else {
1308 // Now run (v)sprintf on it, using any remaining arguments. vsprintf = sprintf but takes an array instead of individual arguments
1309 $this->log(vsprintf($pre_line, $args));
1310 // 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.
1311 $this->log(vsprintf($pre_line, $args), 'notice-restore');
1312 }
1313 }
1314
1315 /**
1316 * 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
1317 *
1318 * @param Number $percent - the amount of the file uploaded
1319 * @param String $extra - anything extra to include in the log message
1320 * @param Boolean $file_path - the full path to the file being uploaded
1321 * @param Boolean $log_it - whether to pass the message to UpdraftPlus::log()
1322 * @return Void
1323 */
1324 public function record_uploaded_chunk($percent, $extra = '', $file_path = false, $log_it = true) {
1325
1326 // Touch the original file, which helps prevent overlapping runs
1327 if ($file_path) touch($file_path);
1328
1329 // 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)
1330 if ($percent > 0.7 * ($this->current_resumption - max($this->jobdata_get('uploaded_lastreset'), 9))) UpdraftPlus_Job_Scheduler::something_useful_happened();
1331
1332 // Log it
1333 global $updraftplus_backup;
1334 $log = empty($updraftplus_backup->current_service) ? '' : ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded";
1335 if ($log && $log_it) $this->log($log.($extra ? " ($extra)" : ''));
1336 // If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
1337 // 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
1338 // 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
1339 // 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
1340
1341 $upload_status = $this->jobdata_get('uploading_substatus');
1342 if (is_array($upload_status)) {
1343 $upload_status['p'] = $percent/100;
1344 $this->jobdata_set('uploading_substatus', $upload_status);
1345 }
1346
1347 }
1348
1349 /**
1350 * Method for helping remote storage methods to upload files in chunks without needing to duplicate all the overhead
1351 *
1352 * @param Object $caller the object to call back to do the actual network API calls; needs to have a chunked_upload() method.
1353 * @param String $file the basename of the file
1354 * @param String $cloudpath this is passed back to the callback function; within this function, it is used only for logging
1355 * @param String $logname the prefix used on log lines. Also passed back to the callback function.
1356 * @param Integer $chunk_size the size, in bytes, of each upload chunk
1357 * @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.
1358 * @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) ?
1359 * @return Boolean
1360 */
1361 public function chunked_upload($caller, $file, $cloudpath, $logname, $chunk_size, $uploaded_size, $singletons = false) {
1362
1363 $fullpath = $this->backups_dir_location().'/'.$file;
1364 $orig_file_size = filesize($fullpath);
1365
1366 if ($uploaded_size >= $orig_file_size && !method_exists($caller, 'chunked_upload_finish')) return true;
1367
1368 $chunks = floor($orig_file_size / $chunk_size);
1369 // There will be a remnant unless the file size was exactly on a chunk boundary
1370 if ($orig_file_size % $chunk_size > 0) $chunks++;
1371
1372 $this->log("$logname upload: $file (chunks: $chunks, of size: $chunk_size) -> $cloudpath ($uploaded_size)");
1373
1374 if (0 == $chunks) {
1375 return 1;
1376 } elseif ($chunks < 2 && !$singletons) {
1377 return 1;
1378 }
1379
1380 // We have multiple chunks
1381 if ($uploaded_size < $orig_file_size) {
1382
1383 if (false == ($fp = @fopen($fullpath, 'rb'))) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1384 $this->log("$logname: failed to open file: $fullpath");
1385 $this->log("$file: ".sprintf(__('%s Error: Failed to open local file', 'updraftplus'), $logname), 'error');
1386 return false;
1387 }
1388
1389 $upload_start = 0;
1390 $upload_end = -1;
1391 $chunk_index = 1;
1392 // The file size minus one equals the byte offset of the final byte
1393 $upload_end = min($chunk_size - 1, $orig_file_size - 1);
1394 $errors_on_this_chunk = 0;
1395
1396 while ($upload_start < $orig_file_size) {
1397
1398 // Don't forget the +1; otherwise the last byte is omitted
1399 $upload_size = $upload_end - $upload_start + 1;
1400
1401 fseek($fp, $upload_start);
1402
1403 /*
1404 * Valid return values for $uploaded are many, as the possibilities have grown over time.
1405 * This could be cleaned up; but, it works, and it's not hugely complex.
1406 *
1407 * WP_Error : an error occured. The only permissible codes are: reduce_chunk_size (only on the first chunk), try_again
1408 * (bool)true : What was requested was done
1409 * (int)1 : What was requested was done, but do not log anything
1410 * (bool)false : There was an error
1411 * (Object) : Properties:
1412 * (bool)log: (bool) - if absent, defaults to true
1413 * (int)new_chunk_size: advisory amount for the chunk size for future chunks
1414 * NOT IMPLEMENTED: (int)bytes_uploaded: Actual number of bytes uploaded (needs to be positive - o/w, should return an error instead)
1415 * 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.
1416 */
1417 $uploaded = $caller->chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $orig_file_size);
1418
1419 // Try again? (Just once - added in 1.12.6 (can make more sophisticated if there is a need))
1420 if (is_wp_error($uploaded) && 'try_again' == $uploaded->get_error_code()) {
1421 // Arbitrary wait
1422 sleep(3);
1423 $this->log("Re-trying after wait (to allow apparent inconsistency to clear)");
1424 $uploaded = $caller->chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $orig_file_size);
1425 }
1426
1427 // This is the only other supported case of a WP_Error - otherwise, a boolean must be returned
1428 // Note that this is only allowed on the first chunk. The caller is responsible to remember its chunk size if it uses this facility.
1429 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)) {
1430 $this->log("Re-trying with new chunk size: ".$new_chunk_size);
1431 return $this->chunked_upload($caller, $file, $cloudpath, $logname, $new_chunk_size, $uploaded_size, $singletons);
1432 }
1433
1434 $uploaded_amount = $chunk_size;
1435
1436 /*
1437 // Not using this approach for now. Instead, going to allow the consumers to increase the next chunk size
1438 if (is_object($uploaded) && isset($uploaded->bytes_uploaded)) {
1439 if (!$uploaded->bytes_uploaded) {
1440 $uploaded = false;
1441 } else {
1442 $uploaded_amount = $uploaded->bytes_uploaded;
1443 $uploaded = (!isset($uploaded->log) || $uploaded->log) ? true : 1;
1444 }
1445 }
1446 */
1447 if (is_object($uploaded) && isset($uploaded->new_chunk_size)) {
1448 if ($uploaded->new_chunk_size >= 1048576) $new_chunk_size = $uploaded->new_chunk_size;
1449 $uploaded = (!isset($uploaded->log) || $uploaded->log) ? true : 1;
1450 }
1451
1452 // The joys of WP/PHP: is_wp_error() is not false-y.
1453 if ($uploaded && !is_wp_error($uploaded)) {
1454 $perc = round(100*($upload_end + 1)/max($orig_file_size, 1), 1);
1455 // Consumers use a return value of (int)1 (rather than (bool)true) to suppress logging
1456 $log_it = (1 === $uploaded) ? false : true;
1457 $this->record_uploaded_chunk($perc, $chunk_index, $fullpath, $log_it);
1458
1459 // $uploaded_bytes = $upload_end + 1;
1460
1461 // If there was an error, then we re-try the same chunk; we don't move on to the next one. Otherwise, we would need more code to handle potential 'intermediate' failed chunks (in case PHP dies before this method eventually returns false, and thus the intermediate chunk failure never gets detected)
1462 $chunk_index++;
1463 $errors_on_this_chunk = 0;
1464 $upload_start = $upload_end + 1;
1465 $upload_end += isset($new_chunk_size) ? $uploaded_amount + $new_chunk_size - $chunk_size : $uploaded_amount;
1466 $upload_end = min($upload_end, $orig_file_size - 1);
1467
1468 } else {
1469
1470 $errors_on_this_chunk++;
1471
1472 // Either $uploaded is false-y, or is a WP_Error
1473 if (is_wp_error($uploaded)) {
1474 $this->log("$logname: Chunk upload ($chunk_index) failed (".$uploaded->get_error_code().'): '.$uploaded->get_error_message());
1475 } else {
1476 $this->log("$logname: Chunk upload ($chunk_index) failed");
1477 }
1478
1479 if ($errors_on_this_chunk >= 3) {
1480 @fclose($fp);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1481 return false;
1482 }
1483 }
1484
1485 }
1486
1487 @fclose($fp);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1488
1489 }
1490
1491 // All chunks are uploaded - now combine the chunks
1492 $ret = true;
1493
1494 // The action calls here exist to aid debugging
1495 if (method_exists($caller, 'chunked_upload_finish')) {
1496 do_action('updraftplus_pre_chunked_upload_finish', $file, $caller);
1497 $ret = $caller->chunked_upload_finish($file);
1498 if (!$ret) {
1499 $this->log("$logname - failed to re-assemble chunks");
1500 $this->log(sprintf(__('%s error - failed to re-assemble chunks', 'updraftplus'), $logname), 'error');
1501 }
1502 do_action('updraftplus_post_chunked_upload_finish', $file, $caller, $ret);
1503 }
1504
1505 if ($ret) {
1506 // We allow chunked_upload_finish to return (int)1 to indicate that it took care of any logging.
1507 if (true === $ret) $this->log("$logname upload: success");
1508 $ret = true;
1509 // UpdraftPlus_RemoteStorage_Addons_Base calls this itself
1510 if (!is_a($caller, 'UpdraftPlus_RemoteStorage_Addons_Base_v2')) $this->uploaded_file($file);
1511 }
1512
1513 return $ret;
1514
1515 }
1516
1517 /**
1518 * Provides a convenience function allowing remote storage methods to download a file in chunks, without duplicated overhead.
1519 *
1520 * @param String $file - The basename of the file being downloaded
1521 * @param Object $method - This remote storage method object needs to have a chunked_download() method to call back
1522 * @param Integer $remote_size - The size, in bytes, of the object being downloaded
1523 * @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)
1524 * @param Mixed $passback - A value to pass back to the callback function
1525 * @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.
1526 */
1527 public function chunked_download($file, $method, $remote_size, $manually_break_up = false, $passback = null, $chunk_size = 1048576) {
1528
1529 try {
1530
1531 $fullpath = $this->backups_dir_location().'/'.$file;
1532 $start_offset = file_exists($fullpath) ? filesize($fullpath) : 0;
1533
1534 if ($start_offset >= $remote_size) {
1535 $this->log("File is already completely downloaded ($start_offset/$remote_size)");
1536 return true;
1537 }
1538
1539 // Some more remains to download - so let's do it
1540 // 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
1541 if (!($fh = fopen($fullpath, 'c+'))) {
1542 $this->log("Error opening local file: $fullpath");
1543 $this->log($file.": ".__("Error", 'updraftplus').": ".__('Error opening local file: Failed to download', 'updraftplus'), 'error');
1544 return false;
1545 }
1546
1547 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
1548
1549 // This only affects logging
1550 $expected_bytes_delivered_so_far = true;
1551
1552 while ($start_offset < $remote_size) {
1553 $headers = array();
1554 // If resuming, then move to the end of the file
1555
1556 $requested_bytes = $last_byte-$start_offset;
1557
1558 if ($expected_bytes_delivered_so_far) {
1559 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next $requested_bytes bytes");
1560 } else {
1561 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next chunk (${start_offset}-)");
1562 }
1563
1564 if ($start_offset > 0 || $last_byte<$remote_size) {
1565 fseek($fh, $start_offset);
1566 // N.B. Don't alter this format without checking what relies upon it
1567 $last_byte_start = $last_byte - 1;
1568 $headers['Range'] = "bytes=$start_offset-$last_byte_start";
1569 }
1570
1571 /*
1572 * 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*.
1573 * The method is free to write/return as much data as it pleases.
1574 */
1575 $ret = $method->chunked_download($file, $headers, $passback, $fh);
1576 if (true === $ret) {
1577 clearstatcache();
1578 // Some SDKs (including AWS/S3) close the resource
1579 // 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
1580 if (is_resource($fh)) {
1581 $ret = ftell($fh);
1582 } else {
1583 $ret = filesize($fullpath);
1584 // fseek returns - on success
1585 if (false == ($fh = fopen($fullpath, 'c+')) || 0 !== fseek($fh, $ret)) {
1586 $this->log("Error opening local file: $fullpath");
1587 $this->log($file.": ".__("Error", 'updraftplus').": ".__('Error opening local file: Failed to download', 'updraftplus'), 'error');
1588 return false;
1589 }
1590 }
1591 if (is_integer($ret)) $ret -= $start_offset;
1592 }
1593
1594 // Note that this covers a false code returned either by chunked_download() or by ftell.
1595 if (false === $ret) return false;
1596
1597 $returned_bytes = is_integer($ret) ? $ret : strlen($ret);
1598
1599 if ($returned_bytes > $requested_bytes || $returned_bytes < $requested_bytes - 1) $expected_bytes_delivered_so_far = false;
1600
1601 if (!is_integer($ret) && !fwrite($fh, $ret)) throw new Exception('Write failure (start offset: '.$start_offset.', bytes: '.strlen($ret).'; requested: '.$requested_bytes.')');
1602
1603 clearstatcache();
1604 $start_offset = ftell($fh);
1605 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
1606
1607 }
1608
1609 } catch (Exception $e) {
1610 $this->log('Error ('.get_class($e).') - failed to download the file ('.$e->getCode().', '.$e->getMessage().', line '.$e->getLine().' in '.$e->getFile().')');
1611 $this->log("$file: ".__('Error - failed to download the file', 'updraftplus').' ('.$e->getCode().', '.$e->getMessage().')', 'error');
1612 return false;
1613 }
1614
1615 // April 1st 2020 - Due to a bug during uploads to Dropbox some backups had string "null" appended to the end which caused warnings, this removes the string "null" from these backups
1616 if ('dropbox' == $method->get_id()) {
1617 fseek($fh, -4, SEEK_END);
1618 $data = fgets($fh, 5);
1619 if ("null" == $data) {
1620 ftruncate($fh, filesize($fullpath) - 4);
1621 }
1622 }
1623
1624 fclose($fh);
1625
1626 return true;
1627 }
1628
1629 /**
1630 * Detect if safe_mode is on. N.B. This is abolished from PHP 7.0
1631 *
1632 * @return Integer - 1 or 0
1633 */
1634 public function detect_safe_mode() {
1635 // @codingStandardsIgnoreLine
1636 return (@ini_get('safe_mode') && 'off' != strtolower(@ini_get('safe_mode'))) ? 1 : 0;
1637 }
1638
1639 /**
1640 * Find, if possible, a working mysqldump executable
1641 *
1642 * @param Boolean $log_it - whether to log the workings or not
1643 * @param Boolean $cacheit - whether to cache the results for subsequent queries or not
1644 *
1645 * @return String|Boolean - either a path to an executable, or false for failure
1646 */
1647 public function find_working_sqldump($log_it = true, $cacheit = true) {
1648
1649 // The hosting provider may have explicitly disabled the popen or proc_open functions
1650 if ($this->detect_safe_mode() || !function_exists('popen') || !function_exists('escapeshellarg')) {
1651 if ($cacheit) $this->jobdata_set('binsqldump', false);
1652 return false;
1653 }
1654 $existing = $this->jobdata_get('binsqldump', null);
1655 // Theoretically, we could have moved machines, due to a migration
1656 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1657
1658 $updraft_dir = $this->backups_dir_location();
1659 global $wpdb;
1660 $table_name = $wpdb->get_blog_prefix().'options';
1661 $pfile = md5(time().rand()).'.tmp';
1662 file_put_contents($updraft_dir.'/'.$pfile, "[mysqldump]\npassword=\"".addslashes(DB_PASSWORD)."\"\n");
1663
1664 $result = false;
1665 foreach (explode(',', UPDRAFTPLUS_MYSQLDUMP_EXECUTABLE) as $potsql) {
1666
1667 if (!@is_executable($potsql)) continue;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1668
1669 if ($log_it) $this->log("Testing potential mysqldump binary: $potsql");
1670
1671 if ('win' == strtolower(substr(PHP_OS, 0, 3))) {
1672 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $updraft_dir))." & ";
1673 $siteurl = "'siteurl'";
1674 if (false !== strpos($potsql, ' ')) $potsql = '"'.$potsql.'"';
1675 } else {
1676 $exec = "cd ".escapeshellarg($updraft_dir)."; ";
1677 $siteurl = "\\'siteurl\\'";
1678 if (false !== strpos($potsql, ' ')) $potsql = "'$potsql'";
1679 }
1680
1681 // Allow --max_allowed_packet to be configured via constant. Experience has shown some customers with complex CMS or pagebuilder setups can have extrememly large postmeta entries.
1682 $msqld_max_allowed_packet = (defined('UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET') && (is_int(UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET) || is_string(UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET))) ? UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET : '1M';
1683
1684 $exec .= "$potsql --defaults-file=$pfile --max_allowed_packet=$msqld_max_allowed_packet --quote-names --add-drop-table";
1685
1686 static $mysql_version = null;
1687 if (null === $mysql_version) {
1688 $mysql_version = $wpdb->get_var('SELECT VERSION()');
1689 if ('' == $mysql_version) $mysql_version = $wpdb->db_version();
1690 }
1691 if ($mysql_version && version_compare($mysql_version, '5.1', '>=')) {
1692 $exec .= " --no-tablespaces";
1693 }
1694
1695 $exec .= " --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --where=option_name=$siteurl --user=".escapeshellarg(DB_USER)." ";
1696
1697 if (preg_match('#^(.*):(\d+)$#', DB_HOST, $matches)) {
1698 // The escapeshellarg() on $matches[2] is only to avoid tripping static analysis tools
1699 $exec .= "--host=".escapeshellarg($matches[1])." --port=".escapeshellarg($matches[2])." ";
1700 } elseif (preg_match('#^(.*):(.*)$#', DB_HOST, $matches) && file_exists($matches[2])) {
1701 $exec .= "--host=".escapeshellarg($matches[1])." --socket=".escapeshellarg($matches[2])." ";
1702 } else {
1703 $exec .= "--host=".escapeshellarg(DB_HOST)." ";
1704 }
1705
1706 $exec .= DB_NAME." ".escapeshellarg($table_name);
1707
1708 $handle = function_exists('popen') ? popen($exec, "r") : false;
1709 if ($handle) {
1710 $output = '';
1711 // We expect the INSERT statement in the first 100KB
1712 while (!feof($handle) && strlen($output) < 102400) {
1713 $output .= fgets($handle, 102400);
1714 }
1715 if ($output && $log_it) {
1716 $log_output = (strlen($output) > 512) ? substr($output, 0, 512).' (truncated - '.strlen($output).' bytes total)' : $output;
1717 $this->log("Output: ".str_replace("\n", '\\n', trim($log_output)));
1718 }
1719 $ret = pclose($handle);
1720 // The manual page for pclose() claims that only -1 indicates an error, but this is untrue
1721 if (0 != $ret) {
1722 if ($log_it) {
1723 $this->log("Binary mysqldump: error (code: $ret)");
1724 }
1725 } else {
1726 if (false !== stripos($output, 'insert into')) {
1727 if ($log_it) $this->log("Working binary mysqldump found: $potsql");
1728 $result = $potsql;
1729 break;
1730 }
1731 }
1732 } else {
1733 if ($log_it) $this->log("Error: popen failed");
1734 }
1735 }
1736
1737 if (file_exists($updraft_dir.'/'.$pfile)) unlink($updraft_dir.'/'.$pfile);
1738
1739 if ($cacheit) $this->jobdata_set('binsqldump', $result);
1740
1741 return $result;
1742 }
1743
1744 /**
1745 * This function will work out which zip object we want to use and return it's name
1746 *
1747 * @return string - the name of the zip object we want to use
1748 */
1749 public function get_zip_object_name() {
1750
1751 if (!class_exists('UpdraftPlus_BinZip')) include_once(UPDRAFTPLUS_DIR . '/includes/class-zip.php');
1752
1753 $zip_object = 'UpdraftPlus_ZipArchive';
1754
1755 // In tests, PclZip was found to be 25% slower than ZipArchive
1756 if (((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
1757 $zip_object = 'UpdraftPlus_PclZip';
1758 }
1759
1760 return $zip_object;
1761 }
1762
1763 /**
1764 * We require -@ and -u -r to work - which is the usual Linux binzip
1765 *
1766 * @param Boolean $log_it - whether to record the results with UpdraftPlus::log()
1767 * @param Boolean $cacheit - whether to cache the results as job data
1768 * @return String|Boolean - the path to a working zip binary, or false
1769 */
1770 public function find_working_bin_zip($log_it = true, $cacheit = true) {
1771 if ($this->detect_safe_mode()) return false;
1772 // The hosting provider may have explicitly disabled the popen or proc_open functions
1773 if (!function_exists('popen') || !function_exists('proc_open') || !function_exists('proc_close') || !function_exists('escapeshellarg')) {
1774 if ($cacheit) $this->jobdata_set('binzip', false);
1775 return false;
1776 }
1777
1778 $existing = $this->jobdata_get('binzip', null);
1779 // Theoretically, we could have moved machines, due to a migration
1780 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1781
1782 $updraft_dir = $this->backups_dir_location();
1783 foreach (explode(',', UPDRAFTPLUS_ZIP_EXECUTABLE) as $potzip) {
1784 if (!@is_executable($potzip)) continue;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1785 if ($log_it) $this->log("Testing: $potzip");
1786
1787 // Test it, see if it is compatible with Info-ZIP
1788 // If you have another kind of zip, then feel free to tell me about it
1789 @mkdir($updraft_dir.'/binziptest/subdir1/subdir2', 0777, true);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1790
1791 if (!file_exists($updraft_dir.'/binziptest/subdir1/subdir2')) return false;
1792
1793 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>');
1794
1795 if (file_exists($updraft_dir.'/binziptest/test.zip')) unlink($updraft_dir.'/binziptest/test.zip');
1796
1797 if (is_file($updraft_dir.'/binziptest/subdir1/subdir2/test.html')) {
1798
1799 $exec = "cd ".escapeshellarg($updraft_dir)."; $potzip";
1800 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1801 $exec .= " -v -u -r binziptest/test.zip binziptest/subdir1";
1802
1803 $all_ok=true;
1804 $handle = function_exists('popen') ? popen($exec, "r") : false;
1805 if ($handle) {
1806 while (!feof($handle)) {
1807 $w = fgets($handle);
1808 if ($w && $log_it) $this->log("Output: ".trim($w));
1809 }
1810 $ret = pclose($handle);
1811 // The manual page for pclose() claims that only -1 indicates an error, but this is untrue
1812 if (0 != $ret) {
1813 if ($log_it) $this->log("Binary zip: error (code: $ret)");
1814 $all_ok = false;
1815 }
1816 } else {
1817 if ($log_it) $this->log("Error: popen failed");
1818 $all_ok = false;
1819 }
1820
1821 // Now test -@
1822 if (true == $all_ok) {
1823 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>');
1824
1825 $exec = $potzip;
1826 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1827 $exec .= " -v -@ binziptest/test.zip";
1828
1829 $all_ok = true;
1830
1831 $descriptorspec = array(
1832 0 => array('pipe', 'r'),
1833 1 => array('pipe', 'w'),
1834 2 => array('pipe', 'w')
1835 );
1836 $handle = proc_open($exec, $descriptorspec, $pipes, $updraft_dir);
1837 if (is_resource($handle)) {
1838 if (!fwrite($pipes[0], "binziptest/subdir1/subdir2/test2.html\n")) {
1839 @fclose($pipes[0]);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1840 @fclose($pipes[1]);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1841 @fclose($pipes[2]);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1842 $all_ok = false;
1843 } else {
1844 fclose($pipes[0]);
1845 while (!feof($pipes[1])) {
1846 $w = fgets($pipes[1]);
1847 if ($w && $log_it) $this->log("Output: ".trim($w));
1848 }
1849 fclose($pipes[1]);
1850
1851 while (!feof($pipes[2])) {
1852 $last_error = fgets($pipes[2]);
1853 if (!empty($last_error) && $log_it) $this->log("Stderr output: ".trim($w));
1854 }
1855 fclose($pipes[2]);
1856
1857 $ret = function_exists('proc_close') ? proc_close($handle) : -1;
1858 if (0 != $ret) {
1859 if ($log_it) $this->log("Binary zip: error (code: $ret)");
1860 $all_ok = false;
1861 }
1862
1863 }
1864
1865 } else {
1866 if ($log_it) $this->log("Error: proc_open failed");
1867 $all_ok = false;
1868 }
1869
1870 }
1871
1872 // Do we now actually have a working zip? Need to test the created object using PclZip
1873 // If it passes, then remove dirs and then return $potzip;
1874 $found_first = false;
1875 $found_second = false;
1876 if ($all_ok && file_exists($updraft_dir.'/binziptest/test.zip')) {
1877 if (function_exists('gzopen')) {
1878 if (!class_exists('PclZip')) include_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
1879 $zip = new PclZip($updraft_dir.'/binziptest/test.zip');
1880 if (($list = $zip->listContent()) != 0) {
1881 foreach ($list as $obj) {
1882 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test.html' == $obj['stored_filename'] && 131 == $obj['size']) $found_first=true;
1883 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test2.html' == $obj['stored_filename'] && 138 == $obj['size']) $found_second=true;
1884 }
1885 }
1886 } else {
1887 // PclZip will die() if gzopen is not found
1888 // 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
1889 $this->log("gzopen function not found; PclZip cannot be invoked; will assume that binary zip works if we have a non-zero file");
1890 if (filesize($updraft_dir.'/binziptest/test.zip') > 0) {
1891 $found_first = true;
1892 $found_second = true;
1893 }
1894 }
1895 }
1896 $this->remove_binzip_test_files($updraft_dir);
1897 if ($found_first && $found_second) {
1898 if ($log_it) $this->log("Working binary zip found: $potzip");
1899 if ($cacheit) $this->jobdata_set('binzip', $potzip);
1900 return $potzip;
1901 }
1902
1903 }
1904 $this->remove_binzip_test_files($updraft_dir);
1905 }
1906 if ($cacheit) $this->jobdata_set('binzip', false);
1907 return false;
1908 }
1909
1910 /**
1911 * Remove potentially existing test files after binzip testing
1912 *
1913 * @param String $updraft_dir - directory to find the files in
1914 */
1915 private function remove_binzip_test_files($updraft_dir) {
1916 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test.html');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1917 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test2.html');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1918 @rmdir($updraft_dir.'/binziptest/subdir1/subdir2');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1919 @rmdir($updraft_dir.'/binziptest/subdir1');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1920 @unlink($updraft_dir.'/binziptest/test.zip');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1921 @rmdir($updraft_dir.'/binziptest');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1922 }
1923
1924 public function option_filter_get($which) {
1925 global $wpdb;
1926 $row = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $which));
1927 // Has to be get_row instead of get_var because of funkiness with 0, false, null values
1928 return (is_object($row)) ? $row->option_value : false;
1929 }
1930
1931 /**
1932 * Indicate which checksums to take for backup files. Abstracted for extensibilty and future changes.
1933 *
1934 * @returns array - a list of hashing algorithms, as understood by PHP's hash() function
1935 */
1936 public function which_checksums() {
1937 return apply_filters('updraftplus_which_checksums', array('sha1', 'sha256'));
1938 }
1939
1940 /**
1941 * Pretty printing of the raw backup information
1942 *
1943 * @param String $description
1944 * @param Array $history
1945 * @param String $entity
1946 * @param Array $checksums
1947 * @param Array $jobdata
1948 * @param Boolean $smaller
1949 * @return String
1950 */
1951 public function printfile($description, $history, $entity, $checksums, $jobdata, $smaller = false) {
1952
1953 if (empty($history[$entity])) return;
1954
1955 // PHP 7.2+ throws a warning if you try to count() a string
1956 $how_many = is_string($history[$entity]) ? 1 : count($history[$entity]);
1957
1958 if ($smaller) {
1959 $pfiles = "<strong>".$description." (".sprintf(__('files: %s', 'updraftplus'), $how_many).")</strong><br>\n";
1960 } else {
1961 $pfiles = "<h3>".$description." (".sprintf(__('files: %s', 'updraftplus'), $how_many).")</h3>\n\n";
1962 }
1963
1964 $is_incremental = (!empty($jobdata) && !empty($jobdata['job_type']) && 'incremental' == $jobdata['job_type'] && 'db' != substr($entity, 0, 2)) ? true : false;
1965
1966 if ($is_incremental) {
1967 $backup_timestamp = $jobdata['backup_time'];
1968 $backup_history = UpdraftPlus_Backup_History::get_history($backup_timestamp);
1969 $pfiles .= "<dl>";
1970 foreach ($backup_history['incremental_sets'] as $timestamp => $backup) {
1971 if (isset($backup[$entity])) {
1972 $pfiles .= "<dt>".get_date_from_gmt(gmdate('Y-m-d H:i:s', (int) $timestamp), 'M d, Y G:i')."\n</dt>\n";
1973 foreach ($backup[$entity] as $ind => $file) {
1974 $pfiles .= "<dd>".$this->get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind)."\n</dd>\n";
1975 }
1976 }
1977 }
1978 $pfiles .= "</dl>\n";
1979 } else {
1980
1981 $pfiles .= "<ul>";
1982 $files = $history[$entity];
1983 if (is_string($files)) $files = array($files);
1984
1985 foreach ($files as $ind => $file) {
1986 $pfiles .= "<li>".$this->get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind)."\n</li>\n";
1987 }
1988 $pfiles .= "</ul>\n";
1989 }
1990
1991 return $pfiles;
1992 }
1993
1994 /**
1995 * This function will use the passed in information to prepare a pretty string describing the backup from the raw backup history
1996 *
1997 * @param String $file - the backup file
1998 * @param Array $history - the backup history
1999 * @param String $entity - the backup entity
2000 * @param Array $checksums - checksums for the backup file
2001 * @param Array $jobdata - the jobdata for this backup
2002 * @param Integer $ind - the index of the file
2003 *
2004 * @return String - returns the entity output string
2005 */
2006 public function get_entity_row($file, $history, $entity, $checksums, $jobdata, $ind) {
2007 $op = htmlspecialchars($file);
2008 $skey = $entity.((0 == $ind) ? '' : $ind).'-size';
2009
2010 $op = apply_filters('updraft_report_downloadable_file_link', $op, $entity, $ind, $jobdata);
2011
2012 $op .= "\n";
2013
2014 $meta = '';
2015 if ('db' == substr($entity, 0, 2) && 'db' != $entity) {
2016 $dind = substr($entity, 2);
2017 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'])) {
2018 $dbinfo = $jobdata['backup_database'][$dind]['dbinfo'];
2019 $meta .= sprintf(__('External database (%s)', 'updraftplus'), $dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'])."<br>";
2020 }
2021 }
2022 if (isset($history[$skey])) $meta .= sprintf(__('Size: %s MB', 'updraftplus'), round($history[$skey]/1048576, 1));
2023 $ckey = $entity.$ind;
2024 foreach ($checksums as $ck) {
2025 $ck_plain = false;
2026 if (isset($history['checksums'][$ck][$ckey])) {
2027 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey]);
2028 $ck_plain = true;
2029 }
2030 if (isset($history['checksums'][$ck][$ckey.'.crypt'])) {
2031 if ($ck_plain) $meta .= ' '.__('(when decrypted)');
2032 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey.'.crypt']);
2033 }
2034 }
2035
2036 $fileinfo = apply_filters("updraftplus_fileinfo_$entity", array(), $ind);
2037 if (is_array($fileinfo) && !empty($fileinfo)) {
2038 if (isset($fileinfo['html'])) {
2039 $meta .= $fileinfo['html'];
2040 }
2041 }
2042
2043 // if ($meta) $meta = " ($meta)";
2044 if ($meta) $meta = "<br><em>$meta</em>";
2045
2046 return $op.$meta;
2047 }
2048
2049 /**
2050 * 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
2051 *
2052 * @param boolean $include_others
2053 * @param boolean $full_info
2054 * @return array
2055 */
2056 public function get_backupable_file_entities($include_others = true, $full_info = false) {
2057
2058 $wp_upload_dir = $this->wp_upload_dir();
2059
2060 if ($full_info) {
2061 $arr = array(
2062 'plugins' => array('path' => untrailingslashit(WP_PLUGIN_DIR), 'description' => __('Plugins', 'updraftplus'), 'singular_description' => __('Plugin', 'updraftplus')),
2063 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes', 'updraftplus'), 'singular_description' => __('Theme', 'updraftplus')),
2064 'uploads' => array('path' => untrailingslashit($wp_upload_dir['basedir']), 'description' => __('Uploads', 'updraftplus'))
2065 );
2066 } else {
2067 $arr = array(
2068 'plugins' => untrailingslashit(WP_PLUGIN_DIR),
2069 'themes' => WP_CONTENT_DIR.'/themes',
2070 'uploads' => untrailingslashit($wp_upload_dir['basedir'])
2071 );
2072 }
2073
2074 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
2075
2076 // We then add 'others' on to the end
2077 if ($include_others) {
2078 if ($full_info) {
2079 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others', 'updraftplus'));
2080 } else {
2081 $arr['others'] = WP_CONTENT_DIR;
2082 }
2083 }
2084
2085 // Entries that should be added after 'others'
2086 $arr = apply_filters('updraft_backupable_file_entities_final', $arr, $full_info);
2087
2088 return $arr;
2089
2090 }
2091
2092 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
2093 switch ($errno) {
2094 case 1:
2095 $e_type = 'E_ERROR';
2096 break;
2097 case 2:
2098 $e_type = 'E_WARNING';
2099 break;
2100 case 4:
2101 $e_type = 'E_PARSE';
2102 break;
2103 case 8:
2104 $e_type = 'E_NOTICE';
2105 break;
2106 case 16:
2107 $e_type = 'E_CORE_ERROR';
2108 break;
2109 case 32:
2110 $e_type = 'E_CORE_WARNING';
2111 break;
2112 case 64:
2113 $e_type = 'E_COMPILE_ERROR';
2114 break;
2115 case 128:
2116 $e_type = 'E_COMPILE_WARNING';
2117 break;
2118 case 256:
2119 $e_type = 'E_USER_ERROR';
2120 break;
2121 case 512:
2122 $e_type = 'E_USER_WARNING';
2123 break;
2124 case 1024:
2125 $e_type = 'E_USER_NOTICE';
2126 break;
2127 case 2048:
2128 $e_type = 'E_STRICT';
2129 break;
2130 case 4096:
2131 $e_type = 'E_RECOVERABLE_ERROR';
2132 break;
2133 case 8192:
2134 $e_type = 'E_DEPRECATED';
2135 break;
2136 case 16384:
2137 $e_type = 'E_USER_DEPRECATED';
2138 break;
2139 case 30719:
2140 $e_type = 'E_ALL';
2141 break;
2142 default:
2143 $e_type = "E_UNKNOWN ($errno)";
2144 break;
2145 }
2146
2147 if (false !== stripos($errstr, 'table which is not valid in this version of Gravity Forms')) return false;
2148
2149 if (!is_string($errstr)) $errstr = serialize($errstr);
2150
2151 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
2152
2153 if ('E_DEPRECATED' == $e_type && !empty($this->no_deprecation_warnings)) {
2154 return false;
2155 }
2156
2157 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
2158
2159 }
2160
2161 public function php_error($errno, $errstr, $errfile, $errline) {
2162 if (0 == error_reporting()) return true;
2163 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
2164 if (false !== $logline) $this->log($logline, 'notice', 'php_event');
2165 // Pass it up the chain
2166 return $this->error_reporting_stop_when_logged;
2167 }
2168
2169 /**
2170 * Proceed with a backup; before calling this, at least all the initial job data must be set up
2171 *
2172 * @param Integer $resumption_no - which resumption this is; from 0 upwards
2173 * @param String $bnonce - the backup job identifier
2174 */
2175 public function backup_resume($resumption_no, $bnonce) {
2176
2177 // Theoretically (N.B. has been seen in the real world), the WP scheduler might call us more than once within the same context (e.g. an incremental run followed by a main backup resumption), leaving us with incorrect internal state if we don't reset.
2178 static $last_bnonce = null;
2179 if ($last_bnonce) $this->jobdata_reset();
2180 $last_bnonce = $bnonce;
2181
2182 set_error_handler(array($this, 'php_error'), E_ALL & ~E_STRICT);
2183
2184 $this->current_resumption = $resumption_no;
2185
2186 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2187 if (function_exists('ignore_user_abort')) @ignore_user_abort(true);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2188
2189 $runs_started = array();
2190 $time_now = microtime(true);
2191
2192 UpdraftPlus_Backup_History::always_get_from_db();
2193
2194 // Restore state
2195 $resumption_extralog = '';
2196 $prev_resumption = $resumption_no - 1;
2197 $last_successful_resumption = -1;
2198 $job_type = 'backup';
2199
2200 if (0 == $resumption_no) {
2201 $label = $this->jobdata_get('label');
2202 if ($label) $resumption_extralog = apply_filters('updraftplus_autobackup_extralog', ", label=$label");
2203 } else {
2204 $this->nonce = $bnonce;
2205 $file_nonce = $this->jobdata_get('file_nonce');
2206 $this->file_nonce = $file_nonce ? $file_nonce : $bnonce;
2207 $this->backup_time = $this->jobdata_get('backup_time');
2208 $this->job_time_ms = $this->jobdata_get('job_time_ms');
2209
2210 // 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)
2211 $warnings = $this->jobdata_get('warnings');
2212
2213 $this->logfile_open($this->file_nonce);
2214
2215 if (!$this->get_backup_job_semaphore_lock($this->nonce, $resumption_no)) {
2216 $this->log('Failed to get backup job lock; possible overlapping resumptions - will abort this instance');
2217 die;
2218 }
2219
2220 // 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
2221 if (is_array($warnings)) {
2222 foreach ($warnings as $warning) {
2223 $this->errors[] = array('level' => 'warning', 'message' => $warning);
2224 }
2225 }
2226
2227 $runs_started = $this->jobdata_get('runs_started');
2228 if (!is_array($runs_started)) $runs_started =array();
2229 $time_passed = $this->jobdata_get('run_times');
2230 if (!is_array($time_passed)) $time_passed = array();
2231
2232 foreach ($time_passed as $run => $passed) {
2233 if (isset($runs_started[$run]) && $runs_started[$run] + $time_passed[$run] + 30 > $time_now) {
2234 // We don't want to increase the resumption if WP has started two copies of the same resumption off
2235 if ($run && $run == $resumption_no) {
2236 $increase_resumption = false;
2237 $this->log("It looks like WordPress's scheduler has started multiple instances of this resumption");
2238 } else {
2239 $increase_resumption = true;
2240 }
2241 UpdraftPlus_Job_Scheduler::terminate_due_to_activity('check-in', round($time_now, 1), round($runs_started[$run] + $time_passed[$run], 1), $increase_resumption);
2242 }
2243 }
2244
2245 $useful_checkins = $this->jobdata_get('useful_checkins', array());
2246 if (!empty($useful_checkins)) {
2247 $last_successful_resumption = min(max($useful_checkins), $prev_resumption);
2248 }
2249
2250 if (isset($time_passed[$prev_resumption])) {
2251 // N.B. A check-in occurred; we haven't yet tested if it was useful
2252 $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 2)."s";
2253 }
2254
2255 // This is just a simple test to catch restorations of old backup sets where the backup includes a resumption of the backup job
2256 if ($time_now - $this->backup_time > 172800 && true == apply_filters('updraftplus_check_obsolete_backup', true, $time_now, $this)) {
2257
2258 // 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.
2259 if (empty($this->backup_time) && empty($this->backup_is_already_complete) && !empty($this->logfile_name) && is_readable($this->logfile_name)) {
2260 $first_log_bit = file_get_contents($this->logfile_name, false, null, 0, 250);
2261 if (preg_match('/\(0\) Opened log file at time: (.*) on /', $first_log_bit, $matches)) {
2262 $first_opened = strtotime($matches[1]);
2263 // 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.
2264 if (time() - $first_opened < 1000) {
2265 $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)).")");
2266 UpdraftPlus_Job_Scheduler::reschedule(120);
2267 die;
2268 }
2269 }
2270 }
2271
2272 // If we are doing a local upload then we do not want to abort the backup as it's possible they are uploading a backup that is older than two days
2273 if (empty($this->jobdata['local_upload'])) {
2274 $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)) . ")");
2275 die;
2276 }
2277 }
2278
2279 }
2280
2281 $this->last_successful_resumption = $last_successful_resumption;
2282
2283 $runs_started[$resumption_no] = $time_now;
2284 if (!empty($this->backup_time)) $this->jobdata_set('runs_started', $runs_started);
2285
2286 // Schedule again, to run in 5 minutes again, in case we again fail
2287 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
2288 $resume_interval = max((int) $this->jobdata_get('resume_interval'), 100);
2289
2290 $btime = $this->backup_time;
2291
2292 $job_type = $this->jobdata_get('job_type');
2293
2294 do_action('updraftplus_resume_backup_'.$job_type);
2295
2296 $updraft_dir = $this->backups_dir_location();
2297
2298 $time_ago = time()-$btime;
2299
2300 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, file_nonce=".$this->file_nonce." begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
2301
2302 // 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.
2303 // 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.
2304 if ($resumption_no >= 1 && 'finished' == $this->jobdata_get('jobstatus')) {
2305 $this->log('Terminate: This backup job is already finished (1).');
2306 die;
2307 } elseif ('clouduploading' != $this->jobdata_get('jobstatus') && 'backup' == $job_type && !empty($this->backup_is_already_complete)) {
2308 $this->jobdata_set('jobstatus', 'finished');
2309 $this->log('Terminate: This backup job is already finished (2).');
2310 die;
2311 }
2312
2313 if ($resumption_no > 0 && isset($runs_started[$prev_resumption])) {
2314 $our_expected_start = $runs_started[$prev_resumption] + $resume_interval;
2315 // If the previous run increased the resumption time, then it is timed from the end of the previous run, not the start
2316 if (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption] > 0) $our_expected_start += $time_passed[$prev_resumption];
2317 $our_expected_start = apply_filters('updraftplus_expected_start', $our_expected_start, $job_type);
2318 // More than 12 minutes late?
2319 if ($time_now > $our_expected_start + 720) {
2320 $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));
2321 $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');
2322 }
2323 }
2324
2325 $this->jobdata_set('current_resumption', $resumption_no);
2326
2327 $first_run = apply_filters('updraftplus_filerun_firstrun', 0);
2328
2329 // April 2022: a similar situation is handled further down, but takes longer to kick in; so extra check has been added (a case where the first runtime under cli was > 4 hours was followed by running under cgi-fci with only 20 minute resumption times; it's better to detect this early)
2330 if ($resumption_no == $first_run + 1 && $resume_interval >= 600 && '' != PHP_SAPI) {
2331
2332 $last_sapi = $this->jobdata_get('last_sapi');
2333
2334 if ('' != $last_sapi && PHP_SAPI != $last_sapi) {
2335 $resume_interval = $this->get_initial_resume_interval();
2336 $this->log(sprintf("Run environment has changed (%s -> %s) - resetting resumption interval to %d", $last_sapi, PHP_SAPI, $resume_interval));
2337 $this->jobdata_set('last_sapi', PHP_SAPI);
2338 }
2339
2340 // We don't want to be in permanent conflict with the overlap detector
2341 } elseif ($resumption_no >= $first_run + 8 && $resumption_no < $first_run + 15 && $resume_interval >= 300) {
2342
2343 // $time_passed is set earlier
2344 list($max_time, $timings_string, $run_times_known) = UpdraftPlus_Manipulation_Functions::max_time_passed($time_passed, $resumption_no - 1, $first_run);
2345
2346 // Do this on resumption 8, or the first time that we have 6 data points. This is only done once to prevent any potential for back-and-forth.
2347 if (($first_run + 8 == $resumption_no && $run_times_known >= 6) || (6 == $run_times_known && !empty($time_passed[$prev_resumption]))) {
2348 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
2349 // 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
2350 if ($resume_interval > $max_time + 52) {
2351 $resume_interval = round($max_time + 52);
2352 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
2353 $this->jobdata_set('resume_interval', $resume_interval);
2354 }
2355
2356 } elseif (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption] > 50 && $resume_interval > 300 && $time_passed[$prev_resumption] < $resume_interval/2) {
2357 // 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 being allowed to run for a much smaller amount.
2358 // 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.
2359 if ('clouduploading' == $this->jobdata_get('jobstatus')) {
2360 $resume_interval = round($time_passed[$prev_resumption] + 52);
2361 $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");
2362 $this->jobdata_set('resume_interval', $resume_interval);
2363 } elseif ($run_times_known > 4) {
2364 // Added in response to the similar HS#66907 - in that case, resumption 0 ran for over an hour; nothing subsequently for more than ~3 minutes; and it didn't reach the uploading stage until resumption 19, so the previous fragment was not helping. The cause was that the backup initially started under WP-CLI, but then resumed through the web - different conditions led to different permitted run-times. (The user could also mitigate this by running WP-Cron in a CLI environment).
2365 $examined_values = 0;
2366 $matching_values = 0;
2367 $looking_at_resumption = $prev_resumption - 1;
2368 $largest_recent = false;
2369 while ($looking_at_resumption > 0 && $examined_values < 3) {
2370 if (isset($time_passed[$looking_at_resumption])) {
2371 $examined_values++;
2372 if ($time_passed[$looking_at_resumption] > 50 && $time_passed[$looking_at_resumption] < $resume_interval/2) {
2373 $matching_values++;
2374 $largest_recent = max($largest_recent, $time_passed[$looking_at_resumption]);
2375 }
2376 }
2377 $looking_at_resumption--;
2378 }
2379 // If the previous three found values were all less than half the resumption interval....
2380 if (3 == $examined_values && 3 == $matching_values) {
2381 $resume_interval = round($largest_recent + 52);
2382 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time). Based on the available data (most recent 3 resumptions compared to longest), we are bringing the resumption interval down to: $resume_interval seconds");
2383 $this->jobdata_set('resume_interval', $resume_interval);
2384 }
2385 }
2386 }
2387
2388 }
2389
2390 // A different argument than before is needed otherwise the event is ignored
2391 $next_resumption = $resumption_no+1;
2392 if ($next_resumption < $first_run + 10) {
2393 if (true === $this->jobdata_get('one_shot')) {
2394 if (true === $this->jobdata_get('reschedule_before_upload') && 1 == $next_resumption) {
2395 $this->log('A resumption will be scheduled for the cloud backup stage');
2396 $schedule_resumption = true;
2397 } else {
2398 $this->log('We are in "one shot" mode - no resumptions will be scheduled');
2399 }
2400 } else {
2401 $schedule_resumption = true;
2402 }
2403 } else {
2404
2405 // 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
2406 // 'useful_checkin' is < 1.16.35 (Nov 2020). It is only supported here for resumptions that span upgrades. Later it can be removed.
2407 $useful_checkin = max($this->jobdata_get('useful_checkin', 0), max((array) $this->jobdata_get('useful_checkins', 0)));
2408
2409 $last_resumption = $resumption_no - 1;
2410 $fail_on_resume = $this->jobdata_get('fail_on_resume');
2411
2412 if (empty($useful_checkin) || $useful_checkin < $last_resumption) {
2413 if (empty($fail_on_resume)) {
2414 $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));
2415 // Internally, we do actually schedule a resumption; but only in order to be able to nicely handle and log the failure, which otherwise may not be logged
2416 $this->jobdata_set('fail_on_resume', $next_resumption);
2417 $schedule_resumption = 1;
2418 }
2419 } else {
2420 // Something useful happened last time
2421 if (!empty($fail_on_resume)) {
2422 $this->jobdata_delete('fail_on_resume');
2423 $fail_on_resume = false;
2424 }
2425 $schedule_resumption = true;
2426 }
2427
2428 if (!isset($time_passed[$prev_resumption])) {
2429 $this->no_checkin_last_time = true;
2430 }
2431
2432 if (!empty($fail_on_resume) && $fail_on_resume == $this->current_resumption) {
2433 $this->log('The backup is being aborted for a repeated failure to progress.', 'updraftplus');
2434 $this->log(__('The backup is being aborted for a repeated failure to progress.', 'updraftplus'), 'error');
2435 $this->backup_finish(true, true);
2436 die;
2437 }
2438 }
2439
2440 // Sanity check
2441 if (empty($this->backup_time)) {
2442 $this->log('The backup_time parameter appears to be empty (usually caused by resuming an already-complete backup).');
2443 return false;
2444 }
2445
2446 if (!empty($schedule_resumption)) {
2447 $schedule_for = time() + $resume_interval;
2448 if (1 === $schedule_resumption) {
2449 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for); but the job will then be aborted unless something happens this time");
2450 $this->resumption_scheduled_for_cleanup = true;
2451 } else {
2452 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
2453 }
2454 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
2455 $this->newresumption_scheduled = $schedule_for;
2456 }
2457
2458 $backup_files = $this->jobdata_get('backup_files');
2459
2460 global $updraftplus_backup;
2461 // Bring in all the backup routines
2462 include_once(UPDRAFTPLUS_DIR.'/backup.php');
2463 $updraftplus_backup = new UpdraftPlus_Backup($backup_files, apply_filters('updraftplus_files_altered_since', -1, $job_type));
2464
2465 $undone_files = array();
2466
2467 if ('no' == $backup_files) {
2468 $this->log('This backup run is not intended for files - skipping');
2469 $our_files = array();
2470 } else {
2471 try {
2472 // This should be always called; if there were no files in this run, it returns us an empty array
2473 $backup_array = $updraftplus_backup->resumable_backup_of_files($resumption_no);
2474 // This save, if there was something, is then immediately picked up again
2475 if (is_array($backup_array)) {
2476 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
2477 $this->save_backup_to_history($backup_array);
2478 }
2479
2480 // Switch of variable name is purely vestigial
2481 $our_files = $backup_array;
2482 if (!is_array($our_files)) $our_files = array();
2483 } catch (Exception $e) {
2484 $log_message = 'Exception ('.get_class($e).') occurred during files backup: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2485 error_log($log_message);
2486 // @codingStandardsIgnoreLine
2487 $log_message .= ' Backtrace: '.str_replace(array(ABSPATH, "\n"), array('', ', '), $e->getTraceAsString());
2488 $this->log($log_message);
2489 $this->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2490 die();
2491 // @codingStandardsIgnoreLine
2492 } catch (Error $e) {
2493 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2494 error_log($log_message);
2495 // @codingStandardsIgnoreLine
2496 $log_message .= ' Backtrace: '.str_replace(array(ABSPATH, "\n"), array('', ', '), $e->getTraceAsString());
2497 $this->log($log_message);
2498 $this->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2499 die();
2500 }
2501
2502 }
2503
2504 do_action('pre_database_backup_setup');
2505
2506 $backup_databases = $this->jobdata_get('backup_database');
2507
2508 if (!is_array($backup_databases)) $backup_databases = array('wp' => $backup_databases);
2509
2510 foreach ($backup_databases as $whichdb => $backup_database) {
2511
2512 if (is_array($backup_database)) {
2513 $dbinfo = $backup_database['dbinfo'];
2514 $backup_database = $backup_database['status'];
2515 } else {
2516 $dbinfo = array();
2517 }
2518
2519 $tindex = ('wp' == $whichdb) ? 'db' : 'db'.$whichdb;
2520
2521 if ('begun' == $backup_database || 'finished' == $backup_database || 'encrypted' == $backup_database) {
2522
2523 if ('wp' == $whichdb) {
2524 $db_descrip = 'WordPress DB';
2525 } else {
2526 if (!empty($dbinfo) && is_array($dbinfo) && !empty($dbinfo['host'])) {
2527 $db_descrip = "External DB $whichdb - ".$dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'];
2528 } else {
2529 $db_descrip = "External DB $whichdb - details appear to be missing";
2530 }
2531 }
2532
2533 if ('begun' == $backup_database) {
2534 if ($resumption_no > 0) {
2535 $this->log("Resuming creation of database dump ($db_descrip)");
2536 } else {
2537 $this->log("Beginning creation of database dump ($db_descrip)");
2538 }
2539 } elseif ('encrypted' == $backup_database) {
2540 $this->log("Database dump ($db_descrip): Creation and encryption were completed already");
2541 } else {
2542 $this->log("Database dump ($db_descrip): Creation was completed already");
2543 }
2544
2545 if ('wp' != $whichdb && (empty($dbinfo) || !is_array($dbinfo) || empty($dbinfo['host']))) {
2546 unset($backup_databases[$whichdb]);
2547 $this->jobdata_set('backup_database', $backup_databases);
2548 continue;
2549 }
2550
2551 // Catch fatal errors through try/catch blocks around the database backup
2552 try {
2553 $db_backup = $updraftplus_backup->backup_db($backup_database, $whichdb, $dbinfo);
2554 } catch (Exception $e) {
2555 $log_message = 'Exception ('.get_class($e).') occurred during files backup: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2556 $this->log($log_message);
2557 error_log($log_message);
2558 $this->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2559 die();
2560 // @codingStandardsIgnoreLine
2561 } catch (Error $e) {
2562 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2563 $this->log($log_message);
2564 error_log($log_message);
2565 $this->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2566 die();
2567 }
2568
2569 if (is_array($our_files) && is_string($db_backup)) $our_files[$tindex] = $db_backup;
2570
2571 if ('encrypted' != $backup_database) {
2572 $backup_databases[$whichdb] = array('status' => 'finished', 'dbinfo' => $dbinfo);
2573 $this->jobdata_set('backup_database', $backup_databases);
2574 }
2575 } elseif ('no' == $backup_database) {
2576 $this->log("No database backup ($whichdb) - not part of this run");
2577 } else {
2578 $this->log("Unrecognised data when trying to ascertain if the database ($whichdb) was backed up (".serialize($backup_database).")");
2579 }
2580
2581 // 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.
2582 $this->save_backup_to_history($our_files);
2583
2584 // Potentially encrypt the database if it is not already
2585 if ('no' != $backup_database && isset($our_files[$tindex]) && !preg_match("/\.crypt$/", $our_files[$tindex]) && 'incremental' != $job_type) {
2586 $our_files[$tindex] = $updraftplus_backup->encrypt_file($our_files[$tindex]);
2587 // No need to save backup history now, as it will happen in a few lines time
2588 if (preg_match("/\.crypt$/", $our_files[$tindex])) {
2589 $backup_databases[$whichdb] = array('status' => 'encrypted', 'dbinfo' => $dbinfo);
2590 $this->jobdata_set('backup_database', $backup_databases);
2591 }
2592 }
2593
2594 if ('no' != $backup_database && isset($our_files[$tindex]) && file_exists($updraft_dir.'/'.$our_files[$tindex])) {
2595 $our_files[$tindex.'-size'] = filesize($updraft_dir.'/'.$our_files[$tindex]);
2596 $this->save_backup_to_history($our_files);
2597 }
2598
2599 }
2600
2601 $backupable_entities = $this->get_backupable_file_entities(true);
2602
2603 $checksum_list = $this->which_checksums();
2604
2605 $checksums = array();
2606
2607 foreach ($checksum_list as $checksum) {
2608 $checksums[$checksum] = array();
2609 }
2610
2611 $total_size = 0;
2612
2613 // Queue files for upload
2614 foreach ($our_files as $key => $files) {
2615 // Only continue if the stored info was about a dump
2616 if (!isset($backupable_entities[$key]) && ('db' != substr($key, 0, 2) || '-size' == substr($key, -5, 5))) continue;
2617 if (is_string($files)) $files = array($files);
2618 foreach ($files as $findex => $file) {
2619
2620 $size_key = (0 == $findex) ? $key.'-size' : $key.$findex.'-size';
2621 $total_size = (false === $total_size || !isset($our_files[$size_key]) || !is_numeric($our_files[$size_key])) ? false : $total_size + $our_files[$size_key];
2622
2623 foreach ($checksum_list as $checksum) {
2624
2625 $cksum = $this->jobdata_get($checksum.'-'.$key.$findex);
2626 if ($cksum) $checksums[$checksum][$key.$findex] = $cksum;
2627 $cksum = $this->jobdata_get($checksum.'-'.$key.$findex.'.crypt');
2628 if ($cksum) $checksums[$checksum][$key.$findex.".crypt"] = $cksum;
2629
2630 }
2631
2632 if ($this->is_uploaded($file)) {
2633 $this->log("$file: $key: This file has already been successfully uploaded");
2634 } elseif (is_file($updraft_dir.'/'.$file)) {
2635 if (!in_array($file, $undone_files)) {
2636 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
2637 $undone_files[$key.$findex] = $file;
2638 } else {
2639 $this->log("$file: $key: This file was already queued for upload (this condition should never be seen)");
2640 }
2641 } elseif (!$this->is_ours_to_upload($file, $key)) {
2642 $this->log("$file: $key: This file is not ours to upload and has been/will be handled by another job.");
2643 } else {
2644 $this->log("$file: $key: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem; now marking as uploaded ($updraft_dir/$file)");
2645 $this->uploaded_file($file, true);
2646 }
2647 }
2648 }
2649 $our_files['checksums'] = $checksums;
2650
2651 // Save again (now that we have checksums)
2652 $size_description = (false === $total_size) ? 'Unknown' : UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($total_size);
2653 $this->log("Saving backup history. Total backup size: $size_description");
2654 $this->save_backup_to_history($our_files);
2655 do_action('updraft_final_backup_history', $our_files);
2656
2657 // We finished; so, low memory was not a problem
2658 $this->log_remove_warning('lowram');
2659
2660 if (0 == count($undone_files)) {
2661 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
2662 if (is_array($our_files)) $this->save_last_backup($our_files);
2663 $this->log("There were no more files that needed uploading");
2664 // No email, as the user probably already got one if something else completed the run
2665 $allow_email = false;
2666 if ('begun' == $this->jobdata_get('prune')) {
2667 // Begun, but not finished
2668 $this->log('Restarting backup prune operation');
2669 $updraftplus_backup->do_prune_standalone();
2670 $allow_email = true;
2671 } elseif ('finished' != $this->jobdata_get('prune')) {
2672 // If prune has not begun or finished but we have undone files then start it
2673 $updraftplus_backup->do_prune_standalone();
2674 $allow_email = true;
2675 }
2676
2677 $this->check_upload_completed();
2678
2679 $this->backup_finish(true, $allow_email);
2680 restore_error_handler();
2681 return;
2682 }
2683
2684 $this->error_count_before_cloud_backup = $this->error_count();
2685
2686 // This is intended for one-shot backups, where we do want a resumption if it's only for uploading
2687 if (empty($this->newresumption_scheduled) && 0 == $resumption_no && 0 == $this->error_count_before_cloud_backup && true === $this->jobdata_get('reschedule_before_upload')) {
2688 $this->log("Cloud backup stage reached on one-shot backup: scheduling resumption for the cloud upload");
2689 UpdraftPlus_Job_Scheduler::reschedule(60);
2690 UpdraftPlus_Job_Scheduler::record_still_alive();
2691 }
2692
2693 $this->log("Requesting upload of the files that have not yet been successfully uploaded (".count($undone_files).")");
2694 // Catch fatal errors through try/catch blocks around the upload to remote storage
2695 $updraftplus_backup->cloud_backup($undone_files);
2696
2697 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
2698 if (is_array($our_files)) $this->save_last_backup($our_files);
2699 $this->backup_finish(true, true);
2700
2701 restore_error_handler();
2702
2703 }
2704
2705 /**
2706 * Get all the job data in a single array
2707 *
2708 * @param String $job_id - the job identifier (nonce) for the job whose data is to be retrieved
2709 *
2710 * @return Array
2711 */
2712 public function jobdata_getarray($job_id) {
2713 return get_site_option('updraft_jobdata_'.$job_id, array());
2714 }
2715
2716 public function jobdata_set_from_array($array) {
2717 $this->jobdata = $array;
2718 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
2719 }
2720
2721 /**
2722 * This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
2723 * You can list the keys/values (keys must be strings) as consecutive/alternating parameters, or send them all in as an array (with no other parameters)
2724 *
2725 * @return null
2726 */
2727 public function jobdata_set_multi() {
2728 if (!is_array($this->jobdata)) $this->jobdata = array();
2729
2730 $args = func_num_args();
2731
2732 // func_get_arg() could not be used in parameter lists prior to PHP 5.3, so, we get it as a variable
2733 if (1 == $args && null !== ($first_arg = func_get_arg(0)) && is_array($first_arg)) {
2734 foreach ($first_arg as $key => $value) {
2735 $this->jobdata[$key] = $value;
2736 }
2737 } else {
2738
2739 for ($i=1; $i<=$args/2; $i++) {
2740 $key = func_get_arg($i*2-2);
2741 $value = func_get_arg($i*2-1);
2742 $this->jobdata[$key] = $value;
2743 }
2744
2745 }
2746 if (!empty($this->nonce)) update_site_option('updraft_jobdata_'.$this->nonce, $this->jobdata);
2747 }
2748
2749 /**
2750 * Set a job-data key/value pair for the current job
2751 *
2752 * @param String $key - the key
2753 * @param Mixed $value - needs to be serializable
2754 *
2755 * @uses update_site_option()
2756 */
2757 public function jobdata_set($key, $value) {
2758 if (empty($this->jobdata)) {
2759 $this->jobdata = empty($this->nonce) ? array() : get_site_option('updraft_jobdata_'.$this->nonce);
2760 if (!is_array($this->jobdata)) $this->jobdata = array();
2761 }
2762 $this->jobdata[$key] = $value;
2763 if ($this->nonce) update_site_option('updraft_jobdata_'.$this->nonce, $this->jobdata);
2764 }
2765
2766 /**
2767 * Delete a jobdata item, by key
2768 *
2769 * @param String $key
2770 */
2771 public function jobdata_delete($key) {
2772 if (!is_array($this->jobdata)) {
2773 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce);
2774 if (!is_array($this->jobdata)) $this->jobdata = array();
2775 }
2776 unset($this->jobdata[$key]);
2777 if ($this->nonce) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
2778 }
2779
2780 public function get_job_option($opt) {
2781 // These are meant to be read-only
2782 if (empty($this->jobdata['option_cache']) || !is_array($this->jobdata['option_cache'])) {
2783 if (!is_array($this->jobdata) && $this->nonce) $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
2784 $this->jobdata['option_cache'] = array();
2785 }
2786 return isset($this->jobdata['option_cache'][$opt]) ? $this->jobdata['option_cache'][$opt] : UpdraftPlus_Options::get_updraft_option($opt);
2787 }
2788
2789 /**
2790 * Get a job data item, or the specified default if it is not yet set
2791 *
2792 * @param String $key
2793 * @param Mixed $default
2794 *
2795 * @return Mixed
2796 */
2797 public function jobdata_get($key, $default = null) {
2798 if (empty($this->jobdata)) {
2799 $this->jobdata = empty($this->nonce) ? array() : get_site_option('updraft_jobdata_'.$this->nonce, array());
2800 if (!is_array($this->jobdata)) return $default;
2801 }
2802 return isset($this->jobdata[$key]) ? $this->jobdata[$key] : $default;
2803 }
2804
2805 /**
2806 * Reset the job data for the currently active job (forcing a re-fetch from the database, if there is any)
2807 */
2808 public function jobdata_reset() {
2809 $this->jobdata = null;
2810 }
2811
2812 /**
2813 * Gets an instance of the "UpdraftPlus_Clone" class which will be
2814 * used to login the user to UpdraftPlus.com
2815 *
2816 * @return object
2817 */
2818 public function get_updraftplus_clone() {
2819 if (!class_exists('UpdraftPlus_Clone')) include_once(UPDRAFTPLUS_DIR.'/includes/updraftplus-clone.php');
2820 return new UpdraftPlus_Clone();
2821 }
2822
2823 /**
2824 * This function will add data to the backup options that is needed for the clone backup job
2825 *
2826 * @param array $options - the backup options array
2827 * @param array $request - the extra data we want to add to the backup options
2828 *
2829 * @return array - the backup options array with the extra data added
2830 */
2831 public function updraftplus_clone_backup_options($options, $request) {
2832 if (!is_array($options)) return $options;
2833
2834 if (!empty($request['clone_id']) && !empty($request['secret_token'])) {
2835 $options['clone_id'] = $request['clone_id'];
2836 $options['secret_token'] = $request['secret_token'];
2837 }
2838
2839 if (isset($request['clone_url'])) $options['clone_url'] = $request['clone_url'];
2840 if (isset($request['key'])) $options['key'] = $request['key'];
2841 if (isset($request['backup_nonce']) && isset($request['backup_timestamp'])) {
2842 if ('current' != $request['backup_nonce'] && 'current' != $request['backup_timestamp']) {
2843 $options['use_nonce'] = $request['backup_nonce'];
2844 $options['use_timestamp'] = $request['backup_timestamp'];
2845 } else {
2846 $options['clone_backup'] = 'current';
2847 }
2848 }
2849
2850 return $options;
2851 }
2852
2853 /**
2854 * This function will set up the backup job data for when we are starting a clone backup job. It changes the initial jobdata so that UpdraftPlus knows it's a clone job and adds the needed information for to lookup the clone and if we have it the URL and migration key for the clone.
2855 *
2856 * @param array $jobdata - the initial job data that we want to change
2857 * @param array $options - options sent from the front end includes the clone id, secret token and maybe clone url and migration key
2858 * @param Integer $split_every - the size we should split the zips at
2859 *
2860 * @return array - the modified jobdata
2861 */
2862 public function updraftplus_clone_backup_jobdata($jobdata, $options, $split_every) {
2863
2864 if (!is_array($jobdata)) return $jobdata;
2865
2866 if (!isset($options['clone_id']) && !isset($options['secret_token']) && !isset($options['clone_url']) && !isset($options['key'])) return $jobdata;
2867
2868 $option_cache_key = array_search('option_cache', $jobdata) + 1;
2869 $option_cache = $jobdata[$option_cache_key];
2870 $option_cache['updraft_encryptionphrase'] = '';
2871 $jobdata[$option_cache_key] = $option_cache;
2872
2873 // Reduce to 100MB if it was above. Since the user isn't expected to directly manipulate these zip files, the potentially higher number of zip files doesn't matter.
2874 $split_every_key = array_search('split_every', $jobdata) + 1;
2875 if ($split_every > 100) $jobdata[$split_every_key] = 100;
2876
2877 $service_key = array_search('service', $jobdata) + 1;
2878 $jobdata[$service_key] = array('remotesend');
2879
2880 $backup_database_key = array_search('backup_database', $jobdata) + 1;
2881 $db_backups = $jobdata[$backup_database_key];
2882
2883 foreach (array_keys($db_backups) as $key) {
2884 if ('wp' != $key) unset($db_backups[$key]);
2885 }
2886
2887 $jobdata[] = 'clone_job';
2888 $jobdata[] = true;
2889 $jobdata[] = 'clone_id';
2890 $jobdata[] = $options['clone_id'];
2891 $jobdata[] = 'secret_token';
2892 $jobdata[] = $options['secret_token'];
2893 $jobdata[] = 'clone_url';
2894 $jobdata[] = $options['clone_url'];
2895 $jobdata[] = 'clone_key';
2896 $jobdata[] = $options['key'];
2897 $jobdata[] = 'remotesend_info';
2898 $jobdata[] = array('url' => $options['clone_url']);
2899 $jobdata[$backup_database_key] = $db_backups;
2900
2901 // if clone_backup is set and is 'current' then theres nothing more that needs to be done, otherwise we need to tweak some more jobdata to skip to the upload stage and use the specified clone backup
2902 if (isset($options['clone_backup']) && 'current' == $options['clone_backup']) return $jobdata;
2903
2904 global $updraftplus_admin;
2905
2906 add_filter('updraftplus_get_backup_file_basename_from_time', array($updraftplus_admin, 'upload_local_backup_name'), 10, 3);
2907
2908 $backup_history = UpdraftPlus_Backup_History::get_history();
2909 $backup = $backup_history[$options['use_timestamp']];
2910
2911 $jobstatus_key = array_search('jobstatus', $jobdata) + 1;
2912 $backup_time_key = array_search('backup_time', $jobdata) + 1;
2913 $backup_files_key = array_search('backup_files', $jobdata) + 1;
2914
2915 $db_backups = $jobdata[$backup_database_key];
2916 $db_backup_info = $this->update_database_jobdata($db_backups, $backup);
2917 $skip_entities = array('more', 'wpcore');
2918 $file_backups = $this->update_files_jobdata($backup, $skip_entities);
2919
2920 $jobdata[$jobstatus_key] = 'clouduploading';
2921 $jobdata[$backup_time_key] = $options['use_timestamp'];
2922 $jobdata[$backup_files_key] = 'finished';
2923 $jobdata[] = 'backup_files_array';
2924 $jobdata[] = $file_backups;
2925 $jobdata[] = 'blog_name';
2926 $jobdata[] = $db_backup_info['blog_name'];
2927 $jobdata[$backup_database_key] = $db_backup_info['db_backups'];
2928 $jobdata[] = 'local_upload';
2929 $jobdata[] = true;
2930
2931 return $jobdata;
2932 }
2933
2934 /**
2935 * This function will update the database backup jobdata and set each entity to finished or encrypted to prevent that entity from being backed up again. This will also return the blog name that the database backup belongs to, just in case it's from another site.
2936 *
2937 * @param array $db_backups - the database backup jobdata
2938 * @param array $backup - the backup history for this backup
2939 *
2940 * @return array - an array that contains the updated database backup jobdata and the blog name
2941 */
2942 public function update_database_jobdata($db_backups, $backup) {
2943
2944 $backup_database_info = array(
2945 'blog_name' => '',
2946 'db_backups' => $db_backups
2947 );
2948
2949 if (!is_array($db_backups)) return $backup_database_info;
2950
2951 /*
2952 We need to tweak the database array here by setting each database entity to finished or encrypted if it's an encrypted archive.
2953 I also grab the backups blog name here ready to be used later, just in case this backup set is from another site.
2954 */
2955 foreach ($db_backups as $key => $db_info) {
2956 $status = 'finished';
2957 $db_index = ('wp' == $key) ? '' : $key;
2958
2959 if (isset($backup['db'.$db_index])) {
2960 $db_backup_name = $backup['db'.$db_index];
2961
2962 if (preg_match('/^backup_([\-0-9]{15})_(.*)_([0-9a-f]{12})-[\-a-z]+([0-9]+)?+(\.(zip|gz|gz\.crypt))?$/i', $db_backup_name, $matches)) {
2963 $backup_database_info['blog_name'] = $matches[2];
2964 }
2965
2966 if (UpdraftPlus_Encryption::is_file_encrypted($db_backup_name)) $status = 'encrypted';
2967
2968 if (is_array($db_info) && isset($db_info['status'])) {
2969 $db_backups[$key]['status'] = $status;
2970 } else {
2971 $db_backups[$key] = $status;
2972 }
2973 } else {
2974 unset($db_backups[$key]);
2975 }
2976 }
2977
2978 $backup_database_info['db_backups'] = $db_backups;
2979
2980 return $backup_database_info;
2981 }
2982
2983 /**
2984 * This function will update the files backup jobdata by constructing the backup entities and their sizes from the backup
2985 *
2986 * @param array $backup - the backup array
2987 * @param array $skip_entities - an array of entities to skip
2988 *
2989 * @return array - the files backup array
2990 */
2991 public function update_files_jobdata($backup, $skip_entities = array()) {
2992
2993 $file_backups = array();
2994 $backupable_entities = $this->get_backupable_file_entities(true);
2995
2996 // We need to construct the expected files array here, this gets added to the jobdata much later in the backup process but we need this before we start
2997 foreach ($backupable_entities as $entity => $path) {
2998 if (in_array($entity, $skip_entities)) continue;
2999 if (isset($backup[$entity])) $file_backups[$entity] = $backup[$entity];
3000 if (isset($backup[$entity . '-size'])) $file_backups[$entity . '-size'] = $backup[$entity . '-size'];
3001 }
3002
3003 return $file_backups;
3004 }
3005
3006 /**
3007 * Start a files backup (used by WP cron)
3008 */
3009 public function backup_files() {
3010 // Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
3011 $this->boot_backup(true, false);
3012 }
3013
3014 /**
3015 * Start a database backup (used by WP cron)
3016 */
3017 public function backup_database() {
3018 // Note that nothing will happen if the file backup had the same schedule
3019 $this->boot_backup(false, true);
3020 }
3021
3022 /**
3023 * Start a files + database backup (used by users manually in WP cron, and 'Backup Now')
3024 *
3025 * @param array $options
3026 * @return Boolean|Void - as for UpdraftPlus::boot_backup()
3027 */
3028 public function backup_all($options) {
3029 $skip_cloud = empty($options['nocloud']) ? false : true;
3030 return $this->boot_backup(1, 1, false, false, $skip_cloud ? 'none' : false, $options);
3031 }
3032
3033 /**
3034 * Start a files backup
3035 *
3036 * @param array $options
3037 * @return Boolean|Void - as for UpdraftPlus::boot_backup()
3038 */
3039 public function backupnow_files($options) {
3040 $skip_cloud = empty($options['nocloud']) ? false : true;
3041 return $this->boot_backup(1, 0, false, false, $skip_cloud ? 'none' : false, $options);
3042 }
3043
3044 /**
3045 * Start a files backup
3046 *
3047 * @param array $options
3048 * @return Boolean|Void - as for UpdraftPlus::boot_backup()
3049 */
3050 public function backupnow_database($options) {
3051 $skip_cloud = empty($options['nocloud']) ? false : true;
3052 return $this->boot_backup(0, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
3053 }
3054
3055 /**
3056 * This function will try and get a lock for the backup, it will return false if it fails to get a lock.
3057 *
3058 * @param Boolean $backup_files - boolean to indicate if we want a lock for files
3059 * @param Boolean $backup_database - boolean to indicate if we want a lock for the database
3060 *
3061 * @return boolean - boolean to indicate if we got a lock or not
3062 */
3063 public function get_semaphore_lock($backup_files, $backup_database) {
3064
3065 $semaphore = ($backup_files ? 'f' : '') . ($backup_database ? 'd' : '');
3066
3067 if (!class_exists('UpdraftPlus_Semaphore')) include_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
3068
3069 UpdraftPlus_Semaphore::ensure_semaphore_exists($semaphore);
3070
3071 // 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
3072 // doing_action() was added in WP 3.9
3073 // wp_cron() can be called from the 'init' action
3074
3075 if (function_exists('doing_action') && (doing_action('init') || (defined('DOING_CRON') && DOING_CRON)) && (doing_action('updraft_backup_database') || doing_action('updraft_backup'))) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3076 $last_scheduled_action_called_at = get_option("updraft_last_scheduled_$semaphore");
3077 // 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.
3078 $seconds_ago = time() - $last_scheduled_action_called_at;
3079 if ($last_scheduled_action_called_at && $seconds_ago < 660 && apply_filters('updraft_check_repeated_scheduled_backups', true)) {
3080 $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));
3081 return false;
3082 }
3083 }
3084
3085 update_option("updraft_last_scheduled_$semaphore", time());
3086
3087 $this->semaphore = UpdraftPlus_Semaphore::factory();
3088 $this->semaphore->lock_name = $semaphore;
3089
3090 $semaphore_log_message = 'Requesting semaphore lock ('.$semaphore.')';
3091 if (!empty($last_scheduled_action_called_at)) {
3092 $semaphore_log_message .= " (apparently via scheduler: last_scheduled_action_called_at=$last_scheduled_action_called_at, seconds_ago=$seconds_ago)";
3093 } else {
3094 $semaphore_log_message .= " (apparently not via scheduler)";
3095 }
3096
3097 $this->log($semaphore_log_message);
3098 if (!$this->semaphore->lock()) {
3099 $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)');
3100 return false;
3101 }
3102
3103 return true;
3104 }
3105
3106 /**
3107 * This function will try and get a lock for the backup job, it will return false if it fails to get a lock.
3108 *
3109 * @param String $job_nonce - the backup job nonce
3110 * @param Integer $resumption_no - the current resumption
3111 *
3112 * @return boolean - boolean to indicate if we got a lock or not
3113 */
3114 public function get_backup_job_semaphore_lock($job_nonce, $resumption_no) {
3115
3116 $semaphore = $job_nonce;
3117
3118 if (!class_exists('Updraft_Semaphore_3_0')) include_once(UPDRAFTPLUS_DIR.'/includes/class-updraft-semaphore.php');
3119
3120 if (empty($this->backup_semaphore)) {
3121 $this->backup_semaphore = new Updraft_Semaphore_3_0($semaphore, 30, array($this));
3122 }
3123
3124 if (1 <= $resumption_no) {
3125
3126 $this->log('Requesting backup semaphore lock ('.$semaphore.')');
3127
3128 if (!$this->backup_semaphore->lock()) {
3129 $this->log('Failed to gain semaphore lock ('.$semaphore.') - another resumption for this job is apparently already active');
3130 return false;
3131 }
3132 }
3133
3134 return true;
3135 }
3136
3137 /**
3138 * This function will check to see if any of the known backups are still running and return true otherwise returns false.
3139 *
3140 * @return boolean|string - returns false if no backup is running or a error code if there is a backup running
3141 */
3142 public function is_backup_running() {
3143
3144 $backup_history = UpdraftPlus_Backup_History::get_history();
3145
3146 foreach ($backup_history as $backup) {
3147 $nonce = $backup['nonce'];
3148
3149 // Check the job is not still running.
3150 $jobdata = $this->jobdata_getarray($nonce);
3151
3152 if (!empty($jobdata) && 'finished' != $jobdata['jobstatus']) {
3153
3154 // Check that there is not a resumption scheduled
3155 if (wp_next_scheduled('updraft_backup_resume')) return "job_resumption_scheduled";
3156
3157 $time_passed = $jobdata['run_times'];
3158
3159 // No runtime found so return
3160 if (!is_array($time_passed)) return "job_scheduled_${nonce}_no_run_times";
3161
3162 // Runtime has been found so make sure last activity is over an hour
3163 $time_passed = end($time_passed);
3164 if (strtotime($time_passed) <= time() - (3600)) continue;
3165
3166 return "job_scheduled_${nonce}_run_time_activity";
3167 }
3168 }
3169
3170 return false;
3171 }
3172
3173 /**
3174 * This function is a filter function which will return the nonce for the incremental backup set we want to add to
3175 *
3176 * @param String $nonce - the backup nonce we want to filter
3177 *
3178 * @return string - the backup nonce
3179 */
3180 public function incremental_backup_file_nonce($nonce) {
3181 if (apply_filters('updraftplus_incremental_addon_installed', false) && !empty($this->file_nonce)) return $this->file_nonce;
3182 return $nonce;
3183 }
3184
3185 /**
3186 * Get the initial resumption interval, in seconds
3187 *
3188 * @return Integer
3189 */
3190 private function get_initial_resume_interval() {
3191 // Allow the resume interval to be more than 300 if last time we know we went beyond that - but never more than 600
3192 if (defined('UPDRAFTPLUS_INITIAL_RESUME_INTERVAL') && is_numeric(UPDRAFTPLUS_INITIAL_RESUME_INTERVAL)) {
3193 $resume_interval = UPDRAFTPLUS_INITIAL_RESUME_INTERVAL;
3194 } else {
3195 $resume_interval = (int) min(max(300, get_site_transient('updraft_initial_resume_interval')), 600);
3196 }
3197 // 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)
3198 delete_site_transient('updraft_initial_resume_interval');
3199 return $resume_interval;
3200 }
3201
3202 /**
3203 * This procedure initiates a backup run
3204 * $backup_files/$backup_database: true/false = yes/no (over-write allowed); 1/0 = yes/no (force)
3205 *
3206 * @param Boolean|Integer $backup_files
3207 * @param Boolean|Integer $backup_database
3208 * @param Boolean|Array $restrict_files_to_override
3209 * @param Boolean $one_shot
3210 * @param Boolean|Array|String $service
3211 * @param Array $options
3212 *
3213 * @return Boolean|Void - false indicates definite failure; true indicates a job was started and ran through as far as possible on this resumption. Note that you should not expect this method to return at all, depending on how long the backup takes, and available PHP run time, etc. In case of failure, currently there may or may not be information logged, and it may or may not be logged at the 'error' level. If more precise feedback is needed, then this can be improved. Void is currently used if no backup was started because none was needed.
3214 */
3215 public function boot_backup($backup_files, $backup_database, $restrict_files_to_override = false, $one_shot = false, $service = false, $options = array()) {
3216
3217 if (function_exists('ignore_user_abort')) @ignore_user_abort(true);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3218 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3219
3220 $is_scheduled_backup = is_bool($backup_files) || is_bool($backup_database);
3221
3222 $hosting_company = $this->get_hosting_info();
3223 if (!empty($options['incremental']) && in_array('only_one_incremental_per_day', $this->is_hosting_backup_limit_reached())) {
3224 $this->log(__("You have reached the daily limit for the number of incremental backups you can create at this time.", 'updraftplus').' '.__(' Your hosting provider only allows you to take one incremental backup per day.', 'updraftplus').' '.sprintf(__('Please contact your hosting company (%s) if you require further support.', 'updraftplus'), $hosting_company['name']));
3225 return false;
3226 } elseif (empty($options['incremental']) && in_array('only_one_backup_per_month', $this->is_hosting_backup_limit_reached())) {
3227 $this->log(__('You have reached the monthly limit for the number of backups you can create at this time.', 'updraftplus').' '.__('Your hosting provider only allows you to take one backup per month.', 'updraftplus').' '.sprintf(__('Please contact your hosting company (%s) if you require further support.', 'updraftplus'), $hosting_company['name']));
3228 return false;
3229 }
3230
3231 if (false === $restrict_files_to_override && isset($options['restrict_files_to_override'])) $restrict_files_to_override = $options['restrict_files_to_override'];
3232 // Generate backup information
3233 $use_nonce = empty($options['use_nonce']) ? false : $options['use_nonce'];
3234 $use_timestamp = empty($options['use_timestamp']) ? false : $options['use_timestamp'];
3235 $this->backup_time_nonce($use_nonce, $use_timestamp);
3236 // The current_resumption is consulted within logfile_open()
3237 $this->current_resumption = 0;
3238 $this->logfile_open($this->file_nonce);
3239
3240 if (!is_file($this->logfile_name)) {
3241 $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.');
3242 $this->log(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.', 'updraftplus'), 'error');
3243 return false;
3244 }
3245
3246 // Some house-cleaning
3247 UpdraftPlus_Filesystem_Functions::clean_temporary_files();
3248
3249 // Log some information that may be helpful
3250 $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').")");
3251
3252 // The is_bool() check here is confirming that we're allowed to adjust the parameters
3253 if (false === $one_shot && is_bool($backup_database)) {
3254 // If the files and database schedules are the same, and if this the file one, then we rope in database too.
3255 // On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
3256
3257 $files_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval');
3258 $db_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
3259
3260 $sched_log_extra = '';
3261
3262 if ('manual' != $files_schedule && false !== $files_schedule) {
3263 if ($files_schedule == $db_schedule || UpdraftPlus_Options::get_updraft_option('updraft_interval_database', 'xyz') == 'xyz') {
3264 $sched_log_extra = 'Combining jobs from identical schedules. ';
3265 $backup_database = (true == $backup_files) ? true : false;
3266 } elseif ($files_schedule && $db_schedule && $files_schedule != $db_schedule) {
3267
3268 // This stored value is the earliest of the two apparently-close jobs
3269 $combine_around = empty($this->combine_jobs_around) ? false : $this->combine_jobs_around;
3270
3271 if (preg_match('/^(cancel:)?(\d+)$/', $combine_around, $matches)) {
3272
3273 $combine_around = $matches[2];
3274
3275 // Re-save the option, since otherwise it will have been reset and not be accessible to the 'other' run
3276 UpdraftPlus_Options::update_updraft_option('updraft_combine_jobs_around', 'cancel:'.$this->combine_jobs_around);
3277
3278 $margin = (defined('UPDRAFTPLUS_COMBINE_MARGIN') && is_numeric(UPDRAFTPLUS_COMBINE_MARGIN)) ? UPDRAFTPLUS_COMBINE_MARGIN : 600;
3279
3280 $time_now = time();
3281
3282 // The margin is doubled, to cope with the lack of predictability in WP's cron system
3283 if ($time_now >= $combine_around && $time_now <= $combine_around + 2*$margin) {
3284
3285 $sched_log_extra = 'Combining jobs from co-inciding events. ';
3286
3287 if ('cancel:' == $matches[1]) {
3288 $backup_database = false;
3289 $backup_files = false;
3290 } else {
3291 // 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).
3292 $backup_database = true;
3293 $backup_files = true;
3294 }
3295
3296 }
3297
3298 }
3299 }
3300 }
3301 $this->log("Processed schedules. ${sched_log_extra}Tasks now: Backup files: $backup_files Backup DB: $backup_database");
3302 }
3303
3304 if (false == apply_filters('updraftplus_boot_backup', true, $backup_files, $backup_database, $one_shot)) {
3305 $this->log("Backup aborted (via filter)");
3306 return false;
3307 }
3308
3309 // All scheduled backups will go through this condition (and some others may too)
3310 // This section sets up default options, filters services/instances, and populates $options['remote_storage_instances']
3311 if (!is_string($service) && !is_array($service)) {
3312 $all_services = !empty($options['remote_storage_instances']) ? array_keys($options['remote_storage_instances']) : UpdraftPlus_Options::get_updraft_option('updraft_service');
3313 if (is_string($all_services)) $all_services = (array) $all_services;
3314
3315 $enabled_storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($all_services);
3316 $legacy_storage_instances = array();
3317
3318 if (!isset($options['remote_storage_instances'])) {
3319
3320 $remote_storage_instances = array();
3321
3322 foreach ($enabled_storage_objects_and_ids as $method_id => $method_info) {
3323
3324 if ($method_info['object']->supports_feature('multi_options')) {
3325 foreach ($method_info['instance_settings'] as $instance_id => $instance_settings) {
3326 // We already know the instance is enabled, as we only selected those. We just want to give add-ons an opportunity to filter it.
3327
3328 if (!apply_filters('updraft_boot_backup_remote_storage_instance_include', true, $instance_settings, $method_id, $instance_id, $is_scheduled_backup)) continue;
3329
3330 if (!isset($remote_storage_instances[$method_id])) $remote_storage_instances[$method_id] = array();
3331
3332 $remote_storage_instances[$method_id][] = $instance_id;
3333 }
3334 } else {
3335 $legacy_storage_instances[] = $method_id;
3336 }
3337
3338 }
3339
3340 $options['remote_storage_instances'] = $remote_storage_instances;
3341 }
3342
3343 $service = array_merge(array_keys($options['remote_storage_instances']), $legacy_storage_instances);
3344 }
3345
3346 $service = $this->just_one($service);
3347 if (is_string($service)) $service = array($service);
3348 if (!is_array($service)) $service = array();
3349
3350 if (!empty($options['extradata']) && !empty($options['extradata']['services']) && preg_match('#remotesend/(\d+)#', $options['extradata']['services'])) {
3351 if (array('none') === $service) $service = array();
3352 $service[] = 'remotesend';
3353 }
3354
3355 $option_cache = array();
3356
3357 $service = $this->get_canonical_service_list($service);
3358
3359 foreach ($service as $serv) {
3360 include_once(UPDRAFTPLUS_DIR.'/methods/'.$serv.'.php');
3361 $cclass = 'UpdraftPlus_BackupModule_'.$serv;
3362 if (!class_exists($cclass)) {
3363 error_log("UpdraftPlus: backup class does not exist: $cclass");
3364 continue;
3365 }
3366 $obj = new $cclass;
3367
3368 if (is_callable(array($obj, 'get_credentials'))) {
3369 $opts = $obj->get_credentials();
3370 if (is_array($opts)) {
3371 foreach ($opts as $opt) $option_cache[$opt] = UpdraftPlus_Options::get_updraft_option($opt);
3372 }
3373 }
3374 }
3375 $option_cache = apply_filters('updraftplus_job_option_cache', $option_cache);
3376
3377 // If nothing to be done, then just finish
3378 if (!$backup_files && !$backup_database) {
3379 $ret = $this->backup_finish(false, false);
3380 // Don't keep useless log files
3381 if (!UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') && !empty($this->logfile_name) && file_exists($this->logfile_name)) {
3382 unlink($this->logfile_name);
3383 }
3384 // Currently backup_finish() appears to have a void return. We don't want to return false, as that indicates failure. But neither was it really a success. Void seems fine for now, given that nothing is currently using it.
3385 return $ret;
3386 }
3387
3388 if (!$this->get_semaphore_lock($backup_files, $backup_database)) {
3389 // get_semaphore_lock() already does some of its own logging (though not currently (Nov 2019) at 'error' level)
3390 return false;
3391 }
3392
3393 $resume_interval = $this->get_initial_resume_interval();
3394
3395 $job_file_entities = array();
3396 if ($backup_files) {
3397 $possible_backups = $this->get_backupable_file_entities(true);
3398 foreach ($possible_backups as $youwhat => $whichdir) {
3399 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))) {
3400 // The 0 indicates the zip file index
3401 $job_file_entities[$youwhat] = array(
3402 'index' => 0
3403 );
3404 }
3405 }
3406 }
3407
3408 $followups_allowed = (((!$one_shot && defined('DOING_CRON') && DOING_CRON)) || (defined('UPDRAFTPLUS_FOLLOWUPS_ALLOWED') && UPDRAFTPLUS_FOLLOWUPS_ALLOWED));
3409
3410 $split_every = max((int) UpdraftPlus_Options::get_updraft_option('updraft_split_every', 400), UPDRAFTPLUS_SPLIT_MIN);
3411
3412 $initial_jobdata = array(
3413 'resume_interval',
3414 $resume_interval,
3415 'job_type',
3416 'backup',
3417 'jobstatus',
3418 'begun',
3419 'backup_time',
3420 $this->backup_time,
3421 'job_time_ms',
3422 $this->job_time_ms,
3423 'service',
3424 $service,
3425 'split_every',
3426 $split_every,
3427 'maxzipbatch',
3428 26214400, // 25MB
3429 'job_file_entities',
3430 $job_file_entities,
3431 'option_cache',
3432 $option_cache,
3433 'uploaded_lastreset',
3434 9,
3435 'one_shot',
3436 $one_shot,
3437 'followsups_allowed',
3438 $followups_allowed,
3439 'last_sapi',
3440 PHP_SAPI,
3441 );
3442
3443 if ($one_shot) update_site_option('updraft_oneshotnonce', $this->nonce);
3444
3445 if ($this->file_nonce && $this->file_nonce != $this->nonce) array_push($initial_jobdata, 'file_nonce', $this->file_nonce);
3446
3447 // 'autobackup' == $options['extradata'] might be set from another plugin so keeping here to keep support
3448 if (!empty($options['extradata']) && (!empty($options['extradata']['autobackup']) || 'autobackup' === $options['extradata'])) array_push($initial_jobdata, 'is_autobackup', true);
3449 // Save what *should* be done, to make it resumable from this point on
3450 if ($backup_database) {
3451 $dbs = apply_filters('updraft_backup_databases', array('wp' => 'begun'));
3452 if (is_array($dbs)) {
3453 foreach ($dbs as $key => $db) {
3454 if ('wp' != $key && (!is_array($db) || empty($db['dbinfo']) || !is_array($db['dbinfo']) || empty($db['dbinfo']['host']))) unset($dbs[$key]);
3455 }
3456 }
3457 } else {
3458 $dbs = 'no';
3459 }
3460
3461 array_push($initial_jobdata, 'backup_database', $dbs);
3462 array_push($initial_jobdata, 'backup_files', (($backup_files) ? 'begun' : 'no'));
3463
3464 if (is_array($options) && !empty($options['label'])) array_push($initial_jobdata, 'label', $options['label']);
3465
3466 if (!empty($options['always_keep'])) array_push($initial_jobdata, 'always_keep', true);
3467
3468 if (!empty($options['remote_storage_instances'])) array_push($initial_jobdata, 'remote_storage_instances', $options['remote_storage_instances']);
3469
3470 try {
3471 // Use of jobdata_set_multi saves around 200ms
3472 call_user_func_array(array($this, 'jobdata_set_multi'), apply_filters('updraftplus_initial_jobdata', $initial_jobdata, $options, $split_every));
3473 } catch (Exception $e) {
3474 $this->log("Exception when calling jobdata_set_multi: ".$e->getMessage().' ('.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')');
3475 return false;
3476 }
3477
3478 // Everything is set up; now go
3479 $this->backup_resume(0, $this->nonce);
3480
3481 if ($one_shot) delete_site_option('updraft_oneshotnonce');
3482
3483 return true;
3484
3485 }
3486
3487 /**
3488 * The purpose of this function is to abstract away historical discrepancies in service lists, by returning in a single, logical form (in particular, no 'none' or '' entries, and always an array)
3489 *
3490 * @param Array|String|Boolean|Null $services - a list of services to canonicalize, or a string indicating a single service. If null is parsed, then the saved settings will be read.
3491 *
3492 * @return Array - an array of service names. All service names will be non-empty strings, and 'none' will not feature. If there are no services, then the array will be empty.
3493 */
3494 public function get_canonical_service_list($services = null) {
3495
3496 if (null === $services) $services = UpdraftPlus_Options::get_updraft_option('updraft_service');
3497
3498 $services = (array) $services;
3499
3500 foreach ($services as $key => $service) {
3501 if ('' === $service || 'none' === $service || false === $service) unset($services[$key]);
3502 }
3503
3504 return $services;
3505 }
3506
3507 /**
3508 * Perform the tasks necessary when a backup has run through all the available steps. N.B. This does not imply that the were all successful or that the backup is finished.
3509 *
3510 * @param Boolean $do_cleanup - if (and only if) this is set will resumptions be unscheduled
3511 * @param Boolean $allow_email - if this is false, then no email will be sent
3512 * @param Boolean $force_abort - set to indicate that the user is manually aborting the backup
3513 */
3514 public function backup_finish($do_cleanup, $allow_email, $force_abort = false) {
3515
3516 if (!empty($this->semaphore)) $this->semaphore->unlock();
3517 if (!empty($this->backup_semaphore)) $this->backup_semaphore->release();
3518
3519 $this->restore_composer_autoloaders();
3520
3521 $delete_jobdata = false;
3522
3523 $clone_job = $this->jobdata_get('clone_job');
3524
3525 if (!empty($clone_job)) {
3526 $clone_id = $this->jobdata_get('clone_id');
3527 $secret_token = $this->jobdata_get('secret_token');
3528 }
3529
3530 // 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)
3531
3532 // 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.
3533 if (0 == $this->error_count() || $force_abort) {
3534 if ($do_cleanup) {
3535 $cancel_event = $this->current_resumption + 1;
3536 $this->log("There were no errors in the uploads, so the 'resume' event ($cancel_event) is being unscheduled");
3537 // 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)
3538 $this->jobdata_set('jobstatus', 'finished');
3539 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
3540 // 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
3541 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+1, $this->nonce));
3542 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+2, $this->nonce));
3543 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+3, $this->nonce));
3544 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+4, $this->nonce));
3545 $delete_jobdata = true;
3546 }
3547 } else {
3548 if ($this->newresumption_scheduled) {
3549 if ($this->current_resumption + 1 != $this->jobdata_get('fail_on_resume')) {
3550 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
3551 $this->jobdata_set('jobstatus', 'resumingforerrors');
3552 }
3553 }
3554 // 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.
3555 if (isset($this->error_count_before_cloud_backup) && 0 === $this->error_count_before_cloud_backup) {
3556 if (0 == $this->current_resumption) {
3557 UpdraftPlus_Job_Scheduler::reschedule(60);
3558 } else {
3559 // Added 27/Feb/2016 - though the cloud service seems to be down, we still don't want to wait too long
3560 $resume_interval = $this->jobdata_get('resume_interval');
3561
3562 // 15 minutes + 2 for each resumption (a modest back-off)
3563 $max_interval = 900 + $this->current_resumption * 120;
3564 if ($resume_interval > $max_interval) {
3565 UpdraftPlus_Job_Scheduler::reschedule($max_interval);
3566 }
3567 }
3568 }
3569 }
3570
3571 // Send the results email if appropriate, which means:
3572 // - The caller allowed it (which is not the case in an 'empty' run)
3573 // - And: An email address was set (which must be so in email mode)
3574 // And one of:
3575 // - Debug mode
3576 // - There were no errors (which means we completed and so this is the final run - time for the final report)
3577 // - It was the tenth resumption; everything failed
3578
3579 $send_an_email = false;
3580 // Save the jobdata's state for the reporting - because it might get changed (e.g. incremental backup is scheduled)
3581 $jobdata_as_was = $this->jobdata;
3582
3583 // Make sure that the final status is shown
3584 if ($force_abort) {
3585 $send_an_email = true;
3586 $final_message = __('The backup was aborted by the user', 'updraftplus');
3587 if (!empty($clone_job)) $this->get_updraftplus_clone()->clone_failed_delete(array('clone_id' => $clone_id, 'secret_token' => $secret_token));
3588 } elseif (0 == $this->error_count()) {
3589 $send_an_email = true;
3590 $service = $this->jobdata_get('service');
3591 $remote_sent = (!empty($service) && ((is_array($service) && in_array('remotesend', $service)) || 'remotesend' === $service)) ? true : false;
3592 if (0 == $this->error_count('warning')) {
3593 $final_message = __('The backup apparently succeeded and is now complete', 'updraftplus');
3594 // 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
3595 if ('The backup apparently succeeded and is now complete' != $final_message) {
3596 $this->log('The backup apparently succeeded and is now complete');
3597 }
3598 } else {
3599 $final_message = __('The backup apparently succeeded (with warnings) and is now complete', 'updraftplus');
3600 if ('The backup apparently succeeded (with warnings) and is now complete' != $final_message) {
3601 $this->log('The backup apparently succeeded (with warnings) and is now complete');
3602 }
3603 }
3604 if ($remote_sent && !$force_abort) {
3605 $final_message .= empty($clone_job) ? '. '.__('To complete your migration/clone, you should now log in to the remote site and restore the backup set.', 'updraftplus') : '. '.__('Your clone will now deploy this data to re-create your site.', 'updraftplus');
3606 }
3607 if ($do_cleanup) $delete_jobdata = apply_filters('updraftplus_backup_complete', $delete_jobdata);
3608 } elseif (false == $this->newresumption_scheduled || $this->current_resumption + 1 == $this->jobdata_get('fail_on_resume')) {
3609
3610 if ($this->current_resumption + 1 == $this->jobdata_get('fail_on_resume')) {
3611 $this->log("The resumption is being cancelled, as it was only scheduled to enable error reporting, which can be performed now");
3612 wp_clear_scheduled_hook('updraft_backup_resume', array($this->current_resumption + 1, $this->nonce));
3613 }
3614
3615 $send_an_email = true;
3616 $final_message = __('The backup attempt has finished, apparently unsuccessfully', 'updraftplus');
3617 if (!empty($clone_job)) $this->get_updraftplus_clone()->clone_failed_delete(array('clone_id' => $clone_id, 'secret_token' => $secret_token));
3618 } else {
3619 // There are errors, but a resumption will be attempted
3620 $final_message = __('The backup has not finished; a resumption is scheduled', 'updraftplus');
3621 }
3622
3623 // Now over-ride the decision to send an email, if needed
3624 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
3625 $send_an_email = true;
3626 $this->log("An email has been scheduled for this job, because we are in debug mode");
3627 }
3628
3629 $email = UpdraftPlus_Options::get_updraft_option('updraft_email');
3630
3631 // If there's no email address, or the set was empty, that is the final over-ride: don't send
3632 if (!$allow_email) {
3633 $send_an_email = false;
3634 $this->log("No email will be sent - this backup set was empty.");
3635 } elseif (empty($email)) {
3636 $send_an_email = false;
3637 $this->log("No email will/can be sent - the user has not configured an email address.");
3638 }
3639
3640 if ($force_abort) $jobdata_as_was['aborted'] = true;
3641 if ($send_an_email) $this->send_results_email($final_message, $jobdata_as_was);
3642
3643 // Make sure this is the final message logged (so it remains on the dashboard)
3644 $this->log($final_message);
3645
3646 @fclose($this->logfile_handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3647 $this->logfile_handle = null;
3648
3649 // This is left until last for the benefit of the front-end UI, which then gets maximum chance to display the 'finished' status
3650 if ($delete_jobdata) delete_site_option('updraft_jobdata_'.$this->nonce);
3651
3652 }
3653
3654 /**
3655 * The jobdata is passed in instead of fetched, because the live jobdata may now differ from that which should be reported on (e.g. an incremental run was subsequently scheduled)
3656 *
3657 * @param String $final_message The final message to be sent
3658 * @param Array $jobdata Full job data
3659 */
3660 private function send_results_email($final_message, $jobdata) {
3661
3662 $debug_mode = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
3663
3664 $sendmail_to = $this->just_one_email(UpdraftPlus_Options::get_updraft_option('updraft_email'));
3665 if (is_string($sendmail_to)) $sendmail_to = array($sendmail_to);
3666
3667 $backup_files = $jobdata['backup_files'];
3668 $backup_db = $jobdata['backup_database'];
3669
3670 if (is_array($backup_db)) $backup_db = $backup_db['wp'];
3671 if (is_array($backup_db)) $backup_db = $backup_db['status'];
3672
3673 $backup_type = ('backup' == $jobdata['job_type']) ? __('Full backup', 'updraftplus') : __('Incremental', 'updraftplus');
3674
3675 $was_aborted = !empty($jobdata['aborted']);
3676
3677 if ($was_aborted) {
3678 $backup_contains = __('The backup was aborted by the user', 'updraftplus');
3679 } elseif ('finished' == $backup_files && ('finished' == $backup_db || 'encrypted' == $backup_db)) {
3680 $backup_contains = __('Files and database', 'updraftplus')." ($backup_type)";
3681 } elseif ('finished' == $backup_files) {
3682 $backup_contains = ('begun' == $backup_db) ? __("Files (database backup has not completed)", 'updraftplus') : __('Files only (database was not part of this particular schedule)', 'updraftplus');
3683 $backup_contains .= " ($backup_type)";
3684 } elseif ('finished' == $backup_db || 'encrypted' == $backup_db) {
3685 $backup_contains = ('begun' == $backup_files) ? __("Database (files backup has not completed)", 'updraftplus') : __('Database only (files were not part of this particular schedule)', 'updraftplus');
3686 } elseif ('begun' == $backup_db || 'begun' == $backup_files) {
3687 $backup_contains = __('Incomplete', 'updraftplus');
3688 } else {
3689 $this->log('Unknown/unexpected status: '.serialize($backup_files).'/'.serialize($backup_db));
3690 $backup_contains = __("Unknown/unexpected error - please raise a support request", 'updraftplus');
3691 }
3692
3693 $append_log = '';
3694 $attachments = array();
3695
3696 $error_count = 0;
3697
3698 if ($this->error_count() > 0) {
3699 $append_log .= __('Errors encountered:', 'updraftplus')."\r\n";
3700 $attachments[0] = $this->logfile_name;
3701 foreach ($this->errors as $err) {
3702 if (is_wp_error($err)) {
3703 foreach ($err->get_error_messages() as $msg) {
3704 $append_log .= "* ".rtrim($msg)."\r\n";
3705 }
3706 } elseif (is_array($err) && 'error' == $err['level']) {
3707 $append_log .= "* ".rtrim($err['message'])."\r\n";
3708 } elseif (is_string($err)) {
3709 $append_log .= "* ".rtrim($err)."\r\n";
3710 }
3711 $error_count++;
3712 }
3713 $append_log .="\r\n";
3714 }
3715 $warnings = (isset($jobdata['warnings'])) ? $jobdata['warnings'] : array();
3716 if (is_array($warnings) && count($warnings) >0) {
3717 $append_log .= __('Warnings encountered:', 'updraftplus')."\r\n";
3718 $attachments[0] = $this->logfile_name;
3719 foreach ($warnings as $err) {
3720 $append_log .= "* ".rtrim($err)."\r\n";
3721 }
3722 $append_log .="\r\n";
3723 }
3724
3725 if ($debug_mode && '' != $this->logfile_name && !in_array($this->logfile_name, $attachments)) {
3726 $append_log .= "\r\n".__('The log file has been attached to this email.', 'updraftplus');
3727 $attachments[0] = $this->logfile_name;
3728 }
3729
3730 // We have to use the action in order to set the MIME type on the attachment - by default, WordPress just puts application/octet-stream
3731
3732 $subject = apply_filters('updraft_report_subject', sprintf(__('Backed up: %s', 'updraftplus'), wp_specialchars_decode(get_option('blogname'), ENT_QUOTES)).' (UpdraftPlus '.$this->version.') '.get_date_from_gmt(gmdate('Y-m-d H:i:s', time()), 'Y-m-d H:i'), $error_count, count($warnings));
3733
3734 // The class_exists() check here is a micro-optimization to prevent a possible HTTP call whose results may be disregarded by the filter
3735 $feed = '';
3736 if (!class_exists('UpdraftPlus_Addon_Reporting') && !defined('UPDRAFTPLUS_NOADS_B') && !defined('UPDRAFTPLUS_NONEWSFEED')) {
3737 $this->log('Fetching RSS news feed');
3738 $rss = $this->get_updraftplus_rssfeed();
3739 $this->log('Fetched RSS news feed; result is a: '.get_class($rss));
3740 if (is_a($rss, 'SimplePie')) {
3741 $feed .= __('Email reports created by UpdraftPlus (free edition) bring you the latest UpdraftPlus.com news', 'updraftplus')." - ".sprintf(__('read more at %s', 'updraftplus'), 'https://updraftplus.com/news/')."\r\n\r\n";
3742 foreach ($rss->get_items(0, 6) as $item) {
3743 $feed .= '* ';
3744 $feed .= $item->get_title();
3745 $feed .= " (".$item->get_date('j F Y').")";
3746 // $feed .= ' - '.$item->get_permalink();
3747 $feed .= "\r\n";
3748 }
3749 }
3750 $feed .= "\r\n\r\n";
3751 }
3752
3753 $extra_messages = apply_filters('updraftplus_report_extramessages', array());
3754 $extra_msg = '';
3755 if (is_array($extra_messages)) {
3756 foreach ($extra_messages as $msg) {
3757 $extra_msg .= '<strong>'.$msg['key'].'</strong>: '.$msg['val']."\r\n";
3758 }
3759 }
3760
3761 foreach ($this->remotestorage_extrainfo as $service => $message) {
3762 if (!empty($this->backup_methods[$service])) $extra_msg .= $this->backup_methods[$service].': '.$message['plain']."\r\n";
3763 }
3764
3765 // Make it available to the filter
3766 $jobdata['remotestorage_extrainfo'] = $this->remotestorage_extrainfo;
3767
3768 if (!class_exists('UpdraftPlus_Notices')) include_once(UPDRAFTPLUS_DIR.'/includes/updraftplus-notices.php');
3769 global $updraftplus_notices;
3770 $ws_advert = $updraftplus_notices->do_notice(false, 'report-plain', true);
3771
3772 $body = apply_filters('updraft_report_body',
3773 __('Backup of:', 'updraftplus').' '.site_url()."\r\n".
3774 "UpdraftPlus ".__('WordPress backup is complete', 'updraftplus').".\r\n".
3775 __('Backup contains:', 'updraftplus')." $backup_contains\r\n".
3776 __('Latest status:', 'updraftplus').' '.$final_message."\r\n".
3777 $extra_msg.
3778 "\r\n".
3779 $feed.
3780 $ws_advert."\r\n".
3781 $append_log,
3782 $final_message,
3783 $backup_contains,
3784 $this->errors,
3785 $warnings,
3786 $jobdata);
3787
3788 $this->attachments = apply_filters('updraft_report_attachments', $attachments);
3789
3790 $attach_size = 0;
3791 $unlink_files = array();
3792
3793 foreach ($this->attachments as $ind => $attach) {
3794 if ($attach == $this->logfile_name && filesize($attach) > 6*1048576) {
3795
3796 $this->log("Log file is large (".round(filesize($attach)/1024, 1)." KB): will compress before e-mailing");
3797
3798 if (!$handle = fopen($attach, "r")) {
3799 $this->log("Error: Failed to open log file for reading: ".$attach);
3800 } else {
3801 if (!$whandle = gzopen($attach.'.gz', 'w')) {
3802 $this->log("Error: Failed to open log file for reading: ".$attach.".gz");
3803 } else {
3804 while (false !== ($line = @stream_get_line($handle, 131072, "\n"))) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3805 @gzwrite($whandle, $line."\n");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3806 }
3807 fclose($handle);
3808 gzclose($whandle);
3809 $this->attachments[$ind] = $attach.'.gz';
3810 $unlink_files[] = $attach.'.gz';
3811 }
3812 }
3813 }
3814 $attach_size += filesize($this->attachments[$ind]);
3815 }
3816
3817 foreach ($sendmail_to as $ind => $mailto) {
3818
3819 if (false === apply_filters('updraft_report_sendto', true, $mailto, $error_count, count($warnings), $ind)) continue;
3820
3821 foreach (explode(',', $mailto) as $sendmail_addr) {
3822 // if the address is a URL then instead of emailing it, POST it to slack
3823 if (preg_match('/^https?:\/\//i', $sendmail_addr)) {
3824 $this->log("Sending to (URL) ('$backup_contains') report (attachments: ".count($attachments).", size: ".round($attach_size/1024, 1)." KB) to: ".substr($sendmail_addr, 0, 5)."...");
3825 $this->post_results_slack($subject, $body, trim($sendmail_addr), $this->file_nonce);
3826 } else {
3827 $this->log("Sending email ('$backup_contains') report (attachments: ".count($attachments).", size: ".round($attach_size/1024, 1)." KB) to: ".substr($sendmail_addr, 0, 5)."...");
3828 $headers = array();
3829 try {
3830 $headers[] = "X-UpdraftPlus-Backup-ID: ".$this->nonce;
3831 $from_email = apply_filters('updraftplus_email_from_header', $this->get_email_from_header());
3832 $from_name = apply_filters('updraftplus_email_from_name_header', $this->get_email_from_name_header());
3833 $use_wp_from_name_filter = '' === $from_email;
3834 // Notice that we don't use the 'wp_mail_from' filter, but only the 'From:' header to set sender name and sender email address, the reason behind it is that some SMTP plugins override the "wp_mail()" function and they do anything they want inside their own "wp_mail()" function, including not to call the php_mailer filter nor the wp_mail_from and wp_mail_from_name filters, but since the function signature remain the same as the WP one, so they may evaluate and do something with the header parameter
3835 if (!$use_wp_from_name_filter) {
3836 $headers[] = sprintf('From: %s <%s>', $from_name, $from_email);
3837 } else {
3838 add_filter('wp_mail_from_name', array($this, 'get_email_from_name_header'), 9);
3839 }
3840 add_action('wp_mail_failed', array($this, 'log_email_delivery_failure'));
3841 wp_mail(trim($sendmail_addr), $subject, $body, $headers, is_array($this->attachments) ? $this->attachments : array());
3842 remove_action('wp_mail_failed', array($this, 'log_email_delivery_failure'));
3843 if ($use_wp_from_name_filter) remove_filter('wp_mail_from_name', array($this, 'get_email_from_name_header'), 9);
3844 } catch (Exception $e) {
3845 $this->log("Exception occurred when sending mail (".get_class($e)."): ".$e->getMessage());
3846 }
3847 }
3848 }
3849 }
3850
3851 foreach ($unlink_files as $file) @unlink($file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3852
3853 do_action('updraft_report_finished');
3854
3855 }
3856
3857 /**
3858 * Log the email delivery failure to the log file when a PHPMailer exception is caught
3859 *
3860 * @param WP_Error $error A WP_Error object with the PHPMailer\PHPMailer\Exception message, and an array containing the mail recipient, subject, message, headers, and attachments.
3861 */
3862 public function log_email_delivery_failure($error) {
3863 $this->log("An error occurred when sending a backup report email and/or backup file(s) via email (".$error->get_error_code()."): ".$error->get_error_message());
3864 }
3865
3866 /**
3867 * Check whether the provided admin_email is under the same domain with the site, and use it as a sender email to increase the chance of an email being sent successfully (if appropiate)
3868 *
3869 * @return String The admin email address if it's found to be in same domain, an empty string otherwise
3870 */
3871 public function get_email_from_header() {
3872 $sitename = preg_replace('/^www\./i', '', strtolower($_SERVER['SERVER_NAME']));
3873 $admin_email = get_bloginfo('admin_email');
3874 $admin_email_domain = preg_replace('/^[^@]+@(.+)$/', "$1", $admin_email);
3875 if (trim(strtolower($sitename)) === trim(strtolower($admin_email_domain))) {
3876 // assuming (non validating) that the email account of the admin email does exist, and the admin email is under the same domain as with the web domain and the domain exists and live as well
3877 return $admin_email;
3878 }
3879 return '';
3880 }
3881
3882 /**
3883 * Build sender name and use something authentic that represents the identity of the plugin and web domain
3884 *
3885 * @return String The sender name
3886 */
3887 public function get_email_from_name_header() {
3888 return sprintf(__('UpdraftPlus on %s', 'updraftplus'), preg_replace('/^www\./i', '', strtolower($_SERVER['SERVER_NAME'])));
3889 }
3890
3891 /**
3892 * Post backup report to slack instead of emailing if the address is a URL
3893 *
3894 * @param string $header report title
3895 * @param string $report_body report content
3896 * @param string $webhook_url url to post report
3897 * @param string $nval backup log file nonce
3898 * @return Void
3899 */
3900 public function post_results_slack($header, $report_body, $webhook_url, $nval) {
3901 $findcontent = __('The log file has been attached to this email.', 'updraftplus');
3902
3903 $report_body = str_replace($findcontent, '', $report_body);
3904 $url = admin_url(UpdraftPlus_Options::admin_page()."?page=updraftplus&action=downloadlog&updraftplus_backup_nonce=$nval");
3905 $response = wp_remote_post($webhook_url, array(
3906 'method' => 'POST',
3907 'headers' => array(),
3908 'body' => json_encode(array(
3909 'blocks' => array(
3910 array(
3911 'type' => 'header',
3912 'text' => array(
3913 'type' => 'plain_text',
3914 'text' => $header,
3915 'emoji' => true
3916 ),
3917 ),
3918 array(
3919 'type' => 'section',
3920 'text' => array(
3921 'type' => 'mrkdwn',
3922 'text' => $report_body
3923 ),
3924 ),
3925 array(
3926 'type' => 'section',
3927 'text' => array(
3928 'type' => 'mrkdwn',
3929 'text' => __('You can view the log by pressing the \'View log\' button.', 'updraftplus')
3930 ),
3931 'accessory' => array(
3932 'type' => 'button',
3933 'text' => array(
3934 'type' => 'plain_text',
3935 'text' => __('View log', 'updraftplus'),
3936 'emoji' => true
3937 ),
3938 'value' => 'view_log_123',
3939 'url' => $url,
3940 'action_id' => 'button-action'
3941 )
3942 ),
3943 )
3944 ))
3945 ));
3946 if (!is_wp_error($response)) {
3947 $response_code = wp_remote_retrieve_response_code($response);
3948 if ($response_code < 200 || $response_code >= 300) {
3949 $this->log('HTTP POST error : '.$response_code.' - '.wp_remote_retrieve_response_message($response));
3950 }
3951 } else {
3952 $this->log('HTTP POST error : '.$response->get_error_code().' - '.$response->get_error_message());
3953 }
3954 }
3955
3956 /**
3957 * 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
3958 *
3959 * @param boolean $check_if_in_use_first
3960 * @return boolean
3961 */
3962 public function mod_rewrite_unavailable($check_if_in_use_first = true) {
3963 if (function_exists('apache_get_modules')) {
3964 global $wp_rewrite;
3965 $mods = apache_get_modules();
3966 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))) {
3967 return true;
3968 }
3969 }
3970 return false;
3971 }
3972
3973 /**
3974 * Count the number of alerts that have occurred at the specified level
3975 *
3976 * @param String $level - the level to count at
3977 *
3978 * @return Integer
3979 */
3980 public function error_count($level = 'error') {
3981 $count = 0;
3982 foreach ($this->errors as $err) {
3983 if (('error' == $level && (is_string($err) || is_wp_error($err))) || (is_array($err) && $level == $err['level'])) {
3984 $count++;
3985 }
3986 }
3987 return $count;
3988 }
3989
3990 public function list_errors() {
3991 echo '<ul style="list-style: disc inside;">';
3992 foreach ($this->errors as $err) {
3993 if (is_wp_error($err)) {
3994 foreach ($err->get_error_messages() as $msg) {
3995 echo '<li>'.htmlspecialchars($msg).'<li>';
3996 }
3997 } elseif (is_array($err) && ('error' == $err['level'] || 'warning' == $err['level'])) {
3998 echo "<li>".htmlspecialchars($err['message'])."</li>";
3999 } elseif (is_string($err)) {
4000 echo "<li>".htmlspecialchars($err)."</li>";
4001 } else {
4002 print "<li>".print_r($err, true)."</li>";
4003 }
4004 }
4005 echo '</ul>';
4006 }
4007
4008 /**
4009 * Save last successful backup information
4010 *
4011 * @param Array $backup_array An array of backup information
4012 */
4013 private function save_last_backup($backup_array) {
4014 $success = ($this->error_count() == 0) ? 1 : 0;
4015 $last_backup = UpdraftPlus_Options::get_updraft_option('updraft_last_backup', array());
4016 if (empty($last_backup)) $last_backup = array();
4017 if ('incremental' === $this->jobdata_get('job_type')) {
4018 $last_backup['incremental_backup_time'] = $this->backup_time; // the incremental_backup_time index is used only for storing time of the incremental job type
4019 } else {
4020 $last_backup['nonincremental_backup_time'] = $this->backup_time; // otherwise the nonincremental_backup_time index is for the backup job type
4021 }
4022 $last_backup = wp_parse_args(array(
4023 'backup_time' => $this->backup_time, // the backup_time index is used for storing either time of backup or incremental job type
4024 'backup_array' => $backup_array,
4025 'success' => $success,
4026 'errors' => $this->errors,
4027 'backup_nonce' => $this->nonce
4028 ), $last_backup);
4029 $last_backup = apply_filters('updraftplus_save_last_backup', $last_backup);
4030 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup, false);
4031 }
4032
4033 /**
4034 * $handle must be either false or a WPDB class (or extension thereof). Other options are not yet fully supported.
4035 *
4036 * @param Resource|Boolean|Object $handle
4037 * @param Boolean $log_it - whether to log information about the check
4038 * @param Boolean $reschedule - whether to schedule a resumption if checking fails
4039 * @param Boolean $allow_bail - whether to allow the connection to fail or throw an error
4040 * @return Boolean|Integer - whether the check succeeded, or -1 for an unknown result
4041 */
4042 public function check_db_connection($handle = false, $log_it = false, $reschedule = false, $allow_bail = false) {
4043
4044 $type = false;
4045 if (false === $handle || is_a($handle, 'wpdb')) {
4046 $type = 'wpdb';
4047 } elseif (is_resource($handle)) {
4048 // Expected: string(10) "mysql link"
4049 $type = get_resource_type($handle);
4050 } elseif (is_object($handle) && is_a($handle, 'mysqli')) {
4051 $type = 'mysqli';
4052 }
4053
4054 if (false === $type) return -1;
4055
4056 $db_connected = -1;
4057
4058 if ('mysql link' == $type || 'mysqli' == $type) {
4059 if ('mysql link' == $type && @mysql_ping($handle)) return true;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved -- Needed to add this as the old ignores no longer work
4060 if ('mysqli' == $type && @mysqli_ping($handle)) return true;// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4061
4062 for ($tries = 1; $tries <= 5; $tries++) {
4063 // to do, if ever needed
4064 // if ($this->db_connect(false )) return true;
4065 // sleep(1);
4066 }
4067
4068 } elseif ('wpdb' == $type) {
4069 if (false === $handle || (is_object($handle) && 'wpdb' == get_class($handle))) {
4070 global $wpdb;
4071 $handle = $wpdb;
4072 }
4073 if (method_exists($handle, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
4074 if (!$handle->check_connection($allow_bail)) {
4075 if ($log_it) $this->log("The database went away, and could not be reconnected to");
4076 // Almost certainly a no-op
4077 if ($reschedule) UpdraftPlus_Job_Scheduler::reschedule(60);
4078 $db_connected = false;
4079 } else {
4080 $db_connected = true;
4081 }
4082 }
4083 }
4084
4085 return $db_connected;
4086
4087 }
4088
4089 /**
4090 * This should be called whenever a file is successfully uploaded
4091 *
4092 * @param String $file - full filepath
4093 * @param Boolean $force - mark as successfully uploaded even if not on the last service
4094 * @return Void
4095 */
4096 public function uploaded_file($file, $force = false) {
4097
4098 global $updraftplus_backup;
4099
4100 $db_connected = $this->check_db_connection(false, true, true);
4101
4102 $service = empty($updraftplus_backup->current_service) ? '' : $updraftplus_backup->current_service;
4103 $instance_id = empty($updraftplus_backup->current_instance) ? '' : $updraftplus_backup->current_instance;
4104 $shash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
4105
4106 if ($force || !empty($updraftplus_backup->last_storage_instance)) {
4107 $this->log("Recording as successfully uploaded: $file");
4108 $new_jobdata = $this->get_uploaded_jobdata_items($file, $service, $instance_id);
4109 } else {
4110 $new_jobdata = array('uploaded_'.$shash => 'yes');
4111 $this->log("Recording as successfully uploaded: $file (".$updraftplus_backup->current_service.", more services to follow)");
4112 }
4113
4114 $upload_status = $this->jobdata_get('uploading_substatus');
4115 if (is_array($upload_status) && isset($upload_status['i'])) {
4116 $upload_status['i']++;
4117 $upload_status['p'] = 0;
4118 $new_jobdata['uploading_substatus'] = $upload_status;
4119 }
4120
4121 $this->jobdata_set_multi($new_jobdata);
4122
4123 // 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
4124 if (false === $db_connected) {
4125 UpdraftPlus_Job_Scheduler::record_still_alive();
4126 die;
4127 }
4128
4129 // Delete local files immediately if the option is set
4130 // Where we are only backing up locally, only the "prune" function should do deleting
4131 $service = $this->jobdata_get('service');
4132 if (!empty($updraftplus_backup->last_storage_instance) && ('' !== $service && ((is_array($service) && count($service)>0 && (count($service) > 1 || (array('') !== $service && array('none') !== $service))) || (is_string($service) && 'none' !== $service)))) {
4133 $this->delete_local($file);
4134 }
4135 }
4136
4137 /**
4138 * Gets the jobdata items to be added to mark a file as uploaded
4139 *
4140 * @param String $file - the file (basename)
4141 * @param String $service - service identifier
4142 * @param String $instance_id - instance identifier
4143 *
4144 * @return Array - jobdata items
4145 */
4146 public function get_uploaded_jobdata_items($file, $service = '', $instance_id = '') {
4147 $hash = md5($file);
4148 $shash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
4149 return array(
4150 'uploaded_lastreset' => $this->current_resumption,
4151 'uploaded_'.$hash => 'yes',
4152 'uploaded_'.$shash =>'yes'
4153 );
4154 }
4155
4156 /**
4157 * Return whether a particular file has been uploaded to a particular remote service
4158 *
4159 * @param String $file - the filename (basename)
4160 * @param String $service - the service identifier; or none, to indicate all services
4161 * @param String $instance_id - the instance identifier
4162 *
4163 * @return Boolean - the result
4164 */
4165 public function is_uploaded($file, $service = '', $instance_id = '') {
4166 $hash = $service.(('' == $service) ? '' : '-').$instance_id.(('' == $instance_id) ? '' : '-').md5($file);
4167 return ('yes' === $this->jobdata_get("uploaded_$hash")) ? true : false;
4168 }
4169
4170 /**
4171 * This function will mark the passed in service and instance id upload as complete
4172 *
4173 * @param String $service - the service identifier
4174 * @param String $instance_id - the instance identifier
4175 *
4176 * @return void
4177 */
4178 public function mark_upload_complete($service, $instance_id = '') {
4179
4180 $upload_completed = $this->jobdata_get('upload_completed', array());
4181
4182 if (empty($instance_id)) {
4183 $upload_completed[$service] = 1;
4184 } else {
4185 if (!is_array($upload_completed[$service])) $upload_completed[$service] = array();
4186 $upload_completed[$service][$instance_id] = 1;
4187 }
4188
4189 $this->jobdata_set('upload_completed', $upload_completed);
4190 }
4191
4192 /**
4193 * This function will check all the remote storage options for this job and ensure that each has completed the upload, if they have mark them as done if they have not completed then call upload_completed() for that service if it exists, otherwise mark as complete.
4194 *
4195 * @return boolean
4196 */
4197 private function check_upload_completed() {
4198
4199 $job_services = $this->jobdata_get('service');
4200 $services = $this->get_canonical_service_list($job_services);
4201 $sent_to_cloud = empty($services) ? false : true;
4202
4203 if (!$sent_to_cloud) return;
4204
4205 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids($services);
4206
4207 foreach ($services as $service) {
4208
4209 if ('email' == $service || 'none' == $service || !$service) continue;
4210
4211 $remote_obj = $storage_objects_and_ids[$service]['object'];
4212 $upload_completed = $this->jobdata_get('upload_completed', array());
4213
4214 if (isset($upload_completed[$service]) && !is_array($upload_completed[$service])) continue;
4215
4216 if (!empty($remote_obj) && !$remote_obj->supports_feature('multi_options')) {
4217
4218 if (is_callable(array($remote_obj, 'upload_completed'))) {
4219 $result = $remote_obj->upload_completed();
4220 if ($result) $this->mark_upload_complete($service);
4221 } else {
4222 $this->mark_upload_complete($service);
4223 }
4224 } elseif (!empty($storage_objects_and_ids[$service]['instance_settings'])) {
4225
4226 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $instance_id => $options) {
4227
4228 if (isset($upload_completed[$service][$instance_id])) continue;
4229
4230 $remote_obj->set_options($options, true, $instance_id);
4231 if (is_callable(array($remote_obj, 'upload_completed'))) {
4232 $remote_obj->upload_completed();
4233 } else {
4234 $this->mark_upload_complete($service, $instance_id);
4235 }
4236 }
4237 }
4238 }
4239 }
4240 /**
4241 * This function will check if the passed-in file is this job's responsibility to upload. Potentially files can belong to a different job, when running an incremental backup run
4242 *
4243 * @param String $file - the name of the file
4244 * @param String $type - the file entity type (db, plugins, themes etc)
4245 *
4246 * @return boolean - whether this is a file this job should upload (at some point)
4247 */
4248 private function is_ours_to_upload($file, $type) {
4249
4250 if ('db' == $type) return false;
4251
4252 $previous_backup_files_array = $this->jobdata_get('previous_backup_files_array', array());
4253
4254 if (isset($previous_backup_files_array[$type]) && in_array($file, $previous_backup_files_array[$type])) return false;
4255
4256 return true;
4257 }
4258
4259 private function delete_local($file) {
4260 $log = "Deleting local file: $file: ";
4261 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local', 1)) {
4262 $fullpath = $this->backups_dir_location().'/'.$file;
4263
4264 // check to make sure it exists before removing
4265 if (realpath($fullpath)) {
4266 $deleted = unlink($fullpath);
4267 $this->log($log.(($deleted) ? 'OK' : 'failed'));
4268 if (file_exists($fullpath.'.list.tmp')) {
4269 $this->log("Deleting zip manifest ({$file}.list.tmp)");
4270 unlink($fullpath.'.list.tmp');
4271 }
4272 return $deleted;
4273 }
4274 } else {
4275 $this->log($log."skipped: user has unchecked updraft_delete_local option");
4276 }
4277 return true;
4278 }
4279
4280 /**
4281 * For detecting another run, and aborting if one was found
4282 *
4283 * @param String $file - full file path of the file to check
4284 */
4285 public function check_recent_modification($file) {
4286 if (file_exists($file)) {
4287 $time_mod = (int) @filemtime($file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4288 $time_now = time();
4289 if ($time_mod > 100 && ($time_now - $time_mod) < 30) {
4290 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($file, $time_now, $time_mod);
4291 }
4292 }
4293 }
4294
4295 public function get_exclude($whichone) {
4296 if ('uploads' == $whichone) {
4297 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE));
4298 } elseif ('others' == $whichone) {
4299 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
4300 } else {
4301 $exclude = apply_filters('updraftplus_include_'.$whichone.'_exclude', array());
4302 }
4303 return (empty($exclude) || !is_array($exclude)) ? array() : $exclude;
4304 }
4305
4306 public function wp_upload_dir() {
4307 if (is_multisite()) {
4308 global $current_site;
4309 switch_to_blog($current_site->blog_id);
4310 }
4311
4312 $wp_upload_dir = wp_upload_dir();
4313
4314 if (is_multisite()) restore_current_blog();
4315
4316 return $wp_upload_dir;
4317 }
4318
4319 public function backup_uploads_dirlist($log_it = false) {
4320 // Create an array of directories to be skipped
4321 // Make the values into the keys
4322 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
4323 if ($log_it) $this->log("Exclusion option setting (uploads): ".$exclude);
4324 $skip = array_flip(preg_split("/,/", $exclude));
4325 $wp_upload_dir = $this->wp_upload_dir();
4326 $uploads_dir = $wp_upload_dir['basedir'];
4327 return $this->compile_folder_list_for_backup($uploads_dir, array(), $skip);
4328 }
4329
4330 public function backup_others_dirlist($log_it = false) {
4331 // Create an array of directories to be skipped
4332 // Make the values into the keys
4333 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE);
4334 if ($log_it) $this->log("Exclusion option setting (others): ".$exclude);
4335 $skip = array_flip(preg_split("/,/", $exclude));
4336 $file_entities = $this->get_backupable_file_entities(false);
4337
4338 // Keys = directory names to avoid; values = the label for that directory (used only in log files)
4339 // $avoid_these_dirs = array_flip($file_entities);
4340 $avoid_these_dirs = array();
4341 foreach ($file_entities as $type => $dirs) {
4342 if (is_string($dirs)) {
4343 $avoid_these_dirs[$dirs] = $type;
4344 } elseif (is_array($dirs)) {
4345 foreach ($dirs as $dir) {
4346 $avoid_these_dirs[$dir] = $type;
4347 }
4348 }
4349 }
4350 return $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $avoid_these_dirs, $skip);
4351 }
4352
4353 /**
4354 * 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_these_dirs are potentially dangerous to include; skip is just a user-level preference). They are allowed to overlap.
4355 *
4356 * @param String $backup_from_inside_dir
4357 * @param Array $avoid_these_dirs
4358 * @param Array $skip_these_dirs
4359 *
4360 * @return Array
4361 */
4362 public function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
4363
4364 // 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.
4365
4366 $dirlist = array();
4367 $added = 0;
4368 $log_skipped = 0;
4369 $log_skipped_last = '';
4370
4371 $this->log('Looking for candidates to backup in: '.$backup_from_inside_dir);
4372 $updraft_dir = $this->backups_dir_location();
4373
4374 if (is_file($backup_from_inside_dir)) {
4375 array_push($dirlist, $backup_from_inside_dir);
4376 $added++;
4377 $this->log("finding files: $backup_from_inside_dir: adding to list ($added)");
4378 } elseif ($handle = opendir($backup_from_inside_dir)) {
4379
4380 while (false !== ($entry = readdir($handle))) {
4381
4382 if ('.' == $entry || '..' == $entry) continue;
4383
4384 // $candidate: full path; $entry = one-level
4385 $candidate = $backup_from_inside_dir.'/'.$entry;
4386
4387 if (isset($avoid_these_dirs[$candidate])) {
4388 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
4389 } elseif ($candidate == $updraft_dir) {
4390 $this->log("finding files: $entry: skipping: this is the updraft directory");
4391 } elseif (isset($skip_these_dirs[$entry])) {
4392 $this->log("finding files: $entry: skipping: excluded by options");
4393 } else {
4394 $add_to_list = true;
4395 // Now deal with entries in $skip_these_dirs ending in * or starting with *
4396 foreach ($skip_these_dirs as $skip => $sind) {
4397 if ('*' == substr($skip, -1, 1) && '*' == substr($skip, 0, 1) && strlen($skip) > 2) {
4398 if (strpos($entry, substr($skip, 1, strlen($skip)-2)) !== false) {
4399 $this->log("finding files: $entry: skipping: excluded by options (glob)");
4400 $add_to_list = false;
4401 }
4402 } elseif ('*' == substr($skip, -1, 1) && strlen($skip) > 1) {
4403 if (substr($entry, 0, strlen($skip)-1) == substr($skip, 0, strlen($skip)-1)) {
4404 $this->log("finding files: $entry: skipping: excluded by options (glob)");
4405 $add_to_list = false;
4406 }
4407 } elseif ('*' == substr($skip, 0, 1) && strlen($skip) > 1) {
4408 if (strlen($entry) >= strlen($skip)-1 && substr($entry, (strlen($skip)-1)*-1) == substr($skip, 1)) {
4409 $this->log("finding files: $entry: skipping: excluded by options (glob)");
4410 $add_to_list = false;
4411 }
4412 }
4413 }
4414 if ($add_to_list) {
4415 array_push($dirlist, $candidate);
4416 $added++;
4417 if ($added > 500) {
4418 if ($log_skipped >= 500) {
4419 $this->log("finding files: $entry: adding to list ($added, $log_skipped log lines skipped)");
4420 $log_skipped = 0;
4421 $log_skipped_last = '';
4422 } else {
4423 $log_skipped++;
4424 $log_skipped_last = $entry;
4425 }
4426 } else {
4427 $skip_dblog = (($added > 50 && 0 != $added % 100) || ($added > 2000 && 0 != $added % 500));
4428 $this->log("finding files: $entry: adding to list ($added)", 'notice', false, $skip_dblog);
4429 }
4430 }
4431 }
4432 }
4433 @closedir($handle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4434 if ($log_skipped > 0) {
4435 $this->log("finding files: $log_skipped_last: adding to list ($added, last; $log_skipped log lines skipped)");
4436 }
4437 } else {
4438 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
4439 $this->log(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir, 'error');
4440 }
4441
4442 return $dirlist;
4443
4444 }
4445
4446 /**
4447 * Save the backup information to the backup history during a running backup (adding information to the currently-running job)
4448 *
4449 * @param Array $backup_array - the backup history
4450 */
4451 private function save_backup_to_history($backup_array) {
4452
4453 if (!is_array($backup_array)) {
4454 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
4455 $this->log(__('Could not save backup history because we have no backup array. Backup probably failed.', 'updraftplus'), 'error');
4456 return;
4457 }
4458
4459 $job_type = $this->jobdata_get('job_type');
4460
4461 $backup_array['nonce'] = $this->file_nonce;
4462 $backup_array['service'] = $this->jobdata_get('service');
4463 $backup_array['service_instance_ids'] = array();
4464 if ('incremental' != $job_type) $backup_array['always_keep'] = $this->jobdata_get('always_keep', false);
4465 $backup_array['files_enumerated_at'] = $this->jobdata_get('files_enumerated_at');
4466 $remote_storage_instances = $this->jobdata_get('remote_storage_instances', array());
4467
4468 // 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.
4469 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($backup_array['service'], $remote_storage_instances);
4470
4471 // N.B. On PHP 5.5+, we'd use array_column()
4472 foreach ($storage_objects_and_ids as $method => $method_information) {
4473 if ('none' == $method || !$method || !$method_information['object']->supports_feature('multi_options')) continue;
4474 $backup_array['service_instance_ids'][$method] = array_keys($method_information['instance_settings']);
4475 }
4476
4477 if ('incremental' != $job_type && '' != ($label = $this->jobdata_get('label', ''))) $backup_array['label'] = $label;
4478 if (!isset($backup_array['created_by_version'])) $backup_array['created_by_version'] = $this->version;
4479 $backup_array['last_saved_by_version'] = $this->version;
4480 $backup_array['is_multisite'] = is_multisite() ? true : false;
4481 $remotesend_info = $this->jobdata_get('remotesend_info');
4482 if (is_array($remotesend_info) && !empty($remotesend_info['url'])) $backup_array['remotesend_url'] = $remotesend_info['url'];
4483 if (false != $this->jobdata_get('is_autobackup', false)) $backup_array['autobackup'] = true;
4484
4485 if (false != ($morefiles_linked_indexes = $this->jobdata_get('morefiles_linked_indexes', false))) $backup_array['morefiles_linked_indexes'] = $morefiles_linked_indexes;
4486 if (false != ($morefiles_more_locations = $this->jobdata_get('morefiles_more_locations', false))) $backup_array['morefiles_more_locations'] = $morefiles_more_locations;
4487
4488 UpdraftPlus_Backup_History::save_backup(apply_filters('updraftplus_save_backup_history_timestamp', $this->backup_time), $backup_array);
4489 }
4490
4491 /**
4492 * If files + db are on different schedules but are scheduled for the same time,
4493 * then combine them $event = (object) array('hook' => $hook, 'timestamp' => $timestamp, 'schedule' => $recurrence, 'args' => $args, 'interval' => $schedules[$recurrence]['interval']);
4494 * See wp_schedule_single_event() and wp_schedule_event() in wp-includes/cron.php
4495 *
4496 * @param Object|Boolean $event - the event being scheduled
4497 * @return Object|Boolean - the filtered value
4498 */
4499 public function schedule_event($event) {
4500
4501 static $scheduled = array();
4502
4503 if (is_object($event) && ('updraft_backup' == $event->hook || 'updraft_backup_database' == $event->hook)) {
4504
4505 // 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)
4506 $this->combine_jobs_around = UpdraftPlus_Options::get_updraft_option('updraft_combine_jobs_around');
4507
4508 UpdraftPlus_Options::delete_updraft_option('updraft_combine_jobs_around');
4509
4510 $scheduled[$event->hook] = true;
4511
4512 // 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.
4513 // We only want to take action on the second call (otherwise, our information is out-of-date already)
4514 // If there is no second call, then that's fine - nothing to do
4515 // if (count($scheduled) < 2) {
4516 // return $event;
4517 // }
4518
4519 $backup_scheduled_for = ('updraft_backup' == $event->hook) ? $event->timestamp : wp_next_scheduled('updraft_backup');
4520 $db_scheduled_for = ('updraft_backup_database' == $event->hook) ? $event->timestamp : wp_next_scheduled('updraft_backup_database');
4521
4522 $diff = absint($backup_scheduled_for - $db_scheduled_for);
4523
4524 $margin = (defined('UPDRAFTPLUS_COMBINE_MARGIN') && is_numeric(UPDRAFTPLUS_COMBINE_MARGIN)) ? UPDRAFTPLUS_COMBINE_MARGIN : 600;
4525
4526 if ($backup_scheduled_for && $db_scheduled_for && $diff < $margin) {
4527 // 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.
4528 UpdraftPlus_Options::update_updraft_option('updraft_combine_jobs_around', min($backup_scheduled_for, $db_scheduled_for));
4529 }
4530
4531 }
4532
4533 return $event;
4534
4535 }
4536
4537 /**
4538 * 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.
4539 *
4540 * @param String $interval
4541 * @return String - filtered value
4542 */
4543 public function schedule_backup($interval) {
4544 $previous_time = wp_next_scheduled('updraft_backup');
4545
4546 // Clear schedule so that we don't stack up scheduled backups
4547 wp_clear_scheduled_hook('updraft_backup');
4548 if ('manual' == $interval) {
4549 // Clear increments schedule as the file schedule is manual
4550 wp_clear_scheduled_hook('updraft_backup_increments');
4551 return 'manual';
4552 }
4553 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval');
4554
4555 $valid_schedules = wp_get_schedules();
4556 if (empty($valid_schedules[$interval])) $interval = 'daily';
4557
4558 // 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.
4559 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : $this->random_schedule_time();
4560 $first_time = apply_filters('updraftplus_schedule_firsttime_files', $default_time);
4561
4562 wp_schedule_event($first_time, $interval, 'updraft_backup');
4563
4564 return $interval;
4565 }
4566
4567 /**
4568 * This function is both the database backup scheduler and a filter callback for saving the option. It is called in the register_setting for the updraft_interval_database, which means when the admin settings are saved it is called.
4569 *
4570 * @param String $interval
4571 * @return String - filtered value
4572 */
4573 public function schedule_backup_database($interval) {
4574 $previous_time = wp_next_scheduled('updraft_backup_database');
4575
4576 // Clear schedule so that we don't stack up scheduled backups
4577 wp_clear_scheduled_hook('updraft_backup_database');
4578 if ('manual' == $interval) return 'manual';
4579
4580 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
4581
4582 $valid_schedules = wp_get_schedules();
4583 if (empty($valid_schedules[$interval])) $interval = 'daily';
4584
4585 // 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.
4586 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : $this->random_schedule_time();
4587
4588 $first_time = apply_filters('updraftplus_schedule_firsttime_db', $default_time);
4589 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
4590
4591 return $interval;
4592 }
4593
4594 /**
4595 * This function is both the increments backup scheduler and a filter callback for saving the option. It is called in the register_setting for the updraft_interval_increments, which means when the admin settings are saved it is called.
4596 *
4597 * @param String $interval
4598 * @return String - filtered value
4599 */
4600 public function schedule_backup_increments($interval) {
4601 $previous_time = wp_next_scheduled('updraft_backup_increments');
4602
4603 // Clear schedule so that we don't stack up scheduled backups
4604 wp_clear_scheduled_hook('updraft_backup_increments');
4605 if ('none' == $interval || empty($interval)) return 'none';
4606 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_increments');
4607
4608 $valid_schedules = wp_get_schedules();
4609 if (empty($valid_schedules[$interval])) $interval = 'daily';
4610
4611 // 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.
4612 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
4613 $first_time = apply_filters('updraftplus_schedule_firsttime_increments', $default_time);
4614
4615 wp_schedule_event($first_time, $interval, 'updraft_backup_increments');
4616
4617 return $interval;
4618 }
4619
4620 /**
4621 * This function will generate a random backup schedule timestamp between the hours of 9PM and 7AM and return it
4622 *
4623 * @return string - the random timestamp
4624 */
4625 private function random_schedule_time() {
4626
4627 static $scheduled_timestamp = false;
4628
4629 if ($scheduled_timestamp) return $scheduled_timestamp;
4630
4631 $valid_hours = array(21, 22, 23, 0, 1, 2, 3, 4, 5, 6, 7);
4632
4633 $current_hour = current_time('G');
4634 $current_timestamp = current_time('timestamp');
4635
4636 if (in_array($current_hour, $valid_hours)) {
4637 $scheduled_timestamp = $current_timestamp;
4638 } else {
4639 $scheduled_timestamp = $current_timestamp + 43200;
4640 }
4641
4642 return $scheduled_timestamp;
4643 }
4644
4645 /**
4646 * Acts as a WordPress options filter
4647 *
4648 * @param Array $options - An array of options
4649 * @param String $option_name - The option name
4650 *
4651 * @return Array - the returned array can either be the set of updated options or a WordPress error array
4652 */
4653 public function storage_options_filter($options, $option_name) {
4654 if ('updraft_' !== substr($option_name, 0, 8)) return $options;
4655 $method = substr($option_name, 8);
4656
4657 $storage = UpdraftPlus_Storage_Methods_Interface::get_storage_object($method);
4658
4659 if (!is_a($storage, 'UpdraftPlus_BackupModule') || !is_callable(array($storage, 'options_filter'))) return $options;
4660
4661 return call_user_func(array($storage, 'options_filter'), $options);
4662 }
4663
4664 /**
4665 * Get the location of UD's internal directory
4666 *
4667 * @param Boolean $allow_cache
4668 * @return String - the directory path. Returns without any trailing slash.
4669 */
4670 public function backups_dir_location($allow_cache = true) {
4671
4672 if ($allow_cache && !empty($this->backup_dir)) return $this->backup_dir;
4673
4674 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
4675 // 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.
4676 if (preg_match('/^wp-content\/(.*)$/', $updraft_dir, $matches) && ABSPATH.'wp-content' === WP_CONTENT_DIR) {
4677 UpdraftPlus_Options::update_updraft_option('updraft_dir', $matches[1]);
4678 $updraft_dir = WP_CONTENT_DIR.'/'.$matches[1];
4679 }
4680
4681 // Default
4682 if (!$updraft_dir) $updraft_dir = WP_CONTENT_DIR.'/updraft';
4683
4684 // Do a test for a relative path
4685 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
4686 // Legacy - file paths stored related to ABSPATH
4687 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')) {
4688 $updraft_dir = ABSPATH.$updraft_dir;
4689 } else {
4690 // File paths stored relative to WP_CONTENT_DIR
4691 $updraft_dir = trailingslashit(WP_CONTENT_DIR).$updraft_dir;
4692 }
4693 }
4694
4695 // Check for the existence of the dir and prevent enumeration
4696 // index.php is for a sanity check - make sure that we're not somewhere unexpected
4697 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')) {
4698 @mkdir($updraft_dir, 0775, true);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4699 @file_put_contents($updraft_dir.'/index.html', "<html><body><a href=\"https://updraftplus.com\" target=\"_blank\">WordPress backups by UpdraftPlus</a></body></html>");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4700 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess', 'deny from all');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4701 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");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4702 }
4703
4704 $this->backup_dir = $updraft_dir;
4705
4706 return $updraft_dir;
4707 }
4708
4709 /**
4710 * This function will work out the total size of the passed in backup and return it.
4711 *
4712 * @param array $backup - an array of information about this backup set
4713 *
4714 * @return integer - the total size of the backup in bytes
4715 */
4716 public function get_total_backup_size($backup) {
4717
4718 $backupable_entities = $this->get_backupable_file_entities(true, true);
4719
4720 // Add the database to the entities array ready to loop over
4721 $backupable_entities['db'] = '';
4722
4723 $total_size = 0;
4724 foreach ($backup as $ekey => $files) {
4725 if (!isset($backupable_entities[$ekey])) continue;
4726 if (is_string($files)) $files = array($files);
4727 foreach ($files as $findex => $file) {
4728 $size_key = (0 == $findex) ? $ekey.'-size' : $ekey.$findex.'-size';
4729 $total_size = (false === $total_size || !isset($backup[$size_key]) || !is_numeric($backup[$size_key])) ? false : $total_size + $backup[$size_key];
4730 }
4731 }
4732
4733 return $total_size;
4734 }
4735
4736 public function spool_file($fullpath, $encryption = '') {
4737 if (function_exists('set_time_limit')) @set_time_limit(900);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4738
4739 if (!file_exists($fullpath) || filesize($fullpath) < 1) {
4740 _e('File not found', 'updraftplus');
4741 return;
4742 }
4743
4744 // Prevent any debug output
4745 // Don't enable this line - it causes 500 HTTP errors in some cases/hosts on some large files, for unknown reason
4746 // @ini_set('display_errors', '0');
4747
4748 if (UpdraftPlus_Encryption::is_file_encrypted($fullpath)) {
4749 if (ob_get_level()) {
4750 $flush_max = min(5, (int) ob_get_level());
4751 for ($i=1; $i<=$flush_max; $i++) {
4752 @ob_end_clean();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4753 }
4754 }
4755 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
4756 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
4757 UpdraftPlus_Encryption::spool_crypted_file($fullpath, (string) $encryption);
4758 return;
4759 }
4760
4761 $content_type = UpdraftPlus_Manipulation_Functions::get_mime_type_from_filename($fullpath, false);
4762
4763 include_once(UPDRAFTPLUS_DIR.'/includes/class-partialfileservlet.php');
4764
4765 // Prevent the file being read into memory
4766 if (ob_get_level()) {
4767 $flush_max = min(5, (int) ob_get_level());
4768 for ($i=1; $i<=$flush_max; $i++) {
4769 @ob_end_clean();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4770 }
4771 }
4772 if (ob_get_level()) @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged --Twice - see HS#6673 - someone at least needed it
4773
4774 if (isset($_SERVER['HTTP_RANGE'])) {
4775 $range_header = trim($_SERVER['HTTP_RANGE']);
4776 } elseif (function_exists('apache_request_headers')) {
4777 foreach (apache_request_headers() as $name => $value) {
4778 if (strtoupper($name) === 'RANGE') {
4779 $range_header = trim($value);
4780 }
4781 }
4782 }
4783
4784 if (empty($range_header)) {
4785 header("Content-Length: ".filesize($fullpath));
4786 header("Content-type: $content_type");
4787 header("Content-Disposition: attachment; filename=\"".basename($fullpath)."\";");
4788 readfile($fullpath);
4789 return;
4790 }
4791
4792 try {
4793 $range_header = UpdraftPlus_RangeHeader::createFromHeaderString($range_header);
4794 $servlet = new UpdraftPlus_PartialFileServlet($range_header);
4795 $servlet->sendFile($fullpath, $content_type);
4796 } catch (UpdraftPlus_InvalidRangeHeaderException $e) {
4797 header("HTTP/1.1 400 Bad Request");
4798 error_log("UpdraftPlus: UpdraftPlus_InvalidRangeHeaderException: ".$e->getMessage());
4799 } catch (UpdraftPlus_UnsatisfiableRangeException $e) {
4800 header("HTTP/1.1 416 Range Not Satisfiable");
4801 } catch (UpdraftPlus_NonExistentFileException $e) {
4802 header("HTTP/1.1 404 Not Found");
4803 } catch (UpdraftPlus_UnreadableFileException $e) {
4804 header("HTTP/1.1 500 Internal Server Error");
4805 }
4806
4807 }
4808
4809 public function just_one_email($input, $required = false) {
4810 $x = $this->just_one($input, 'saveemails', (empty($input) && false === $required) ? '' : get_bloginfo('admin_email'));
4811 if (is_array($x)) {
4812 foreach ($x as $ind => $val) {
4813 if (empty($val)) unset($x[$ind]);
4814 }
4815 if (empty($x)) $x = '';
4816 }
4817 return $x;
4818 }
4819
4820 /**
4821 * Filter the values down to just one (subject to being filtered)
4822 *
4823 * @param Array|String $input - input
4824 * @param String $filter - filter suffix to use
4825 * @param Boolean|String $rinput - a 'preferred' value (unless false) if no filtering is done
4826 *
4827 * @return Array|String|Null - output, after filtering
4828 */
4829 public function just_one($input, $filter = 'savestorage', $rinput = false) {
4830 $oinput = $input;
4831 if (false === $rinput) $rinput = is_array($input) ? array_pop($input) : $input;
4832 if (is_string($rinput) && false !== strpos($rinput, ',')) $rinput = substr($rinput, 0, strpos($rinput, ','));
4833 return apply_filters('updraftplus_'.$filter, $rinput, $oinput);
4834 }
4835
4836 /**
4837 * Enqueue the JavaScript and CSS for the select2 library
4838 */
4839 public function enqueue_select2() {
4840 // 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)
4841 wp_deregister_script('select2');
4842 wp_deregister_style('select2');
4843 $select2_version = $this->use_unminified_scripts() ? '4.1.0-rc.0'.'.'.time() : '4.1.0-rc.0';
4844 $min_or_not = $this->use_unminified_scripts() ? '' : '.min';
4845 wp_enqueue_script('select2', UPDRAFTPLUS_URL."/includes/select2/select2".$min_or_not.".js", array('jquery'), $select2_version);
4846 wp_enqueue_style('select2', UPDRAFTPLUS_URL."/includes/select2/select2".$min_or_not.".css", array(), $select2_version);
4847 }
4848
4849 public function memory_check_current($memory_limit = false) {
4850 // Returns in megabytes
4851 if (false == $memory_limit) $memory_limit = ini_get('memory_limit');
4852 $memory_limit = rtrim($memory_limit);
4853 $memory_unit = $memory_limit[strlen($memory_limit)-1];
4854 if (0 == (int) $memory_unit && '0' !== $memory_unit) {
4855 $memory_limit = substr($memory_limit, 0, strlen($memory_limit)-1);
4856 } else {
4857 $memory_unit = '';
4858 }
4859 switch ($memory_unit) {
4860 case '':
4861 $memory_limit = floor($memory_limit/1048576);
4862 break;
4863 case 'K':
4864 case 'k':
4865 $memory_limit = floor($memory_limit/1024);
4866 break;
4867 case 'G':
4868 $memory_limit = $memory_limit*1024;
4869 break;
4870 case 'M':
4871 // assumed size, no change needed
4872 break;
4873 }
4874 return $memory_limit;
4875 }
4876
4877 public function memory_check($memory, $check_using = false) {
4878 $memory_limit = $this->memory_check_current($check_using);
4879 return ($memory_limit >= $memory) ? true : false;
4880 }
4881
4882 /**
4883 * Get the UpdraftPlus RSS feed
4884 *
4885 * @uses fetch_feed()
4886 *
4887 * @return WP_Error|SimplePie WP_Error object on failure or SimplePie object on success
4888 */
4889 public function get_updraftplus_rssfeed() {
4890 if (!function_exists('fetch_feed')) include(ABSPATH.WPINC.'/feed.php');
4891 return fetch_feed('http://feeds.feedburner.com/updraftplus/');
4892 }
4893
4894 /**
4895 * Sets up the nonce, basic job data, opens a log file for a new restore job, and makes sure that the Updraft_Restorer class is available
4896 *
4897 * @param Boolean|string $nonce - the job nonce we want to use or false for a new one
4898 *
4899 * @return void
4900 */
4901 public function initiate_restore_job($nonce = false) {
4902 $this->backup_time_nonce($nonce);
4903 // we reset here so that we ensure the correct jobdata gets loaded while we resume
4904 $this->jobdata_reset();
4905 $this->jobdata_set('job_type', 'restore');
4906 $this->jobdata_set('job_time_ms', $this->job_time_ms);
4907 $this->logfile_open($this->nonce);
4908 if (!class_exists('Updraft_Restorer')) include_once(UPDRAFTPLUS_DIR.'/restorer.php');
4909 }
4910
4911 /**
4912 * Analyse a database file and return information about it
4913 *
4914 * @param Integer $timestamp - the database time in the backup history
4915 * @param Array $res - accompanying data. The key 'updraft_encryptionphrase' will be used for decryption if relevant.
4916 * @param Boolean|String $db_file - the path to the file to analyse; if not specified (false), then it will be obtained from the backup history
4917 * @param Boolean $header_only - whether or not to stop analysis once the header ends
4918 *
4919 * @return Array - containing arrays for the resulting messages, warnings, errors and meta information
4920 */
4921 public function analyse_db_file($timestamp, $res, $db_file = false, $header_only = false) {
4922
4923 $mess = array();
4924 $warn = array();
4925 $err = array();
4926 $info = array();
4927 $wp_version = $this->get_wordpress_version();
4928 global $wpdb;
4929
4930 if (!class_exists('UpdraftPlus_Database_Utility')) include_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
4931
4932 $updraft_dir = $this->backups_dir_location();
4933
4934 if (false === $db_file) {
4935 // 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.
4936 $this->max_packet_size();
4937
4938 $backup = UpdraftPlus_Backup_History::get_history($timestamp);
4939 if (!isset($backup['nonce']) || !isset($backup['db'])) return array($mess, $warn, $err, $info);
4940
4941 $db_file = is_string($backup['db']) ? $updraft_dir.'/'.$backup['db'] : $updraft_dir.'/'.$backup['db'][0];
4942 }
4943
4944 if (!is_readable($db_file)) return array($mess, $warn, $err, $info);
4945
4946 // Encrypted - decrypt it
4947 if (UpdraftPlus_Encryption::is_file_encrypted($db_file)) {
4948
4949 $encryption = empty($res['updraft_encryptionphrase']) ? UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase') : $res['updraft_encryptionphrase'];
4950
4951 if (!$encryption) {
4952 if (class_exists('UpdraftPlus_Addon_MoreDatabase')) {
4953 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted, but you have no encryption key entered.', 'updraftplus'));
4954 } else {
4955 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted.', 'updraftplus'));
4956 }
4957 return array($mess, $warn, $err, $info);
4958 }
4959
4960 $decrypted_file = UpdraftPlus_Encryption::decrypt($db_file, $encryption);
4961
4962 if (is_array($decrypted_file)) {
4963 $db_file = $decrypted_file['fullpath'];
4964 } else {
4965 $err[] = __('Decryption failed. The most likely cause is that you used the wrong key.', 'updraftplus');
4966 return array($mess, $warn, $err, $info);
4967 }
4968 }
4969
4970 // 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.
4971 if (filesize($db_file) < 1000) {
4972 $err[] = sprintf(__('The database is too small to be a valid WordPress database (size: %s Kb).', 'updraftplus'), round(filesize($db_file)/1024, 1));
4973 return array($mess, $warn, $err, $info);
4974 }
4975
4976 // If the backup is not from UpdraftPlus and it's not a simple SQL file then we don't want to scan
4977 if (!empty($backup['meta_foreign']) && 'genericsql' != $backup['meta_foreign']) {
4978 $info['skipped_db_scan'] = 1;
4979 return array($mess, $warn, $err, $info);
4980 }
4981
4982 $is_plain = ('.gz' == substr($db_file, -3, 3)) ? false : true;
4983
4984 $dbhandle = $is_plain ? fopen($db_file, 'r') : UpdraftPlus_Filesystem_Functions::gzopen_for_read($db_file, $warn, $err);
4985 if (!is_resource($dbhandle)) {
4986 $err[] = __('Failed to open database file.', 'updraftplus');
4987 return array($mess, $warn, $err, $info);
4988 }
4989
4990 $info['timestamp'] = $timestamp;
4991
4992 // Analyse the file, print the results.
4993
4994 $line = 0;
4995 $old_siteurl = '';
4996 $old_home = '';
4997 $old_table_prefix = null;
4998 $old_siteinfo = array();
4999 $gathering_siteinfo = true;
5000 $old_wp_version = '';
5001 $old_php_version = '';
5002
5003 $tables_found = array();
5004 $db_charsets_found = array();
5005
5006 $db_scan_timed_out = false;
5007 $php_max_input_vars_exceeded = false;
5008
5009 // 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
5010
5011 $wanted_tables = array('terms', 'term_taxonomy', 'term_relationships', 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', 'users', 'usermeta');
5012
5013 $migration_warning = false;
5014 $processing_create = false;
5015 $processing_routine = false;
5016 $db_version = $wpdb->db_version();
5017
5018 // Don't set too high - we want a timely response returned to the browser
5019 // 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.
5020 // "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)
5021 $default_dbscan_timeout = (filesize($db_file) < 31457280) ? 120 : 240;
5022 $dbscan_timeout = (defined('UPDRAFTPLUS_DBSCAN_TIMEOUT') && is_numeric(UPDRAFTPLUS_DBSCAN_TIMEOUT)) ? UPDRAFTPLUS_DBSCAN_TIMEOUT : $default_dbscan_timeout;
5023 if (function_exists('set_time_limit')) @set_time_limit($dbscan_timeout);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
5024
5025 // We limit the time that we spend scanning the file for character sets
5026 $db_charset_collate_scan_timeout = (defined('UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT') && is_numeric(UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT)) ? UPDRAFTPLUS_DB_CHARSET_COLLATE_SCAN_TIMEOUT : 10;
5027 $charset_scan_start_time = microtime(true);
5028 $db_supported_character_sets = (array) $GLOBALS['wpdb']->get_results('SHOW CHARACTER SET', OBJECT_K);
5029 $db_supported_collations = (array) $GLOBALS['wpdb']->get_results('SHOW COLLATION', OBJECT_K);
5030 $db_charsets_found = array();
5031 $db_collates_found = array();
5032 $db_supported_charset_related_to_unsupported_collation = false;
5033 $db_supported_charsets_related_to_unsupported_collations = array();
5034 while ((($is_plain && !feof($dbhandle)) || (!$is_plain && !gzeof($dbhandle))) && ($line<100 || (!$header_only && count($wanted_tables)>0) || ((microtime(true) - $charset_scan_start_time) < $db_charset_collate_scan_timeout && !empty($db_supported_character_sets)))) {
5035 $line++;
5036 // Up to 1MB
5037 $buffer = $is_plain ? rtrim(fgets($dbhandle, 1048576)) : rtrim(gzgets($dbhandle, 1048576));
5038 // Comments are what we are interested in
5039 if (substr($buffer, 0, 1) == '#') {
5040 $processing_create = false;
5041 $processing_routine = false;
5042 if ('' == $old_siteurl && preg_match('/^\# Backup of: (http(.*))$/', $buffer, $matches)) {
5043 $old_siteurl = untrailingslashit($matches[1]);
5044 $mess[] = __('Backup of:', 'updraftplus').' '.htmlspecialchars($old_siteurl).((!empty($old_wp_version)) ? ' '.sprintf(__('(version: %s)', 'updraftplus'), $old_wp_version) : '');
5045 // Check for should-be migration
5046 if (untrailingslashit(site_url()) != $old_siteurl) {
5047 if (!$migration_warning) {
5048 $migration_warning = true;
5049 $info['migration'] = true;
5050 // && !class_exists('UpdraftPlus_Addons_Migrator')
5051 if (UpdraftPlus_Manipulation_Functions::normalise_url($old_siteurl) == UpdraftPlus_Manipulation_Functions::normalise_url(site_url())) {
5052 // Same site migration with only http/https difference
5053 $info['same_url'] = false;
5054 $info['url_scheme_change'] = true;
5055 $old_siteurl_parsed = parse_url($old_siteurl);
5056 $actual_siteurl_parsed = parse_url(site_url());
5057 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)) {
5058 $powarn = sprintf(__('The website address in the backup set (%s) is slightly different from that of the site now (%s). This is not expected to be a problem for restoring the site, as long as visits to the former address still reach the site.', 'updraftplus'), $old_siteurl, site_url()).' ';
5059 } else {
5060 $powarn = '';
5061 }
5062 if (('https' == $old_siteurl_parsed['scheme'] && 'http' == $actual_siteurl_parsed['scheme']) || ('http' == $old_siteurl_parsed['scheme'] && 'https' == $actual_siteurl_parsed['scheme'])) {
5063 $powarn .= sprintf(__('This backup set is of this site, but at the time of the backup you were using %s, whereas the site now uses %s.', 'updraftplus'), $old_siteurl_parsed['scheme'], $actual_siteurl_parsed['scheme']);
5064 if ('https' == $old_siteurl_parsed['scheme']) {
5065 $powarn .= ' '.apply_filters('updraftplus_https_to_http_additional_warning', sprintf(__('This restoration will work if you still have an SSL certificate (i.e. can use https) to access the site. Otherwise, you will want to use %s to search/replace the site address so that the site can be visited without https.', 'updraftplus'), '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.__('the migrator add-on', 'updraftplus').'</a>'));
5066 } else {
5067 $powarn .= ' '.apply_filters('updraftplus_http_to_https_additional_warning', sprintf(__('As long as your web hosting allows http (i.e. non-SSL access) or will forward requests to https (which is almost always the case), this is no problem. If that is not yet set up, then you should set it up, or use %s so that the non-https links are automatically replaced.', 'updraftplus'), apply_filters('updraftplus_migrator_addon_link', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.__('the migrator add-on', 'updraftplus').'</a>')));
5068 }
5069 } else {
5070 $powarn .= apply_filters('updraftplus_dbscan_urlchange_www_append_warning', '');
5071 }
5072 $warn[] = $powarn;
5073 } else {
5074 // For completely different site migration
5075 $info['same_url'] = false;
5076 $info['url_scheme_change'] = false;
5077 $warn[] = apply_filters('updraftplus_dbscan_urlchange', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.sprintf(__('This backup set is from a different site (%s) - this is not a restoration, but a migration. You need the Migrator add-on in order to make this work.', 'updraftplus'), htmlspecialchars($old_siteurl.' / '.untrailingslashit(site_url()))).'</a>', $old_siteurl, $res);
5078 }
5079 if (!class_exists('UpdraftPlus_Addons_Migrator')) {
5080 $warn[] .= '<strong><a href="'.apply_filters('updraftplus_com_link', "https://updraftplus.com/faqs/tell-me-more-about-the-search-and-replace-site-location-in-the-database-option/").'" target="_blank">'.__('You can search and replace your database (for migrating a website to a new location/URL) with the Migrator add-on - follow this link for more information', 'updraftplus').'</a></strong>';
5081 }
5082 }
5083
5084 if ($this->mod_rewrite_unavailable(false)) {
5085 $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/');
5086 }
5087
5088 } else {
5089 // For exactly same URL site restoration
5090 $info['same_url'] = true;
5091 $info['url_scheme_change'] = false;
5092 }
5093 } elseif ('' == $old_home && preg_match('/^\# Home URL: (http(.*))$/', $buffer, $matches)) {
5094 $old_home = untrailingslashit($matches[1]);
5095 // Check for should-be migration
5096 if (!$migration_warning && UpdraftPlus_Manipulation_Functions::normalise_url(home_url()) != UpdraftPlus_Manipulation_Functions::normalise_url($old_home)) {
5097 $migration_warning = true;
5098 $powarn = apply_filters('updraftplus_dbscan_urlchange', '<a href="https://updraftplus.com/shop/migrator/" target="_blank">'.sprintf(__('This backup set is from a different site (%s) - this is not a restoration, but a migration. You need the Migrator add-on in order to make this work.', 'updraftplus'), htmlspecialchars($old_home.' / '.home_url())).'</a>', $old_home, $res);
5099 if (!empty($powarn)) $warn[] = $powarn;
5100 }
5101 } elseif (!isset($info['created_by_version']) && preg_match('/^\# Created by UpdraftPlus version ([\d\.]+)/', $buffer, $matches)) {
5102 $info['created_by_version'] = trim($matches[1]);
5103 } elseif ('' == $old_wp_version && preg_match('/^\# WordPress Version: ([0-9]+(\.[0-9]+)+)(-[-a-z0-9]+,)?(.*)$/', $buffer, $matches)) {
5104 $old_wp_version = $matches[1];
5105 if (!empty($matches[3])) $old_wp_version .= substr($matches[3], 0, strlen($matches[3])-1);
5106 if (version_compare($old_wp_version, $wp_version, '>')) {
5107 // $mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
5108 $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);
5109 }
5110 if (preg_match('/running on PHP ([0-9]+\.[0-9]+)(\s|\.)/', $matches[4], $nmatches) && preg_match('/^([0-9]+\.[0-9]+)(\s|\.)/', PHP_VERSION, $cmatches)) {
5111 $old_php_version = $nmatches[1];
5112 $current_php_version = $cmatches[1];
5113 if (version_compare($old_php_version, $current_php_version, '>')) {
5114 // $mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
5115 $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');
5116 } elseif (version_compare($old_php_version, $current_php_version, '<')) {
5117 $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 older than the server which you are now restoring onto (version %s).', 'updraftplus'), PHP_VERSION).' '.sprintf(__('You should only proceed if you have checked and are confident (or willing to risk) that your plugins/themes/etc. are compatible with the new %s version.', 'updraftplus'), 'PHP').' '.sprintf(__('Any support requests to do with %s should be raised with your web hosting company.', 'updraftplus'), 'PHP');
5118 }
5119 }
5120 } elseif (null === $old_table_prefix && (preg_match('/^\# Table prefix: ?(\S*)$/', $buffer, $matches) || preg_match('/^-- Table prefix: ?(\S*)$/i', $buffer, $matches))) {
5121 $old_table_prefix = $matches[1];
5122 // echo '<strong>'.__('Old table prefix:', 'updraftplus').'</strong> '.htmlspecialchars($old_table_prefix).'<br>';
5123 } elseif (empty($info['label']) && preg_match('/^\# Label: (.*)$/', $buffer, $matches)) {
5124 $info['label'] = $matches[1];
5125 $mess[] = __('Backup label:', 'updraftplus').' '.htmlspecialchars($info['label']);
5126 } elseif ($gathering_siteinfo && preg_match('/^\# Site info: (\S+)$/', $buffer, $matches)) {
5127 if ('end' == $matches[1]) {
5128 $gathering_siteinfo = false;
5129 // Sanity checks
5130 if (isset($old_siteinfo['multisite']) && !$old_siteinfo['multisite'] && is_multisite()) {
5131 // Just need to check that you're crazy
5132 // if (!defined('UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE') || !UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE) {
5133 // $err[] = sprintf(__('Error: %s', 'updraftplus'), __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus'));
5134 // return array($mess, $warn, $err, $info);
5135 // } else {
5136 $warn[] = __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus').' '.__('It will be imported as a new site.', 'updraftplus').' <a href="https://updraftplus.com/information-on-importing-a-single-site-wordpress-backup-into-a-wordpress-network-i-e-multisite/" target="_blank">'.__('Please read this link for important information on this process.', 'updraftplus').'</a>';
5137 // }
5138 // Got the needed code?
5139 if (!class_exists('UpdraftPlusAddOn_MultiSite') || !class_exists('UpdraftPlus_Addons_Migrator')) {
5140 $err[] = sprintf(__('Error: %s', 'updraftplus'), sprintf(__('To import an ordinary WordPress site into a multisite installation requires %s.', 'updraftplus'), 'UpdraftPlus Premium'));
5141 return array($mess, $warn, $err, $info);
5142 }
5143 } elseif (isset($old_siteinfo['multisite']) && $old_siteinfo['multisite'] && !is_multisite()) {
5144 $warn[] = __('Warning:', 'updraftplus').' '.__('Your backup is of a WordPress multisite install; but this site is not. Only the first site of the network will be accessible.', 'updraftplus').' <a href="https://codex.wordpress.org/Create_A_Network" target="_blank">'.__('If you want to restore a multisite backup, you should first set up your WordPress installation as a multisite.', 'updraftplus').'</a>';
5145 }
5146 } elseif (preg_match('/^([^=]+)=(.*)$/', $matches[1], $kvmatches)) {
5147 $key = $kvmatches[1];
5148 $val = $kvmatches[2];
5149 if ('multisite' == $key) {
5150 $info['multisite'] = $val ? true : false;
5151 if ($val) $mess[] = '<strong>'.__('Site information:', 'updraftplus').'</strong> '.'backup is of a WordPress Network';
5152 }
5153 $old_siteinfo[$key] = $val;
5154 }
5155 } elseif (preg_match('/^\# Skipped tables: (.*)$/', $buffer, $matches)) {
5156 $skipped_tables = explode(',', $matches[1]);
5157 }
5158
5159 } elseif (preg_match('#^\s*/\*\!40\d+ SET NAMES (.*)\*\/#i', $buffer, $smatches)) {
5160 $db_charsets_found[] = rtrim($smatches[1]);
5161 } elseif (!$processing_routine && !$processing_create && preg_match("/^[^'\"]*create[^'\"]*(?:definer\s*=\s*(?:`.{1,17}`@`[^\s]+`|'.{1,17}'@'[^\s]+'))?.+?(?:function(?:\s\s*if\s\s*not\s\s*exists)?|procedure)\s*`([^\r\n]+)`/is", $buffer, $matches)) {
5162 // ^\s*create\s\s*(?:or\s\s*replace\s\s*)?.*?(?:aggregate\s\s*function|function|procedure)\s\s*`(.+)`(?:\s\s*if\s\s*not\s\s*exists\s*|\s*)?\(
5163 if (!preg_match('/END\s*(?:\*\/)?;;\s*$/is', $buffer) && !preg_match('/\;\s*;;\s*$/is', $buffer) && !preg_match('/\s*(?:\*\/)?;;\s*$/is', $buffer)) $processing_routine = true;
5164 } elseif (!$processing_routine && preg_match('/^\s*create table \`?([^\`\(]*)\`?\s*\(/i', $buffer, $matches)) {
5165 $table = $matches[1];
5166 $tables_found[] = $table;
5167 if (null !== $old_table_prefix) {
5168 // Remove prefix
5169 $table = $old_table_prefix ? UpdraftPlus_Manipulation_Functions::str_replace_once($old_table_prefix, '', $table) : $table;
5170 if (in_array($table, $wanted_tables)) {
5171 $wanted_tables = array_diff($wanted_tables, array($table));
5172 }
5173 }
5174 if (empty($old_siteurl) && !empty($backup['meta_foreign'])) {
5175 $info['migration'] = true;
5176 }
5177 if (';' != substr($buffer, -1, 1)) {
5178 $processing_create = true;
5179 $db_supported_charset_related_to_unsupported_collation = true;
5180 }
5181 } elseif ($processing_create) {
5182 if (!empty($db_supported_collations)) {
5183 if (preg_match('/ COLLATE=([^\s;]+)/i', $buffer, $collate_match)) {
5184 $db_collates_found[] = $collate_match[1];
5185 if (!isset($db_supported_collations[$collate_match[1]])) {
5186 $db_supported_charset_related_to_unsupported_collation = true;
5187 }
5188 }
5189 if (preg_match('/ COLLATE ([a-zA-Z0-9._-]+),/i', $buffer, $collate_match)) {
5190 $db_collates_found[] = $collate_match[1];
5191 if (!isset($db_supported_collations[$collate_match[1]])) {
5192 $db_supported_charset_related_to_unsupported_collation = true;
5193 }
5194 }
5195 if (preg_match('/ COLLATE ([a-zA-Z0-9._-]+) /i', $buffer, $collate_match)) {
5196 $db_collates_found[] = $collate_match[1];
5197 if (!isset($db_supported_collations[$collate_match[1]])) {
5198 $db_supported_charset_related_to_unsupported_collation = true;
5199 }
5200 }
5201 }
5202 if (!empty($db_supported_character_sets)) {
5203 if (preg_match('/ CHARSET=([^\s;]+)/i', $buffer, $charset_match)) {
5204 $db_charsets_found[] = $charset_match[1];
5205 if ($db_supported_charset_related_to_unsupported_collation && !in_array($charset_match[1], $db_supported_charsets_related_to_unsupported_collations)) {
5206 $db_supported_charsets_related_to_unsupported_collations[] = $charset_match[1];
5207 }
5208 }
5209 }
5210 if (';' == substr($buffer, -1, 1)) {
5211 $processing_create = false;
5212 $db_supported_charset_related_to_unsupported_collation = false;
5213 }
5214 static $mysql_version_warned = false;
5215 if (!$mysql_version_warned && version_compare($db_version, '5.2.0', '<') && preg_match('/(CHARSET|COLLATE)[= ]utf8mb4/', $buffer)) {
5216 $mysql_version_warned = true;
5217 $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'));
5218 }
5219 } elseif ($processing_routine) {
5220 if ((preg_match('/END\s*(?:\*\/)?;;\s*$/is', $buffer) || preg_match('/\;\s*;;\s*$/is', $buffer) || preg_match('/\s*(?:\*\/)?;;\s*$/is', $buffer)) && !preg_match('/(?:--|#).+?;;\s*$/i', $buffer)) $processing_routine = false;
5221 }
5222 }
5223 if ($is_plain) {
5224 if (!feof($dbhandle)) $db_scan_timed_out = true;
5225 @fclose($dbhandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
5226 } else {
5227 if (!gzeof($dbhandle)) $db_scan_timed_out = true;
5228 @gzclose($dbhandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
5229 }
5230 if (!empty($db_supported_character_sets)) {
5231 $db_charsets_found_unique = array_unique($db_charsets_found);
5232 $db_unsupported_charset = array();
5233 $db_charset_forbidden = false;
5234 foreach ($db_charsets_found_unique as $db_charset) {
5235 if (!isset($db_supported_character_sets[$db_charset])) {
5236 $db_unsupported_charset[] = $db_charset;
5237 $db_charset_forbidden = true;
5238 }
5239 }
5240 if ($db_charset_forbidden) {
5241 $db_unsupported_charset_unique = array_unique($db_unsupported_charset);
5242 $warn[] = sprintf(_n("The database server that this WordPress site is running on doesn't support the character set (%s) which you are trying to import.", "The database server that this WordPress site is running on doesn't support the character sets (%s) which you are trying to import.", count($db_unsupported_charset_unique), 'updraftplus'), implode(', ', $db_unsupported_charset_unique)).' '.__('You can choose another suitable character set instead and continue with the restoration at your own risk.', 'updraftplus').' <a target="_blank" href="https://updraftplus.com/faqs/implications-changing-tables-character-set/" target="_blank">'.__('Go here for more information.', 'updraftplus').'</a>'.' <a target="_blank" href="https://updraftplus.com/faqs/implications-changing-tables-character-set/" target="_blank">'.__('Go here for more information.', 'updraftplus').'</a>';
5243 $db_supported_character_sets = array_keys($db_supported_character_sets);
5244 $similar_type_charset = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_charset_unique, $db_supported_character_sets, true);
5245 if (empty($similar_type_charset)) {
5246 $row = $GLOBALS['wpdb']->get_row('show variables like "character_set_database"');
5247 $similar_type_charset = (null !== $row) ? $row->Value : '';
5248 }
5249 if (empty($similar_type_charset) && !empty($db_supported_character_sets[0])) {
5250 $similar_type_charset = $db_supported_character_sets[0];
5251 }
5252 $charset_select_html = '<label>'.__('Your chosen character set to use instead:', 'updraftplus').'</label> ';
5253 $charset_select_html .= '<select name="updraft_restorer_charset" id="updraft_restorer_charset">';
5254 if (is_array($db_supported_character_sets)) {
5255 foreach ($db_supported_character_sets as $character_set) {
5256 if ($character_set == $similar_type_charset) $info['supported_charset'] = $character_set;
5257 $charset_select_html .= '<option value="'.esc_attr($character_set).'" '.selected($character_set, $similar_type_charset, false).'>'.esc_html($character_set).'</option>';
5258 }
5259 }
5260 $charset_select_html .= '</select>';
5261 if (empty($info['addui'])) $info['addui'] = '';
5262 $info['addui'] .= $charset_select_html;
5263 }
5264 }
5265 if (!empty($db_supported_collations)) {
5266 $db_collates_found_unique = array_unique($db_collates_found);
5267 $db_unsupported_collate = array();
5268 $db_collate_forbidden = false;
5269 foreach ($db_collates_found_unique as $db_collate) {
5270 if (!isset($db_supported_collations[$db_collate])) {
5271 $db_unsupported_collate[] = $db_collate;
5272 $db_collate_forbidden = true;
5273 }
5274 }
5275 if ($db_collate_forbidden) {
5276 $db_unsupported_collate_unique = array_unique($db_unsupported_collate);
5277 $warn[] = sprintf(_n("The database server that this WordPress site is running on doesn't support the collation (%s) used in the database which you are trying to import.", "The database server that this WordPress site is running on doesn't support multiple collations (%s) used in the database which you are trying to import.", count($db_unsupported_collate_unique), 'updraftplus'), implode(', ', $db_unsupported_collate_unique)).' '.__('You can choose another suitable collation instead and continue with the restoration (at your own risk).', 'updraftplus');
5278 $similar_type_collate = '';
5279 if ($db_charset_forbidden && !empty($similar_type_charset)) {
5280 $similar_type_collate = $this->get_similar_collate_related_to_charset($db_supported_collations, $db_unsupported_collate_unique, $similar_type_charset);
5281 }
5282 if (empty($similar_type_collate) && !empty($db_supported_charsets_related_to_unsupported_collations)) {
5283 $db_supported_collations_related_to_charset = array();
5284 foreach ($db_supported_collations as $db_supported_collation => $db_supported_collations_info_obj) {
5285 if (isset($db_supported_collations_info_obj->Charset) && in_array($db_supported_collations_info_obj->Charset, $db_supported_charsets_related_to_unsupported_collations)) {
5286 $db_supported_collations_related_to_charset[] = $db_supported_collation;
5287 }
5288 }
5289 if (!empty($db_supported_collations_related_to_charset)) {
5290 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, $db_supported_collations_related_to_charset, false);
5291 }
5292 }
5293 if (empty($similar_type_collate)) {
5294 $similar_type_collate = $this->get_similar_collate_based_on_ocuurence_count($db_collates_found, $db_supported_collations, $db_supported_charsets_related_to_unsupported_collations);
5295 }
5296 if (empty($similar_type_collate)) {
5297 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, array_keys($db_supported_collations), false);
5298 }
5299
5300 $collate_select_html = '<div class="notice below-h2 updraft-restore-option"><label>'.__('Your chosen replacement collation', 'updraftplus').':</label>';
5301 $collate_select_html .= '<select name="updraft_restorer_collate" id="updraft_restorer_collate">';
5302 $db_charsets_found_unique = array_unique($db_charsets_found);
5303 foreach ($db_supported_collations as $collate => $collate_info_obj) {
5304 $option_other_attr = array();
5305 if ($db_charset_forbidden && isset($collate_info_obj->Charset)) {
5306 $option_other_attr[] = 'data-charset='.esc_attr($collate_info_obj->Charset);
5307 if ($similar_type_charset != $collate_info_obj->Charset) {
5308 $option_other_attr[] = 'style="display:none;"';
5309 }
5310 } else {
5311 if (1 == count($db_charsets_found_unique)) {
5312 if (!in_array($collate_info_obj->Charset, $db_charsets_found_unique)) {
5313 $option_other_attr[] = 'style="display:none;"';
5314 }
5315 } else {
5316 $option_other_attr[] = 'style="display:none;"';
5317 }
5318 }
5319 $collate_select_html .= '<option value="'.esc_attr($collate).'" '.selected($collate, $similar_type_collate, false).' '.implode(' ', $option_other_attr).'>'.esc_html($collate).'</option>';
5320 }
5321
5322 if (count($db_charsets_found_unique) > 1 && !$db_charset_forbidden) {
5323 $collate_select_html .= '<option value="choose_a_default_for_each_table" selected="selected">'.__('Choose a default for each table', 'updraftplus').'</option>';
5324 }
5325 $collate_select_html .= '</select>';
5326 $collate_select_html .= '</div>';
5327
5328 $info['addui'] = empty($info['addui']) ? $collate_select_html : $info['addui'].'<br>'.$collate_select_html;
5329
5330 if ($db_charset_forbidden) {
5331 $collate_change_on_charset_selection_data = array(
5332 'db_supported_collations' => $db_supported_collations,
5333 'db_unsupported_collate_unique' => $db_unsupported_collate_unique,
5334 'db_collates_found' => $db_collates_found,
5335 );
5336 $info['addui'] .= '<input type="hidden" name="collate_change_on_charset_selection_data" id="collate_change_on_charset_selection_data" value="'.esc_attr(json_encode($collate_change_on_charset_selection_data)).'">';
5337 }
5338 }
5339 }
5340 /* $blog_tables = "CREATE TABLE $wpdb->terms (
5341 CREATE TABLE $wpdb->term_taxonomy (
5342 CREATE TABLE $wpdb->term_relationships (
5343 CREATE TABLE $wpdb->commentmeta (
5344 CREATE TABLE $wpdb->comments (
5345 CREATE TABLE $wpdb->links (
5346 CREATE TABLE $wpdb->options (
5347 CREATE TABLE $wpdb->postmeta (
5348 CREATE TABLE $wpdb->posts (
5349 $users_single_table = "CREATE TABLE $wpdb->users (
5350 $users_multi_table = "CREATE TABLE $wpdb->users (
5351 $usermeta_table = "CREATE TABLE $wpdb->usermeta (
5352 $ms_global_tables = "CREATE TABLE $wpdb->blogs (
5353 CREATE TABLE $wpdb->blog_versions (
5354 CREATE TABLE $wpdb->registration_log (
5355 CREATE TABLE $wpdb->site (
5356 CREATE TABLE $wpdb->sitemeta (
5357 CREATE TABLE $wpdb->signups (
5358 */
5359 if (!isset($skipped_tables)) $skipped_tables = array();
5360 $missing_tables = array();
5361
5362 if (null !== $old_table_prefix) {
5363
5364 if ('' === $old_table_prefix) $warn[] = __('This backup is of a site with an empty table prefix, which WordPress does not officially support; the results may be unreliable.', 'updraftplus');
5365
5366 if (!$header_only) {
5367 foreach ($wanted_tables as $table) {
5368 if (!in_array($old_table_prefix.$table, $tables_found)) {
5369 $missing_tables[] = $table;
5370 }
5371 }
5372
5373 foreach ($missing_tables as $key => $value) {
5374 if (in_array($old_table_prefix.$value, $skipped_tables)) {
5375 unset($missing_tables[$key]);
5376 }
5377 }
5378
5379 if (count($missing_tables)>0) {
5380 $warn[] = sprintf(__('This database backup is missing core WordPress tables: %s', 'updraftplus'), implode(', ', $missing_tables));
5381 }
5382 if (count($skipped_tables)>0) {
5383 $warn[] = sprintf(__('This database backup has the following WordPress tables excluded: %s', 'updraftplus'), implode(', ', $skipped_tables));
5384 }
5385 }
5386 } else {
5387 if (empty($backup['meta_foreign'])) {
5388 $warn[] = __('UpdraftPlus was unable to find the table prefix when scanning the database backup.', 'updraftplus');
5389 }
5390 }
5391
5392 $php_max_input_vars = ini_get("max_input_vars"); // phpcs:ignore PHPCompatibility.IniDirectives.NewIniDirectives.max_input_varsFound -- does not exist in PHP 5.2
5393
5394 if (false == $php_max_input_vars) {
5395 $php_max_input_vars_exceeded = true;
5396 } elseif (count($tables_found) >= 0.90 * $php_max_input_vars) {
5397 $php_max_input_vars_exceeded = true;
5398 // If the amount of tables exceed 90% of the php max input vars then truncate the list to 50% of the php max input vars value
5399 $tables_found = array_splice($tables_found, 0, $php_max_input_vars / 2);
5400 }
5401
5402 $php_max_input_vars_value = false == $php_max_input_vars ? 0 : $php_max_input_vars;
5403 $info['php_max_input_vars'] = $php_max_input_vars_value;
5404
5405 // On UD 1.16.30 - 1.16.34 there was a serious bug that did not backup all content in composite key tables, if this is not a migration and the backup was created on one of these versions do not restore this table.
5406 $skip_composite_tables = (!empty($info['created_by_version']) && version_compare("1" . substr($info['created_by_version'], 1), '1.16.30', '>=') && version_compare("1" . substr($info['created_by_version'], 1), '1.16.34', '<=')) ? true : false;
5407
5408 if ($skip_composite_tables) {
5409 if (!empty($info['migration'])) {
5410 $skip_composite_tables = false;
5411 $warn[] = sprintf(__('This backup was created on a previous UpdraftPlus version (%s) which did not correctly backup tables with composite primary keys (such as the term_relationships table, which records tags and product attributes).', 'updraftplus').' '.__('Therefore it is advised that you take a fresh backup on the source site, using a later version.', 'updraftplus'), $info['created_by_version']);
5412 } else {
5413 $warn[] = sprintf(__('This backup was created on a previous UpdraftPlus version (%s) which did not correctly backup tables with composite primary keys (such as the term_relationships table, which records tags and product attributes).', 'updraftplus').' '.__('Therefore, affected tables on the current site which already exist will not be replaced by default, to avoid corrupting them (you can review this in the list of tables below).', 'updraftplus'), $info['created_by_version']);
5414 }
5415 }
5416
5417 if (empty($tables_found)) {
5418 $warn[] = __('UpdraftPlus was unable to find any tables when scanning the database backup; it maybe corrupt.', 'updraftplus');
5419 } else {
5420 $select_restore_tables = '<div class="notice below-h2 updraft-restore-option">';
5421 $select_restore_tables .= '<p>'.__('If you do not want to restore all your database tables, then choose some to exclude here.', 'updraftplus').'(<a href="#" id="updraftplus_restore_tables_showmoreoptions">...</a>)</p>';
5422
5423 $select_restore_tables .= '<div class="updraftplus_restore_tables_options_container" style="display:none;">';
5424
5425 if ($db_scan_timed_out || $php_max_input_vars_exceeded) {
5426 if ($db_scan_timed_out) $all_other_table_title = __('The database scan was taking too long and consequently the list of all tables in the database could not be completed. This option will ensure all tables not found will be backed up.', 'updraftplus');
5427 if ($php_max_input_vars_exceeded) $all_other_table_title = __('The amount of database tables scanned is near or over the php_max_input_vars value so some tables maybe truncated. This option will ensure all tables not found will be backed up.', 'updraftplus');
5428 $select_restore_tables .= '<input class="updraft_restore_tables_options" id="updraft_restore_table_udp_all_other_tables" checked="checked" type="checkbox" name="updraft_restore_tables_options[]" value="udp_all_other_tables"> ';
5429 $select_restore_tables .= '<label for="updraft_restore_table_udp_all_other_tables" title="'.$all_other_table_title.'">'.__('Include all tables not listed below', 'updraftplus').'</label><br>';
5430 }
5431
5432 foreach ($tables_found as $table) {
5433 $checked = $skip_composite_tables && UpdraftPlus_Database_Utility::table_has_composite_private_key($table) ? '' : 'checked="checked"';
5434 $select_restore_tables .= '<input class="updraft_restore_tables_options" id="updraft_restore_table_'.$table.'" '. $checked .' type="checkbox" name="updraft_restore_tables_options[]" value="'.$table.'"> ';
5435 $select_restore_tables .= '<label for="updraft_restore_table_'.$table.'">'.$table.'</label><br>';
5436 }
5437 $select_restore_tables .= '</div></div>';
5438
5439 $info['addui'] = empty($info['addui']) ? $select_restore_tables : $info['addui'].'<br>'.$select_restore_tables;
5440 }
5441
5442 // //need to make sure that we reset the file back to .crypt before clean temp files
5443 // $db_file = $decrypted_file['fullpath'].'.crypt';
5444 // unlink($decrypted_file['fullpath']);
5445
5446 return array($mess, $warn, $err, $info);
5447 }
5448
5449 /**
5450 * Get the current outgoing IP address. Use this wisely; of course, it's not guaranteed to always be the same.
5451 *
5452 * @param Boolean $use_ipv6_service True to check the IP address using the IPv6 service with IPv4 fallback, false to use the IPv4 service only
5453 * @return String|Boolean - returns false upon failure
5454 */
5455 public function get_outgoing_ip_address($use_ipv6_service = false) {
5456 $urls = array('https://ipvigilante.com/json');
5457 if ($use_ipv6_service) array_unshift($urls, 'http://ip6.me/api');
5458 $urls = apply_filters('updraftplus_get_outgoing_ip_address', $urls);
5459 foreach ($urls as $url) {
5460 $ip_lookup = wp_remote_get($url, array('timeout' => 6));
5461 if (200 === wp_remote_retrieve_response_code($ip_lookup)) {
5462 $body = wp_remote_retrieve_body($ip_lookup);
5463 $info = json_decode($body, true);
5464 if (is_array($info)) {
5465 if (!empty($info['status']) && !empty($info['data']) && 'success' === $info['status']);
5466 if (!empty($info['data']['ipv4'])) return $info['data']['ipv4'];
5467 if (!empty($info['data']['ipv6'])) return $info['data']['ipv6'];
5468 } elseif (preg_match_all('/([^"\',]+|"(?:[^"]|")*?"|\'(?:[^\']|\')*?\')?(?:,|$)/is', $body, $matches)) { // https://regex101.com/r/Q8XjT4/1/
5469 $matches[1][0] = strtolower(trim($matches[1][0], ',\'" '));
5470 if (('ipv4' === $matches[1][0] || 'ipv6' === $matches[1][0]) && !empty($matches[1][1])) return trim($matches[1][1], ',\'" ');
5471 }
5472 }
5473 }
5474 return false;
5475 }
5476
5477 /**
5478 * Get default substitute similar collate related to charset
5479 *
5480 * @param array $db_supported_collations Supported collations. It should contain result of 'SHOW COLLATION' query
5481 * @param array $db_unsupported_collate_unique Unsupported unique collates collection
5482 * @param String $similar_type_charset Charset for which need to get default collate substitution
5483 * @return string $similar_type_collate default substitute collate which is best suitable or blank string
5484 */
5485 public function get_similar_collate_related_to_charset($db_supported_collations, $db_unsupported_collate_unique, $similar_type_charset) {
5486 $similar_type_collate = '';
5487 $db_supported_collations_related_to_charset = array();
5488 foreach ($db_supported_collations as $db_supported_collation => $db_supported_collations_info_obj) {
5489 if (isset($db_supported_collations_info_obj->Charset) && $db_supported_collations_info_obj->Charset == $similar_type_charset) {
5490 $db_supported_collations_related_to_charset[] = $db_supported_collation;
5491 }
5492 }
5493 if (!empty($db_supported_collations_related_to_charset)) {
5494 $similar_type_collate = UpdraftPlus_Manipulation_Functions::get_matching_str_from_array_elems($db_unsupported_collate_unique, $db_supported_collations_related_to_charset, false);
5495 }
5496 return $similar_type_collate;
5497 }
5498
5499 /**
5500 * Get default substitute similar collate based on existing supported collates count in database backup file
5501 *
5502 * @param array $db_collates_found All collates which have found in database backup file regardless whether they are supported or unsupported
5503 * @param array $db_supported_collations Supported collations. It should contain result of 'SHOW COLLATION' query
5504 * @param array $db_supported_charsets_related_to_unsupported_collations All charset which are related to unsupported collation
5505 *
5506 * @return string $similar_type_collate default substitute collate which is best suitable or blank string
5507 */
5508 public function get_similar_collate_based_on_ocuurence_count($db_collates_found, $db_supported_collations, $db_supported_charsets_related_to_unsupported_collations) {
5509 $similar_type_collate = '';
5510 $db_supported_collates_found_with_occurrence = array();
5511 foreach ($db_collates_found as $db_collate_found) {
5512 if (isset($db_supported_collations[$db_collate_found])) {
5513 if (isset($db_supported_collates_found_with_occurrence[$db_collate_found])) {
5514 $db_supported_collates_found_with_occurrence[$db_collate_found] = (int) $db_supported_collates_found_with_occurrence[$db_collate_found] + 1;
5515 } else {
5516 $db_supported_collates_found_with_occurrence[$db_collate_found] = 1;
5517 }
5518 }
5519 }
5520 if (!empty($db_supported_collates_found_with_occurrence)) {
5521 arsort($db_supported_collates_found_with_occurrence);
5522 if (!empty($db_supported_charsets_related_to_unsupported_collations)) {
5523 foreach ($db_supported_collates_found_with_occurrence as $db_supported_collate_with_occurrence => $occurrence_count) {
5524 if (isset($db_supported_collations[$db_supported_collate_with_occurrence]) && isset($db_supported_collations[$db_supported_collate_with_occurrence]->Charset) && in_array($db_supported_collations[$db_supported_collate_with_occurrence]->Charset, $db_supported_charsets_related_to_unsupported_collations)) {
5525 $similar_type_collate = $db_supported_collate_with_occurrence;
5526 break;
5527 }
5528 }
5529 } else {
5530 $similar_type_collate = array_search(max($db_supported_collates_found_with_occurrence), $db_supported_collates_found_with_occurrence);
5531 }
5532 }
5533 return $similar_type_collate;
5534 }
5535
5536 /**
5537 * Retrieves current clean url for anchor link where href attribute value is not url (for ex. #div) or empty. Output is not escaped - caller should escape.
5538 *
5539 * @return String - current clean url
5540 */
5541 public static function get_current_clean_url() {
5542
5543 // Within an UpdraftCentral context, there should be no prefix on the anchor link
5544 if (defined('UPDRAFTCENTRAL_COMMAND') && UPDRAFTCENTRAL_COMMAND || defined('WP_CLI') && WP_CLI) return '';
5545
5546 if (defined('DOING_AJAX') && DOING_AJAX && !empty($_SERVER['HTTP_REFERER'])) {
5547 $current_url = $_SERVER['HTTP_REFERER'];
5548 } else {
5549 $url_prefix = is_ssl() ? 'https' : 'http';
5550 $host = empty($_SERVER['HTTP_HOST']) ? parse_url(network_site_url(), PHP_URL_HOST) : $_SERVER['HTTP_HOST'];
5551 $current_url = $url_prefix."://".$host.$_SERVER['REQUEST_URI'];
5552 }
5553 $remove_query_args = array('state', 'action', 'oauth_verifier', 'nonce', 'updraftplus_instance', 'access_token', 'user_id', 'updraftplus_googledriveauth');
5554
5555 return UpdraftPlus_Manipulation_Functions::wp_unslash(remove_query_arg($remove_query_args, $current_url));
5556 }
5557
5558 /**
5559 * TODO: Remove legacy storage setting keys from here
5560 * 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
5561 *
5562 * @return Array - the list of keys
5563 */
5564 public function get_settings_keys() {
5565 // N.B. updraft_backup_history is not included here, as we don't want that wiped
5566 return array(
5567 'updraft_autobackup_default',
5568 'updraft_dropbox',
5569 'updraft_googledrive',
5570 'updraftplus_tmp_googledrive_access_token',
5571 'updraftplus_dismissedautobackup',
5572 'dismissed_general_notices_until',
5573 'dismissed_review_notice',
5574 'dismissed_clone_php_notices_until',
5575 'dismissed_clone_wc_notices_until',
5576 'dismissed_season_notices_until',
5577 'updraftplus_dismissedexpiry',
5578 'updraftplus_dismisseddashnotice',
5579 'updraft_interval',
5580 'updraft_interval_increments',
5581 'updraft_interval_database',
5582 'updraft_retain',
5583 'updraft_retain_db',
5584 'updraft_encryptionphrase',
5585 'updraft_service',
5586 'updraft_googledrive_clientid',
5587 'updraft_googledrive_secret',
5588 'updraft_googledrive_remotepath',
5589 'updraft_ftp',
5590 'updraft_backblaze',
5591 'updraft_server_address',
5592 'updraft_dir',
5593 'updraft_email',
5594 'updraft_delete_local',
5595 'updraft_debug_mode',
5596 'updraft_include_plugins',
5597 'updraft_include_themes',
5598 'updraft_include_uploads',
5599 'updraft_include_others',
5600 'updraft_include_wpcore',
5601 'updraft_include_wpcore_exclude',
5602 'updraft_include_more',
5603 'updraft_include_blogs',
5604 'updraft_include_mu-plugins',
5605 'updraft_auto_updates', // since WordPress 5.5, updraft_auto_updates option is no longer used and has been removed from the code, but the HTML IDs which use the same name that represent the automatic update setting are still zealously preserved so this one cannot be removed
5606 'updraft_include_others_exclude',
5607 'updraft_include_uploads_exclude',
5608 'updraft_lastmessage',
5609 'updraft_googledrive_token',
5610 'updraft_dropboxtk_request_token',
5611 'updraft_dropboxtk_access_token',
5612 'updraft_adminlocking',
5613 'updraft_updraftvault',
5614 'updraft_remotesites',
5615 'updraft_migrator_localkeys',
5616 'updraft_central_localkeys',
5617 'updraft_retain_extrarules',
5618 'updraft_googlecloud',
5619 'updraft_include_more_path',
5620 'updraft_split_every',
5621 'updraft_ssl_nossl',
5622 'updraft_backupdb_nonwp',
5623 'updraft_extradbs',
5624 'updraft_combine_jobs_around',
5625 'updraft_last_backup',
5626 'updraft_starttime_files',
5627 'updraft_starttime_db',
5628 'updraft_startday_db',
5629 'updraft_startday_files',
5630 'updraft_sftp',
5631 'updraft_s3',
5632 'updraft_s3generic',
5633 'updraft_dreamhost',
5634 'updraft_s3generic_login',
5635 'updraft_s3generic_pass',
5636 'updraft_s3generic_remote_path',
5637 'updraft_s3generic_endpoint',
5638 'updraft_webdav',
5639 'updraft_openstack',
5640 'updraft_onedrive',
5641 'updraft_azure',
5642 'updraft_cloudfiles',
5643 'updraft_cloudfiles_user',
5644 'updraft_cloudfiles_apikey',
5645 'updraft_cloudfiles_path',
5646 'updraft_cloudfiles_authurl',
5647 'updraft_ssl_useservercerts',
5648 'updraft_ssl_disableverify',
5649 'updraft_s3_login',
5650 'updraft_s3_pass',
5651 'updraft_s3_remote_path',
5652 'updraft_dreamobjects_login',
5653 'updraft_dreamobjects_pass',
5654 'updraft_dreamobjects_remote_path',
5655 'updraft_dreamobjects',
5656 'updraft_report_warningsonly',
5657 'updraft_report_wholebackup',
5658 'updraft_report_dbbackup',
5659 'updraft_log_syslog',
5660 'updraft_extradatabases',
5661 'updraftplus_tour_cancelled_on',
5662 'updraftplus_version',
5663 );
5664 }
5665
5666 /**
5667 * 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.
5668 *
5669 * @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' => ''))
5670 * 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
5671 * @return Array - databases and their table names
5672 */
5673 public function get_database_tables($dbsinfo = array('wp' => array())) {
5674
5675 global $wpdb;
5676
5677 if (!class_exists('UpdraftPlus_Database_Utility')) include_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
5678
5679 $dbhandle = '';
5680 $db_tables_array = array();
5681
5682 foreach ($dbsinfo as $key => $value) {
5683 if ('wp' == $key) {
5684 // The unfiltered table prefix - i.e. the real prefix that things are relative to
5685 $table_prefix_raw = $this->get_table_prefix(false);
5686 $dbhandle = $wpdb;
5687 } else {
5688 $dbhandle = new UpdraftPlus_WPDB_OtherDB_Utility($dbsinfo[$key]['user'], $dbsinfo[$key]['pass'], $dbsinfo[$key]['name'], $dbsinfo[$key]['host']);
5689 if (!empty($dbhandle->error)) {
5690 return $this->log_wp_error($dbhandle->error);
5691 }
5692 $table_prefix_raw = $dbsinfo[$key]['prefix'];
5693 }
5694
5695 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
5696 $all_tables = $dbhandle->get_results("SHOW FULL TABLES", ARRAY_N);
5697
5698 if (empty($all_tables) && !empty($dbhandle->last_error)) {
5699 $all_tables = $dbhandle->get_results("SHOW TABLES", ARRAY_N);
5700 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
5701 } else {
5702 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
5703 }
5704
5705 // If this is not the WP database, then we do not consider it a fatal error if there are no tables
5706 if ('wp' == $key && 0 == count($all_tables)) {
5707 return $this->log_wp_error("No tables found in wp database.");
5708 die;
5709 }
5710
5711 // Put the options table first
5712 $updraftplus_database_utility = new UpdraftPlus_Database_Utility($key, $table_prefix_raw, $dbhandle);
5713 usort($all_tables, array($updraftplus_database_utility, 'backup_db_sorttables'));
5714
5715 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
5716 $db_tables_array[$key] = $all_table_names;
5717 }
5718
5719 return $db_tables_array;
5720 }
5721
5722 /**
5723 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
5724 *
5725 * @param Array $a - the array
5726 *
5727 * @return Array - with keys 'name' and 'type'
5728 */
5729 private function cb_get_name_base_type($a) {
5730 return array('name' => $a[0], 'type' => 'BASE TABLE');
5731 }
5732
5733 /**
5734 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
5735 *
5736 * @param Array $a - the array
5737 *
5738 * @return Array - keys are 'name' and 'type'
5739 */
5740 private function cb_get_name_type($a) {
5741 return array('name' => $a[0], 'type' => $a[1]);
5742 }
5743
5744 /**
5745 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
5746 *
5747 * @param Array $a - the array
5748 *
5749 * @return Mixed - the value with key (string)'name'
5750 */
5751 private function cb_get_name($a) {
5752 return $a['name'];
5753 }
5754
5755 /**
5756 * Retrieves the appropriate URL for the given target page
5757 *
5758 * @internal
5759 * @param String $which_page The target page
5760 * @return String - The requested URL for a given page
5761 */
5762 public function get_url($which_page = false) {
5763 switch ($which_page) {
5764 case 'my-account':
5765 return apply_filters('updraftplus_com_myaccount', 'https://updraftplus.com/my-account/');
5766 break;
5767 case 'shop':
5768 return apply_filters('updraftplus_com_shop', 'https://updraftplus.com/shop/');
5769 break;
5770 case 'premium':
5771 return apply_filters('updraftplus_com_premium', 'https://updraftplus.com/shop/updraftplus-premium/');
5772 break;
5773 case 'buy-tokens':
5774 return apply_filters('updraftplus_com_updraftclone_tokens', 'https://updraftplus.com/shop/updraftclone-tokens/');
5775 break;
5776 case 'lost-password':
5777 return apply_filters('updraftplus_com_myaccount_lostpassword', 'https://updraftplus.com/my-account/lost-password/');
5778 break;
5779 case 'mothership':
5780 return apply_filters('updraftplus_com_mothership', 'https://updraftplus.com/plugin-info');
5781 break;
5782 case 'shop_premium':
5783 return apply_filters('updraftplus_com_shop_premium', 'https://updraftplus.com/shop/updraftplus-premium/');
5784 break;
5785 case 'shop_vault_5':
5786 return apply_filters('updraftplus_com_shop_vault_5', 'https://updraftplus.com/shop/updraftplus-vault-storage-5-gb/');
5787 break;
5788 case 'shop_vault_15':
5789 return apply_filters('updraftplus_com_shop_vault_15', 'https://updraftplus.com/shop/updraftplus-vault-storage-15-gb/');
5790 break;
5791 case 'shop_vault_50':
5792 return apply_filters('updraftplus_com_shop_vault_50', 'https://updraftplus.com/shop/updraftplus-vault-storage-50-gb/');
5793 break;
5794 case 'anon_backups':
5795 return apply_filters('updraftplus_com_anon_backups', 'https://updraftplus.com/upcoming-updraftplus-feature-clone-data-anonymisation/');
5796 break;
5797 case 'clone_packages':
5798 return apply_filters('updraftplus_com_clone_packages', 'https://updraftplus.com/faqs/what-is-the-largest-site-that-i-can-clone-with-updraftclone/');
5799 break;
5800 default:
5801 return 'URL not found ('.$which_page.')';
5802 }
5803 }
5804
5805 /**
5806 * Get log message for permission failure
5807 *
5808 * @param String $path full path of file or folder
5809 * @param String $log_message_prefix action which is performed to path
5810 * @param String $directory_prefix_in_log_message Directory Prefix. It should be either "Parent" or "Destination"
5811 * @return string|boolean log message (HTML). If posix function doesn't exist, It returns false
5812 */
5813 public function log_permission_failure_message($path, $log_message_prefix, $directory_prefix_in_log_message = 'Parent') {
5814 if ($this->do_posix_functions_exist()) {
5815 $stat_data = stat($path);
5816 $log_message = $log_message_prefix.': Failed. ';
5817 $log_message .= $directory_prefix_in_log_message.' Directory UID='.$stat_data['uid'].', GID='.$stat_data['gid'].'. ';
5818 $log_message .= $this->get_log_message_for_current_uid_and_gid();
5819 return $log_message;
5820 } else {
5821 return false;
5822 }
5823 }
5824
5825 /**
5826 * Get log message for current uid and gid
5827 *
5828 * @return String log message of current process (HTML)
5829 */
5830 private function get_log_message_for_current_uid_and_gid() {
5831 $log_message = 'Effective/real user IDs of the current process: '.posix_geteuid().'/'.posix_getuid().'. ';
5832 $log_message .= 'Effective/real group IDs of the current process: '.posix_getegid().'/'.posix_getgid().'. ';
5833 return $log_message;
5834 }
5835
5836 /**
5837 * Restore any previously-removed autoloaders
5838 */
5839 public function restore_composer_autoloaders() {
5840 foreach ($this->removed_autoloaders as $callable) {
5841 if (is_callable($callable, false, $callable_name)) {
5842 $this->log("Clean-up: re-registering composer autoloader: $callable_name");
5843 spl_autoload_register($callable, false);
5844 }
5845 }
5846 $this->removed_autoloaders = array();
5847 }
5848
5849 /**
5850 * Remove any potentially clashing composer PSR4 autoloaders. Only to be used inside a backup when no other plugins' libraries should be needed
5851 *
5852 * @param Array $prefixes
5853 */
5854 public function potentially_remove_composer_autoloaders($prefixes) {
5855
5856 if (!defined('UPDRAFTPLUS_REMOVE_COMPOSER_AUTOLOADERS') || !UPDRAFTPLUS_REMOVE_COMPOSER_AUTOLOADERS) return;
5857
5858 $functions = spl_autoload_functions();
5859 foreach ($functions as $callable) {
5860 if (!is_array($callable) || !isset($callable[0]) || !is_object($callable[0])) continue;
5861 if (!is_a($callable[0], 'Composer\Autoload\ClassLoader') || !is_callable(array($callable[0], 'getPrefixesPsr4'))) continue;
5862 $prefixes_psr4 = $callable[0]->getPrefixesPsr4();
5863 if (!is_array($prefixes_psr4)) continue;
5864 foreach ($prefixes as $prefix) {
5865 if (!isset($prefixes_psr4[$prefix])) continue;
5866 $is_ud = false;
5867 if (is_array($prefixes_psr4[$prefix])) {
5868 foreach ($prefixes_psr4[$prefix] as $path) {
5869 if (false !== strpos(UpdraftPlus_Manipulation_Functions::wp_normalize_path($path), '/'.basename(UpdraftPlus_Manipulation_Functions::wp_normalize_path(UPDRAFTPLUS_DIR)).'/vendor/')) {
5870 $is_ud = true;
5871 }
5872 }
5873 }
5874 if ($is_ud) continue;
5875 if (is_callable($callable, false, $callable_name)) {
5876 $this->log("Conflict prevention: de-registering composer autoloader: $callable_name");
5877 }
5878 $this->removed_autoloaders[] = $callable;
5879 spl_autoload_unregister($callable);
5880 break;
5881 }
5882 }
5883 }
5884
5885 /**
5886 * Try to deal with other plugins with incompatible versions and bugs
5887 */
5888 public function mitigate_guzzle_autoloader_conflicts() {
5889 // Work round bug in the JetPack autoloader which loads a file in a different namespace
5890 $potentially_include_in = array('guzzlehttp/guzzle', 'guzzlehttp/promises', 'guzzlehttp/psr7');
5891 foreach ($potentially_include_in as $package) {
5892 $file = UPDRAFTPLUS_DIR.'/vendor/'.$package.'/src/functions_include.php';
5893 // Avoid conflicting with Google Ads and Listings which has already loaded this function
5894 if ('guzzlehttp/guzzle' == $package && function_exists('\GuzzleHttp\choose_handler')) continue;
5895 if (file_exists($file)) include_once($file);
5896 }
5897 }
5898
5899 /**
5900 * Checks whether POSIX functions exists or not
5901 *
5902 * @return boolean true if POSIX functions exists or not
5903 */
5904 private function do_posix_functions_exist() {
5905 return function_exists('posix_geteuid') && function_exists('posix_getuid') && function_exists('posix_getegid') && function_exists('posix_getgid');
5906 }
5907
5908 /**
5909 * Wipe state-related data (e.g. on wiping settings, or on a restore). Note that there is some internal knowledge within the method below of how it is being used (if not including locks, then check for an active job)
5910 *
5911 * @param Boolean $include_locks Whether to also wipe out data other than just updraft_jobdata (e.g. updraft semaphore, lock, schedule, etc.)
5912 * @param String $table What table the data is in. It recognises only two tables ('options', 'sitemeta'), the default is 'options'
5913 */
5914 public function wipe_state_data($include_locks = false, $table = 'options') {
5915 // These aren't in get_settings_keys() because they are always in the options table, regardless of context
5916 global $wpdb;
5917 switch ($table) {
5918 case 'sitemeta':
5919 $table = $wpdb->sitemeta;
5920 $field = 'meta_key';
5921 break;
5922 default:
5923 $table = $wpdb->options;
5924 $field = 'option_name';
5925 // if multisite do we need site_id column in the where clause?
5926 break;
5927 }
5928 if (!class_exists('UpdraftPlus_Database_Utility')) include_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
5929 if ($include_locks) {
5930 $wpdb->query($wpdb->prepare("DELETE FROM $table WHERE ($field LIKE %s OR $field LIKE %s OR $field LIKE %s OR $field LIKE %s OR $field LIKE %s OR $field LIKE %s)", UpdraftPlus_Database_Utility::esc_like('updraftplus_unlocked_').'%', UpdraftPlus_Database_Utility::esc_like('updraftplus_locked_').'%', UpdraftPlus_Database_Utility::esc_like('updraftplus_last_lock_time_').'%', UpdraftPlus_Database_Utility::esc_like('updraftplus_semaphore_').'%', UpdraftPlus_Database_Utility::esc_like('updraft_jobdata_').'%', UpdraftPlus_Database_Utility::esc_like('updraft_last_scheduled_').'%'));
5931 } else {
5932 $sql = '';
5933 if (!empty($this->nonce)) {
5934 $sql = $wpdb->prepare("DELETE FROM $table WHERE $field LIKE %s AND $field != %s", UpdraftPlus_Database_Utility::esc_like('updraft_jobdata_').'%', "updraft_jobdata_{$this->nonce}");
5935 } else {
5936 $sql = $wpdb->prepare("DELETE FROM $table WHERE $field LIKE %s", UpdraftPlus_Database_Utility::esc_like('updraft_jobdata_').'%');
5937 }
5938 $wpdb->query($sql);
5939 }
5940 }
5941
5942 /**
5943 * Checks whether debug mode is on or not. If it is on then unminified script will be used.
5944 *
5945 * @return boolean true indicate use the unminified script
5946 */
5947 public function use_unminified_scripts() {
5948 return UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') || (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG);
5949 }
5950
5951 /**
5952 * This function has checks in place to see if a restore is still in progress
5953 * Currently used in this->block_updates_during_restore_progress and admin->print_restore_in_progress_box_if_needed
5954 *
5955 * @uses $_REQUEST['action']
5956 * @param Int $job_time_greater_than Specify the time in seconds. Default is 120 seconds but function like block_updates_during_restore_progress has a 1 second time set as we want to check as soon as a restore is kicked off
5957 * @return void|array There is a possibility if there is no restore in progress this can return a void. However, in every other case, it will return an array.
5958 */
5959 public function check_restore_progress($job_time_greater_than = 120) {
5960 $restore_progress = array();
5961 $restore_progress['status'] = false;
5962 $restore_in_progress = get_site_option('updraft_restore_in_progress');
5963 if (empty($restore_in_progress)) return;
5964
5965 $restore_jobdata = $this->jobdata_getarray($restore_in_progress);
5966 if (is_array($restore_jobdata) && !empty($restore_jobdata)) {
5967 // Only print if within the last 24 hours; and only after 2 minutes
5968 if (isset($restore_jobdata['job_type']) && 'restore' == $restore_jobdata['job_type'] && isset($restore_jobdata['second_loop_entities']) && !empty($restore_jobdata['second_loop_entities']) && isset($restore_jobdata['job_time_ms']) && (time() - $restore_jobdata['job_time_ms'] > $job_time_greater_than || (defined('UPDRAFTPLUS_RESTORE_PROGRESS_ALWAYS_SHOW') && UPDRAFTPLUS_RESTORE_PROGRESS_ALWAYS_SHOW)) && time() - $restore_jobdata['job_time_ms'] < 86400 && (empty($_REQUEST['action']) || ('updraft_restore' != $_REQUEST['action'] && 'updraft_restore_continue' != $_REQUEST['action']))) {
5969
5970 $restore_progress['status'] = true;
5971 $restore_progress['restore_jobdata'] = $restore_jobdata;
5972 $restore_progress['restore_in_progress'] = $restore_in_progress;
5973
5974 return $restore_progress;
5975 }
5976 }
5977 }
5978
5979 /**
5980 * Checking to see if a restore is in progress before
5981 * Turning off WP updates while restoration is in progress
5982 */
5983 public function block_updates_during_restore_progress() {
5984 $check_restore_progress = $this->check_restore_progress(1);
5985 // Check to see if the restore is still in progress
5986 if (is_array($check_restore_progress) && true == $check_restore_progress['status']) {
5987 add_filter('pre_site_transient_update_core', '__return_false'); // Disable WordPress core updates
5988 add_filter('pre_site_transient_update_plugins', '__return_false'); // Disable WordPress plugin updates
5989 add_filter('pre_site_transient_update_themes', '__return_false'); // Disable WordPress themes updates
5990 }
5991 }
5992
5993 /**
5994 * Retrieve the list of server (Apache, Nginx, PHP, etc..) configuration file names
5995 */
5996 public function server_configuration_file_list() {
5997 $server_config_filenames = array(
5998 '.user.ini',
5999 '.htaccess',
6000 );
6001 // the default value for user_ini.filename setting in PHP.ini file is '.user.ini' but this could be set to use different file name
6002 $server_config_filenames[] = ini_get('user_ini.filename'); // phpcs:ignore PHPCompatibility.IniDirectives.NewIniDirectives.user_ini_filenameFound
6003 $server_config_filenames = array_unique($server_config_filenames);
6004 return $server_config_filenames;
6005 }
6006
6007 /**
6008 * Check what hosting company that this plugin is installed onto and if there appear to be any restriction being applied to it
6009 *
6010 * @return Array An array of information regarding the hosting company or empty array if this method fails to recognise the hosting company
6011 */
6012 public function get_hosting_info() {
6013
6014 $hosting_company = array(
6015 'name' => '',
6016 'website' => '',
6017 'restriction' => array(),
6018 );
6019
6020 if (array_key_exists('KINSTA_CACHE_ZONE', $_SERVER)) {
6021 $hosting_company = array(
6022 'name' => 'Kinsta',
6023 'website' => 'kinsta.com',
6024 'restriction' => array(
6025 'only_one_backup_per_month',
6026 'only_one_incremental_per_day',
6027 )
6028 );
6029 }
6030
6031 return apply_filters('updraftplus_get_hosting_info', $hosting_company);
6032 }
6033
6034 /**
6035 * Check whether the hosting provider has some restriction
6036 *
6037 * @param String|Array $restriction An array or string of restriction
6038 * @return Boolean True if the hosting provider has the given restriction, false otherwise
6039 */
6040 public function is_restricted_hosting($restriction) {
6041
6042 $restriction = (array) $restriction;
6043
6044 $hosting_company = $this->get_hosting_info();
6045
6046 if (empty($hosting_company)) return false;
6047
6048 foreach ($restriction as $rstc) {
6049 if (in_array($rstc, $hosting_company['restriction'])) return true;
6050 }
6051
6052 return false;
6053 }
6054
6055 /**
6056 * Check whether the hosting has a number of backups restrictions that can be created at a particular time and whether that number has reached the limit or the time elapsed has passed the limit
6057 */
6058 public function is_hosting_backup_limit_reached() {
6059 $res = array();
6060 $last_backup = UpdraftPlus_Options::get_updraft_option('updraft_last_backup', array());
6061 if (empty($last_backup)) $last_backup = array();
6062 $current_time = time();
6063 if (!empty($last_backup['incremental_backup_time'])) {
6064 // $next_day_from_last_backup = strtotime(gmdate('Y-m-d', (int) $last_backup['backup_time'])) + 86400;
6065 $next_24hours_from_last_backup = strtotime(gmdate('Y-m-d H:i:s', (int) $last_backup['incremental_backup_time'])) + 86400;
6066 // one incremental per day and the time has gone 24 hours past the last incremental backup time
6067 if ($this->is_restricted_hosting('only_one_incremental_per_day') && $current_time < $next_24hours_from_last_backup) $res[] = 'only_one_incremental_per_day';
6068 }
6069 if (!empty($last_backup['nonincremental_backup_time'])) {
6070 // $first_day_of_next_month_from_last_backup = strtotime(gmdate('Y-m-t', (int) $last_backup['backup_time']))+86400;
6071 $next_thirty_days_from_last_backup = strtotime(gmdate('Y-m-d H:i:s', (int) $last_backup['nonincremental_backup_time'])) + (86400 * 30);
6072 // Check whether the hosting provider permits only one backup per month and whether the time has gone 30 days past the last backup time
6073 if ($this->is_restricted_hosting('only_one_backup_per_month') && $current_time < $next_thirty_days_from_last_backup) $res[] = 'only_one_backup_per_month';
6074 }
6075 return $res;
6076 }
6077
6078 /**
6079 * Maintain compatibility on all versions between WordPress and UpdraftPlus, specifically since WordPress 5.5
6080 */
6081 public function wordpress_55_updates_potential_migration() {
6082 // Due to the new WP's auto-updates interface in WordPress version 5.5, we need to maintain the auto update compatibility on all versions of WordPress and UpdraftPlus
6083 $udp_saved_version = UpdraftPlus_Options::get_updraft_option('updraftplus_version');
6084 $updraft_auto_updates = UpdraftPlus_Options::get_updraft_option('updraft_auto_updates');
6085 if (!$udp_saved_version || version_compare($udp_saved_version, '1.16.34', '<=') || (version_compare($udp_saved_version, '2.0.0', '>=') && version_compare($udp_saved_version, '2.16.34', '<=')) || null !== $updraft_auto_updates) {
6086 $this->replace_auto_updates_option();
6087 }
6088 }
6089
6090 /**
6091 * Remove the use of updraft_auto_updates option/meta (single & multisite) and replace it with auto_update_plugins site option that is used in WordPress's core since version 5.5
6092 * This needs to be done in order to maintain auto-updates compatibility between WordPress and Updraftplus and to synchronise the auto-updates setting for both
6093 */
6094 private function replace_auto_updates_option() {
6095 $old_setting_value = UpdraftPlus_Options::get_updraft_option('updraft_auto_updates');
6096 UpdraftPlus_Options::delete_updraft_option('updraft_auto_updates');
6097 $new_setting_value = (array) get_site_option('auto_update_plugins', array());
6098 if (!empty($old_setting_value)) $new_setting_value[] = basename(UPDRAFTPLUS_DIR).'/updraftplus.php';
6099 $new_setting_value = array_unique($new_setting_value);
6100 update_site_option('auto_update_plugins', $new_setting_value);
6101 }
6102
6103 /**
6104 * Set the plugin's automatic updates setting to either on or off by removing/adding plugin basename from/into the auto_update_plugins option
6105 *
6106 * @param Mixed $value The new value which auto_update_plugins option value is replaced with
6107 */
6108 public function set_automatic_updates($value) {
6109 $auto_update_plugins = (array) get_site_option('auto_update_plugins', array());
6110 if (!empty($value)) {
6111 $auto_update_plugins[] = basename(UPDRAFTPLUS_DIR).'/updraftplus.php';
6112 $auto_update_plugins = array_unique($auto_update_plugins);
6113 } else {
6114 $auto_update_plugins = array_diff($auto_update_plugins, array(basename(UPDRAFTPLUS_DIR).'/updraftplus.php'));
6115 }
6116 update_site_option('auto_update_plugins', $auto_update_plugins);
6117 }
6118
6119 /**
6120 * Check whether the automatic-updates is set for UpdraftPlus
6121 *
6122 * @return Boolean True if set, false otherwise
6123 */
6124 public function is_automatic_updating_enabled() {
6125 $auto_update_plugins = (array) get_site_option('auto_update_plugins', array());
6126 return in_array(basename(UPDRAFTPLUS_DIR).'/updraftplus.php', $auto_update_plugins, true);
6127 }
6128
6129 /**
6130 * Perform conditional checking of two values with the specified operator
6131 *
6132 * @param Mixed $value1 the first value to compare
6133 * @param String $operator the operator that is used for comparison of the two values
6134 * @param Mixed $value2 the second value to compare
6135 *
6136 * @return Boolean true if the first value matches against the second value, false otherwise
6137 */
6138 public function if_cond($value1, $operator, $value2) {
6139 switch (strtolower($operator)) {
6140 case 'is':
6141 case '==':
6142 return $value1 == $value2;
6143 break;
6144 case 'is_not':
6145 case '!=':
6146 return $value1 != $value2;
6147 break;
6148 default:
6149 throw new Exception(__METHOD__.": Unsupported (".$operator.") operator", 1);
6150 break;
6151 }
6152 }
6153
6154 /**
6155 * Return a listing of days of the week
6156 *
6157 * @param Boolean $respect_start_of_week whether to use the WordPress's start_of_week setting
6158 * @return Array the days of the week
6159 */
6160 public function list_days_of_the_week($respect_start_of_week = true) {
6161 global $wp_locale;
6162 $days_of_the_week = array();
6163 $i = $j = $respect_start_of_week ? (int) get_option('start_of_week', 1) : 1;
6164 while ($i < $j + 7) { // 7 days
6165 $days_of_the_week[] = array(
6166 'index' => $i % 7,
6167 'value' => $wp_locale->get_weekday($i % 7),
6168 );
6169 $i++;
6170 }
6171 return $days_of_the_week;
6172 }
6173 }
6174