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