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