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

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