PluginProbe ʕ •ᴥ•ʔ
UpdraftPlus: WP Backup & Migration Plugin / 1.11.26
UpdraftPlus: WP Backup & Migration Plugin v1.11.26
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 css 10 years ago images 10 years ago includes 10 years ago languages 10 years ago methods 10 years ago vendor 10 years ago admin.php 10 years ago backup.php 10 years ago class-updraftplus.php 10 years ago class-zip.php 10 years ago clean-composer.sh 10 years ago composer.json 10 years ago composer.lock 10 years ago example-decrypt.php 10 years ago index.html 10 years ago options.php 10 years ago readme.txt 10 years ago restorer.php 10 years ago updraftplus.php 10 years ago
class-updraftplus.php
3740 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 'copycom' => 'Copy.Com',
22 'sftp' => 'SFTP / SCP',
23 'googlecloud' => 'Google Cloud',
24 'webdav' => 'WebDAV',
25 's3generic' => 'S3-Compatible (Generic)',
26 'openstack' => 'OpenStack (Swift)',
27 'dreamobjects' => 'DreamObjects',
28 'email' => 'Email'
29 );
30
31 public $errors = array();
32 public $nonce;
33 public $logfile_name = "";
34 public $logfile_handle = false;
35 public $backup_time;
36 public $job_time_ms;
37
38 public $opened_log_time;
39 private $backup_dir;
40
41 private $jobdata;
42
43 public $something_useful_happened = false;
44 public $have_addons = false;
45
46 // Used to schedule resumption attempts beyond the tenth, if needed
47 public $current_resumption;
48 public $newresumption_scheduled = false;
49
50 public $cpanel_quota_readable = false;
51
52 public $error_reporting_stop_when_logged = false;
53
54 public function __construct() {
55
56 # Bitcasa support is deprecated
57 if (is_file(UPDRAFTPLUS_DIR.'/addons/bitcasa.php')) $this->backup_methods['bitcasa'] = 'Bitcasa';
58
59 // Initialisation actions - takes place on plugin load
60
61 if ($fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
62 $file_data = fread($fp, 1024);
63 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
64 $this->version = $matches[1];
65 }
66 fclose($fp);
67 }
68
69 # Create admin page
70 add_action('init', array($this, 'handle_url_actions'));
71 // Run earlier than default - hence earlier than other components
72 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
73 add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
74 # Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
75 add_action('admin_init', array($this, 'admin_menu'), 9);
76
77 # The two actions which we schedule upon
78 add_action('updraft_backup', array($this, 'backup_files'));
79 add_action('updraft_backup_database', array($this, 'backup_database'));
80
81 # The three actions that can be called from "Backup Now"
82 add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
83 add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
84 add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
85
86 # backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
87 add_action('updraft_backup_all', array($this, 'backup_all'));
88
89 # This is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
90 add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
91
92 add_action('plugins_loaded', array($this, 'plugins_loaded'));
93
94 # Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
95 add_filter('itsec_has_external_backup', '__return_true', 999);
96 add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
97 add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
98
99 # register_deactivation_hook(__FILE__, array($this, 'deactivation'));
100
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(UPDRAFTPLUS_DIR.'/includes/class-udrpc.php');
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 create_remote_control_key($name_hash, $extra_info = array(), $post_it = false) {
139
140 $indicator_name = $name_hash.'.central.updraftplus.com';
141
142 $our_keys = UpdraftPlus_Options::get_updraft_option('updraft_central_localkeys');
143 if (!is_array($our_keys)) $our_keys = array();
144
145 if (isset($our_keys[$name_hash])) {
146 unset($our_keys[$name_hash]);
147 }
148
149 $ud_rpc = $this->get_udrpc($indicator_name);
150
151 if (is_object($ud_rpc) && $ud_rpc->generate_new_keypair()) {
152
153 if ($post_it) {
154 // This option allows the key to be sent to the other side via a known-secure channel (e.g. http over SSL), rather than potentially allowing it to travel over an unencrypted channel (e.g. http back to the user's browser). As such, if specified, it is compulsory for it to work.
155 $sent_key = wp_remote_post(
156 $post_it,
157 array(
158 'timeout' => 45,
159 'body' => array(
160 'key' => $ud_rpc->get_key_remote()
161 )
162 )
163 );
164 if (is_wp_error($sent_key) || empty($sent_key)) {
165 $err_msg = sprintf(__('A key was created, but the attempt to register it with %s was unsuccessful - please try again later.', 'updraftplus'), (string)$post_it);
166 if (is_wp_error($sent_key)) $err_msg .= ' '.$sent_key->get_error_message().' ('.$sent_key->get_error_code().')';
167 return array(
168 'r' => $err_msg
169 );
170 }
171
172 $response = json_decode($sent_key['body'], true);
173
174 if (!is_array($response) || !isset($response['key_id']) || !isset($response['key_public'])) {
175 return array(
176 'r' => sprintf(__('A key was created, but the attempt to register it with %s was unsuccessful - please try again later.', 'updraftplus'), (string)$post_it),
177 'raw' => $sent_key['body']
178 );
179 }
180
181 $key_hash = hash('sha256', $ud_rpc->get_key_remote());
182
183 $local_bundle = $ud_rpc->get_portable_bundle('base64_with_count', $extra_info, array('key' => array('key_hash' => $key_hash, 'key_id' => $response['key_id'])));
184
185 } else {
186 $local_bundle = $ud_rpc->get_portable_bundle('base64_with_count', $extra_info, array('key' => $ud_rpc->get_key_remote()));
187 }
188
189
190 $our_keys[$name_hash] = array(
191 'name' => 'Updraft Remote Control',
192 'key' => $ud_rpc->get_key_local(),
193 'extra_info' => $extra_info
194 );
195 // Store the other side's public key
196 if (!empty($response) && is_array($response) && !empty($response['key_public'])) {
197 $our_keys[$name_hash]['publickey_remote'] = $response['key_public'];
198 }
199 UpdraftPlus_Options::update_updraft_option('updraft_central_localkeys', $our_keys);
200
201 return array(
202 'bundle' => $local_bundle,
203 'r' => __('Key created successfully.', 'updraftplus').' '.__('You must copy and paste this key now - it cannot be shown again.', 'updraftplus'),
204 // 'selector' => $this->get_remotesites_selector(array()),
205 // 'ourkeys' => $this->list_our_keys($our_keys),
206 );
207 }
208
209 return false;
210
211 }
212
213 public function ensure_phpseclib($classes = false, $class_paths = false) {
214
215 if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
216
217 $this->no_deprecation_warnings_on_php7();
218
219 if ($classes) {
220 $any_missing = false;
221 if (is_string($classes)) $classes = array($classes);
222 foreach ($classes as $cl) {
223 if (!class_exists($cl)) $any_missing = true;
224 }
225 if (!$any_missing) return;
226 }
227
228 if ($class_paths) {
229 if (is_string($class_paths)) $class_paths = array($class_paths);
230 foreach ($class_paths as $cp) {
231 require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/'.$cp.'.php');
232 }
233 }
234 }
235
236 // Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
237 private function no_deprecation_warnings_on_php7() {
238 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
239 // 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).
240 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
241 $old_level = error_reporting();
242 $new_level = $old_level & ~E_DEPRECATED;
243 if ($old_level != $new_level) error_reporting($new_level);
244 }
245 }
246
247 public function close_browser_connection($txt = '') {
248 // Close browser connection so that it can resume AJAX polling
249 header('Content-Length: '.((!empty($txt)) ? 4+strlen($txt) : '0'));
250 header('Connection: close');
251 header('Content-Encoding: none');
252 if (session_id()) session_write_close();
253 echo "\r\n\r\n";
254 echo $txt;
255 // These two added - 19-Feb-15 - started being required on local dev machine, for unknown reason (probably some plugin that started an output buffer).
256 if (ob_get_level()) ob_end_flush();
257 flush();
258 }
259
260 // Returns the number of bytes free, if it can be detected; otherwise, false
261 // Presently, we only detect CPanel. If you know of others, then feel free to contribute!
262 public function get_hosting_disk_quota_free() {
263 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'))) return false;
264
265 $perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';
266
267 $exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
268
269 $handle = @popen($exec, 'r');
270 if (!is_resource($handle)) return false;
271
272 $found = false;
273 $lines = 0;
274 while (false === $found && !feof($handle) && $lines<100) {
275 $lines++;
276 $w = fgets($handle);
277 # Used, limit, remain
278 if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) { $found = true; }
279 }
280 $ret = pclose($handle);
281 if (false === $found ||$ret != 0) return false;
282
283 if ((int)$matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
284
285 $this->cpanel_quota_readable = true;
286
287 return $matches;
288 }
289
290 public function last_modified_log() {
291 $updraft_dir = $this->backups_dir_location();
292
293 $log_file = '';
294 $mod_time = 0;
295 $nonce = '';
296
297 if ($handle = @opendir($updraft_dir)) {
298 while (false !== ($entry = readdir($handle))) {
299 // The latter match is for files created internally by zipArchive::addFile
300 if (preg_match('/^log\.([a-z0-9]+)\.txt$/i', $entry, $matches)) {
301 $mtime = filemtime($updraft_dir.'/'.$entry);
302 if ($mtime > $mod_time) {
303 $mod_time = $mtime;
304 $log_file = $updraft_dir.'/'.$entry;
305 $nonce = $matches[1];
306 }
307 }
308 }
309 @closedir($handle);
310 }
311
312 return array($mod_time, $log_file, $nonce);
313 }
314
315 // This function may get called multiple times, so write accordingly
316 public function admin_menu() {
317 // We are in the admin area: now load all that code
318 global $updraftplus_admin;
319 if (empty($updraftplus_admin)) require_once(UPDRAFTPLUS_DIR.'/admin.php');
320
321 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadlatestmodlog' && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
322
323 list ($mod_time, $log_file, $nonce) = $this->last_modified_log();
324
325 if ($mod_time >0) {
326 if (is_readable($log_file)) {
327 header('Content-type: text/plain');
328 readfile($log_file);
329 exit;
330 } else {
331 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
332 }
333 } else {
334 add_action('all_admin_notices', array($this,'show_admin_warning_nolog') );
335 }
336 }
337
338 }
339
340 public function modify_http_options($opts) {
341
342 if (!is_array($opts)) return $opts;
343
344 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) $opts['sslcertificates'] = UPDRAFTPLUS_DIR.'/includes/cacert.pem';
345
346 $opts['sslverify'] = (UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')) ? false : true;
347
348 return $opts;
349
350 }
351
352 // Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?action=updraftmethod-googledrive-auth&page=updraftplus
353 // 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.
354 // Also handle action=downloadlog
355 public function handle_url_actions() {
356
357 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
358 // Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
359 if (isset($_SERVER['REQUEST_METHOD']) && 'GET' == $_SERVER['REQUEST_METHOD'] && isset($_GET['action'])) {
360 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php') && UpdraftPlus_Options::user_can_manage()) {
361 $_GET['page'] = 'updraftplus';
362 $_REQUEST['page'] = 'updraftplus';
363 $method = $matches[1];
364 require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
365 $call_class = "UpdraftPlus_BackupModule_".$method;
366 $call_method = "action_".$matches[2];
367 $backup_obj = new $call_class;
368 add_action('http_request_args', array($this, 'modify_http_options'));
369 try {
370 if (method_exists($backup_obj, $call_method)) {
371 call_user_func(array($backup_obj, $call_method));
372 } elseif (method_exists($backup_obj, 'action_handler')) {
373 call_user_func(array($backup_obj, 'action_handler'), $matches[2]);
374 }
375 } catch (Exception $e) {
376 $this->log(sprintf(__("%s error: %s", 'updraftplus'), $method, $e->getMessage().' ('.$e->getCode().')', 'error'));
377 }
378 remove_action('http_request_args', array($this, 'modify_http_options'));
379 } 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()) {
380 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
381 $updraft_dir = $this->backups_dir_location();
382 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
383 if (is_readable($log_file)) {
384 header('Content-type: text/plain');
385 if (!empty($_GET['force_download'])) header('Content-Disposition: attachment; filename="'.basename($log_file).'"');
386 readfile($log_file);
387 exit;
388 } else {
389 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
390 }
391 } 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()) {
392 $updraft_dir = $this->backups_dir_location();
393 $spool_file = $updraft_dir.'/'.basename($_GET['updraftplus_file']);
394 if (is_readable($spool_file)) {
395 $dkey = isset($_GET['decrypt_key']) ? $_GET['decrypt_key'] : "";
396 $this->spool_file('db', $spool_file, $dkey);
397 exit;
398 } else {
399 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablefile') );
400 }
401 }
402 }
403 }
404
405 public function get_table_prefix($allow_override = false) {
406 global $wpdb;
407 if (is_multisite() && !defined('MULTISITE')) {
408 # 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.
409 $prefix = $wpdb->base_prefix;
410 } else {
411 $prefix = $wpdb->get_blog_prefix(0);
412 }
413 return ($allow_override) ? apply_filters('updraftplus_get_table_prefix', $prefix) : $prefix;
414 }
415
416 public function siteid() {
417 $sid = get_site_option('updraftplus-addons_siteid');
418 if (!is_string($sid) || empty($sid)) {
419 $sid = md5(rand().microtime(true).home_url());
420 update_site_option('updraftplus-addons_siteid', $sid);
421 }
422 return $sid;
423 }
424
425 public function show_admin_warning_unreadablelog() {
426 global $updraftplus_admin;
427 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The log file could not be read.','updraftplus'));
428 }
429
430 public function show_admin_warning_nolog() {
431 global $updraftplus_admin;
432 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('No log files were found.','updraftplus'));
433 }
434
435 public function show_admin_warning_unreadablefile() {
436 global $updraftplus_admin;
437 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
438 }
439
440 public function plugins_loaded() {
441
442 // Tell WordPress where to find the translations
443 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages/');
444
445 // The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
446 if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && $_GET['page'] == 'updraftplus')) {
447 remove_action('init', 'ganalyticator_stats_init');
448 # Appointments+ does the same; but provides a cleaner way to disable it
449 define('APP_GCAL_DISABLE', true);
450 return;
451 }
452
453 if (!class_exists('UpdraftPlus_RemoteControl')) {
454 if (!file_exists(UPDRAFTPLUS_DIR.'/remote.php')) return;
455 require_once(UPDRAFTPLUS_DIR.'/remote.php');
456 }
457
458 // Remote control keys
459 // These are different from the remote send keys, which are set up in the Migrator add-on
460 $our_keys = UpdraftPlus_Options::get_updraft_option('updraft_central_localkeys');
461 if (is_array($our_keys) && !empty($our_keys)) {
462 $remote_control = new UpdraftPlus_RemoteControl($our_keys);
463 }
464
465 }
466
467 // Cleans up temporary files found in the updraft directory (and some in the site root - pclzip)
468 // Always cleans up temporary files over 12 hours old.
469 // With parameters, also cleans up those.
470 // Also cleans out old job data older than 12 hours old (immutable value)
471 // include_cachelist also looks to match any files of cached file analysis data
472 public function clean_temporary_files($match = '', $older_than = 43200, $include_cachelist = false) {
473 # Clean out old job data
474 if ($older_than > 10000) {
475 global $wpdb;
476
477 $all_jobs = $wpdb->get_results("SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'updraft_jobdata_%'", ARRAY_A);
478 foreach ($all_jobs as $job) {
479 $val = maybe_unserialize($job['option_value']);
480 # TODO: Can simplify this after a while (now all jobs use job_time_ms) - 1 Jan 2014
481 $delete = false;
482 if (!empty($val['next_increment_start_scheduled_for'])) {
483 if (time() > $val['next_increment_start_scheduled_for'] + 86400) $delete = true;
484 } elseif (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) {
485 $delete = true;
486 } elseif (!empty($val['job_time_ms']) && time() > $val['job_time_ms'] + 86400) {
487 $delete = true;
488 } elseif (!empty($val['job_type']) && 'backup' != $val['job_type'] && empty($val['backup_time_ms']) && empty($val['job_time_ms'])) {
489 $delete = true;
490 }
491 if ($delete) delete_option($job['option_name']);
492 }
493 }
494 $updraft_dir = $this->backups_dir_location();
495 $now_time=time();
496 if ($handle = opendir($updraft_dir)) {
497 while (false !== ($entry = readdir($handle))) {
498 $manifest_match = preg_match("/^udmanifest$match\.json$/i", $entry);
499 // This match is for files created internally by zipArchive::addFile
500 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $entry);
501 // 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.
502 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry);
503 $cachelist_match = ($include_cachelist) ? preg_match("/$match-cachelist-.*.tmp$/i", $entry) : false;
504 # Temporary files from the database dump process - not needed, as is caught by the catch-all
505 # $table_match = preg_match("/${match}-table-(.*)\.table(\.tmp)?\.gz$/i", $entry);
506 # The gz goes in with the txt, because we *don't* want to reap the raw .txt files
507 if ((preg_match("/$match\.(tmp|table|txt\.gz)(\.gz)?$/i", $entry) || $cachelist_match || $ziparchive_match || $binzip_match || $manifest_match) && is_file($updraft_dir.'/'.$entry)) {
508 // 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
509 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) {
510 $this->log("Deleting old temporary file: $entry");
511 @unlink($updraft_dir.'/'.$entry);
512 }
513 }
514 }
515 @closedir($handle);
516 }
517 # Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both
518 # 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.
519 foreach (array(ABSPATH, ABSPATH.'wp-admin/', $updraft_dir.'/') as $path) {
520 if ($handle = opendir($path)) {
521 while (false !== ($entry = readdir($handle))) {
522 # 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
523 if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) {
524 $this->log("Deleting old PclZip temporary file: $entry");
525 @unlink($path.$entry);
526 }
527 }
528 @closedir($handle);
529 }
530 }
531 }
532
533 public function backup_time_nonce($nonce = false) {
534 $this->job_time_ms = microtime(true);
535 $this->backup_time = time();
536 if (false === $nonce) $nonce = substr(md5(time().rand()), 20);
537 $this->nonce = $nonce;
538 return $nonce;
539 }
540
541 public function get_wordpress_version() {
542 static $got_wp_version = false;
543 if (!$got_wp_version) {
544 global $wp_version;
545 @include(ABSPATH.WPINC.'/version.php');
546 $got_wp_version = $wp_version;
547 }
548 return $got_wp_version;
549 }
550
551 public function logfile_open($nonce) {
552
553 //set log file name and open log file
554 $updraft_dir = $this->backups_dir_location();
555 $this->logfile_name = $updraft_dir."/log.$nonce.txt";
556
557 if (file_exists($this->logfile_name)) {
558 $seek_to = max((filesize($this->logfile_name) - 340), 1);
559 $handle = fopen($this->logfile_name, 'r');
560 if (is_resource($handle)) {
561 # Returns 0 on success
562 if (0 === @fseek($handle, $seek_to)) {
563 $bytes_back = filesize($this->logfile_name) - $seek_to;
564 # Return to the end of the file
565 $read_recent = fread($handle, $bytes_back);
566 # Move to end of file - ought to be redundant
567 if (false !== strpos($read_recent, 'The backup apparently succeeded') && false !== strpos($read_recent, 'and is now complete')) {
568 $this->backup_is_already_complete = true;
569 }
570 }
571 fclose($handle);
572 }
573 }
574
575 $this->logfile_handle = fopen($this->logfile_name, 'a');
576
577 $this->opened_log_time = microtime(true);
578 $this->log('Opened log file at time: '.date('r').' on '.network_site_url());
579 global $wpdb;
580 $wp_version = $this->get_wordpress_version();
581
582 $mysql_version = $wpdb->db_version();
583
584 $safe_mode = $this->detect_safe_mode();
585
586 $memory_limit = ini_get('memory_limit');
587 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
588 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
589
590 # Attempt to raise limit to avoid false positives
591 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
592 $max_execution_time = (int)@ini_get("max_execution_time");
593
594 $logline = "UpdraftPlus WordPress backup plugin (https://updraftplus.com): ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".@php_uname().") MySQL: $mysql_version 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')." mcrypt: ".((function_exists('mcrypt_encrypt')) ? 'Y' : 'N')." LANG: ".getenv('LANG')." ZipArchive::addFile: ";
595
596 // method_exists causes some faulty PHP installations to segfault, leading to support requests
597 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
598 $logline .= 'Y';
599 } else {
600 $logline .= (class_exists('ZipArchive') && method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
601 }
602
603 // $w3oc = 'N';
604 if (0 === $this->current_resumption) {
605 $memlim = $this->memory_check_current();
606 if ($memlim<65 && $memlim>0) {
607 $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');
608 }
609 if ($max_execution_time>0 && $max_execution_time<20) {
610 $this->log(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');
611 }
612 // if (defined('W3TC') && W3TC == true && function_exists('w3_instance')) {
613 // $modules = w3_instance('W3_ModuleStatus');
614 // if ($modules->is_enabled('objectcache')) {
615 // $w3oc = 'Y';
616 // }
617 // }
618 // $logline .= " W3TC/ObjectCache: $w3oc";
619
620 }
621
622 $this->log($logline);
623
624 $hosting_bytes_free = $this->get_hosting_disk_quota_free();
625 if (is_array($hosting_bytes_free)) {
626 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
627 $quota_free = ' / '.sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." Mb", "$perc %");
628 if ($hosting_bytes_free[3] < 1048576*50) {
629 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
630 $this->log(sprintf(__('Your free space in your hosting account is very low - only %s Mb remain', 'updraftplus'), $quota_free_mb), 'warning', 'lowaccountspace'.$quota_free_mb);
631 }
632 } else {
633 $quota_free = '';
634 }
635
636 $disk_free_space = @disk_free_space($updraft_dir);
637 # == 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.
638 if ($disk_free_space == false) {
639 $this->log("Free space on disk containing Updraft's temporary directory: Unknown".$quota_free);
640 } else {
641 $this->log("Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576,1)." Mb".$quota_free);
642 $disk_free_mb = round($disk_free_space/1048576, 1);
643 if ($disk_free_space < 50*1048576) $this->log(sprintf(__('Your free disk space is very low - only %s Mb remain', 'updraftplus'), round($disk_free_space/1048576, 1)), 'warning', 'lowdiskspace'.$disk_free_mb);
644 }
645
646 }
647
648 /* Logs the given line, adding (relative) time stamp and newline
649 Note these subtleties of log handling:
650 - 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.
651 - 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...
652 - ... 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
653 $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
654
655 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
656 */
657
658 public function verify_free_memory($how_many_bytes_needed) {
659 // This returns in Mb
660 $memory_limit = $this->memory_check_current();
661 if (!is_numeric($memory_limit)) return false;
662 $memory_limit = $memory_limit * 1048576;
663 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
664 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
665 if ($memory_limit - $memory_usage > $how_many_bytes_needed && $memory_limit - $memory_usage2 > $how_many_bytes_needed) return true;
666 return false;
667 }
668
669 public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
670
671 if ('error' == $level || 'warning' == $level) {
672 if ('error' == $level && 0 == $this->error_count()) $this->log('An error condition has occurred for the first time during this job');
673 if ($uniq_id) {
674 $this->errors[$uniq_id] = array('level' => $level, 'message' => $line);
675 } else {
676 $this->errors[] = array('level' => $level, 'message' => $line);
677 }
678 # Errors are logged separately
679 if ('error' == $level) return;
680 # It's a warning
681 $warnings = $this->jobdata_get('warnings');
682 if (!is_array($warnings)) $warnings=array();
683 if ($uniq_id) {
684 $warnings[$uniq_id] = $line;
685 } else {
686 $warnings[] = $line;
687 }
688 $this->jobdata_set('warnings', $warnings);
689 }
690
691 do_action('updraftplus_logline', $line, $this->nonce, $level, $uniq_id);
692
693 if ($this->logfile_handle) {
694 # Record log file times relative to the backup start, if possible
695 $rtime = (!empty($this->job_time_ms)) ? microtime(true)-$this->job_time_ms : microtime(true)-$this->opened_log_time;
696 fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") ".(('notice' != $level) ? '['.ucfirst($level).'] ' : '').$line."\n");
697 }
698
699 switch ($this->jobdata_get('job_type')) {
700 case 'download':
701 // Download messages are keyed on the job (since they could be running several), and type
702 // The values of the POST array were checked before
703 $findex = empty($_POST['findex']) ? 0 : $_POST['findex'];
704
705 if (!empty($_POST['timestamp']) && !empty($_POST['type'])) $this->jobdata_set('dlmessage_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line);
706
707 break;
708 case 'restore':
709 #if ('debug' != $level) echo $line."\n";
710 break;
711 default:
712 if (!$skip_dblog && 'debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")", false);
713 break;
714 }
715
716 if (defined('UPDRAFTPLUS_CONSOLELOG')) print $line."\n";
717 if (defined('UPDRAFTPLUS_BROWSERLOG')) print htmlentities($line)."<br>\n";
718 }
719
720 public function log_removewarning($uniq_id) {
721 $warnings = $this->jobdata_get('warnings');
722 if (!is_array($warnings)) $warnings=array();
723 unset($warnings[$uniq_id]);
724 $this->jobdata_set('warnings', $warnings);
725 unset($this->errors[$uniq_id]);
726 }
727
728 # For efficiency, you can also feed false or a string into this function
729 public function log_wp_error($err, $echo = false, $logerror = false) {
730 if (false === $err) return false;
731 if (is_string($err)) {
732 $this->log("Error message: $err");
733 if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($err))."<br>";
734 if ($logerror) $this->log($err, 'error');
735 return false;
736 }
737 foreach ($err->get_error_messages() as $msg) {
738 $this->log("Error message: $msg");
739 if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($msg))."<br>";
740 if ($logerror) $this->log($msg, 'error');
741 }
742 $codes = $err->get_error_codes();
743 if (is_array($codes)) {
744 foreach ($codes as $code) {
745 $data = $err->get_error_data($code);
746 if (!empty($data)) {
747 $ll = (is_string($data)) ? $data : serialize($data);
748 $this->log("Error data (".$code."): ".$ll);
749 }
750 }
751 }
752 # Returns false so that callers can return with false more efficiently if they wish
753 return false;
754 }
755
756 public function get_max_packet_size() {
757 global $wpdb;
758 $mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
759 # Default to 1Mb
760 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
761 # 32Mb
762 if ($mp < 33554432) {
763 $save = $wpdb->show_errors(false);
764 $req = @$wpdb->query("SET GLOBAL max_allowed_packet=33554432");
765 $wpdb->show_errors($save);
766 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).")");
767 $mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
768 # Default to 1Mb
769 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
770 }
771 $this->log("Max packet size: ".round($mp/1048576, 1)." Mb");
772 return $mp;
773 }
774
775 # 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()).
776 # 1st argument = the line to be logged (obligatory)
777 # Further arguments = parameters for sprintf()
778 public function log_e() {
779 $args = func_get_args();
780 # Get first argument
781 $pre_line = array_shift($args);
782 # Log it whilst still in English
783 if (is_wp_error($pre_line)) {
784 $this->log_wp_error($pre_line);
785 } else {
786 # Now run (v)sprintf on it, using any remaining arguments. vsprintf = sprintf but takes an array instead of individual arguments
787 $this->log(vsprintf($pre_line, $args));
788 echo vsprintf(__($pre_line, 'updraftplus'), $args).'<br>';
789 }
790 }
791
792 // 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
793 public function record_uploaded_chunk($percent, $extra = '', $file_path = false, $log_it = true) {
794
795 // Touch the original file, which helps prevent overlapping runs
796 if ($file_path) touch($file_path);
797
798 // 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)
799 if ($percent > 0.7 * ($this->current_resumption - max($this->jobdata_get('uploaded_lastreset'), 9))) $this->something_useful_happened();
800
801 // Log it
802 global $updraftplus_backup;
803 $log = (!empty($updraftplus_backup->current_service)) ? ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded" : '';
804 if ($log && $log_it) $this->log($log.(($extra) ? " ($extra)" : ''));
805 // If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
806 // 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
807 // 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
808 // 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
809
810 $upload_status = $this->jobdata_get('uploading_substatus');
811 if (is_array($upload_status)) {
812 $upload_status['p'] = $percent/100;
813 $this->jobdata_set('uploading_substatus', $upload_status);
814 }
815
816 }
817
818 // $singletons : whether to upload a file that only has one chunk, or whether instead to return 1 in that case
819 public function chunked_upload($caller, $file, $cloudpath, $logname, $chunk_size, $uploaded_size, $singletons=false) {
820
821 $fullpath = $this->backups_dir_location().'/'.$file;
822 $orig_file_size = filesize($fullpath);
823 if ($uploaded_size >= $orig_file_size) return true;
824
825 $chunks = floor($orig_file_size / $chunk_size);
826 // There will be a remnant unless the file size was exactly on a 5Mb boundary
827 if ($orig_file_size % $chunk_size > 0) $chunks++;
828
829 $this->log("$logname upload: $file (chunks: $chunks, size: $chunk_size) -> $cloudpath ($uploaded_size)");
830
831 if ($chunks == 0) {
832 return 1;
833 } elseif ($chunks < 2 && !$singletons) {
834 return 1;
835 } else {
836
837 if (false == ($fp = @fopen($fullpath, 'rb'))) {
838 $this->log("$logname: failed to open file: $fullpath");
839 $this->log("$file: ".sprintf(__('%s Error: Failed to open local file','updraftplus'), $logname), 'error');
840 return false;
841 }
842
843 $errors_so_far = 0;
844 for ($i = 1 ; $i <= $chunks; $i++) {
845
846 $upload_start = ($i-1)*$chunk_size;
847 // The file size -1 equals the byte offset of the final byte
848 $upload_end = min($i*$chunk_size-1, $orig_file_size-1);
849 // Don't forget the +1; otherwise the last byte is omitted
850 $upload_size = $upload_end - $upload_start + 1;
851
852 fseek($fp, $upload_start);
853
854 $uploaded = $caller->chunked_upload($file, $fp, $i, $upload_size, $upload_start, $upload_end);
855
856 // This is the only supported case of a WP_Error - otherwise, a boolean must be returned
857 // Note that this is only allowed on the first chunk. The caller is responsible to remember its chunk size if it uses this facility.
858 if (1 == $i && is_wp_error($uploaded) && 'reduce_chunk_size' == $uploaded->get_error_code() && false != ($new_chunk_size = $uploaded->get_error_data()) && is_numeric($new_chunk_size)) {
859 $this->log("Re-trying with new chunk size: ".$new_chunk_size);
860 return $this->chunked_upload($caller, $file, $cloudpath, $logname, $new_chunk_size, $uploaded_size, $singletons=false);
861 }
862
863 if ($uploaded) {
864 $perc = round(100*((($i-1) * $chunk_size) + $upload_size)/max($orig_file_size, 1), 1);
865 # $perc = round(100*$i/$chunks,1); # Takes no notice of last chunk likely being smaller
866 // Implementations used a return value of (int)1 (rather than (bool)true) to suppress logging
867 $log_it = ($uploaded === 1) ? false : true;
868 $this->record_uploaded_chunk($perc, $i, $fullpath, $log_it);
869 } else {
870 $errors_so_far++;
871 if ($errors_so_far>=3) { @fclose($fp); return false; }
872 }
873 }
874
875 @fclose($fp);
876
877 if ($errors_so_far) return false;
878
879 // All chunks are uploaded - now combine the chunks
880 $ret = true;
881 if (method_exists($caller, 'chunked_upload_finish')) {
882 $ret = $caller->chunked_upload_finish($file);
883 if (!$ret) {
884 $this->log("$logname - failed to re-assemble chunks (".$e->getMessage().')');
885 $this->log(sprintf(__('%s error - failed to re-assemble chunks', 'updraftplus'), $logname), 'error');
886 }
887 }
888 if ($ret) {
889 $this->log("$logname upload: success");
890 # UpdraftPlus_RemoteStorage_Addons_Base calls this itself
891 if (!is_a($caller, 'UpdraftPlus_RemoteStorage_Addons_Base')) $this->uploaded_file($file);
892 }
893
894 return $ret;
895
896 }
897 }
898
899 public function chunked_download($file, $method, $remote_size, $manually_break_up = false, $passback = null, $chunk_size = 1048576) {
900
901 try {
902
903 $fullpath = $this->backups_dir_location().'/'.$file;
904 $start_offset = (file_exists($fullpath)) ? filesize($fullpath): 0;
905
906 if ($start_offset >= $remote_size) {
907 $this->log("File is already completely downloaded ($start_offset/$remote_size)");
908 return true;
909 }
910
911 // Some more remains to download - so let's do it
912 if (!$fh = fopen($fullpath, 'a')) {
913 $this->log("Error opening local file: $fullpath");
914 $this->log($file.": ".__("Error",'updraftplus').": ".__('Error opening local file: Failed to download','updraftplus'), 'error');
915 return false;
916 }
917
918 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
919
920 # This only affects logging
921 $expected_bytes_delivered_so_far = true;
922
923 while ($start_offset < $remote_size) {
924 $headers = array();
925 // If resuming, then move to the end of the file
926
927 $requested_bytes = $last_byte-$start_offset;
928
929 if ($expected_bytes_delivered_so_far) {
930 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next $requested_bytes bytes");
931 } else {
932 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next chunk (${start_offset}-)");
933 }
934
935 if ($start_offset >0 || $last_byte<$remote_size) {
936 fseek($fh, $start_offset);
937 // N.B. Don't alter this format without checking what relies upon it
938 $headers['Range'] = "bytes=$start_offset-$last_byte";
939 }
940
941 # The method is free to return as much data as it pleases
942 $ret = $method->chunked_download($file, $headers, $passback);
943 if (false === $ret) return false;
944
945 if (strlen($ret) > $requested_bytes || strlen($ret) < $requested_bytes - 1) $expected_bytes_delivered_so_far = false;
946
947 if (!fwrite($fh, $ret)) throw new Exception('Write failure (start offset: '.$start_offset.', bytes: '.strlen($ret).'; requested: '.$requested_bytes.')');
948
949 clearstatcache();
950 $start_offset = ftell($fh);
951 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + $chunk_size) : $remote_size;
952
953 }
954
955 } catch(Exception $e) {
956 $this->log('Error ('.get_class($e).') - failed to download the file ('.$e->getCode().', '.$e->getMessage().')');
957 $this->log("$file: ".__('Error - failed to download the file','updraftplus').' ('.$e->getCode().', '.$e->getMessage().')' ,'error');
958 return false;
959 }
960
961 fclose($fh);
962
963 return true;
964 }
965
966 public function decrypt($fullpath, $key, $ciphertext = false) {
967 $this->ensure_phpseclib('Crypt_Rijndael', 'Crypt/Rijndael');
968 $rijndael = new Crypt_Rijndael();
969 $rijndael->setKey($key);
970 return (false == $ciphertext) ? $rijndael->decrypt(file_get_contents($fullpath)) : $rijndael->decrypt($ciphertext);
971 }
972
973 public function detect_safe_mode() {
974 return (@ini_get('safe_mode') && strtolower(@ini_get('safe_mode')) != "off") ? 1 : 0;
975 }
976
977 public function find_working_sqldump($logit = true, $cacheit = true) {
978
979 // The hosting provider may have explicitly disabled the popen or proc_open functions
980 if ($this->detect_safe_mode() || !function_exists('popen') || !function_exists('escapeshellarg')) {
981 if ($cacheit) $this->jobdata_set('binsqldump', false);
982 return false;
983 }
984 $existing = $this->jobdata_get('binsqldump', null);
985 # Theoretically, we could have moved machines, due to a migration
986 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
987
988 $updraft_dir = $this->backups_dir_location();
989 global $wpdb;
990 $table_name = $wpdb->get_blog_prefix().'options';
991 $tmp_file = md5(time().rand()).".sqltest.tmp";
992 $pfile = md5(time().rand()).'.tmp';
993 file_put_contents($updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".DB_PASSWORD."\n");
994
995 $result = false;
996 foreach (explode(',', UPDRAFTPLUS_MYSQLDUMP_EXECUTABLE) as $potsql) {
997 if (!@is_executable($potsql)) continue;
998 if ($logit) $this->log("Testing: $potsql");
999
1000 $exec = "cd ".escapeshellarg($updraft_dir)."; $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)." >$tmp_file";
1001
1002 $handle = popen($exec, "r");
1003 if ($handle) {
1004 while (!feof($handle)) {
1005 $w = fgets($handle);
1006 if ($w && $logit) $this->log("Output: ".trim($w));
1007 }
1008 $ret = pclose($handle);
1009 if ($ret !=0) {
1010 if ($logit) $this->log("Binary mysqldump: error (code: $ret)");
1011 } else {
1012 $dumped = file_get_contents($updraft_dir.'/'.$tmp_file, false, null, 0, 4096);
1013 if (stripos($dumped, 'insert into') !== false) {
1014 if ($logit) $this->log("Working binary mysqldump found: $potsql");
1015 $result = $potsql;
1016 break;
1017 }
1018 }
1019 } else {
1020 if ($logit) $this->log("Error: popen failed");
1021 }
1022 }
1023
1024 @unlink($updraft_dir.'/'.$pfile);
1025 @unlink($updraft_dir.'/'.$tmp_file);
1026
1027 if ($cacheit) $this->jobdata_set('binsqldump', $result);
1028
1029 return $result;
1030 }
1031
1032 # We require -@ and -u -r to work - which is the usual Linux binzip
1033 public function find_working_bin_zip($logit = true, $cacheit = true) {
1034 if ($this->detect_safe_mode()) return false;
1035 // The hosting provider may have explicitly disabled the popen or proc_open functions
1036 if (!function_exists('popen') || !function_exists('proc_open') || !function_exists('escapeshellarg')) {
1037 if ($cacheit) $this->jobdata_set('binzip', false);
1038 return false;
1039 }
1040
1041 $existing = $this->jobdata_get('binzip', null);
1042 # Theoretically, we could have moved machines, due to a migration
1043 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
1044
1045 $updraft_dir = $this->backups_dir_location();
1046 foreach (explode(',', UPDRAFTPLUS_ZIP_EXECUTABLE) as $potzip) {
1047 if (!@is_executable($potzip)) continue;
1048 if ($logit) $this->log("Testing: $potzip");
1049
1050 # Test it, see if it is compatible with Info-ZIP
1051 # If you have another kind of zip, then feel free to tell me about it
1052 @mkdir($updraft_dir.'/binziptest/subdir1/subdir2', 0777, true);
1053 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.</body></html>');
1054 @unlink($updraft_dir.'/binziptest/test.zip');
1055 if (is_file($updraft_dir.'/binziptest/subdir1/subdir2/test.html')) {
1056
1057 $exec = "cd ".escapeshellarg($updraft_dir)."; $potzip";
1058 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1059 $exec .= " -v -u -r binziptest/test.zip binziptest/subdir1";
1060
1061 $all_ok=true;
1062 $handle = popen($exec, "r");
1063 if ($handle) {
1064 while (!feof($handle)) {
1065 $w = fgets($handle);
1066 if ($w && $logit) $this->log("Output: ".trim($w));
1067 }
1068 $ret = pclose($handle);
1069 if ($ret !=0) {
1070 if ($logit) $this->log("Binary zip: error (code: $ret)");
1071 $all_ok = false;
1072 }
1073 } else {
1074 if ($logit) $this->log("Error: popen failed");
1075 $all_ok = false;
1076 }
1077
1078 # Now test -@
1079 if (true == $all_ok) {
1080 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.</body></html>');
1081
1082 $exec = $potzip;
1083 if (defined('UPDRAFTPLUS_BINZIP_OPTS') && UPDRAFTPLUS_BINZIP_OPTS) $exec .= ' '.UPDRAFTPLUS_BINZIP_OPTS;
1084 $exec .= " -v -@ binziptest/test.zip";
1085
1086 $all_ok=true;
1087
1088 $descriptorspec = array(
1089 0 => array('pipe', 'r'),
1090 1 => array('pipe', 'w'),
1091 2 => array('pipe', 'w')
1092 );
1093 $handle = proc_open($exec, $descriptorspec, $pipes, $updraft_dir);
1094 if (is_resource($handle)) {
1095 if (!fwrite($pipes[0], "binziptest/subdir1/subdir2/test2.html\n")) {
1096 @fclose($pipes[0]);
1097 @fclose($pipes[1]);
1098 @fclose($pipes[2]);
1099 $all_ok = false;
1100 } else {
1101 fclose($pipes[0]);
1102 while (!feof($pipes[1])) {
1103 $w = fgets($pipes[1]);
1104 if ($w && $logit) $this->log("Output: ".trim($w));
1105 }
1106 fclose($pipes[1]);
1107
1108 while (!feof($pipes[2])) {
1109 $last_error = fgets($pipes[2]);
1110 if (!empty($last_error) && $logit) $this->log("Stderr output: ".trim($w));
1111 }
1112 fclose($pipes[2]);
1113
1114 $ret = proc_close($handle);
1115 if ($ret !=0) {
1116 if ($logit) $this->log("Binary zip: error (code: $ret)");
1117 $all_ok = false;
1118 }
1119
1120 }
1121
1122 } else {
1123 if ($logit) $this->log("Error: proc_open failed");
1124 $all_ok = false;
1125 }
1126
1127 }
1128
1129 // Do we now actually have a working zip? Need to test the created object using PclZip
1130 // If it passes, then remove dirs and then return $potzip;
1131 $found_first = false;
1132 $found_second = false;
1133 if ($all_ok && file_exists($updraft_dir.'/binziptest/test.zip')) {
1134 if (function_exists('gzopen')) {
1135 if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
1136 $zip = new PclZip($updraft_dir.'/binziptest/test.zip');
1137 $foundit = 0;
1138 if (($list = $zip->listContent()) != 0) {
1139 foreach ($list as $obj) {
1140 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test.html' == $obj['stored_filename'] && $obj['size']==128) $found_first=true;
1141 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test2.html' == $obj['stored_filename'] && $obj['size']==135) $found_second=true;
1142 }
1143 }
1144 } else {
1145 // PclZip will die() if gzopen is not found
1146 // 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
1147 $this->log("gzopen function not found; PclZip cannot be invoked; will assume that binary zip works if we have a non-zero file");
1148 if (filesize($updraft_dir.'/binziptest/test.zip') > 0) {
1149 $found_first = true;
1150 $found_second = true;
1151 }
1152 }
1153 }
1154 $this->remove_binzip_test_files($updraft_dir);
1155 if ($found_first && $found_second) {
1156 if ($logit) $this->log("Working binary zip found: $potzip");
1157 if ($cacheit) $this->jobdata_set('binzip', $potzip);
1158 return $potzip;
1159 }
1160
1161 }
1162 $this->remove_binzip_test_files($updraft_dir);
1163 }
1164 if ($cacheit) $this->jobdata_set('binzip', false);
1165 return false;
1166 }
1167
1168 private function remove_binzip_test_files($updraft_dir) {
1169 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test.html');
1170 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test2.html');
1171 @rmdir($updraft_dir.'/binziptest/subdir1/subdir2');
1172 @rmdir($updraft_dir.'/binziptest/subdir1');
1173 @unlink($updraft_dir.'/binziptest/test.zip');
1174 @rmdir($updraft_dir.'/binziptest');
1175 }
1176
1177 // This function is purely for timing - we just want to know the maximum run-time; not whether we have achieved anything during it
1178 public function record_still_alive() {
1179 // Update the record of maximum detected runtime on each run
1180 $time_passed = $this->jobdata_get('run_times');
1181 if (!is_array($time_passed)) $time_passed = array();
1182
1183 $time_this_run = microtime(true)-$this->opened_log_time;
1184 $time_passed[$this->current_resumption] = $time_this_run;
1185 $this->jobdata_set('run_times', $time_passed);
1186
1187 $resume_interval = $this->jobdata_get('resume_interval');
1188 if ($time_this_run + 30 > $resume_interval) {
1189 $new_interval = ceil($time_this_run + 30);
1190 set_site_transient('updraft_initial_resume_interval', (int)$new_interval, 8*86400);
1191 $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");
1192 $this->jobdata_set('resume_interval', $new_interval);
1193 }
1194
1195 }
1196
1197 public function something_useful_happened() {
1198
1199 $this->record_still_alive();
1200
1201 if (!$this->something_useful_happened) {
1202 $useful_checkin = $this->jobdata_get('useful_checkin');
1203 if (empty($useful_checkin) || $this->current_resumption > $useful_checkin) $this->jobdata_set('useful_checkin', $this->current_resumption);
1204 }
1205
1206 $this->something_useful_happened = true;
1207
1208 $updraft_dir = $this->backups_dir_location();
1209 if (file_exists($updraft_dir.'/deleteflag-'.$this->nonce.'.txt')) {
1210 $this->log("User request for abort: backup job will be immediately halted");
1211 @unlink($updraft_dir.'/deleteflag-'.$this->nonce.'.txt');
1212 $this->backup_finish($this->current_resumption + 1, true, true, $this->current_resumption, true);
1213 die;
1214 }
1215
1216 if ($this->current_resumption >= 9 && false == $this->newresumption_scheduled) {
1217 $this->log("This is resumption ".$this->current_resumption.", but meaningful activity is still taking place; so a new one will be scheduled");
1218 // We just use max here to make sure we get a number at all
1219 $resume_interval = max($this->jobdata_get('resume_interval'), 75);
1220 // Don't consult the minimum here
1221 // if (!is_numeric($resume_interval) || $resume_interval<300) { $resume_interval = 300; }
1222 $schedule_for = time()+$resume_interval;
1223 $this->newresumption_scheduled = $schedule_for;
1224 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($this->current_resumption + 1, $this->nonce));
1225 } else {
1226 $this->reschedule_if_needed();
1227 }
1228 }
1229
1230 public function option_filter_get($which) {
1231 global $wpdb;
1232 $row = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $which));
1233 // Has to be get_row instead of get_var because of funkiness with 0, false, null values
1234 return (is_object($row)) ? $row->option_value : false;
1235 }
1236
1237 public function parse_filename($filename) {
1238 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)) {
1239 return array(
1240 'date' => strtotime($matches[1].' '.$matches[2]),
1241 'nonce' => $matches[3],
1242 'type' => $matches[4],
1243 'index' => (empty($matches[5]) ? 0 : $matches[5]-1),
1244 'extension' => $matches[6]);
1245 } else {
1246 return false;
1247 }
1248 }
1249
1250 // Pretty printing
1251 public function printfile($description, $history, $entity, $checksums, $jobdata, $smaller=false) {
1252
1253 if (empty($history[$entity])) return;
1254
1255 if ($smaller) {
1256 $pfiles = "<strong>".$description." (".sprintf(__('files: %s', 'updraftplus'), count($history[$entity])).")</strong><br>\n";
1257 } else {
1258 $pfiles = "<h3>".$description." (".sprintf(__('files: %s', 'updraftplus'), count($history[$entity])).")</h3>\n\n";
1259 }
1260
1261 $pfiles .= '<ul>';
1262 $files = $history[$entity];
1263 if (is_string($files)) $files = array($files);
1264
1265 foreach ($files as $ind => $file) {
1266
1267 $op = htmlspecialchars($file)."\n";
1268 $skey = $entity.((0 == $ind) ? '' : $ind).'-size';
1269
1270 $meta = '';
1271 if ('db' == substr($entity, 0, 2) && 'db' != $entity) {
1272 $dind = substr($entity, 2);
1273 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'])) {
1274 $dbinfo = $jobdata['backup_database'][$dind]['dbinfo'];
1275 $meta .= sprintf(__('External database (%s)', 'updraftplus'), $dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'])."<br>";
1276 }
1277 }
1278 if (isset($history[$skey])) $meta .= sprintf(__('Size: %s Mb', 'updraftplus'), round($history[$skey]/1048576, 1));
1279 $ckey = $entity.$ind;
1280 foreach ($checksums as $ck) {
1281 $ck_plain = false;
1282 if (isset($history['checksums'][$ck][$ckey])) {
1283 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey]);
1284 $ck_plain = true;
1285 }
1286 if (isset($history['checksums'][$ck][$ckey.'.crypt'])) {
1287 if ($ck_plain) $meta .= ' '.__('(when decrypted)');
1288 $meta .= (($meta) ? ', ' : '').sprintf(__('%s checksum: %s', 'updraftplus'), strtoupper($ck), $history['checksums'][$ck][$ckey.'.crypt']);
1289 }
1290 }
1291
1292 $fileinfo = apply_filters("updraftplus_fileinfo_$entity", array(), $ind);
1293 if (is_array($fileinfo) && !empty($fileinfo)) {
1294 if (isset($fileinfo['html'])) {
1295 $meta .= $fileinfo['html'];
1296 }
1297 }
1298
1299 #if ($meta) $meta = " ($meta)";
1300 if ($meta) $meta = "<br><em>$meta</em>";
1301 $pfiles .= '<li>'.$op.$meta."\n</li>\n";
1302 }
1303
1304 $pfiles .= "</ul>\n";
1305
1306 return $pfiles;
1307
1308 }
1309
1310 // 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
1311 public function get_backupable_file_entities($include_others = true, $full_info = false) {
1312
1313 $wp_upload_dir = $this->wp_upload_dir();
1314
1315 if ($full_info) {
1316 $arr = array(
1317 'plugins' => array('path' => untrailingslashit(WP_PLUGIN_DIR), 'description' => __('Plugins','updraftplus')),
1318 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes','updraftplus')),
1319 'uploads' => array('path' => untrailingslashit($wp_upload_dir['basedir']), 'description' => __('Uploads','updraftplus'))
1320 );
1321 } else {
1322 $arr = array(
1323 'plugins' => untrailingslashit(WP_PLUGIN_DIR),
1324 'themes' => WP_CONTENT_DIR.'/themes',
1325 'uploads' => untrailingslashit($wp_upload_dir['basedir'])
1326 );
1327 }
1328
1329 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
1330
1331 // We then add 'others' on to the end
1332 if ($include_others) {
1333 if ($full_info) {
1334 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others', 'updraftplus'));
1335 } else {
1336 $arr['others'] = WP_CONTENT_DIR;
1337 }
1338 }
1339
1340 // Entries that should be added after 'others'
1341 $arr = apply_filters('updraft_backupable_file_entities_final', $arr, $full_info);
1342
1343 return $arr;
1344
1345 }
1346
1347 # 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)
1348 public function filter_updraft_backup_history($v) {
1349 global $wpdb;
1350 $row = $wpdb->get_row( $wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", 'updraft_backup_history' ) );
1351 if (is_object($row )) return maybe_unserialize($row->option_value);
1352 return false;
1353 }
1354
1355 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
1356 switch ($errno) {
1357 case 1: $e_type = 'E_ERROR'; break;
1358 case 2: $e_type = 'E_WARNING'; break;
1359 case 4: $e_type = 'E_PARSE'; break;
1360 case 8: $e_type = 'E_NOTICE'; break;
1361 case 16: $e_type = 'E_CORE_ERROR'; break;
1362 case 32: $e_type = 'E_CORE_WARNING'; break;
1363 case 64: $e_type = 'E_COMPILE_ERROR'; break;
1364 case 128: $e_type = 'E_COMPILE_WARNING'; break;
1365 case 256: $e_type = 'E_USER_ERROR'; break;
1366 case 512: $e_type = 'E_USER_WARNING'; break;
1367 case 1024: $e_type = 'E_USER_NOTICE'; break;
1368 case 2048: $e_type = 'E_STRICT'; break;
1369 case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break;
1370 case 8192: $e_type = 'E_DEPRECATED'; break;
1371 case 16384: $e_type = 'E_USER_DEPRECATED'; break;
1372 case 30719: $e_type = 'E_ALL'; break;
1373 default: $e_type = "E_UNKNOWN ($errno)"; break;
1374 }
1375
1376 if (!is_string($errstr)) $errstr = serialize($errstr);
1377
1378 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
1379
1380 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
1381
1382 }
1383
1384 public function php_error($errno, $errstr, $errfile, $errline) {
1385 if (0 == error_reporting()) return true;
1386 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
1387 $this->log($logline, 'notice', 'php_event');
1388 // Pass it up the chain
1389 return $this->error_reporting_stop_when_logged;
1390 }
1391
1392 public function backup_resume($resumption_no, $bnonce) {
1393
1394 set_error_handler(array($this, 'php_error'), E_ALL & ~E_STRICT);
1395
1396 $this->current_resumption = $resumption_no;
1397
1398 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1399 @ignore_user_abort(true);
1400
1401 $runs_started = array();
1402 $time_now = microtime(true);
1403
1404 add_filter('pre_option_updraft_backup_history', array($this, 'filter_updraft_backup_history'));
1405
1406 // Restore state
1407 $resumption_extralog = '';
1408 $prev_resumption = $resumption_no - 1;
1409 $last_successful_resumption = -1;
1410 $job_type = 'backup';
1411
1412 if ($resumption_no > 0) {
1413
1414 $this->nonce = $bnonce;
1415 $this->backup_time = $this->jobdata_get('backup_time');
1416 $this->job_time_ms = $this->jobdata_get('job_time_ms');
1417
1418 # 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)
1419 $warnings = $this->jobdata_get('warnings');
1420
1421 $this->logfile_open($bnonce);
1422
1423 // 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
1424 if (is_array($warnings)) {
1425 foreach ($warnings as $warning) {
1426 $this->errors[] = array('level' => 'warning', 'message' => $warning);
1427 }
1428 }
1429
1430 $runs_started = $this->jobdata_get('runs_started');
1431 if (!is_array($runs_started)) $runs_started=array();
1432 $time_passed = $this->jobdata_get('run_times');
1433 if (!is_array($time_passed)) $time_passed = array();
1434
1435 foreach ($time_passed as $run => $passed) {
1436 if (isset($runs_started[$run]) && $runs_started[$run] + $time_passed[$run] + 30 > $time_now) {
1437 // We don't want to increase the resumption if WP has started two copies of the same resumption off
1438 if ($run && $run == $resumption_no) {
1439 $increase_resumption = false;
1440 $this->log("It looks like WordPress's scheduler has started multiple instances of this resumption");
1441 } else {
1442 $increase_resumption = true;
1443 }
1444 $this->terminate_due_to_activity('check-in', round($time_now, 1), round($runs_started[$run] + $time_passed[$run], 1), $increase_resumption);
1445 }
1446 }
1447
1448 for ($i = 0; $i<=$prev_resumption; $i++) {
1449 if (isset($time_passed[$i])) $last_successful_resumption = $i;
1450 }
1451
1452 if (isset($time_passed[$prev_resumption])) {
1453 $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 1)."s";
1454 } else {
1455 $this->no_checkin_last_time = true;
1456 }
1457
1458 // This is just a simple test to catch restorations of old backup sets where the backup includes a resumption of the backup job
1459 if ($time_now - $this->backup_time > 172800 && true == apply_filters('updraftplus_check_obsolete_backup', true, $time_now, $this)) {
1460
1461 // 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.
1462 if (empty($this->backup_time) && empty($this->backup_is_already_complete) && !empty($this->logfile_name) && is_readable($this->logfile_name)) {
1463 $first_log_bit = file_get_contents($this->logfile_name, false, null, 0, 250);
1464 if (preg_match('/\(0\) Opened log file at time: (.*) on /', $first_log_bit, $matches)) {
1465 $first_opened = strtotime($matches[1]);
1466 // 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.
1467 if (time() - $first_opened < 1000) {
1468 $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)).")");
1469 $this->reschedule(120);
1470 die;
1471 }
1472 }
1473 }
1474
1475 $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)).")");
1476 die;
1477 }
1478
1479 } else {
1480 $label = $this->jobdata_get('label');
1481 if ($label) $resumption_extralog = ", label=$label";
1482 }
1483
1484 $this->last_successful_resumption = $last_successful_resumption;
1485
1486 $runs_started[$resumption_no] = $time_now;
1487 if (!empty($this->backup_time)) $this->jobdata_set('runs_started', $runs_started);
1488
1489 // Schedule again, to run in 5 minutes again, in case we again fail
1490 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
1491 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 100);
1492
1493 $btime = $this->backup_time;
1494
1495 $job_type = $this->jobdata_get('job_type');
1496
1497 do_action('updraftplus_resume_backup_'.$job_type);
1498
1499 $updraft_dir = $this->backups_dir_location();
1500
1501 $time_ago = time()-$btime;
1502
1503 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
1504
1505 // 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.
1506 // 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.
1507 if ($resumption_no >= 1 && 'finished' == $this->jobdata_get('jobstatus')) {
1508 $this->log('Terminate: This backup job is already finished (1).');
1509 die;
1510 } elseif ('backup' == $job_type && !empty($this->backup_is_already_complete)) {
1511 $this->log('Terminate: This backup job is already finished (2).');
1512 die;
1513 }
1514
1515 if ($resumption_no > 0 && isset($runs_started[$prev_resumption])) {
1516 $our_expected_start = $runs_started[$prev_resumption] + $resume_interval;
1517 # If the previous run increased the resumption time, then it is timed from the end of the previous run, not the start
1518 if (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption]>0) $our_expected_start += $time_passed[$prev_resumption];
1519 $our_expected_start = apply_filters('updraftplus_expected_start', $our_expected_start, $job_type);
1520 # More than 12 minutes late?
1521 if ($time_now > $our_expected_start + 720) {
1522 $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));
1523 $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');
1524 }
1525 }
1526
1527 $this->jobdata_set('current_resumption', $resumption_no);
1528
1529 $first_run = apply_filters('updraftplus_filerun_firstrun', 0);
1530
1531 // We just do this once, as we don't want to be in permanent conflict with the overlap detector
1532 if ($resumption_no >= $first_run + 8 && $resumption_no < $first_run + 15 && $resume_interval >= 300) {
1533
1534 // $time_passed is set earlier
1535 list($max_time, $timings_string, $run_times_known) = $this->max_time_passed($time_passed, $resumption_no - 1, $first_run);
1536
1537 # Do this on resumption 8, or the first time that we have 6 data points
1538 if (($first_run + 8 == $resumption_no && $run_times_known >= 6) || (6 == $run_times_known && !empty($time_passed[$prev_resumption]))) {
1539 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
1540 // 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
1541 if ($max_time + 52 < $resume_interval) {
1542 $resume_interval = round($max_time + 52);
1543 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
1544 $this->jobdata_set('resume_interval', $resume_interval);
1545 }
1546 }
1547
1548 }
1549
1550 // A different argument than before is needed otherwise the event is ignored
1551 $next_resumption = $resumption_no+1;
1552 if ($next_resumption < $first_run + 10) {
1553 if (true === $this->jobdata_get('one_shot')) {
1554 if (true === $this->jobdata_get('reschedule_before_upload') && 1 == $next_resumption) {
1555 $this->log('A resumption will be scheduled for the cloud backup stage');
1556 $schedule_resumption = true;
1557 } else {
1558 $this->log('We are in "one shot" mode - no resumptions will be scheduled');
1559 }
1560 } else {
1561 $schedule_resumption = true;
1562 }
1563 } else {
1564 // 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
1565 $useful_checkin = $this->jobdata_get('useful_checkin');
1566 $last_resumption = $resumption_no-1;
1567
1568 if (empty($useful_checkin) || $useful_checkin < $last_resumption) {
1569 $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));
1570 } else {
1571 $schedule_resumption = true;
1572 }
1573 }
1574
1575 // Sanity check
1576 if (empty($this->backup_time)) {
1577 $this->log('The backup_time parameter appears to be empty (usually caused by resuming an already-complete backup).');
1578 return false;
1579 }
1580
1581 if (isset($schedule_resumption)) {
1582 $schedule_for = time()+$resume_interval;
1583 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
1584 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
1585 $this->newresumption_scheduled = $schedule_for;
1586 }
1587
1588 $backup_files = $this->jobdata_get('backup_files');
1589
1590 global $updraftplus_backup;
1591 // Bring in all the backup routines
1592 require_once(UPDRAFTPLUS_DIR.'/backup.php');
1593 $updraftplus_backup = new UpdraftPlus_Backup($backup_files, apply_filters('updraftplus_files_altered_since', -1, $job_type));
1594
1595 $undone_files = array();
1596
1597 if ('no' == $backup_files) {
1598 $this->log("This backup run is not intended for files - skipping");
1599 $our_files = array();
1600 } else {
1601
1602 // This should be always called; if there were no files in this run, it returns us an empty array
1603 $backup_array = $updraftplus_backup->resumable_backup_of_files($resumption_no);
1604
1605 // This save, if there was something, is then immediately picked up again
1606 if (is_array($backup_array)) {
1607 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
1608 $this->save_backup_history($backup_array);
1609 }
1610
1611 // Switch of variable name is purely vestigial
1612 $our_files = $backup_array;
1613 if (!is_array($our_files)) $our_files = array();
1614
1615 }
1616
1617 $backup_databases = $this->jobdata_get('backup_database');
1618
1619 if (!is_array($backup_databases)) $backup_databases = array('wp' => $backup_databases);
1620
1621 foreach ($backup_databases as $whichdb => $backup_database) {
1622
1623 if (is_array($backup_database)) {
1624 $dbinfo = $backup_database['dbinfo'];
1625 $backup_database = $backup_database['status'];
1626 } else {
1627 $dbinfo = array();
1628 }
1629
1630 $tindex = ('wp' == $whichdb) ? 'db' : 'db'.$whichdb;
1631
1632 if ('begun' == $backup_database || 'finished' == $backup_database || 'encrypted' == $backup_database) {
1633
1634 if ('wp' == $whichdb) {
1635 $db_descrip = 'WordPress DB';
1636 } else {
1637 if (!empty($dbinfo) && is_array($dbinfo) && !empty($dbinfo['host'])) {
1638 $db_descrip = "External DB $whichdb - ".$dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'];
1639 } else {
1640 $db_descrip = "External DB $whichdb - details appear to be missing";
1641 }
1642 }
1643
1644 if ('begun' == $backup_database) {
1645 if ($resumption_no > 0) {
1646 $this->log("Resuming creation of database dump ($db_descrip)");
1647 } else {
1648 $this->log("Beginning creation of database dump ($db_descrip)");
1649 }
1650 } elseif ('encrypted' == $backup_database) {
1651 $this->log("Database dump ($db_descrip): Creation and encryption were completed already");
1652 } else {
1653 $this->log("Database dump ($db_descrip): Creation was completed already");
1654 }
1655
1656 if ('wp' != $whichdb && (empty($dbinfo) || !is_array($dbinfo) || empty($dbinfo['host']))) {
1657 unset($backup_databases[$whichdb]);
1658 $this->jobdata_set('backup_database', $backup_databases);
1659 continue;
1660 }
1661
1662 $db_backup = $updraftplus_backup->backup_db($backup_database, $whichdb, $dbinfo);
1663
1664 if(is_array($our_files) && is_string($db_backup)) $our_files[$tindex] = $db_backup;
1665
1666 if ('encrypted' != $backup_database) {
1667 $backup_databases[$whichdb] = array('status' => 'finished', 'dbinfo' => $dbinfo);
1668 $this->jobdata_set('backup_database', $backup_databases);
1669 }
1670 } elseif ('no' == $backup_database) {
1671 $this->log("No database backup ($whichdb) - not part of this run");
1672 } else {
1673 $this->log("Unrecognised data when trying to ascertain if the database ($whichdb) was backed up (".serialize($backup_database).")");
1674 }
1675
1676 // 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.
1677 $this->save_backup_history($our_files);
1678
1679 // Potentially encrypt the database if it is not already
1680 if ('no' != $backup_database && isset($our_files[$tindex]) && !preg_match("/\.crypt$/", $our_files[$tindex])) {
1681 $our_files[$tindex] = $updraftplus_backup->encrypt_file($our_files[$tindex]);
1682 // No need to save backup history now, as it will happen in a few lines time
1683 if (preg_match("/\.crypt$/", $our_files[$tindex])) {
1684 $backup_databases[$whichdb] = array('status' => 'encrypted', 'dbinfo' => $dbinfo);
1685 $this->jobdata_set('backup_database', $backup_databases);
1686 }
1687 }
1688
1689 if ('no' != $backup_database && isset($our_files[$tindex]) && file_exists($updraft_dir.'/'.$our_files[$tindex])) {
1690 $our_files[$tindex.'-size'] = filesize($updraft_dir.'/'.$our_files[$tindex]);
1691 $this->save_backup_history($our_files);
1692 }
1693
1694 }
1695
1696 $backupable_entities = $this->get_backupable_file_entities(true);
1697
1698 $checksums = array('sha1' => array());
1699
1700 $total_size = 0;
1701
1702 // Queue files for upload
1703 foreach ($our_files as $key => $files) {
1704 // Only continue if the stored info was about a dump
1705 if (!isset($backupable_entities[$key]) && ('db' != substr($key, 0, 2) || '-size' == substr($key, -5, 5))) continue;
1706 if (is_string($files)) $files = array($files);
1707 foreach ($files as $findex => $file) {
1708
1709 $size_key = (0 == $findex) ? $key.'-size' : $key.$findex.'-size';
1710 $total_size = (false === $total_size || !isset($our_files[$size_key]) || !is_numeric($our_files[$size_key])) ? false : $total_size + $our_files[$size_key];
1711
1712 $sha = $this->jobdata_get('sha1-'.$key.$findex);
1713 if ($sha) $checksums['sha1'][$key.$findex] = $sha;
1714 $sha = $this->jobdata_get('sha1-'.$key.$findex.'.crypt');
1715 if ($sha) $checksums['sha1'][$key.$findex.".crypt"] = $sha;
1716 if ($this->is_uploaded($file)) {
1717 $this->log("$file: $key: This file has already been successfully uploaded");
1718 } elseif (is_file($updraft_dir.'/'.$file)) {
1719 if (!in_array($file, $undone_files)) {
1720 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
1721 $undone_files[$key.$findex] = $file;
1722 } else {
1723 $this->log("$file: $key: This file was already queued for upload (this condition should never be seen)");
1724 }
1725 } else {
1726 $this->log("$file: $key: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem ($updraft_dir/$file)");
1727 $this->uploaded_file($file, true);
1728 }
1729 }
1730 }
1731 $our_files['checksums'] = $checksums;
1732
1733 // Save again (now that we have checksums)
1734 $size_description = (false === $total_size) ? 'Unknown' : $this->convert_numeric_size_to_text($total_size);
1735 $this->log("Saving backup history. Total backup size: $size_description");
1736 $this->save_backup_history($our_files);
1737 do_action('updraft_final_backup_history', $our_files);
1738
1739 // We finished; so, low memory was not a problem
1740 $this->log_removewarning('lowram');
1741
1742 if (0 == count($undone_files)) {
1743 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1744 if (is_array($our_files)) $this->save_last_backup($our_files);
1745 $this->log("There were no more files that needed uploading");
1746 // No email, as the user probably already got one if something else completed the run
1747 $allow_email = false;
1748 if ('begun' == $this->jobdata_get('prune')) {
1749 // Begun, but not finished
1750 $this->log("Restarting backup prune operation");
1751 $updraftplus_backup->do_prune_standalone();
1752 $allow_email = true;
1753 }
1754 $this->backup_finish($next_resumption, true, $allow_email, $resumption_no);
1755 restore_error_handler();
1756 return;
1757 }
1758
1759 $this->error_count_before_cloud_backup = $this->error_count();
1760
1761 // This is intended for one-shot backups, where we do want a resumption if it's only for uploading
1762 if (empty($this->newresumption_scheduled) && 0 == $resumption_no && 0 == $this->error_count_before_cloud_backup && true === $this->jobdata_get('reschedule_before_upload')) {
1763 $this->log("Cloud backup stage reached on one-shot backup: scheduling resumption for the cloud upload");
1764 $this->reschedule(60);
1765 $this->record_still_alive();
1766 }
1767
1768 $this->log("Requesting upload of the files that have not yet been successfully uploaded (".count($undone_files).")");
1769
1770 $updraftplus_backup->cloud_backup($undone_files);
1771
1772 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1773 if (is_array($our_files)) $this->save_last_backup($our_files);
1774 $this->backup_finish($next_resumption, true, true, $resumption_no);
1775
1776 restore_error_handler();
1777
1778 }
1779
1780 public function convert_numeric_size_to_text($size) {
1781 if ($size > 1073741824) {
1782 return round($size / 1073741824, 1).' Gb';
1783 } elseif ($size > 1048576) {
1784 return round($size / 1048576, 1).' Mb';
1785 } elseif ($size > 1024) {
1786 return round($size / 1024, 1).' Kb';
1787 } else {
1788 return round($size, 1).' b';
1789 }
1790 }
1791
1792 public function max_time_passed($time_passed, $upto, $first_run) {
1793 $max_time = 0;
1794 $timings_string = "";
1795 $run_times_known=0;
1796 for ($i=$first_run; $i<=$upto; $i++) {
1797 $timings_string .= "$i:";
1798 if (isset($time_passed[$i])) {
1799 $timings_string .= round($time_passed[$i], 1).' ';
1800 $run_times_known++;
1801 if ($time_passed[$i] > $max_time) $max_time = round($time_passed[$i]);
1802 } else {
1803 $timings_string .= '? ';
1804 }
1805 }
1806 return array($max_time, $timings_string, $run_times_known);
1807 }
1808
1809 public function jobdata_getarray($non) {
1810 return get_site_option("updraft_jobdata_".$non, array());
1811 }
1812
1813 public function jobdata_set_from_array($array) {
1814 $this->jobdata = $array;
1815 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1816 }
1817
1818 // This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
1819 public function jobdata_set_multi() {
1820 if (!is_array($this->jobdata)) $this->jobdata = array();
1821
1822 $args = func_num_args();
1823
1824 for ($i=1; $i<=$args/2; $i++) {
1825 $key = func_get_arg($i*2-2);
1826 $value = func_get_arg($i*2-1);
1827 $this->jobdata[$key] = $value;
1828 }
1829 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1830 }
1831
1832 public function jobdata_set($key, $value) {
1833 if (empty($this->jobdata)) {
1834 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce);
1835 if (!is_array($this->jobdata)) $this->jobdata = array();
1836 }
1837 $this->jobdata[$key] = $value;
1838 if ($this->nonce) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1839 }
1840
1841 public function jobdata_delete($key) {
1842 if (!is_array($this->jobdata)) {
1843 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce);
1844 if (!is_array($this->jobdata)) $this->jobdata = array();
1845 }
1846 unset($this->jobdata[$key]);
1847 if ($this->nonce) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1848 }
1849
1850 public function get_job_option($opt) {
1851 // These are meant to be read-only
1852 if (empty($this->jobdata['option_cache']) || !is_array($this->jobdata['option_cache'])) {
1853 if (!is_array($this->jobdata)) $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
1854 $this->jobdata['option_cache'] = array();
1855 }
1856 return (isset($this->jobdata['option_cache'][$opt])) ? $this->jobdata['option_cache'][$opt] : UpdraftPlus_Options::get_updraft_option($opt);
1857 }
1858
1859 public function jobdata_get($key, $default = null) {
1860 if (empty($this->jobdata)) {
1861 $this->jobdata = empty($this->nonce) ? array() : get_site_option("updraft_jobdata_".$this->nonce, array());
1862 if (!is_array($this->jobdata)) return $default;
1863 }
1864 return isset($this->jobdata[$key]) ? $this->jobdata[$key] : $default;
1865 }
1866
1867 public function jobdata_reset() {
1868 $this->jobdata = null;
1869 }
1870
1871 private function ensure_semaphore_exists($semaphore) {
1872 // Make sure the options for semaphores exist
1873 global $wpdb;
1874 $results = $wpdb->get_results("
1875 SELECT option_id
1876 FROM $wpdb->options
1877 WHERE option_name IN ('updraftplus_locked_$semaphore', 'updraftplus_unlocked_$semaphore', 'updraftplus_last_lock_time_$semaphore', 'updraftplus_semaphore_$semaphore')
1878 ");
1879 // Use of update_option() is correct here - since it is what is used in class-semaphore.php
1880 if (!is_array($results) || count($results) < 3) {
1881 if (is_array($results) && count($results) > 0) $this->log("Semaphore ($semaphore) in an impossible/broken state - fixing (".count($results).")");
1882 update_option('updraftplus_unlocked_'.$semaphore, '1');
1883 delete_option('updraftplus_locked_'.$semaphore);
1884 update_option('updraftplus_last_lock_time_'.$semaphore, current_time('mysql', 1));
1885 update_option('updraftplus_semaphore_'.$semaphore, '0');
1886 }
1887 }
1888
1889 public function backup_files() {
1890 # Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
1891 $this->boot_backup(true, false);
1892 }
1893
1894 public function backup_database() {
1895 # Note that nothing will happen if the file backup had the same schedule
1896 $this->boot_backup(false, true);
1897 }
1898
1899 public function backup_all($options) {
1900 $skip_cloud = empty($options['nocloud']) ? false : true;
1901 $this->boot_backup(1, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
1902 }
1903
1904 public function backupnow_files($options) {
1905 $skip_cloud = empty($options['nocloud']) ? false : true;
1906 $this->boot_backup(1, 0, false, false, ($skip_cloud) ? 'none' : false, $options);
1907 }
1908
1909 public function backupnow_database($options) {
1910 $skip_cloud = empty($options['nocloud']) ? false : true;
1911 $this->boot_backup(0, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
1912 }
1913
1914 // This procedure initiates a backup run
1915 // $backup_files/$backup_database: true/false = yes/no (over-write allowed); 1/0 = yes/no (force)
1916 public function boot_backup($backup_files, $backup_database, $restrict_files_to_override = false, $one_shot = false, $service = false, $options = array()) {
1917
1918 @ignore_user_abort(true);
1919 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1920
1921 if (false === $restrict_files_to_override && isset($options['restrict_files_to_override'])) $restrict_files_to_override = $options['restrict_files_to_override'];
1922 // Generate backup information
1923 $use_nonce = (empty($options['use_nonce'])) ? false : $options['use_nonce'];
1924 $this->backup_time_nonce($use_nonce);
1925 // The current_resumption is consulted within logfile_open()
1926 $this->current_resumption = 0;
1927 $this->logfile_open($this->nonce);
1928
1929 if (!is_file($this->logfile_name)) {
1930 $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.');
1931 $this->log(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.','updraftplus'), 'error');
1932 return false;
1933 }
1934
1935 // Some house-cleaning
1936 $this->clean_temporary_files();
1937 // Log some information that may be helpful
1938 $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').")");
1939
1940 if (false === $one_shot && is_bool($backup_database)) {
1941 # If the files and database schedules are the same, and if this the file one, then we rope in database too.
1942 # On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
1943 if ('manual' != UpdraftPlus_Options::get_updraft_option('updraft_interval') && (UpdraftPlus_Options::get_updraft_option('updraft_interval') == UpdraftPlus_Options::get_updraft_option('updraft_interval_database') || UpdraftPlus_Options::get_updraft_option('updraft_interval_database', 'xyz') == 'xyz' )) {
1944 $backup_database = ($backup_files == true) ? true : false;
1945 }
1946 $this->log("Processed schedules. Tasks now: Backup files: $backup_files Backup DB: $backup_database");
1947 }
1948
1949 $semaphore = (($backup_files) ? 'f' : '') . (($backup_database) ? 'd' : '');
1950 $this->ensure_semaphore_exists($semaphore);
1951
1952 if (false == apply_filters('updraftplus_boot_backup', true, $backup_files, $backup_database, $one_shot)) {
1953 $this->log("Backup aborted (via filter)");
1954 return false;
1955 }
1956
1957 if (!is_string($service) && !is_array($service)) $service = UpdraftPlus_Options::get_updraft_option('updraft_service');
1958 $service = $this->just_one($service);
1959 if (is_string($service)) $service = array($service);
1960 if (!is_array($service)) $service = array('none');
1961
1962 if (!empty($options['extradata']) && preg_match('#services=remotesend/(\d+)#', $options['extradata'])) {
1963 if ($service === array('none')) $service = array();
1964 $service[] = 'remotesend';
1965 }
1966
1967 $option_cache = array();
1968
1969 foreach ($service as $serv) {
1970 if ('' == $serv || 'none' == $serv) continue;
1971 include_once(UPDRAFTPLUS_DIR.'/methods/'.$serv.'.php');
1972 $cclass = 'UpdraftPlus_BackupModule_'.$serv;
1973 $obj = new $cclass;
1974
1975 if (method_exists($cclass, 'get_credentials')) {
1976 $opts = $obj->get_credentials();
1977 if (is_array($opts)) {
1978 foreach ($opts as $opt) $option_cache[$opt] = UpdraftPlus_Options::get_updraft_option($opt);
1979 }
1980 }
1981 }
1982 $option_cache = apply_filters('updraftplus_job_option_cache', $option_cache);
1983
1984 // If nothing to be done, then just finish
1985 if (!$backup_files && !$backup_database) {
1986 $ret = $this->backup_finish(1, false, false, 0);
1987 // Don't keep useless log files
1988 if (!UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') && !empty($this->logfile_name) && file_exists($this->logfile_name)) {
1989 unlink($this->logfile_name);
1990 }
1991 return $ret;
1992 }
1993
1994 // 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
1995 // doing_action() was added in WP 3.9
1996 // wp_cron() is called from the 'init' action
1997 if (function_exists('doing_action') && doing_action('init') && (doing_action('updraft_backup_database') || doing_action('updraft_backup'))) {
1998 $last_scheduled_action_called_at = get_option("updraft_last_scheduled_$semaphore");
1999 // 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.
2000 if ($last_scheduled_action_called_at && time() - $last_scheduled_action_called_at < 660 && apply_filters('updraft_check_repeated_scheduled_backups', true)) {
2001 $seconds_ago = time() - $last_scheduled_action_called_at;
2002 $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));
2003 return;
2004 }
2005 }
2006 update_option("updraft_last_scheduled_$semaphore", time());
2007
2008 require_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
2009 $this->semaphore = UpdraftPlus_Semaphore::factory();
2010 $this->semaphore->lock_name = $semaphore;
2011 $this->log('Requesting semaphore lock ('.$semaphore.')');
2012 if (!$this->semaphore->lock()) {
2013 $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)');
2014 return;
2015 }
2016
2017 // Allow the resume interval to be more than 300 if last time we know we went beyond that - but never more than 600
2018 if (defined('UPDRAFTPLUS_INITIAL_RESUME_INTERVAL') && is_numeric(UPDRAFTPLUS_INITIAL_RESUME_INTERVAL)) {
2019 $resume_interval = UPDRAFTPLUS_INITIAL_RESUME_INTERVAL;
2020 } else {
2021 $resume_interval = (int)min(max(300, get_site_transient('updraft_initial_resume_interval')), 600);
2022 }
2023 # 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)
2024 delete_site_transient('updraft_initial_resume_interval');
2025
2026 $job_file_entities = array();
2027 if ($backup_files) {
2028 $possible_backups = $this->get_backupable_file_entities(true);
2029 foreach ($possible_backups as $youwhat => $whichdir) {
2030 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))) {
2031 // The 0 indicates the zip file index
2032 $job_file_entities[$youwhat] = array(
2033 'index' => 0
2034 );
2035 }
2036 }
2037 }
2038
2039 $followups_allowed = (((!$one_shot && defined('DOING_CRON') && DOING_CRON)) || (defined('UPDRAFTPLUS_FOLLOWUPS_ALLOWED') && UPDRAFTPLUS_FOLLOWUPS_ALLOWED));
2040
2041 $split_every = max(intval(UpdraftPlus_Options::get_updraft_option('updraft_split_every', 400)), UPDRAFTPLUS_SPLIT_MIN);
2042
2043 $initial_jobdata = array(
2044 'resume_interval', $resume_interval,
2045 'job_type', 'backup',
2046 'jobstatus', 'begun',
2047 'backup_time', $this->backup_time,
2048 'job_time_ms', $this->job_time_ms,
2049 'service', $service,
2050 'split_every', $split_every,
2051 'maxzipbatch', 26214400, #25Mb
2052 'job_file_entities', $job_file_entities,
2053 'option_cache', $option_cache,
2054 'uploaded_lastreset', 9,
2055 'one_shot', $one_shot,
2056 'followsups_allowed', $followups_allowed
2057 );
2058
2059 if ($one_shot) update_site_option('updraft_oneshotnonce', $this->nonce);
2060
2061 if (!empty($options['extradata']) && 'autobackup' == $options['extradata']) array_push($initial_jobdata, 'is_autobackup', true);
2062
2063 // Save what *should* be done, to make it resumable from this point on
2064 if ($backup_database) {
2065 $dbs = apply_filters('updraft_backup_databases', array('wp' => 'begun'));
2066 if (is_array($dbs)) {
2067 foreach ($dbs as $key => $db) {
2068 if ('wp' != $key && (!is_array($db) || empty($db['dbinfo']) || !is_array($db['dbinfo']) || empty($db['dbinfo']['host']))) unset($dbs[$key]);
2069 }
2070 }
2071 } else {
2072 $dbs = "no";
2073 }
2074
2075 array_push($initial_jobdata, 'backup_database', $dbs);
2076 array_push($initial_jobdata, 'backup_files', (($backup_files) ? 'begun' : 'no'));
2077
2078 if (is_array($options) && !empty($options['label'])) array_push($initial_jobdata, 'label', $options['label']);
2079
2080 try {
2081 // Use of jobdata_set_multi saves around 200ms
2082 call_user_func_array(array($this, 'jobdata_set_multi'), apply_filters('updraftplus_initial_jobdata', $initial_jobdata, $options, $split_every));
2083 } catch (Exception $e) {
2084 $this->log($e->getMessage());
2085 return false;
2086 }
2087
2088 // Everything is set up; now go
2089 $this->backup_resume(0, $this->nonce);
2090
2091 if ($one_shot) delete_site_option('updraft_oneshotnonce');
2092
2093 }
2094
2095 // 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').
2096 // If $remotescan is set, then remote storage is also scanned
2097 // $only_add_this_file : an array with keys 'name' and (optionally) 'label'
2098 public function rebuild_backup_history($remotescan = false, $only_add_this_file = false) {
2099
2100 # TODO: Make compatible with incremental naming scheme
2101
2102 $messages = array();
2103 $gmt_offset = get_option('gmt_offset');
2104
2105 // Array of nonces keyed by filename
2106 $known_files = array();
2107 // Array of backup times keyed by nonce
2108 $known_nonces = array();
2109 $changes = false;
2110
2111 $backupable_entities = $this->get_backupable_file_entities(true, false);
2112
2113 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
2114 if (!is_array($backup_history)) $backup_history = array();
2115 $updraft_dir = $this->backups_dir_location();
2116 if (!is_dir($updraft_dir)) return;
2117
2118 $accept = apply_filters('updraftplus_accept_archivename', array());
2119 if (!is_array($accept)) $accept = array();
2120 // Process what is known from the database backup history; this means populating $known_files and $known_nonces
2121 foreach ($backup_history as $btime => $bdata) {
2122 $found_file = false;
2123 foreach ($bdata as $key => $values) {
2124 if ('db' != $key && !isset($backupable_entities[$key])) continue;
2125 // Record which set this file is found in
2126 if (!is_array($values)) $values=array($values);
2127 foreach ($values as $val) {
2128 if (!is_string($val)) continue;
2129 if (preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-[\-a-z]+([0-9]+)?+(\.(zip|gz|gz\.crypt))?$/i', $val, $matches)) {
2130 $nonce = $matches[2];
2131 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)) {
2132 # File without remote storage is no longer present
2133 } else {
2134 $found_file = true;
2135 $known_files[$val] = $nonce;
2136 $known_nonces[$nonce] = (empty($known_nonces[$nonce]) || $known_nonces[$nonce]<100) ? $btime : min($btime, $known_nonces[$nonce]);
2137 }
2138 } else {
2139 $accepted = false;
2140 foreach ($accept as $fkey => $acc) {
2141 if (preg_match('/'.$acc['pattern'].'/i', $val)) $accepted = $fkey;
2142 }
2143 if (!empty($accepted) && (false != ($btime = apply_filters('updraftplus_foreign_gettime', false, $accepted, $val))) && $btime > 0) {
2144 $found_file = true;
2145 # Generate a nonce; this needs to be deterministic and based on the filename only
2146 $nonce = substr(md5($val), 0, 12);
2147 $known_files[$val] = $nonce;
2148 $known_nonces[$nonce] = (empty($known_nonces[$nonce]) || $known_nonces[$nonce]<100) ? $btime : min($btime, $known_nonces[$nonce]);
2149 }
2150 }
2151 }
2152 }
2153 if (!$found_file) {
2154 # 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
2155 unset($backup_history[$btime]);
2156 $changes = true;
2157 }
2158 }
2159
2160 $remotefiles = array();
2161 $remotesizes = array();
2162 # Scan remote storage and get back lists of files and their sizes
2163 # TODO: Make compatible with incremental naming
2164 if ($remotescan) {
2165 add_action('http_request_args', array($this, 'modify_http_options'));
2166 foreach ($this->backup_methods as $method => $desc) {
2167 require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
2168 $objname = 'UpdraftPlus_BackupModule_'.$method;
2169 $obj = new $objname;
2170 if (!method_exists($obj, 'listfiles')) continue;
2171 $files = $obj->listfiles('backup_');
2172 if (is_array($files)) {
2173 foreach ($files as $entry) {
2174 $n = $entry['name'];
2175 if (!preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?(\.(zip|gz|gz\.crypt))?$/i', $n, $matches)) continue;
2176 if (isset($remotefiles[$n])) {
2177 $remotefiles[$n][] = $method;
2178 } else {
2179 $remotefiles[$n] = array($method);
2180 }
2181 if (!empty($entry['size'])) {
2182 if (empty($remotesizes[$n]) || $remotesizes[$n] < $entry['size']) $remotesizes[$n] = $entry['size'];
2183 }
2184 }
2185 } elseif (is_wp_error($files)) {
2186 foreach ($files->get_error_codes() as $code) {
2187 if ('no_settings' == $code || 'no_addon' == $code || 'insufficient_php' == $code || 'no_listing' == $code) continue;
2188 $messages[] = array(
2189 'method' => $method,
2190 'desc' => $desc,
2191 'code' => $code,
2192 'message' => $files->get_error_message($code)
2193 );
2194 }
2195 }
2196 }
2197 remove_action('http_request_args', array($this, 'modify_http_options'));
2198 }
2199
2200 if (!$handle = opendir($updraft_dir)) return;
2201
2202 // See if there are any more files in the local directory than the ones already known about
2203 while (false !== ($entry = readdir($handle))) {
2204 $accepted_foreign = false;
2205 $potmessage = false;
2206
2207 if ($only_add_this_file !== false && $entry != $only_add_this_file['file']) continue;
2208
2209 if ('.' == $entry || '..' == $entry) continue;
2210
2211 # TODO: Make compatible with Incremental naming
2212 if (preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?(\.(zip|gz|gz\.crypt))?$/i', $entry, $matches)) {
2213 // Interpret the time as one from the blog's local timezone, rather than as UTC
2214 # $matches[1] is YYYY-MM-DD-HHmm, to be interpreted as being the local timezone
2215 $btime2 = strtotime($matches[1]);
2216 $btime = (!empty($gmt_offset)) ? $btime2 - $gmt_offset*3600 : $btime2;
2217 $nonce = $matches[2];
2218 $type = $matches[3];
2219 if ('db' == $type) {
2220 $type .= (!empty($matches[4])) ? $matches[4] : '';
2221 $index = 0;
2222 } else {
2223 $index = (empty($matches[4])) ? '0' : (max((int)$matches[4]-1,0));
2224 }
2225 $itext = ($index == 0) ? '' : $index;
2226 } elseif (false != ($accepted_foreign = apply_filters('updraftplus_accept_foreign', false, $entry)) && false !== ($btime = apply_filters('updraftplus_foreign_gettime', false, $accepted_foreign, $entry))) {
2227 $nonce = substr(md5($entry), 0, 12);
2228 $type = (preg_match('/\.sql(\.(bz2|gz))?$/i', $entry) || preg_match('/-database-([-0-9]+)\.zip$/i', $entry) || preg_match('/backup_db_/', $entry)) ? 'db' : 'wpcore';
2229 $index = apply_filters('updraftplus_accepted_foreign_index', 0, $entry, $accepted_foreign);
2230 $itext = $index ? $index : '';
2231 $potmessage = array(
2232 'code' => 'foundforeign_'.md5($entry),
2233 'desc' => $entry,
2234 'method' => '',
2235 'message' => sprintf(__('Backup created by: %s.', 'updraftplus'), $accept[$accepted_foreign]['desc'])
2236 );
2237 } elseif ('.zip' == strtolower(substr($entry, -4, 4)) || preg_match('/\.sql(\.(bz2|gz))?$/i', $entry)) {
2238 $potmessage = array(
2239 'code' => 'possibleforeign_'.md5($entry),
2240 'desc' => $entry,
2241 'method' => '',
2242 '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>'
2243 );
2244 $messages[$potmessage['code']] = $potmessage;
2245 continue;
2246 } else {
2247 continue;
2248 }
2249 // The time from the filename does not include seconds. Need to identify the seconds to get the right time
2250 if (isset($known_nonces[$nonce])) {
2251 $btime_exact = $known_nonces[$nonce];
2252 # 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)
2253 if ($btime > 100 && $btime_exact - $btime > 60 && !empty($backup_history[$btime_exact])) {
2254 # TODO: This needs testing
2255 # The code below assumes that $backup_history[$btime] is presently empty
2256 # Re-key array, indicating the newly-found time to be the start of the backup set
2257 $backup_history[$btime] = $backup_history[$btime_exact];
2258 unset($backup_history[$btime_exact]);
2259 $btime_exact = $btime;
2260 }
2261 $btime = $btime_exact;
2262 }
2263 if ($btime <= 100) continue;
2264 $fs = @filesize($updraft_dir.'/'.$entry);
2265
2266 if (!isset($known_files[$entry])) {
2267 $changes = true;
2268 if (is_array($potmessage)) $messages[$potmessage['code']] = $potmessage;
2269 if (is_array($only_add_this_file)) {
2270 if (isset($only_add_this_file['label'])) $backup_history[$btime]['label'] = $only_add_this_file['label'];
2271 $backup_history[$btime]['native'] = false;
2272 } elseif ('db' == $type && !$accepted_foreign) {
2273 list ($mess, $warn, $err, $info) = $this->analyse_db_file(false, array(), $updraft_dir.'/'.$entry, true);
2274 if (!empty($info['label'])) {
2275 $backup_history[$btime]['label'] = $info['label'];
2276 }
2277 if (!empty($info['created_by_version'])) {
2278 $backup_history[$btime]['created_by_version'] = $info['created_by_version'];
2279 }
2280 }
2281 }
2282
2283 # TODO: Code below here has not been reviewed or adjusted for compatibility with incremental backups
2284 # Make sure we have the right list of services
2285 $current_services = (!empty($backup_history[$btime]) && !empty($backup_history[$btime]['service'])) ? $backup_history[$btime]['service'] : array();
2286 if (is_string($current_services)) $current_services = array($current_services);
2287 if (!is_array($current_services)) $current_services = array();
2288 if (!empty($remotefiles[$entry])) {
2289 if (0 == count(array_diff($current_services, $remotefiles[$entry]))) {
2290 $backup_history[$btime]['service'] = $remotefiles[$entry];
2291 $changes = true;
2292 }
2293 # Get the right size (our local copy may be too small)
2294 foreach ($remotefiles[$entry] as $rem) {
2295 if (!empty($rem['size']) && $rem['size'] > $fs) {
2296 $fs = $rem['size'];
2297 $changes = true;
2298 }
2299 }
2300 # Remove from $remotefiles, so that we can later see what was left over
2301 unset($remotefiles[$entry]);
2302 } else {
2303 # Not known remotely
2304 if (!empty($backup_history[$btime])) {
2305 if (empty($backup_history[$btime]['service']) || ('none' !== $backup_history[$btime]['service'] && '' !== $backup_history[$btime]['service'] && array('none') !== $backup_history[$btime]['service'])) {
2306 $backup_history[$btime]['service'] = 'none';
2307 $changes = true;
2308 }
2309 } else {
2310 $backup_history[$btime]['service'] = 'none';
2311 $changes = true;
2312 }
2313 }
2314
2315 $backup_history[$btime][$type][$index] = $entry;
2316 if ($fs > 0) $backup_history[$btime][$type.$itext.'-size'] = $fs;
2317 $backup_history[$btime]['nonce'] = $nonce;
2318 if (!empty($accepted_foreign)) $backup_history[$btime]['meta_foreign'] = $accepted_foreign;
2319 }
2320
2321 # Any found in remote storage that we did not previously know about?
2322 # Compare $remotefiles with $known_files / $known_nonces, and adjust $backup_history
2323 if (count($remotefiles) > 0) {
2324
2325 # $backup_history[$btime]['nonce'] = $nonce
2326 foreach ($remotefiles as $file => $services) {
2327 if (!preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?(\.(zip|gz|gz\.crypt))?$/i', $file, $matches)) continue;
2328 $nonce = $matches[2];
2329 $type = $matches[3];
2330 if ('db' == $type) {
2331 $index = 0;
2332 $type .= empty($matches[4]) ? $matches[4] : '';
2333 } else {
2334 $index = (empty($matches[4])) ? '0' : (max((int)$matches[4]-1,0));
2335 }
2336 $itext = ($index == 0) ? '' : $index;
2337 $btime2 = strtotime($matches[1]);
2338 $btime = (!empty($gmt_offset)) ? $btime2 - $gmt_offset*3600 : $btime2;
2339
2340 if (isset($known_nonces[$nonce])) $btime = $known_nonces[$nonce];
2341 if ($btime <= 100) continue;
2342 # Remember that at this point, we already know that the file is not known about locally
2343 if (isset($backup_history[$btime])) {
2344 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']))) {
2345 $changes = true;
2346 $backup_history[$btime]['service'] = $services;
2347 $backup_history[$btime]['nonce'] = $nonce;
2348 }
2349 if (!isset($backup_history[$btime][$type][$index])) {
2350 $changes = true;
2351 $backup_history[$btime][$type][$index] = $file;
2352 $backup_history[$btime]['nonce'] = $nonce;
2353 if (!empty($remotesizes[$file])) $backup_history[$btime][$type.$itext.'-size'] = $remotesizes[$file];
2354 }
2355 } else {
2356 $changes = true;
2357 $backup_history[$btime]['service'] = $services;
2358 $backup_history[$btime][$type][$index] = $file;
2359 $backup_history[$btime]['nonce'] = $nonce;
2360 if (!empty($remotesizes[$file])) $backup_history[$btime][$type.$itext.'-size'] = $remotesizes[$file];
2361 $backup_history[$btime]['native'] = false;
2362 $messages['nonnative'] = array(
2363 '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'),
2364 'code' => 'nonnative',
2365 'desc' => '',
2366 'method' => ''
2367 );
2368 }
2369
2370 }
2371 }
2372
2373 if ($changes) UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history);
2374
2375 return $messages;
2376
2377 }
2378
2379 private function backup_finish($cancel_event, $do_cleanup, $allow_email, $resumption_no, $force_abort = false) {
2380
2381 if (!empty($this->semaphore)) $this->semaphore->unlock();
2382
2383 $delete_jobdata = false;
2384
2385 // 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)
2386
2387 // 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.
2388 if (0 == $this->error_count() || $force_abort) {
2389 if ($do_cleanup) {
2390 $this->log("There were no errors in the uploads, so the 'resume' event ($cancel_event) is being unscheduled");
2391 # 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)
2392 $this->jobdata_set('jobstatus', 'finished');
2393 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
2394 # 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
2395 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+1, $this->nonce));
2396 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+2, $this->nonce));
2397 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+3, $this->nonce));
2398 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+4, $this->nonce));
2399 $delete_jobdata = true;
2400 }
2401 } else {
2402 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
2403 $this->jobdata_set('jobstatus', 'resumingforerrors');
2404 # 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.
2405 if (isset($this->error_count_before_cloud_backup) && 0 == $resumption_no && 0 === $this->error_count_before_cloud_backup) {
2406 $this->reschedule(60);
2407 }
2408 }
2409
2410 // Send the results email if appropriate, which means:
2411 // - The caller allowed it (which is not the case in an 'empty' run)
2412 // - And: An email address was set (which must be so in email mode)
2413 // And one of:
2414 // - Debug mode
2415 // - There were no errors (which means we completed and so this is the final run - time for the final report)
2416 // - It was the tenth resumption; everything failed
2417
2418 $send_an_email = false;
2419 # Save the jobdata's state for the reporting - because it might get changed (e.g. incremental backup is scheduled)
2420 $jobdata_as_was = $this->jobdata;
2421
2422 // Make sure that the final status is shown
2423 if ($force_abort) {
2424 $send_an_email = true;
2425 $final_message = __('The backup was aborted by the user', 'updraftplus');
2426 } elseif (0 == $this->error_count()) {
2427 $send_an_email = true;
2428 $service = $this->jobdata_get('service');
2429 $remote_sent = (!empty($service) && ((is_array($service) && in_array('remotesend', $service)) || 'remotesend' === $service)) ? true : false;
2430 if (0 == $this->error_count('warning')) {
2431 $final_message = __('The backup apparently succeeded and is now complete', 'updraftplus');
2432 # 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
2433 if ('The backup apparently succeeded and is now complete' != $final_message) {
2434 $this->log('The backup apparently succeeded and is now complete');
2435 }
2436 } else {
2437 $final_message = __('The backup apparently succeeded (with warnings) and is now complete','updraftplus');
2438 if ('The backup apparently succeeded (with warnings) and is now complete' != $final_message) {
2439 $this->log('The backup apparently succeeded (with warnings) and is now complete');
2440 }
2441 }
2442 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');
2443 if ($do_cleanup) $delete_jobdata = apply_filters('updraftplus_backup_complete', $delete_jobdata);
2444 } elseif (false == $this->newresumption_scheduled) {
2445 $send_an_email = true;
2446 $final_message = __('The backup attempt has finished, apparently unsuccessfully', 'updraftplus');
2447 } else {
2448 // There are errors, but a resumption will be attempted
2449 $final_message = __('The backup has not finished; a resumption is scheduled', 'updraftplus');
2450 }
2451
2452 // Now over-ride the decision to send an email, if needed
2453 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
2454 $send_an_email = true;
2455 $this->log("An email has been scheduled for this job, because we are in debug mode");
2456 }
2457
2458 $email = UpdraftPlus_Options::get_updraft_option('updraft_email');
2459
2460 // If there's no email address, or the set was empty, that is the final over-ride: don't send
2461 if (!$allow_email) {
2462 $send_an_email = false;
2463 $this->log("No email will be sent - this backup set was empty.");
2464 } elseif (empty($email)) {
2465 $send_an_email = false;
2466 $this->log("No email will/can be sent - the user has not configured an email address.");
2467 }
2468
2469 global $updraftplus_backup;
2470 if ($force_abort) $jobdata_as_was['aborted'] = true;
2471 if ($send_an_email) $updraftplus_backup->send_results_email($final_message, $jobdata_as_was);
2472
2473 # Make sure this is the final message logged (so it remains on the dashboard)
2474 $this->log($final_message);
2475
2476 @fclose($this->logfile_handle);
2477 $this->logfile_handle = null;
2478
2479 // This is left until last for the benefit of the front-end UI, which then gets maximum chance to display the 'finished' status
2480 if ($delete_jobdata) delete_site_option('updraft_jobdata_'.$this->nonce);
2481
2482 }
2483
2484 public function error_count($level = 'error') {
2485 $count = 0;
2486 foreach ($this->errors as $err) {
2487 if (('error' == $level && (is_string($err) || is_wp_error($err))) || (is_array($err) && $level == $err['level']) ) { $count++; }
2488 }
2489 return $count;
2490 }
2491
2492 public function list_errors() {
2493 echo '<ul style="list-style: disc inside;">';
2494 foreach ($this->errors as $err) {
2495 if (is_wp_error($err)) {
2496 foreach ($err->get_error_messages() as $msg) {
2497 echo '<li>'.htmlspecialchars($msg).'<li>';
2498 }
2499 } elseif (is_array($err) && 'error' == $err['level']) {
2500 echo "<li>".htmlspecialchars($err['message'])."</li>";
2501 } elseif (is_string($err)) {
2502 echo "<li>".htmlspecialchars($err)."</li>";
2503 } else {
2504 print "<li>".print_r($err,true)."</li>";
2505 }
2506 }
2507 echo '</ul>';
2508 }
2509
2510 private function save_last_backup($backup_array) {
2511 $success = ($this->error_count() == 0) ? 1 : 0;
2512 $last_backup = apply_filters('updraftplus_save_last_backup', array(
2513 'backup_time' => $this->backup_time,
2514 'backup_array' => $backup_array,
2515 'success' => $success,
2516 'errors' => $this->errors,
2517 'backup_nonce' => $this->nonce
2518 ));
2519 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup, false);
2520 }
2521
2522 # $handle must be either false or a WPDB class (or extension thereof). Other options are not yet fully supported.
2523 public function check_db_connection($handle = false, $logit = false, $reschedule = false) {
2524
2525 $type = false;
2526 if (false === $handle || is_a($handle, 'wpdb')) {
2527 $type='wpdb';
2528 } elseif (is_resource($handle)) {
2529 # Expected: string(10) "mysql link"
2530 $type=get_resource_type($handle);
2531 } elseif (is_object($handle) && is_a($handle, 'mysqli')) {
2532 $type='mysqli';
2533 }
2534
2535 if (false === $type) return -1;
2536
2537 $db_connected = -1;
2538
2539 if ('mysql link' == $type || 'mysqli' == $type) {
2540 if ('mysql link' == $type && @mysql_ping($handle)) return true;
2541 if ('mysqli' == $type && @mysqli_ping($handle)) return true;
2542
2543 for ( $tries = 1; $tries <= 5; $tries++ ) {
2544 # to do, if ever needed
2545 // if ( $this->db_connect( false ) ) return true;
2546 // sleep( 1 );
2547 }
2548
2549 } elseif ('wpdb' == $type) {
2550 if (false === $handle || (is_object($handle) && 'wpdb' == get_class($handle))) {
2551 global $wpdb;
2552 $handle = $wpdb;
2553 }
2554 if (method_exists($handle, 'check_connection')) {
2555 if (!$handle->check_connection(false)) {
2556 if ($logit) $this->log("The database went away, and could not be reconnected to");
2557 # Almost certainly a no-op
2558 if ($reschedule) $this->reschedule(60);
2559 $db_connected = false;
2560 } else {
2561 $db_connected = true;
2562 }
2563 }
2564 }
2565
2566 return $db_connected;
2567
2568 }
2569
2570 // This should be called whenever a file is successfully uploaded
2571 public function uploaded_file($file, $force = false) {
2572
2573 global $updraftplus_backup;
2574
2575 $db_connected = $this->check_db_connection(false, true, true);
2576
2577 $service = (empty($updraftplus_backup->current_service)) ? '' : $updraftplus_backup->current_service;
2578 $shash = $service.'-'.md5($file);
2579
2580 $this->jobdata_set("uploaded_".$shash, 'yes');
2581
2582 if ($force || !empty($updraftplus_backup->last_service)) {
2583 $hash = md5($file);
2584 $this->log("Recording as successfully uploaded: $file ($hash)");
2585 $this->jobdata_set('uploaded_lastreset', $this->current_resumption);
2586 $this->jobdata_set("uploaded_".$hash, 'yes');
2587 } else {
2588 $this->log("Recording as successfully uploaded: $file (".$updraftplus_backup->current_service.", more services to follow)");
2589 }
2590
2591 $upload_status = $this->jobdata_get('uploading_substatus');
2592 if (is_array($upload_status) && isset($upload_status['i'])) {
2593 $upload_status['i']++;
2594 $upload_status['p']=0;
2595 $this->jobdata_set('uploading_substatus', $upload_status);
2596 }
2597
2598 # 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
2599 if (false === $db_connected) {
2600 $this->record_still_alive();
2601 die;
2602 }
2603
2604 // Delete local files immediately if the option is set
2605 // Where we are only backing up locally, only the "prune" function should do deleting
2606 $service = $this->jobdata_get('service');
2607 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')))) {
2608 $this->delete_local($file);
2609 }
2610 }
2611
2612 public function is_uploaded($file, $service = '') {
2613 $hash = $service.(('' == $service) ? '' : '-').md5($file);
2614 return ($this->jobdata_get("uploaded_$hash") === "yes") ? true : false;
2615 }
2616
2617 private function delete_local($file) {
2618 $log = "Deleting local file: $file: ";
2619 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local')) {
2620 $fullpath = $this->backups_dir_location().'/'.$file;
2621 $deleted = unlink($fullpath);
2622 $this->log($log.(($deleted) ? 'OK' : 'failed'));
2623 return $deleted;
2624 } else {
2625 $this->log($log."skipped: user has unchecked updraft_delete_local option");
2626 }
2627 return true;
2628 }
2629
2630 // This function is not needed for backup success, according to the design, but it helps with efficient scheduling
2631 private function reschedule_if_needed() {
2632 // If nothing is scheduled, then return
2633 if (empty($this->newresumption_scheduled)) return;
2634 $time_now = time();
2635 $time_away = $this->newresumption_scheduled - $time_now;
2636 // 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)
2637 if ($time_away >1 && $time_away <= 45) {
2638 $this->log('The scheduled resumption is within 45 seconds - will reschedule');
2639 // Push 45 seconds into the future
2640 // $this->reschedule(60);
2641 // Increase interval generally by 45 seconds, on the assumption that our prior estimates were innaccurate (i.e. not just 45 seconds *this* time)
2642 $this->increase_resume_and_reschedule(45);
2643 }
2644 }
2645
2646 public function reschedule($how_far_ahead) {
2647 // Reschedule - remove presently scheduled event
2648 $next_resumption = $this->current_resumption + 1;
2649 wp_clear_scheduled_hook('updraft_backup_resume', array($next_resumption, $this->nonce));
2650 // Add new event
2651 # This next line may be too cautious; but until 14-Aug-2014, it was 300.
2652 # Update 20-Mar-2015 - lowered from 180
2653 if ($how_far_ahead < 120) $how_far_ahead=120;
2654 $schedule_for = time() + $how_far_ahead;
2655 $this->log("Rescheduling resumption $next_resumption: moving to $how_far_ahead seconds from now ($schedule_for)");
2656 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $this->nonce));
2657 $this->newresumption_scheduled = $schedule_for;
2658 }
2659
2660 private function increase_resume_and_reschedule($howmuch = 120, $force_schedule = false) {
2661
2662 $resume_interval = max(intval($this->jobdata_get('resume_interval')), ($howmuch === 0) ? 120 : 300);
2663
2664 if (empty($this->newresumption_scheduled) && $force_schedule) {
2665 $this->log("A new resumption will be scheduled to prevent the job ending");
2666 }
2667
2668 $new_resume = $resume_interval + $howmuch;
2669 # 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
2670 if ($this->opened_log_time > 100 && microtime(true)-$this->opened_log_time > $new_resume) {
2671 $new_resume = ceil(microtime(true)-$this->opened_log_time)+45;
2672 $howmuch = $new_resume-$resume_interval;
2673 }
2674
2675 # 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.
2676 # Actually, let's not try this yet. I think it is safe, but think there is a more conservative solution available.
2677 #$how_far_ahead = min($new_resume, 600);
2678 $how_far_ahead = $new_resume;
2679 # If it is very long-running, then that would normally be known soon.
2680 # 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.
2681 if (1 >= $this->current_resumption && $new_resume > 720) $how_far_ahead = 600;
2682
2683 if (!empty($this->newresumption_scheduled) || $force_schedule) $this->reschedule($how_far_ahead);
2684 $this->jobdata_set('resume_interval', $new_resume);
2685
2686 $this->log("To decrease the likelihood of overlaps, increasing resumption interval to: $resume_interval + $howmuch = $new_resume");
2687 }
2688
2689 // For detecting another run, and aborting if one was found
2690 public function check_recent_modification($file) {
2691 if (file_exists($file)) {
2692 $time_mod = (int)@filemtime($file);
2693 $time_now = time();
2694 if ($time_mod>100 && ($time_now-$time_mod)<30) {
2695 $this->terminate_due_to_activity($file, $time_now, $time_mod);
2696 }
2697 }
2698 }
2699
2700 public function get_exclude($whichone) {
2701 if ('uploads' == $whichone) {
2702 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE));
2703 } elseif ('others' == $whichone) {
2704 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
2705 } else {
2706 $exclude = apply_filters('updraftplus_include_'.$whichone.'_exclude', array());
2707 }
2708 return (empty($exclude) || !is_array($exclude)) ? array() : $exclude;
2709 }
2710
2711 public function really_is_writable($dir) {
2712 // Suppress warnings, since if the user is dumping warnings to screen, then invalid JavaScript results and the screen breaks.
2713 if (!@is_writable($dir)) return false;
2714 // Found a case - GoDaddy server, Windows, PHP 5.2.17 - where is_writable returned true, but writing failed
2715 $rand_file = "$dir/test-".md5(rand().time()).".txt";
2716 while (file_exists($rand_file)) {
2717 $rand_file = "$dir/test-".md5(rand().time()).".txt";
2718 }
2719 $ret = @file_put_contents($rand_file, 'testing...');
2720 @unlink($rand_file);
2721 return ($ret > 0);
2722 }
2723
2724 public function wp_upload_dir() {
2725 if (is_multisite()) {
2726 global $current_site;
2727 switch_to_blog($current_site->blog_id);
2728 }
2729
2730 $wp_upload_dir = wp_upload_dir();
2731
2732 if (is_multisite()) restore_current_blog();
2733
2734 return $wp_upload_dir;
2735 }
2736
2737 public function backup_uploads_dirlist($logit = false) {
2738 # Create an array of directories to be skipped
2739 # Make the values into the keys
2740 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
2741 if ($logit) $this->log("Exclusion option setting (uploads): ".$exclude);
2742 $skip = array_flip(preg_split("/,/", $exclude));
2743 $wp_upload_dir = $this->wp_upload_dir();
2744 $uploads_dir = $wp_upload_dir['basedir'];
2745 return $this->compile_folder_list_for_backup($uploads_dir, array(), $skip);
2746 }
2747
2748 public function backup_others_dirlist($logit = false) {
2749 # Create an array of directories to be skipped
2750 # Make the values into the keys
2751 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE);
2752 if ($logit) $this->log("Exclusion option setting (others): ".$exclude);
2753 $skip = array_flip(preg_split("/,/", $exclude));
2754 $file_entities = $this->get_backupable_file_entities(false);
2755
2756 # Keys = directory names to avoid; values = the label for that directory (used only in log files)
2757 #$avoid_these_dirs = array_flip($file_entities);
2758 $avoid_these_dirs = array();
2759 foreach ($file_entities as $type => $dirs) {
2760 if (is_string($dirs)) {
2761 $avoid_these_dirs[$dirs] = $type;
2762 } elseif (is_array($dirs)) {
2763 foreach ($dirs as $dir) {
2764 $avoid_these_dirs[$dir] = $type;
2765 }
2766 }
2767 }
2768 return $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $avoid_these_dirs, $skip);
2769 }
2770
2771 // Add backquotes to tables and db-names in SQL queries. Taken from phpMyAdmin.
2772 public function backquote($a_name) {
2773 if (!empty($a_name) && $a_name != '*') {
2774 if (is_array($a_name)) {
2775 $result = array();
2776 reset($a_name);
2777 while(list($key, $val) = each($a_name))
2778 $result[$key] = '`'.$val.'`';
2779 return $result;
2780 } else {
2781 return '`'.$a_name.'`';
2782 }
2783 } else {
2784 return $a_name;
2785 }
2786 }
2787
2788 public function strip_dirslash($string) {
2789 return preg_replace('#/+(,|$)#', '$1', $string);
2790 }
2791
2792 public function remove_empties($list) {
2793 if (!is_array($list)) return $list;
2794 foreach ($list as $ind => $entry) {
2795 if (empty($entry)) unset($list[$ind]);
2796 }
2797 return $list;
2798 }
2799
2800 // 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.
2801 public function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
2802
2803 // 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.
2804
2805 $dirlist = array();
2806 $added = 0;
2807
2808 $this->log('Looking for candidates to back up in: '.$backup_from_inside_dir);
2809 $updraft_dir = $this->backups_dir_location();
2810
2811 if (is_file($backup_from_inside_dir)) {
2812 array_push($dirlist, $backup_from_inside_dir);
2813 $added++;
2814 $this->log("finding files: $backup_from_inside_dir: adding to list ($added)");
2815 } elseif ($handle = opendir($backup_from_inside_dir)) {
2816
2817 while (false !== ($entry = readdir($handle))) {
2818 // $candidate: full path; $entry = one-level
2819 $candidate = $backup_from_inside_dir.'/'.$entry;
2820 if ($entry != "." && $entry != "..") {
2821 if (isset($avoid_these_dirs[$candidate])) {
2822 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
2823 } elseif ($candidate == $updraft_dir) {
2824 $this->log("finding files: $entry: skipping: this is the updraft directory");
2825 } elseif (isset($skip_these_dirs[$entry])) {
2826 $this->log("finding files: $entry: skipping: excluded by options");
2827 } else {
2828 $add_to_list = true;
2829 // Now deal with entries in $skip_these_dirs ending in * or starting with *
2830 foreach ($skip_these_dirs as $skip => $sind) {
2831 if ('*' == substr($skip, -1, 1) && '*' == substr($skip, 0, 1) && strlen($skip) > 2) {
2832 if (strpos($entry, substr($skip, 1, strlen($skip-2))) !== false) {
2833 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2834 $add_to_list = false;
2835 }
2836 } elseif ('*' == substr($skip, -1, 1) && strlen($skip) > 1) {
2837 if (substr($entry, 0, strlen($skip)-1) == substr($skip, 0, strlen($skip)-1)) {
2838 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2839 $add_to_list = false;
2840 }
2841 } elseif ('*' == substr($skip, 0, 1) && strlen($skip) > 1) {
2842 if (strlen($entry) >= strlen($skip)-1 && substr($entry, (strlen($skip)-1)*-1) == substr($skip, 1)) {
2843 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2844 $add_to_list = false;
2845 }
2846 }
2847 }
2848 if ($add_to_list) {
2849 array_push($dirlist, $candidate);
2850 $added++;
2851 $skip_dblog = ($added > 50 && 0 != $added % 100);
2852 $this->log("finding files: $entry: adding to list ($added)", 'notice', false, $skip_dblog);
2853 }
2854 }
2855 }
2856 }
2857 @closedir($handle);
2858 } else {
2859 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
2860 $this->log(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir, 'error');
2861 }
2862
2863 return $dirlist;
2864
2865 }
2866
2867 private function save_backup_history($backup_array) {
2868 if(is_array($backup_array)) {
2869 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
2870 $backup_history = (is_array($backup_history)) ? $backup_history : array();
2871 $backup_array['nonce'] = $this->nonce;
2872 $backup_array['service'] = $this->jobdata_get('service');
2873 if ('' != ($label = $this->jobdata_get('label', ''))) $backup_array['label'] = $label;
2874 $backup_array['created_by_version'] = $this->version;
2875 $backup_array['is_multisite'] = is_multisite() ? true : false;
2876 $remotesend_info = $this->jobdata_get('remotesend_info');
2877 if (is_array($remotesend_info) && !empty($remotesend_info['url'])) $backup_array['remotesend_url'] = $remotesend_info['url'];
2878 if (false != ($autobackup = $this->jobdata_get('is_autobackup', false))) $backup_array['autobackup'] = true;
2879 $backup_history[$this->backup_time] = $backup_array;
2880 UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history, false);
2881 } else {
2882 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
2883 $this->log(__('Could not save backup history because we have no backup array. Backup probably failed.','updraftplus'), 'error');
2884 }
2885 }
2886
2887 public function is_db_encrypted($file) {
2888 return preg_match('/\.crypt$/i', $file);
2889 }
2890
2891 public function get_backup_history($timestamp = false) {
2892 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
2893 // The line below actually *introduces* a race condition
2894 // global $wpdb;
2895 // $backup_history = @unserialize($wpdb->get_var($wpdb->prepare("SELECT option_value from $wpdb->options WHERE option_name='updraft_backup_history'")));
2896 if (is_array($backup_history)) {
2897 krsort($backup_history); //reverse sort so earliest backup is last on the array. Then we can array_pop.
2898 } else {
2899 $backup_history = array();
2900 }
2901 if (!$timestamp) return $backup_history;
2902 return (isset($backup_history[$timestamp])) ? $backup_history[$timestamp] : array();
2903 }
2904
2905 public function terminate_due_to_activity($file, $time_now, $time_mod, $increase_resumption = true) {
2906 # We check-in, to avoid 'no check in last time!' detectors firing
2907 $this->record_still_alive();
2908 $file_size = file_exists($file) ? round(filesize($file)/1024,1). 'Kb' : 'n/a';
2909 $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.");
2910 $increase_by = ($increase_resumption) ? 120 : 0;
2911 $this->increase_resume_and_reschedule($increase_by, true);
2912 if (!defined('UPDRAFTPLUS_ALLOW_RECENT_ACTIVITY') || true != UPDRAFTPLUS_ALLOW_RECENT_ACTIVITY) die;
2913 }
2914
2915 # Replace last occurence
2916 public function str_lreplace($search, $replace, $subject) {
2917 $pos = strrpos($subject, $search);
2918 if($pos !== false) $subject = substr_replace($subject, $replace, $pos, strlen($search));
2919 return $subject;
2920 }
2921
2922 public function str_replace_once($needle, $replace, $haystack) {
2923 $pos = strpos($haystack, $needle);
2924 return ($pos !== false) ? substr_replace($haystack,$replace,$pos,strlen($needle)) : $haystack;
2925 }
2926
2927 /*
2928 This function is both the backup scheduler and a filter callback for saving the option.
2929 It is called in the register_setting for the updraft_interval, which means when the
2930 admin settings are saved it is called.
2931 */
2932 public function schedule_backup($interval) {
2933 $previous_time = wp_next_scheduled('updraft_backup');
2934
2935 // Clear schedule so that we don't stack up scheduled backups
2936 wp_clear_scheduled_hook('updraft_backup');
2937 if ('manual' == $interval) return 'manual';
2938
2939 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval');
2940
2941 $valid_schedules = wp_get_schedules();
2942 if (empty($valid_schedules[$interval])) $interval = 'daily';
2943
2944 // 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.
2945 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
2946 $first_time = apply_filters('updraftplus_schedule_firsttime_files', $default_time);
2947
2948 wp_schedule_event($first_time, $interval, 'updraft_backup');
2949
2950 return $interval;
2951 }
2952
2953 public function schedule_backup_database($interval) {
2954 $previous_time = wp_next_scheduled('updraft_backup_database');
2955
2956 // Clear schedule so that we don't stack up scheduled backups
2957 wp_clear_scheduled_hook('updraft_backup_database');
2958 if ('manual' == $interval) return 'manual';
2959
2960 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
2961
2962 $valid_schedules = wp_get_schedules();
2963 if (empty($valid_schedules[$interval])) $interval = 'daily';
2964
2965 // 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.
2966 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
2967
2968 $first_time = apply_filters('updraftplus_schedule_firsttime_db', $default_time);
2969 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
2970
2971 return $interval;
2972 }
2973
2974 // Acts as a WordPress options filter
2975 public function onedrive_checkchange($onedrive) {
2976 $opts = UpdraftPlus_Options::get_updraft_option('updraft_onedrive');
2977 if (!is_array($opts)) $opts = array();
2978 if (!is_array($onedrive)) return $opts;
2979 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
2980 if (!empty($opts['token']) && $old_client_id != $onedrive['clientid']) {
2981 unset($opts['token']);
2982 unset($opts['tokensecret']);
2983 unset($opts['ownername']);
2984 }
2985 foreach ($onedrive as $key => $value) {
2986 if ('folder' == $key) $value = trim(str_replace('\\', '/', $value), '/');
2987 $opts[$key] = ('clientid' == $key || 'secret' == $key) ? trim($value) : $value;
2988 }
2989 return $opts;
2990 }
2991
2992 // This is a WordPress options filter
2993 public function azure_checkchange($azure) {
2994 $opts = UpdraftPlus_Options::get_updraft_option('updraft_azure');
2995 if (!is_array($opts)) $opts = array();
2996 if (!is_array($azure)) return $opts;
2997 foreach ($azure as $key => $value) {
2998 if ('folder' == $key) $value = trim(str_replace('\\', '/', $value), '/');
2999 // Only lower-case containers are permitted - enforce this
3000 if ('container' == $key) $value = strtolower($value);
3001 $opts[$key] = ('key' == $key || 'account_name' == $key) ? trim($value) : $value;
3002 // Convert one likely misunderstanding of the format to enter the account name in
3003 if ('account_name' == $key && preg_match('#^https?://(.*)\.blob\.core\.windows#i', $opts['account_name'], $matches)) {
3004 $opts['account_name'] = $matches[1];
3005 }
3006 }
3007 return $opts;
3008 }
3009
3010
3011 // Acts as a WordPress options filter
3012 public function googledrive_checkchange($google) {
3013 $opts = UpdraftPlus_Options::get_updraft_option('updraft_googledrive');
3014 if (!is_array($google)) return $opts;
3015 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
3016 if (!empty($opts['token']) && $old_client_id != $google['clientid']) {
3017 require_once(UPDRAFTPLUS_DIR.'/methods/googledrive.php');
3018 add_action('http_request_args', array($this, 'modify_http_options'));
3019 UpdraftPlus_BackupModule_googledrive::gdrive_auth_revoke(false);
3020 remove_action('http_request_args', array($this, 'modify_http_options'));
3021 $google['token'] = '';
3022 unset($opts['ownername']);
3023 }
3024 foreach ($google as $key => $value) {
3025 // Trim spaces - I got support requests from users who didn't spot the spaces they introduced when copy/pasting
3026 $opts[$key] = ('clientid' == $key || 'secret' == $key) ? trim($value) : $value;
3027 }
3028 if (isset($opts['folder'])) {
3029 $opts['folder'] = apply_filters('updraftplus_options_googledrive_foldername', 'UpdraftPlus', $opts['folder']);
3030 unset($opts['parentid']);
3031 }
3032 return $opts;
3033 }
3034
3035 // Acts as a WordPress options filter
3036 public function googlecloud_checkchange($google) {
3037 $opts = UpdraftPlus_Options::get_updraft_option('updraft_googlecloud');
3038 if (!is_array($google)) return $opts;
3039
3040 $old_token = (empty($opts['token'])) ? '' : $opts['token'];
3041 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
3042 $old_client_secret = (empty($opts['secret'])) ? '' : $opts['secret'];
3043
3044 if($old_client_id == $google['clientid'] && $old_client_secret == $google['secret']){
3045 $google['token'] = $old_token;
3046 }
3047 if (!empty($opts['token']) && $old_client_id != $google['clientid']) {
3048 add_action('http_request_args', array($this, 'modify_http_options'));
3049 UpdraftPlus_Addons_RemoteStorage_googlecloud::gcloud_auth_revoke(false);
3050 remove_action('http_request_args', array($this, 'modify_http_options'));
3051 $google['token'] = '';
3052 unset($opts['ownername']);
3053 }
3054 foreach ($google as $key => $value) {
3055 // Trim spaces - I got support requests from users who didn't spot the spaces they introduced when copy/pasting
3056 $opts[$key] = ('clientid' == $key || 'secret' == $key) ? trim($value) : $value;
3057 if ($key == 'bucket_location') $opts[$key] = trim(strtolower($value));
3058 }
3059
3060 return $google;
3061 }
3062
3063 public function ftp_sanitise($ftp) {
3064 if (is_array($ftp) && !empty($ftp['host']) && preg_match('#ftp(es|s)?://(.*)#i', $ftp['host'], $matches)) {
3065 $ftp['host'] = untrailingslashit($matches[2]);
3066 }
3067 return $ftp;
3068 }
3069
3070 public function s3_sanitise($s3) {
3071 if (is_array($s3) && !empty($s3['path']) && '/' == substr($s3['path'], 0, 1)) {
3072 $s3['path'] = substr($s3['path'], 1);
3073 }
3074 return $s3;
3075 }
3076
3077 // Acts as a WordPress options filter
3078 public function bitcasa_checkchange($bitcasa) {
3079 $opts = UpdraftPlus_Options::get_updraft_option('updraft_bitcasa');
3080 if (!is_array($opts)) $opts = array();
3081 if (!is_array($bitcasa)) return $opts;
3082 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
3083 if (!empty($opts['token']) && $old_client_id != $bitcasa['clientid']) {
3084 unset($opts['token']);
3085 unset($opts['ownername']);
3086 }
3087 foreach ($bitcasa as $key => $value) { $opts[$key] = $value; }
3088 return $opts;
3089 }
3090
3091 // Acts as a WordPress options filter
3092 public function copycom_checkchange($copycom) {
3093 $opts = UpdraftPlus_Options::get_updraft_option('updraft_copycom');
3094 if (!is_array($opts)) $opts = array();
3095 if (!is_array($copycom)) return $opts;
3096 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
3097 if (!empty($opts['token']) && $old_client_id != $copycom['clientid']) {
3098 unset($opts['token']);
3099 unset($opts['tokensecret']);
3100 unset($opts['ownername']);
3101 }
3102 foreach ($copycom as $key => $value) {
3103 if ('clientid' == $key || 'secret' == $key) {
3104 $opts[$key] = trim($value);
3105 } else {
3106 $opts[$key] = $value;
3107 }
3108 }
3109 return $opts;
3110 }
3111
3112 // Acts as a WordPress options filter
3113 public function dropbox_checkchange($dropbox) {
3114 $opts = UpdraftPlus_Options::get_updraft_option('updraft_dropbox');
3115 if (!is_array($opts)) $opts = array();
3116 if (!is_array($dropbox)) return $opts;
3117 foreach ($dropbox as $key => $value) { $opts[$key] = $value; }
3118 if (!empty($opts['folder']) && preg_match('#^https?://(www.)dropbox\.com/home/Apps/UpdraftPlus([^/]*)/(.*)$#i', $opts['folder'], $matches)) $opts['folder'] = $matches[3];
3119 return $opts;
3120 }
3121
3122 public function remove_local_directory($dir, $contents_only = false) {
3123 // PHP 5.3+ only
3124 //foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
3125 // $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
3126 //}
3127 //return rmdir($dir);
3128
3129 if ($handle = @opendir($dir)) {
3130 while (false !== ($entry = readdir($handle))) {
3131 if ('.' !== $entry && '..' !== $entry) {
3132 if (is_dir($dir.'/'.$entry)) {
3133 $this->remove_local_directory($dir.'/'.$entry, false);
3134 } else {
3135 @unlink($dir.'/'.$entry);
3136 }
3137 }
3138 }
3139 @closedir($handle);
3140 }
3141
3142 return ($contents_only) ? true : rmdir($dir);
3143 }
3144
3145 // Returns without any trailing slash
3146 public function backups_dir_location() {
3147
3148 if (!empty($this->backup_dir)) return $this->backup_dir;
3149
3150 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
3151 # 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.
3152 if (preg_match('/^wp-content\/(.*)$/', $updraft_dir, $matches) && ABSPATH.'wp-content' === WP_CONTENT_DIR) {
3153 UpdraftPlus_Options::update_updraft_option('updraft_dir', $matches[1]);
3154 $updraft_dir = WP_CONTENT_DIR.'/'.$matches[1];
3155 }
3156 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
3157 $updraft_dir = ($updraft_dir) ? $updraft_dir : $default_backup_dir;
3158
3159 // Do a test for a relative path
3160 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
3161 # Legacy - file paths stored related to ABSPATH
3162 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')) {
3163 $updraft_dir = ABSPATH.$updraft_dir;
3164 } else {
3165 # File paths stored relative to WP_CONTENT_DIR
3166 $updraft_dir = trailingslashit(WP_CONTENT_DIR).$updraft_dir;
3167 }
3168 }
3169
3170 // Check for the existence of the dir and prevent enumeration
3171 // index.php is for a sanity check - make sure that we're not somewhere unexpected
3172 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')) {
3173 @mkdir($updraft_dir, 0775, true);
3174 @file_put_contents($updraft_dir.'/index.html',"<html><body><a href=\"https://updraftplus.com\">WordPress backups by UpdraftPlus</a></body></html>");
3175 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess','deny from all');
3176 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");
3177 }
3178
3179 $this->backup_dir = $updraft_dir;
3180
3181 return $updraft_dir;
3182 }
3183
3184 private function spool_crypted_file($fullpath, $encryption) {
3185 if ('' == $encryption) $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
3186 if ('' == $encryption) {
3187 header('Content-type: text/plain');
3188 _e("Decryption failed. The database file is encrypted, but you have no encryption key entered.", 'updraftplus');
3189 $this->log('Decryption of database failed: the database file is encrypted, but you have no encryption key entered.', 'error');
3190 } else {
3191 $ciphertext = $this->decrypt($fullpath, $encryption);
3192 if ($ciphertext) {
3193 header('Content-type: application/x-gzip');
3194 header("Content-Disposition: attachment; filename=\"".substr(basename($fullpath), 0, -6)."\";");
3195 header("Content-Length: ".strlen($ciphertext));
3196 print $ciphertext;
3197 } else {
3198 header('Content-type: text/plain');
3199 echo __("Decryption failed. The most likely cause is that you used the wrong key.",'updraftplus')." ".__('The decryption key used:','updraftplus').' '.$encryption;
3200
3201 }
3202 }
3203 }
3204
3205 public function get_mime_type_from_filename($filename, $allow_gzip = true) {
3206 if ('.zip' == substr($filename, -4, 4)) {
3207 return 'application/zip';
3208 } elseif ('.tar' == substr($filename, -4, 4)) {
3209 return 'application/x-tar';
3210 } elseif ('.tar.gz' == substr($filename, -7, 7)) {
3211 return 'application/x-tgz';
3212 } elseif ('.tar.bz2' == substr($filename, -8, 8)) {
3213 return 'application/x-bzip-compressed-tar';
3214 } elseif ($allow_gzip && '.gz' == substr($filename, -3, 3)) {
3215 // 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)
3216 return 'application/x-gzip';
3217 } else {
3218 return 'application/octet-stream';
3219 }
3220 }
3221
3222 public function spool_file($type, $fullpath, $encryption = '') {
3223 @set_time_limit(900);
3224
3225 if (file_exists($fullpath)) {
3226
3227 // Prevent any debug output
3228 // Don't enable this line - it causes 500 HTTP errors in some cases/hosts on some large files, for unknown reason
3229 //@ini_set('display_errors', '0');
3230
3231 $spooled = false;
3232 if ('.crypt' == substr($fullpath, -6, 6)) {
3233 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
3234 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
3235 $this->spool_crypted_file($fullpath, (string)$encryption);
3236 return;
3237 }
3238
3239 $content_type = $this->get_mime_type_from_filename($fullpath, false);
3240
3241 require_once(UPDRAFTPLUS_DIR.'/includes/class-partialfileservlet.php');
3242
3243 //Prevent the file being read into memory
3244 @ob_end_flush();
3245
3246 if (isset($_SERVER['HTTP_RANGE'])) {
3247 $range_header = trim($_SERVER['HTTP_RANGE']);
3248 } elseif (function_exists('apache_request_headers')) {
3249 foreach (apache_request_headers() as $name => $value) {
3250 if (strtoupper($name) === 'RANGE') {
3251 $range_header = trim($value);
3252 }
3253 }
3254 }
3255
3256 if (empty($range_header)) {
3257 header("Content-Length: ".filesize($fullpath));
3258 header("Content-type: $content_type");
3259 header("Content-Disposition: attachment; filename=\"".basename($fullpath)."\";");
3260 readfile($fullpath);
3261 return;
3262 }
3263
3264 try {
3265 $range_header = UpdraftPlus_RangeHeader::createFromHeaderString($range_header);
3266 $servlet = new UpdraftPlus_PartialFileServlet($range_header);
3267 $servlet->sendFile($fullpath, $content_type);
3268 } catch (UpdraftPlus_InvalidRangeHeaderException $e) {
3269 header("HTTP/1.1 400 Bad Request");
3270 error_log("UpdraftPlus: UpdraftPlus_InvalidRangeHeaderException: ".$e->getMessage());
3271 } catch (UpdraftPlus_UnsatisfiableRangeException $e) {
3272 header("HTTP/1.1 416 Range Not Satisfiable");
3273 } catch (UpdraftPlus_NonExistentFileException $e) {
3274 header("HTTP/1.1 404 Not Found");
3275 } catch (UpdraftPlus_UnreadableFileException $e) {
3276 header("HTTP/1.1 500 Internal Server Error");
3277 }
3278
3279 } else {
3280 echo __('File not found', 'updraftplus');
3281 }
3282 }
3283
3284 public function retain_range($input) {
3285 $input = (int)$input;
3286 return ($input > 0) ? min($input, 9999) : 1;
3287 }
3288
3289 public function replace_http_with_webdav($input) {
3290 if (!empty($input['url']) && 'http' == substr($input['url'], 0, 4)) $input['url'] = 'webdav'.substr($input['url'], 4);
3291 return $input;
3292 }
3293
3294 public function just_one_email($input, $required = false) {
3295 $x = $this->just_one($input, 'saveemails', (empty($input) && false === $required) ? '' : get_bloginfo('admin_email'));
3296 if (is_array($x)) {
3297 foreach ($x as $ind => $val) {
3298 if (empty($val)) unset($x[$ind]);
3299 }
3300 if (empty($x)) $x = '';
3301 }
3302 return $x;
3303 }
3304
3305 public function just_one($input, $filter = 'savestorage', $rinput = false) {
3306 $oinput = $input;
3307 if (false === $rinput) $rinput = (is_array($input)) ? array_pop($input) : $input;
3308 if (is_string($rinput) && false !== strpos($rinput, ',')) $rinput = substr($rinput, 0, strpos($rinput, ','));
3309 return apply_filters('updraftplus_'.$filter, $rinput, $oinput);
3310 }
3311
3312 public function memory_check_current($memory_limit = false) {
3313 # Returns in megabytes
3314 if ($memory_limit == false) $memory_limit = ini_get('memory_limit');
3315 $memory_limit = rtrim($memory_limit);
3316 $memory_unit = $memory_limit[strlen($memory_limit)-1];
3317 if ((int)$memory_unit == 0 && $memory_unit !== '0') {
3318 $memory_limit = substr($memory_limit,0,strlen($memory_limit)-1);
3319 } else {
3320 $memory_unit = '';
3321 }
3322 switch($memory_unit) {
3323 case '':
3324 $memory_limit = floor($memory_limit/1048576);
3325 break;
3326 case 'K':
3327 case 'k':
3328 $memory_limit = floor($memory_limit/1024);
3329 break;
3330 case 'G':
3331 $memory_limit = $memory_limit*1024;
3332 break;
3333 case 'M':
3334 //assumed size, no change needed
3335 break;
3336 }
3337 return $memory_limit;
3338 }
3339
3340 public function memory_check($memory, $check_using = false) {
3341 $memory_limit = $this->memory_check_current($check_using);
3342 return ($memory_limit >= $memory)?true:false;
3343 }
3344
3345 private function url_start($urls, $url, $https = false) {
3346 $proto = ($https) ? 'https' : 'http';
3347 return ($urls) ? "<a href=\"$proto://$url\">" : "";
3348 }
3349
3350 private function url_end($urls, $url, $https = false) {
3351 $proto = ($https) ? 'https' : 'http';
3352 return ($urls) ? '</a>' : " ($proto://$url)";
3353 }
3354
3355 public function get_updraftplus_rssfeed() {
3356 if (!function_exists('fetch_feed')) require(ABSPATH . WPINC . '/feed.php');
3357 return fetch_feed('http://feeds.feedburner.com/updraftplus/');
3358 }
3359
3360 public function get_wplang() {
3361 # See: https://core.trac.wordpress.org/changeset/29630
3362 global $wp_current_db_version;
3363 if ( $wp_current_db_version < 29630 ) {
3364 return (defined('WPLANG')) ? WPLANG : '';
3365 } else {
3366 return get_option('WPLANG', '');
3367 }
3368 }
3369
3370 public function wordshell_random_advert($urls) {
3371 if (defined('UPDRAFTPLUS_NOADS_B')) return "";
3372 $rad = rand(0, 8);
3373 switch ($rad) {
3374 case 0:
3375 return $this->url_start($urls,'updraftplus.com').__("Want more features or paid, guaranteed support? Check out UpdraftPlus.Com", 'updraftplus').$this->url_end($urls,'updraftplus.com');
3376 break;
3377 case 1:
3378 $wplang = $this->get_wplang();
3379 if (strlen($wplang)>0 && !is_file(UPDRAFTPLUS_DIR.'/languages/updraftplus-'.$wplang.
3380 '.mo')) return __('Can you translate? Want to improve UpdraftPlus for speakers of your language?','updraftplus').' '.$this->url_start($urls,'updraftplus.com/translate/')."Please go here for instructions - it is easy.".$this->url_end($urls,'updraftplus.com/translate/');
3381
3382 return __('UpdraftPlus is on social media - check us out here:','updraftplus').' '.$this->url_start($urls,'twitter.com/updraftplus', true).__('Twitter', 'updraftplus').$this->url_end($urls,'twitter.com/updraftplus', true).' - '.$this->url_start($urls,'facebook.com/updraftplus', true).__('Facebook', 'updraftplus').$this->url_end($urls,'facebook.com/updraftplus', true).' - '.$this->url_start($urls,'plus.google.com/u/0/b/112313994681166369508/112313994681166369508/about', true).__('Google+', 'updraftplus').$this->url_end($urls,'plus.google.com/u/0/b/112313994681166369508/112313994681166369508/about', true).' - '.$this->url_start($urls,'www.linkedin.com/company/updraftplus', true).__('LinkedIn', 'updraftplus').$this->url_end($urls,'www.linkedin.com/company/updraftplus', true);
3383 break;
3384 case 2:
3385 return $this->url_start($urls,'wordshell.net').__("Check out WordShell", 'updraftplus').$this->url_end($urls,'www.wordshell.net')." - ".__('manage WordPress from the command line - huge time-saver', 'updraftplus');
3386 break;
3387 case 3:
3388 return __('Like UpdraftPlus and can spare one minute?','updraftplus').$this->url_start($urls,'wordpress.org/support/view/plugin-reviews/updraftplus#postform').' '.__('Please help UpdraftPlus by giving a positive review at wordpress.org','updraftplus').$this->url_end($urls,'wordpress.org/support/view/plugin-reviews/updraftplus#postform');
3389 break;
3390 case 4:
3391 return $this->url_start($urls,'updraftplus.com/newsletter-signup', true).__("Follow this link to sign up for the UpdraftPlus newsletter.", 'updraftplus').$this->url_end($urls,'updraftplus.com/newsletter-signup', true);
3392 break;
3393 case 5:
3394 if (!defined('UPDRAFTPLUS_NOADS_B')) {
3395 return $this->url_start($urls,'updraftplus.com').__("Need even more features and support? Check out UpdraftPlus Premium",'updraftplus').$this->url_end($urls,'updraftplus.com');
3396 } else {
3397 return "Thanks for being an UpdraftPlus premium user. Keep visiting ".$this->url_start($urls,'updraftplus.com')."updraftplus.com".$this->url_end($urls,'updraftplus.com')." to see what's going on.";
3398 }
3399 break;
3400 case 6:
3401 // return "Need custom WordPress services from experts (including bespoke development)?".$this->url_start($urls,'www.simbahosting.co.uk/s3/products-and-services/wordpress-experts/')." Get them from the creators of UpdraftPlus.".$this->url_end($urls,'www.simbahosting.co.uk/s3/products-and-services/wordpress-experts/');
3402 return __("Subscribe to the UpdraftPlus blog to get up-to-date news and offers",'updraftplus')." - ".$this->url_start($urls,'updraftplus.com/news/').__("Blog link",'updraftplus').$this->url_end($urls,'updraftplus.com/news/').' - '.$this->url_start($urls,'feeds.feedburner.com/UpdraftPlus').__("RSS link",'updraftplus').$this->url_end($urls,'feeds.feedburner.com/UpdraftPlus');
3403 break;
3404 case 7:
3405 return $this->url_start($urls,'updraftplus.com').__("Check out UpdraftPlus.Com for help, add-ons and support",'updraftplus').$this->url_end($urls,'updraftplus.com');
3406 break;
3407 // case 8:
3408 // return __("Want to say thank-you for UpdraftPlus?",'updraftplus').$this->url_start($urls,'updraftplus.com/shop/', true)." ".__("Please buy our very cheap 'no adverts' add-on.",'updraftplus').$this->url_end($urls,'updraftplus.com/shop/', true);
3409 // break;
3410 case 8:
3411 return __('UpdraftPlus is on social media - check us out here:','updraftplus').' '.$this->url_start($urls,'twitter.com/updraftplus', true).__('Twitter', 'updraftplus').$this->url_end($urls,'twitter.com/updraftplus', true).' - '.$this->url_start($urls,'facebook.com/updraftplus', true).__('Facebook', 'updraftplus').$this->url_end($urls,'facebook.com/updraftplus', true).' - '.$this->url_start($urls,'plus.google.com/u/0/b/112313994681166369508/112313994681166369508/about', true).__('Google+', 'updraftplus').$this->url_end($urls,'plus.google.com/u/0/b/112313994681166369508/112313994681166369508/about', true).' - '.$this->url_start($urls,'www.linkedin.com/company/updraftplus', true).__('LinkedIn', 'updraftplus').$this->url_end($urls,'www.linkedin.com/company/updraftplus', true);
3412 break;
3413 }
3414 }
3415
3416 public function analyse_db_file($timestamp, $res, $db_file = false, $header_only = false) {
3417
3418 $mess = array(); $warn = array(); $err = array(); $info = array();
3419
3420 $wp_version = $this->get_wordpress_version();
3421 global $wpdb;
3422
3423 $updraft_dir = $this->backups_dir_location();
3424
3425 if (false === $db_file) {
3426 # 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.
3427 $this->get_max_packet_size();
3428
3429 $backup = $this->get_backup_history($timestamp);
3430 if (!isset($backup['nonce']) || !isset($backup['db'])) return array($mess, $warn, $err, $info);
3431
3432 $db_file = (is_string($backup['db'])) ? $updraft_dir.'/'.$backup['db'] : $updraft_dir.'/'.$backup['db'][0];
3433 }
3434
3435 if (!is_readable($db_file)) return array($mess, $warn, $err, $info);
3436
3437 // Encrypted - decrypt it
3438 if ($this->is_db_encrypted($db_file)) {
3439
3440 $encryption = empty($res['updraft_encryptionphrase']) ? UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase') : $res['updraft_encryptionphrase'];
3441
3442 if (!$encryption) {
3443 if (class_exists('UpdraftPlus_Addon_MoreDatabase')) {
3444 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted, but you have no encryption key entered.', 'updraftplus'));
3445 } else {
3446 $err[] = sprintf(__('Error: %s', 'updraftplus'), __('Decryption failed. The database file is encrypted.', 'updraftplus'));
3447 }
3448 return array($mess, $warn, $err, $info);
3449 }
3450
3451 $ciphertext = $this->decrypt($db_file, $encryption);
3452
3453 if ($ciphertext) {
3454 $new_db_file = $updraft_dir.'/'.basename($db_file, '.crypt');
3455 if (!file_put_contents($new_db_file, $ciphertext)) {
3456 $err[] = __('Failed to write out the decrypted database to the filesystem.','updraftplus');
3457 return array($mess, $warn, $err, $info);
3458 }
3459 $db_file = $new_db_file;
3460 } else {
3461 $err[] = __('Decryption failed. The most likely cause is that you used the wrong key.','updraftplus');
3462 return array($mess, $warn, $err, $info);
3463 }
3464 }
3465
3466 # 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.
3467 if (filesize($db_file) < 1000) {
3468 $err[] = sprintf(__('The database is too small to be a valid WordPress database (size: %s Kb).','updraftplus'), round(filesize($db_file)/1024, 1));
3469 return array($mess, $warn, $err, $info);
3470 }
3471
3472 $is_plain = ('.gz' == substr($db_file, -3, 3)) ? false : true;
3473
3474 $dbhandle = ($is_plain) ? fopen($db_file, 'r') : $this->gzopen_for_read($db_file, $warn, $err);
3475 if (!is_resource($dbhandle)) {
3476 $err[] = __('Failed to open database file.', 'updraftplus');
3477 return array($mess, $warn, $err, $info);
3478 }
3479
3480 $info['timestamp'] = $timestamp;
3481
3482 # Analyse the file, print the results.
3483
3484 $line = 0;
3485 $old_siteurl = '';
3486 $old_home = '';
3487 $old_table_prefix = '';
3488 $old_siteinfo = array();
3489 $gathering_siteinfo = true;
3490 $old_wp_version = '';
3491 $old_php_version = '';
3492
3493 $tables_found = array();
3494
3495 // 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
3496
3497 $wanted_tables = array('terms', 'term_taxonomy', 'term_relationships', 'commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', 'users', 'usermeta');
3498
3499 $migration_warning = false;
3500 $processing_create = false;
3501 $db_version = $wpdb->db_version();
3502
3503 // Don't set too high - we want a timely response returned to the browser
3504 // 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.
3505 // "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)
3506 $default_dbscan_timeout = (filesize($db_file) < 31457280) ? 120 : 240;
3507 $dbscan_timeout = (defined('UPDRAFTPLUS_DBSCAN_TIMEOUT') && is_numeric(UPDRAFTPLUS_DBSCAN_TIMEOUT)) ? UPDRAFTPLUS_DBSCAN_TIMEOUT : $default_dbscan_timeout;
3508 @set_time_limit($dbscan_timeout);
3509
3510 while ((($is_plain && !feof($dbhandle)) || (!$is_plain && !gzeof($dbhandle))) && ($line<100 || (!$header_only && count($wanted_tables)>0))) {
3511 $line++;
3512 // Up to 1Mb
3513 $buffer = ($is_plain) ? rtrim(fgets($dbhandle, 1048576)) : rtrim(gzgets($dbhandle, 1048576));
3514 // Comments are what we are interested in
3515 if (substr($buffer, 0, 1) == '#') {
3516 $processing_create = false;
3517 if ('' == $old_siteurl && preg_match('/^\# Backup of: (http(.*))$/', $buffer, $matches)) {
3518 $old_siteurl = untrailingslashit($matches[1]);
3519 $mess[] = __('Backup of:', 'updraftplus').' '.htmlspecialchars($old_siteurl).((!empty($old_wp_version)) ? ' '.sprintf(__('(version: %s)', 'updraftplus'), $old_wp_version) : '');
3520 // Check for should-be migration
3521 if ($old_siteurl != untrailingslashit(site_url())) {
3522 if (!$migration_warning) {
3523 $migration_warning = true;
3524 $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);
3525 if (!empty($powarn)) $warn[] = $powarn;
3526 }
3527 // Explicitly set it, allowing the consumer to detect when the result was unknown
3528 $info['same_url'] = false;
3529 } else {
3530 $info['same_url'] = true;
3531 }
3532 } elseif ('' == $old_home && preg_match('/^\# Home URL: (http(.*))$/', $buffer, $matches)) {
3533 $old_home = untrailingslashit($matches[1]);
3534 // Check for should-be migration
3535 if (!$migration_warning && $old_home != home_url()) {
3536 $migration_warning = true;
3537 $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);
3538 if (!empty($powarn)) $warn[] = $powarn;
3539 }
3540 } elseif (!isset($info['created_by_version']) && preg_match('/^\# Created by UpdraftPlus version ([\d\.]+)/', $buffer, $matches)) {
3541 $info['created_by_version'] = trim($matches[1]);
3542 } elseif ('' == $old_wp_version && preg_match('/^\# WordPress Version: ([0-9]+(\.[0-9]+)+)(-[-a-z0-9]+,)?(.*)$/', $buffer, $matches)) {
3543 $old_wp_version = $matches[1];
3544 if (!empty($matches[3])) $old_wp_version .= substr($matches[3], 0, strlen($matches[3])-1);
3545 if (version_compare($old_wp_version, $wp_version, '>')) {
3546 //$mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
3547 $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);
3548 }
3549 if (preg_match('/running on PHP ([0-9]+\.[0-9]+)(\s|\.)/', $matches[4], $nmatches) && preg_match('/^([0-9]+\.[0-9]+)(\s|\.)/', PHP_VERSION, $cmatches)) {
3550 $old_php_version = $nmatches[1];
3551 $current_php_version = $cmatches[1];
3552 if (version_compare($old_php_version, $current_php_version, '>')) {
3553 //$mess[] = sprintf(__('%s version: %s', 'updraftplus'), 'WordPress', $old_wp_version);
3554 $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');
3555 }
3556 }
3557 } elseif ('' == $old_table_prefix && (preg_match('/^\# Table prefix: (\S+)$/', $buffer, $matches) || preg_match('/^-- Table prefix: (\S+)$/i', $buffer, $matches))) {
3558 $old_table_prefix = $matches[1];
3559 // echo '<strong>'.__('Old table prefix:', 'updraftplus').'</strong> '.htmlspecialchars($old_table_prefix).'<br>';
3560 } elseif (empty($info['label']) && preg_match('/^\# Label: (.*)$/', $buffer, $matches)) {
3561 $info['label'] = $matches[1];
3562 $mess[] = __('Backup label:', 'updraftplus').' '.htmlspecialchars($info['label']);
3563 } elseif ($gathering_siteinfo && preg_match('/^\# Site info: (\S+)$/', $buffer, $matches)) {
3564 if ('end' == $matches[1]) {
3565 $gathering_siteinfo = false;
3566 // Sanity checks
3567 if (isset($old_siteinfo['multisite']) && !$old_siteinfo['multisite'] && is_multisite()) {
3568 // Just need to check that you're crazy
3569 //if (!defined('UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE') || !UPDRAFTPLUS_EXPERIMENTAL_IMPORTINTOMULTISITE) {
3570 //$err[] = sprintf(__('Error: %s', 'updraftplus'), __('You are running on WordPress multisite - but your backup is not of a multisite site.', 'updraftplus'));
3571 //return array($mess, $warn, $err, $info);
3572 //} else {
3573 $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>';
3574 //}
3575 // Got the needed code?
3576 if (!class_exists('UpdraftPlusAddOn_MultiSite') || !class_exists('UpdraftPlus_Addons_Migrator')) {
3577 $err[] = sprintf(__('Error: %s', 'updraftplus'), sprintf(__('To import an ordinary WordPress site into a multisite installation requires %s.', 'updraftplus'), 'UpdraftPlus Premium'));
3578 return array($mess, $warn, $err, $info);
3579 }
3580 } elseif (isset($old_siteinfo['multisite']) && $old_siteinfo['multisite'] && !is_multisite()) {
3581 $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>';
3582 }
3583 } elseif (preg_match('/^([^=]+)=(.*)$/', $matches[1], $kvmatches)) {
3584 $key = $kvmatches[1];
3585 $val = $kvmatches[2];
3586 if ('multisite' == $key) {
3587 $info['multisite'] = $val ? true : false;
3588 if ($val) $mess[] = '<strong>'.__('Site information:', 'updraftplus').'</strong> '.'backup is of a WordPress Network';
3589 }
3590 $old_siteinfo[$key]=$val;
3591 }
3592 }
3593
3594 } elseif (preg_match('/^\s*create table \`?([^\`\(]*)\`?\s*\(/i', $buffer, $matches)) {
3595 $table = $matches[1];
3596 $tables_found[] = $table;
3597 if ($old_table_prefix) {
3598 // Remove prefix
3599 $table = $this->str_replace_once($old_table_prefix, '', $table);
3600 if (in_array($table, $wanted_tables)) {
3601 $wanted_tables = array_diff($wanted_tables, array($table));
3602 }
3603 }
3604 if (substr($buffer, -1, 1) != ';') $processing_create = true;
3605 } elseif ($processing_create) {
3606 if (substr($buffer, -1, 1) == ';') $processing_create = false;
3607 static $mysql_version_warned = false;
3608 if (!$mysql_version_warned && version_compare($db_version, '5.2.0', '<') && preg_match('/(CHARSET|COLLATE)[= ]utf8mb4/', $buffer)) {
3609 $mysql_version_warned = true;
3610 $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'));
3611 }
3612 }
3613 }
3614
3615 if ($is_plain) {
3616 @fclose($dbhandle);
3617 } else {
3618 @gzclose($dbhandle);
3619 }
3620
3621 /* $blog_tables = "CREATE TABLE $wpdb->terms (
3622 CREATE TABLE $wpdb->term_taxonomy (
3623 CREATE TABLE $wpdb->term_relationships (
3624 CREATE TABLE $wpdb->commentmeta (
3625 CREATE TABLE $wpdb->comments (
3626 CREATE TABLE $wpdb->links (
3627 CREATE TABLE $wpdb->options (
3628 CREATE TABLE $wpdb->postmeta (
3629 CREATE TABLE $wpdb->posts (
3630 $users_single_table = "CREATE TABLE $wpdb->users (
3631 $users_multi_table = "CREATE TABLE $wpdb->users (
3632 $usermeta_table = "CREATE TABLE $wpdb->usermeta (
3633 $ms_global_tables = "CREATE TABLE $wpdb->blogs (
3634 CREATE TABLE $wpdb->blog_versions (
3635 CREATE TABLE $wpdb->registration_log (
3636 CREATE TABLE $wpdb->site (
3637 CREATE TABLE $wpdb->sitemeta (
3638 CREATE TABLE $wpdb->signups (
3639 */
3640
3641 $missing_tables = array();
3642 if ($old_table_prefix) {
3643 if (!$header_only) {
3644 foreach ($wanted_tables as $table) {
3645 if (!in_array($old_table_prefix.$table, $tables_found)) {
3646 $missing_tables[] = $table;
3647 }
3648 }
3649 if (count($missing_tables)>0) {
3650 $warn[] = sprintf(__('This database backup is missing core WordPress tables: %s', 'updraftplus'), implode(', ', $missing_tables));
3651 }
3652 }
3653 } else {
3654 if (empty($backup['meta_foreign'])) {
3655 $warn[] = __('UpdraftPlus was unable to find the table prefix when scanning the database backup.', 'updraftplus');
3656 }
3657 }
3658
3659 return array($mess, $warn, $err, $info);
3660
3661 }
3662
3663 private function gzopen_for_read($file, &$warn, &$err) {
3664 if (!function_exists('gzopen') || !function_exists('gzread')) {
3665 $missing = '';
3666 if (!function_exists('gzopen')) $missing .= 'gzopen';
3667 if (!function_exists('gzread')) $missing .= ($missing) ? ', gzread' : 'gzread';
3668 $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'));
3669 return false;
3670 }
3671 if (false === ($dbhandle = gzopen($file, 'r'))) return false;
3672
3673 if (!function_exists('gzseek')) return $dbhandle;
3674
3675 if (false === ($bytes = gzread($dbhandle, 3))) return false;
3676 # Double-gzipped?
3677 if ('H4sI' != base64_encode($bytes)) {
3678 if (0 === gzseek($dbhandle, 0)) {
3679 return $dbhandle;
3680 } else {
3681 @gzclose($dbhandle);
3682 return gzopen($file, 'r');
3683 }
3684 }
3685 # Yes, it's double-gzipped
3686
3687 $what_to_return = false;
3688 $mess = __('The database file appears to have been compressed twice - probably the website you downloaded it from had a mis-configured webserver.', 'updraftplus');
3689 $messkey = 'doublecompress';
3690 $err_msg = '';
3691
3692 if (false === ($fnew = fopen($file.".tmp", 'w')) || !is_resource($fnew)) {
3693
3694 @gzclose($dbhandle);
3695 $err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus');
3696
3697 } else {
3698
3699 @fwrite($fnew, $bytes);
3700 $emptimes = 0;
3701 while (!gzeof($dbhandle)) {
3702 $bytes = @gzread($dbhandle, 262144);
3703 if (empty($bytes)) {
3704 $emptimes++;
3705 $this->log("Got empty gzread ($emptimes times)");
3706 if ($emptimes>2) break;
3707 } else {
3708 @fwrite($fnew, $bytes);
3709 }
3710 }
3711
3712 gzclose($dbhandle);
3713 fclose($fnew);
3714 # On some systems (all Windows?) you can't rename a gz file whilst it's gzopened
3715 if (!rename($file.".tmp", $file)) {
3716 $err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus');
3717 } else {
3718 $mess .= ' '.__('The attempt to undo the double-compression succeeded.', 'updraftplus');
3719 $messkey = 'doublecompressfixed';
3720 $what_to_return = gzopen($file, 'r');
3721 }
3722
3723 }
3724
3725 $warn[$messkey] = $mess;
3726 if (!empty($err_msg)) $err[] = $err_msg;
3727 return $what_to_return;
3728 }
3729
3730 # TODO: Remove legacy storage setting keys from here
3731 // 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
3732 public function get_settings_keys() {
3733 // N.B. updraft_backup_history is not included here, as we don't want that wiped
3734 return array('updraft_autobackup_default', 'updraft_dropbox', 'updraft_googledrive', 'updraftplus_tmp_googledrive_access_token', 'updraftplus_dismissedautobackup', 'updraftplus_dismissedexpiry', 'updraftplus_dismisseddashnotice', 'updraft_interval', 'updraft_interval_increments', 'updraft_interval_database', 'updraft_retain', 'updraft_retain_db', 'updraft_encryptionphrase', 'updraft_service', 'updraft_dropbox_appkey', 'updraft_dropbox_secret', 'updraft_googledrive_clientid', 'updraft_googledrive_secret', 'updraft_googledrive_remotepath', 'updraft_ftp', 'updraft_ftp_login', 'updraft_ftp_pass', 'updraft_ftp_remote_path', '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',
3735 'updraft_include_others_exclude', 'updraft_include_uploads_exclude', 'updraft_lastmessage', 'updraft_googledrive_token', 'updraft_dropboxtk_request_token', 'updraft_dropboxtk_access_token', 'updraft_dropbox_folder', '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',
3736 '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_bitcasa', 'updraft_copycom', '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');
3737 }
3738
3739 }
3740