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