PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.9.31
UpdraftPlus: WP Backup & Migration Plugin v1.9.31
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / class-updraftplus.php

class-updraftplus.php in UpdraftPlus: WP Backup & Migration Plugin 1.9.31, at class-updraftplus.php

2,536 lines 112.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 'dropbox' => 'Dropbox',
14 's3' => 'Amazon S3',
15 'cloudfiles' => 'Rackspace Cloud Files',
16 'googledrive' => 'Google Drive',
17 'ftp' => 'FTP',
18 'copycom' => 'Copy.Com',
19 'sftp' => 'SFTP / SCP',
20 'webdav' => 'WebDAV',
21 's3generic' => 'S3-Compatible (Generic)',
22 'openstack' => 'OpenStack (Swift)',
23 'dreamobjects' => 'DreamObjects',
24 'email' => 'Email'
25 );
26
27 public $errors = array();
28 public $nonce;
29 public $logfile_name = "";
30 public $logfile_handle = false;
31 public $backup_time;
32 public $job_time_ms;
33
34 public $opened_log_time;
35 private $backup_dir;
36
37 private $jobdata;
38
39 public $something_useful_happened = false;
40 public $have_addons = false;
41
42 // Used to schedule resumption attempts beyond the tenth, if needed
43 public $current_resumption;
44 public $newresumption_scheduled = false;
45
46 public $cpanel_quota_readable = false;
47
48 public function __construct() {
49
50 # Bitcasa support is deprecated
51 if (is_file(UPDRAFTPLUS_DIR.'/addons/bitcasa.php')) $this->backup_methods['bitcasa'] = 'Bitcasa';
52
53 // Initialisation actions - takes place on plugin load
54
55 if ($fp = fopen(UPDRAFTPLUS_DIR.'/updraftplus.php', 'r')) {
56 $file_data = fread($fp, 1024);
57 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
58 $this->version = $matches[1];
59 }
60 fclose($fp);
61 }
62
63 # Create admin page
64 add_action('init', array($this, 'handle_url_actions'));
65 // Run earlier than default - hence earlier than other components
66 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
67 add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
68 # Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
69 add_action('admin_init', array($this, 'admin_menu'), 9);
70
71 # The two actions which we schedule upon
72 add_action('updraft_backup', array($this, 'backup_files'));
73 add_action('updraft_backup_database', array($this, 'backup_database'));
74
75 # The three actions that can be called from "Backup Now"
76 add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
77 add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
78 add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
79
80 # backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
81 add_action('updraft_backup_all', array($this, 'backup_all'));
82
83 # This is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
84 add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
85
86 add_action('plugins_loaded', array($this, 'load_translations'));
87
88 # Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
89 add_filter('itsec_has_external_backup', '__return_true', 999);
90 add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
91 add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
92
93 # register_deactivation_hook(__FILE__, array($this, 'deactivation'));
94
95 }
96
97 public function itsec_scheduled_external_backup($x) { return (!wp_next_scheduled('updraft_backup')) ? false : true; }
98 public function itsec_external_backup_link($x) { return UpdraftPlus_Options::admin_page_url().'?page=updraftplus'; }
99
100 public function ensure_phpseclib($class = false, $class_path = false) {
101 if ($class && class_exists($class)) return;
102 if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
103 if ($class_path) require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/'.$class_path.'.php');
104 }
105
106 // Returns the number of bytes free, if it can be detected; otherwise, false
107 // Presently, we only detect CPanel. If you know of others, then feel free to contribute!
108 public function get_hosting_disk_quota_free() {
109 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;
110
111 $perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';
112
113 $exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
114
115 $handle = @popen($exec, 'r');
116 if (!is_resource($handle)) return false;
117
118 $found = false;
119 $lines = 0;
120 while (false === $found && !feof($handle) && $lines<100) {
121 $lines++;
122 $w = fgets($handle);
123 # Used, limit, remain
124 if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) { $found = true; }
125 }
126 $ret = pclose($handle);
127 if (false === $found ||$ret != 0) return false;
128
129 if ((int)$matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
130
131 $this->cpanel_quota_readable = true;
132
133 return $matches;
134 }
135
136 public function last_modified_log() {
137 $updraft_dir = $this->backups_dir_location();
138
139 $log_file = '';
140 $mod_time = 0;
141 $nonce = '';
142
143 if ($handle = @opendir($updraft_dir)) {
144 while (false !== ($entry = readdir($handle))) {
145 // The latter match is for files created internally by zipArchive::addFile
146 if (preg_match('/^log\.([a-z0-9]+)\.txt$/i', $entry, $matches)) {
147 $mtime = filemtime($updraft_dir.'/'.$entry);
148 if ($mtime > $mod_time) {
149 $mod_time = $mtime;
150 $log_file = $updraft_dir.'/'.$entry;
151 $nonce = $matches[1];
152 }
153 }
154 }
155 @closedir($handle);
156 }
157
158 return array($mod_time, $log_file, $nonce);
159 }
160
161 // This function may get called multiple times, so write accordingly
162 public function admin_menu() {
163 // We are in the admin area: now load all that code
164 global $updraftplus_admin;
165 if (empty($updraftplus_admin)) require_once(UPDRAFTPLUS_DIR.'/admin.php');
166
167 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadlatestmodlog' && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
168
169 list ($mod_time, $log_file, $nonce) = $this->last_modified_log();
170
171 if ($mod_time >0) {
172 if (is_readable($log_file)) {
173 header('Content-type: text/plain');
174 readfile($log_file);
175 exit;
176 } else {
177 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
178 }
179 } else {
180 add_action('all_admin_notices', array($this,'show_admin_warning_nolog') );
181 }
182 }
183
184 }
185
186 public function modify_http_options($opts) {
187
188 if (!is_array($opts)) return $opts;
189
190 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) $opts['sslcertificates'] = UPDRAFTPLUS_DIR.'/includes/cacert.pem';
191
192 $opts['sslverify'] = (UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')) ? false : true;
193
194 return $opts;
195
196 }
197
198 // Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?action=updraftmethod-googledrive-auth&page=updraftplus
199 // 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.
200 // Also handle action=downloadlog
201 public function handle_url_actions() {
202
203 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
204 // Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
205 if (isset($_SERVER['REQUEST_METHOD']) && 'GET' == $_SERVER['REQUEST_METHOD'] && isset($_GET['action'])) {
206 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php') && UpdraftPlus_Options::user_can_manage()) {
207 $_GET['page'] = 'updraftplus';
208 $_REQUEST['page'] = 'updraftplus';
209 $method = $matches[1];
210 require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
211 $call_class = "UpdraftPlus_BackupModule_".$method;
212 $call_method = "action_".$matches[2];
213 $backup_obj = new $call_class;
214 add_action('http_request_args', array($this, 'modify_http_options'));
215 try {
216 if (method_exists($backup_obj, $call_method)) {
217 call_user_func(array($backup_obj, $call_method));
218 } elseif (method_exists($backup_obj, 'action_handler')) {
219 call_user_func(array($backup_obj, 'action_handler'), $matches[2]);
220 }
221 } catch (Exception $e) {
222 $this->log(sprintf(__("%s error: %s", 'updraftplus'), $method, $e->getMessage().' ('.$e->getCode().')', 'error'));
223 }
224 remove_action('http_request_args', array($this, 'modify_http_options'));
225 } 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()) {
226 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
227 $updraft_dir = $this->backups_dir_location();
228 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
229 if (is_readable($log_file)) {
230 header('Content-type: text/plain');
231 if (!empty($_GET['force_download'])) header('Content-Disposition: attachment; filename="'.basename($log_file).'"');
232 readfile($log_file);
233 exit;
234 } else {
235 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
236 }
237 } 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()) {
238 $updraft_dir = $this->backups_dir_location();
239 $spool_file = $updraft_dir.'/'.basename($_GET['updraftplus_file']);
240 if (is_readable($spool_file)) {
241 $dkey = (isset($_GET['decrypt_key'])) ? $_GET['decrypt_key'] : "";
242 $this->spool_file('db', $spool_file, $dkey);
243 exit;
244 } else {
245 add_action('all_admin_notices', array($this,'show_admin_warning_unreadablefile') );
246 }
247 }
248 }
249 }
250
251 public function get_table_prefix($allow_override = false) {
252 global $wpdb;
253 if (is_multisite() && !defined('MULTISITE')) {
254 # 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.
255 $prefix = $wpdb->base_prefix;
256 } else {
257 $prefix = $wpdb->get_blog_prefix(0);
258 }
259 return ($allow_override) ? apply_filters('updraftplus_get_table_prefix', $prefix) : $prefix;
260 }
261
262 public function show_admin_warning_unreadablelog() {
263 global $updraftplus_admin;
264 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The log file could not be read.','updraftplus'));
265 }
266
267 public function show_admin_warning_nolog() {
268 global $updraftplus_admin;
269 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('No log files were found.','updraftplus'));
270 }
271
272 public function show_admin_warning_unreadablefile() {
273 global $updraftplus_admin;
274 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
275 }
276
277 public function load_translations() {
278 // Tell WordPress where to find the translations
279 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages/');
280 # The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
281 if ((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX && isset($_REQUEST['subaction']) && 'backupnow' == $_REQUEST['subaction']) || (isset($_GET['page']) && $_GET['page'] == 'updraftplus')) {
282 remove_action('init', 'ganalyticator_stats_init');
283 # Appointments+ does the same; but provides a cleaner way to disable it
284 define('APP_GCAL_DISABLE', true);
285 }
286 }
287
288 // Cleans up temporary files found in the updraft directory (and some in the site root - pclzip)
289 // Always cleans up temporary files over 12 hours old.
290 // With parameters, also cleans up those.
291 // Also cleans out old job data older than 12 hours old (immutable value)
292 public function clean_temporary_files($match = '', $older_than = 43200) {
293 # Clean out old job data
294 if ($older_than > 10000) {
295 global $wpdb;
296
297 $all_jobs = $wpdb->get_results("SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'updraft_jobdata_%'", ARRAY_A);
298 foreach ($all_jobs as $job) {
299 $val = maybe_unserialize($job['option_value']);
300 # TODO: Can simplify this after a while (now all jobs use job_time_ms) - 1 Jan 2014
301 $delete = false;
302 if (!empty($val['next_increment_start_scheduled_for'])) {
303 if (time() > $val['next_increment_start_scheduled_for'] + 86400) $delete = true;
304 } elseif (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) {
305 $delete = true;
306 } elseif (!empty($val['job_time_ms']) && time() > $val['job_time_ms'] + 86400) {
307 $delete = true;
308 } elseif (!empty($val['job_type']) && 'backup' != $val['job_type'] && empty($val['backup_time_ms']) && empty($val['job_time_ms'])) {
309 $delete = true;
310 }
311 if ($delete) delete_option($job['option_name']);
312 }
313 }
314 $updraft_dir = $this->backups_dir_location();
315 $now_time=time();
316 if ($handle = opendir($updraft_dir)) {
317 while (false !== ($entry = readdir($handle))) {
318 $manifest_match = preg_match("/^udmanifest$match\.json$/i", $entry);
319 // This match is for files created internally by zipArchive::addFile
320 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $entry);
321 // 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.
322 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry);
323 # Temporary files from the database dump process - not needed, as is caught by the catch-all
324 # $table_match = preg_match("/${match}-table-(.*)\.table(\.tmp)?\.gz$/i", $entry);
325 # The gz goes in with the txt, because we *don't* want to reap the raw .txt files
326 if ((preg_match("/$match\.(tmp|table|txt\.gz)(\.gz)?$/i", $entry) || $ziparchive_match || $binzip_match || $manifest_match) && is_file($updraft_dir.'/'.$entry)) {
327 // 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
328 if (($match && ($ziparchive_match || $binzip_match || $manifest_match || 0 == $older_than) && $now_time-filemtime($updraft_dir.'/'.$entry) >= $older_than) || $now_time-filemtime($updraft_dir.'/'.$entry)>43200) {
329 $this->log("Deleting old temporary file: $entry");
330 @unlink($updraft_dir.'/'.$entry);
331 }
332 }
333 }
334 @closedir($handle);
335 }
336 # Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both
337 foreach (array(ABSPATH, ABSPATH.'wp-admin/') as $path) {
338 if ($handle = opendir($path)) {
339 while (false !== ($entry = readdir($handle))) {
340 # 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
341 if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) {
342 $this->log("Deleting old PclZip temporary file: $entry");
343 @unlink($path.$entry);
344 }
345 }
346 @closedir($handle);
347 }
348 }
349 }
350
351 public function backup_time_nonce($nonce = false) {
352 $this->job_time_ms = microtime(true);
353 $this->backup_time = time();
354 if (false === $nonce) $nonce = substr(md5(time().rand()), 20);
355 $this->nonce = $nonce;
356 }
357
358 public function logfile_open($nonce) {
359
360 //set log file name and open log file
361 $updraft_dir = $this->backups_dir_location();
362 $this->logfile_name = $updraft_dir."/log.$nonce.txt";
363
364 if (file_exists($this->logfile_name)) {
365 $seek_to = max((filesize($this->logfile_name) - 340), 1);
366 $handle = fopen($this->logfile_name, 'r');
367 if (is_resource($handle)) {
368 # Returns 0 on success
369 if (0 === @fseek($handle, $seek_to)) {
370 $bytes_back = filesize($this->logfile_name) - $seek_to;
371 # Return to the end of the file
372 $read_recent = fread($handle, $bytes_back);
373 # Move to end of file - ought to be redundant
374 if (false !== strpos($read_recent, 'The backup apparently succeeded') && false !== strpos($read_recent, 'and is now complete')) {
375 $this->backup_is_already_complete = true;
376 }
377 }
378 fclose($handle);
379 }
380 }
381
382 $this->logfile_handle = fopen($this->logfile_name, 'a');
383
384 $this->opened_log_time = microtime(true);
385 $this->log('Opened log file at time: '.date('r').' on '.site_url());
386 global $wp_version;
387 @include(ABSPATH.WPINC.'/version.php');
388
389 // Will need updating when WP stops being just plain MySQL
390 $mysql_version = (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
391
392 $safe_mode = $this->detect_safe_mode();
393
394 $memory_limit = ini_get('memory_limit');
395 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
396 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
397
398 # Attempt to raise limit to avoid false positives
399 @set_time_limit(900);
400 $max_execution_time = (int)@ini_get("max_execution_time");
401
402 $logline = "UpdraftPlus WordPress backup plugin (http://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: ";
403
404 // method_exists causes some faulty PHP installations to segfault, leading to support requests
405 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
406 $logline .= 'Y';
407 } else {
408 $logline .= (class_exists('ZipArchive') && method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
409 }
410
411 // $w3oc = 'N';
412 if (0 === $this->current_resumption) {
413 $memlim = $this->memory_check_current();
414 if ($memlim<65) {
415 $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');
416 }
417 if ($max_execution_time>0 && $max_execution_time<20) {
418 $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');
419 }
420 // if (defined('W3TC') && W3TC == true && function_exists('w3_instance')) {
421 // $modules = w3_instance('W3_ModuleStatus');
422 // if ($modules->is_enabled('objectcache')) {
423 // $w3oc = 'Y';
424 // }
425 // }
426 // $logline .= " W3TC/ObjectCache: $w3oc";
427 }
428
429 $this->log($logline);
430
431 $hosting_bytes_free = $this->get_hosting_disk_quota_free();
432 if (is_array($hosting_bytes_free)) {
433 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
434 $quota_free = ' / '.sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." Mb", "$perc %");
435 if ($hosting_bytes_free[3] < 1048576*50) {
436 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
437 $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);
438 }
439 } else {
440 $quota_free = '';
441 }
442
443 $disk_free_space = @disk_free_space($updraft_dir);
444 # == 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.
445 if ($disk_free_space == false) {
446 $this->log("Free space on disk containing Updraft's temporary directory: Unknown".$quota_free);
447 } else {
448 $this->log("Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576,1)." Mb".$quota_free);
449 $disk_free_mb = round($disk_free_space/1048576, 1);
450 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);
451 }
452
453 }
454
455 /* Logs the given line, adding (relative) time stamp and newline
456 Note these subtleties of log handling:
457 - 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.
458 - 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...
459 - ... 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
460 $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
461 */
462
463 public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
464
465 if ('error' == $level || 'warning' == $level) {
466 if ('error' == $level && 0 == $this->error_count()) $this->log('An error condition has occurred for the first time during this job');
467 if ($uniq_id) {
468 $this->errors[$uniq_id] = array('level' => $level, 'message' => $line);
469 } else {
470 $this->errors[] = array('level' => $level, 'message' => $line);
471 }
472 # Errors are logged separately
473 if ('error' == $level) return;
474 # It's a warning
475 $warnings = $this->jobdata_get('warnings');
476 if (!is_array($warnings)) $warnings=array();
477 if ($uniq_id) {
478 $warnings[$uniq_id] = $line;
479 } else {
480 $warnings[] = $line;
481 }
482 $this->jobdata_set('warnings', $warnings);
483 }
484
485 do_action('updraftplus_logline', $line, $this->nonce, $level, $uniq_id);
486
487 if ($this->logfile_handle) {
488 # Record log file times relative to the backup start, if possible
489 $rtime = (!empty($this->job_time_ms)) ? microtime(true)-$this->job_time_ms : microtime(true)-$this->opened_log_time;
490 fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") ".(('notice' != $level) ? '['.ucfirst($level).'] ' : '').$line."\n");
491 }
492
493 switch ($this->jobdata_get('job_type')) {
494 case 'download':
495 // Download messages are keyed on the job (since they could be running several), and type
496 // The values of the POST array were checked before
497 $findex = (!empty($_POST['findex'])) ? $_POST['findex'] : 0;
498
499 $this->jobdata_set('dlmessage_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line);
500
501 break;
502 case 'restore':
503 #if ('debug' != $level) echo $line."\n";
504 break;
505 default:
506 if (!$skip_dblog && 'debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")", false);
507 break;
508 }
509
510 if (defined('UPDRAFTPLUS_CONSOLELOG')) print $line."\n";
511 if (defined('UPDRAFTPLUS_BROWSERLOG')) print htmlentities($line)."<br>\n";
512 }
513
514 public function log_removewarning($uniq_id) {
515 $warnings = $this->jobdata_get('warnings');
516 if (!is_array($warnings)) $warnings=array();
517 unset($warnings[$uniq_id]);
518 $this->jobdata_set('warnings', $warnings);
519 unset($this->errors[$uniq_id]);
520 }
521
522 # For efficiency, you can also feed false or a string into this function
523 public function log_wp_error($err, $echo = false, $logerror = false) {
524 if (false === $err) return false;
525 if (is_string($err)) {
526 $this->log("Error message: $err");
527 if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($err))."<br>";
528 if ($logerror) $this->log($err, 'error');
529 return false;
530 }
531 foreach ($err->get_error_messages() as $msg) {
532 $this->log("Error message: $msg");
533 if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($msg))."<br>";
534 if ($logerror) $this->log($msg, 'error');
535 }
536 $codes = $err->get_error_codes();
537 if (is_array($codes)) {
538 foreach ($codes as $code) {
539 $data = $err->get_error_data($code);
540 if (!empty($data)) {
541 $ll = (is_string($data)) ? $data : serialize($data);
542 $this->log("Error data (".$code."): ".$ll);
543 }
544 }
545 }
546 # Returns false so that callers can return with false more efficiently if they wish
547 return false;
548 }
549
550 public function get_max_packet_size() {
551 global $wpdb, $updraftplus;
552 $mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
553 # Default to 1Mb
554 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
555 # 32Mb
556 if ($mp < 33554432) {
557 $save = $wpdb->show_errors(false);
558 $req = @$wpdb->query("SET GLOBAL max_allowed_packet=33554432");
559 $wpdb->show_errors($save);
560 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).")");
561 $mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
562 # Default to 1Mb
563 $mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
564 }
565 $updraftplus->log("Max packet size: ".round($mp/1048576, 1)." Mb");
566 return $mp;
567 }
568
569 # 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()).
570 # 1st argument = the line to be logged (obligatory)
571 # Further arguments = parameters for sprintf()
572 public function log_e() {
573 $args = func_get_args();
574 # Get first argument
575 $pre_line = array_shift($args);
576 # Log it whilst still in English
577 if (is_wp_error($pre_line)) {
578 $this->log_wp_error($pre_line);
579 } else {
580 # Now run (v)sprintf on it, using any remaining arguments. vsprintf = sprintf but takes an array instead of individual arguments
581 $this->log(vsprintf($pre_line, $args));
582 echo vsprintf(__($pre_line, 'updraftplus'), $args).'<br>';
583 }
584 }
585
586 // 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
587 public function record_uploaded_chunk($percent, $extra = '', $file_path = false) {
588
589 // Touch the original file, which helps prevent overlapping runs
590 if ($file_path) touch($file_path);
591
592 // 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)
593 if ($percent > 0.7 * ($this->current_resumption - max($this->jobdata_get('uploaded_lastreset'), 9))) $this->something_useful_happened();
594
595 // Log it
596 global $updraftplus_backup;
597 $log = (!empty($updraftplus_backup->current_service)) ? ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded" : '';
598 if ($log) $this->log($log.(($extra) ? " ($extra)" : ''));
599 // If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
600 // 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
601 // 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
602 // 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
603
604 $upload_status = $this->jobdata_get('uploading_substatus');
605 if (is_array($upload_status)) {
606 $upload_status['p'] = $percent/100;
607 $this->jobdata_set('uploading_substatus', $upload_status);
608 }
609
610 }
611
612 public function chunked_upload($caller, $file, $cloudpath, $logname, $chunk_size, $uploaded_size, $singletons=false) {
613
614 $fullpath = $this->backups_dir_location().'/'.$file;
615 $orig_file_size = filesize($fullpath);
616 if ($uploaded_size >= $orig_file_size) return true;
617
618 $fp = @fopen($fullpath, 'rb');
619 if (!$fp) {
620 $this->log("$logname: failed to open file: $fullpath");
621 $this->log("$file: ".sprintf(__('%s Error: Failed to open local file','updraftplus'), $logname), 'error');
622 return false;
623 }
624
625 $chunks = floor($orig_file_size / $chunk_size);
626 // There will be a remnant unless the file size was exactly on a 5Mb boundary
627 if ($orig_file_size % $chunk_size > 0) $chunks++;
628
629 $this->log("$logname upload: $file (chunks: $chunks) -> $cloudpath ($uploaded_size)");
630
631 if ($chunks == 0) {
632 return 1;
633 } elseif ($chunks < 2 && !$singletons) {
634 return 1;
635 } else {
636 $errors_so_far = 0;
637 for ($i = 1 ; $i <= $chunks; $i++) {
638
639 $upload_start = ($i-1)*$chunk_size;
640 // The file size -1 equals the byte offset of the final byte
641 $upload_end = min($i*$chunk_size-1, $orig_file_size-1);
642 // Don't forget the +1; otherwise the last byte is omitted
643 $upload_size = $upload_end - $upload_start + 1;
644
645 fseek($fp, $upload_start);
646
647 $uploaded = $caller->chunked_upload($file, $fp, $i, $upload_size, $upload_start, $upload_end);
648
649 if ($uploaded) {
650 $perc = round(100*((($i-1) * $chunk_size) + $upload_size)/max($orig_file_size, 1), 1);
651 # $perc = round(100*$i/$chunks,1); # Takes no notice of last chunk likely being smaller
652 $this->record_uploaded_chunk($perc, $i, $fullpath);
653 } else {
654 $errors_so_far++;
655 if ($errors_so_far>=3) return false;
656 }
657 }
658 if ($errors_so_far) return false;
659
660 // All chunks are uploaded - now combine the chunks
661 $ret = true;
662 if (method_exists($caller, 'chunked_upload_finish')) {
663 $ret = $caller->chunked_upload_finish($file);
664 if (!$ret) {
665 $this->log("$logname - failed to re-assemble chunks (".$e->getMessage().')');
666 $this->log(sprintf(__('%s error - failed to re-assemble chunks', 'updraftplus'), $logname).' ('.$e->getMessage().')', 'error');
667 }
668 }
669 if ($ret) {
670 $this->log("$logname upload: success");
671 # UpdraftPlus_RemoteStorage_Addons_Base calls this itself
672 if (!is_a($caller, 'UpdraftPlus_RemoteStorage_Addons_Base')) $this->uploaded_file($file);
673 }
674
675 return $ret;
676
677 }
678 }
679
680 public function chunked_download($file, $method, $remote_size, $manually_break_up = false, $passback = null) {
681
682 try {
683
684 $fullpath = $this->backups_dir_location().'/'.$file;
685 $start_offset = (file_exists($fullpath)) ? filesize($fullpath): 0;
686
687 if ($start_offset >= $remote_size) {
688 $this->log("File is already completely downloaded ($start_offset/$remote_size)");
689 return true;
690 }
691
692 // Some more remains to download - so let's do it
693 if (!$fh = fopen($fullpath, 'a')) {
694 $this->log("Error opening local file: $fullpath");
695 $this->log($file.": ".__("Error",'updraftplus').": ".__('Error opening local file: Failed to download','updraftplus'), 'error');
696 return false;
697 }
698
699 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + 1048576) : $remote_size;
700
701 # This only affects logging
702 $expected_bytes_delivered_so_far = true;
703
704 while ($start_offset < $remote_size) {
705 $headers = array();
706 // If resuming, then move to the end of the file
707
708 $requested_bytes = $last_byte-$start_offset;
709
710 if ($expected_bytes_delivered_so_far) {
711 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next $requested_bytes bytes");
712 } else {
713 $this->log("$file: local file is status: $start_offset/$remote_size bytes; requesting next chunk (${start_offset}-)");
714 }
715
716 if ($start_offset >0 || $last_byte<$remote_size) {
717 fseek($fh, $start_offset);
718 $headers['Range'] = "bytes=$start_offset-$last_byte";
719 }
720
721 # The method is free to return as much data as it pleases
722 $ret = $method->chunked_download($file, $headers, $passback);
723 if (false === $ret) return false;
724
725 if (strlen($ret) > $requested_bytes || strlen($ret) < $requested_bytes - 1) $expected_bytes_delivered_so_far = false;
726
727 if (!fwrite($fh, $ret)) throw new Exception('Write failure');
728
729 clearstatcache();
730 $start_offset = ftell($fh);
731 $last_byte = ($manually_break_up) ? min($remote_size, $start_offset + 1048576) : $remote_size;
732
733 }
734
735 } catch(Exception $e) {
736 $this->log('Error ('.get_class($e).') - failed to download the file ('.$e->getCode().', '.$e->getMessage().')');
737 $this->log("$file: ".__('Error - failed to download the file','updraftplus').' ('.$e->getCode().', '.$e->getMessage().')' ,'error');
738 return false;
739 }
740
741 fclose($fh);
742
743 return true;
744 }
745
746 public function decrypt($fullpath, $key, $ciphertext = false) {
747 $this->ensure_phpseclib('Crypt_Rijndael', 'Crypt/Rijndael');
748 $rijndael = new Crypt_Rijndael();
749 $rijndael->setKey($key);
750 return (false == $ciphertext) ? $rijndael->decrypt(file_get_contents($fullpath)) : $rijndael->decrypt($ciphertext);
751 }
752
753 function detect_safe_mode() {
754 return (@ini_get('safe_mode') && strtolower(@ini_get('safe_mode')) != "off") ? 1 : 0;
755 }
756
757 public function find_working_sqldump($logit = true, $cacheit = true) {
758
759 // The hosting provider may have explicitly disabled the popen or proc_open functions
760 if ($this->detect_safe_mode() || !function_exists('popen') || !function_exists('escapeshellarg')) {
761 if ($cacheit) $this->jobdata_set('binsqldump', false);
762 return false;
763 }
764 $existing = $this->jobdata_get('binsqldump', null);
765 # Theoretically, we could have moved machines, due to a migration
766 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
767
768 $updraft_dir = $this->backups_dir_location();
769 global $wpdb;
770 $table_name = $wpdb->get_blog_prefix().'options';
771 $tmp_file = md5(time().rand()).".sqltest.tmp";
772 $pfile = md5(time().rand()).'.tmp';
773 file_put_contents($updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".DB_PASSWORD."\n");
774
775 $result = false;
776 foreach (explode(',', UPDRAFTPLUS_MYSQLDUMP_EXECUTABLE) as $potsql) {
777 if (!@is_executable($potsql)) continue;
778 if ($logit) $this->log("Testing: $potsql");
779
780 $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";
781
782 $handle = popen($exec, "r");
783 if ($handle) {
784 while (!feof($handle)) {
785 $w = fgets($handle);
786 if ($w && $logit) $this->log("Output: ".trim($w));
787 }
788 $ret = pclose($handle);
789 if ($ret !=0) {
790 if ($logit) $this->log("Binary mysqldump: error (code: $ret)");
791 } else {
792 $dumped = file_get_contents($updraft_dir.'/'.$tmp_file, false, null, 0, 4096);
793 if (stripos($dumped, 'insert into') !== false) {
794 if ($logit) $this->log("Working binary mysqldump found: $potsql");
795 $result = $potsql;
796 break;
797 }
798 }
799 } else {
800 if ($logit) $this->log("Error: popen failed");
801 }
802 }
803
804 @unlink($updraft_dir.'/'.$pfile);
805 @unlink($updraft_dir.'/'.$tmp_file);
806
807 if ($cacheit) $this->jobdata_set('binsqldump', $result);
808
809 return $result;
810 }
811
812 # We require -@ and -u -r to work - which is the usual Linux binzip
813 function find_working_bin_zip($logit = true, $cacheit = true) {
814 if ($this->detect_safe_mode()) return false;
815 // The hosting provider may have explicitly disabled the popen or proc_open functions
816 if (!function_exists('popen') || !function_exists('proc_open') || !function_exists('escapeshellarg')) {
817 if ($cacheit) $this->jobdata_set('binzip', false);
818 return false;
819 }
820
821 $existing = $this->jobdata_get('binzip', null);
822 # Theoretically, we could have moved machines, due to a migration
823 if (null !== $existing && (!is_string($existing) || @is_executable($existing))) return $existing;
824
825 $updraft_dir = $this->backups_dir_location();
826 foreach (explode(',', UPDRAFTPLUS_ZIP_EXECUTABLE) as $potzip) {
827 if (!@is_executable($potzip)) continue;
828 if ($logit) $this->log("Testing: $potzip");
829
830 # Test it, see if it is compatible with Info-ZIP
831 # If you have another kind of zip, then feel free to tell me about it
832 @mkdir($updraft_dir.'/binziptest/subdir1/subdir2', 0777, true);
833 file_put_contents($updraft_dir.'/binziptest/subdir1/subdir2/test.html', '<html></body><a href="http://updraftplus.com">UpdraftPlus is a great backup and restoration plugin for WordPress.</body></html>');
834 @unlink($updraft_dir.'/binziptest/test.zip');
835 if (is_file($updraft_dir.'/binziptest/subdir1/subdir2/test.html')) {
836
837 $exec = "cd ".escapeshellarg($updraft_dir)."; $potzip -v -u -r binziptest/test.zip binziptest/subdir1";
838
839 $all_ok=true;
840 $handle = popen($exec, "r");
841 if ($handle) {
842 while (!feof($handle)) {
843 $w = fgets($handle);
844 if ($w && $logit) $this->log("Output: ".trim($w));
845 }
846 $ret = pclose($handle);
847 if ($ret !=0) {
848 if ($logit) $this->log("Binary zip: error (code: $ret)");
849 $all_ok = false;
850 }
851 } else {
852 if ($logit) $this->log("Error: popen failed");
853 $all_ok = false;
854 }
855
856 # Now test -@
857 if (true == $all_ok) {
858 file_put_contents($updraft_dir.'/binziptest/subdir1/subdir2/test2.html', '<html></body><a href="http://updraftplus.com">UpdraftPlus is a really great backup and restoration plugin for WordPress.</body></html>');
859
860 $exec = $potzip." -v -@ binziptest/test.zip";
861
862 $all_ok=true;
863
864 $descriptorspec = array(
865 0 => array('pipe', 'r'),
866 1 => array('pipe', 'w'),
867 2 => array('pipe', 'w')
868 );
869 $handle = proc_open($exec, $descriptorspec, $pipes, $updraft_dir);
870 if (is_resource($handle)) {
871 if (!fwrite($pipes[0], "binziptest/subdir1/subdir2/test2.html\n")) {
872 @fclose($pipes[0]);
873 @fclose($pipes[1]);
874 @fclose($pipes[2]);
875 $all_ok = false;
876 } else {
877 fclose($pipes[0]);
878 while (!feof($pipes[1])) {
879 $w = fgets($pipes[1]);
880 if ($w && $logit) $this->log("Output: ".trim($w));
881 }
882 fclose($pipes[1]);
883
884 while (!feof($pipes[2])) {
885 $last_error = fgets($pipes[2]);
886 if (!empty($last_error) && $logit) $this->log("Stderr output: ".trim($w));
887 }
888 fclose($pipes[2]);
889
890 $ret = proc_close($handle);
891 if ($ret !=0) {
892 if ($logit) $this->log("Binary zip: error (code: $ret)");
893 $all_ok = false;
894 }
895
896 }
897
898 } else {
899 if ($logit) $this->log("Error: proc_open failed");
900 $all_ok = false;
901 }
902
903 }
904
905 // Do we now actually have a working zip? Need to test the created object using PclZip
906 // If it passes, then remove dirs and then return $potzip;
907 $found_first = false;
908 $found_second = false;
909 if ($all_ok && file_exists($updraft_dir.'/binziptest/test.zip')) {
910 if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
911 $zip = new PclZip($updraft_dir.'/binziptest/test.zip');
912 $foundit = 0;
913 if (($list = $zip->listContent()) != 0) {
914 foreach ($list as $obj) {
915 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test.html' == $obj['stored_filename'] && $obj['size']==127) $found_first=true;
916 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test2.html' == $obj['stored_filename'] && $obj['size']==134) $found_second=true;
917 }
918 }
919 }
920 $this->remove_binzip_test_files($updraft_dir);
921 if ($found_first && $found_second) {
922 if ($logit) $this->log("Working binary zip found: $potzip");
923 if ($cacheit) $this->jobdata_set('binzip', $potzip);
924 return $potzip;
925 }
926
927 }
928 $this->remove_binzip_test_files($updraft_dir);
929 }
930 if ($cacheit) $this->jobdata_set('binzip', false);
931 return false;
932 }
933
934 function remove_binzip_test_files($updraft_dir) {
935 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test.html');
936 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test2.html');
937 @rmdir($updraft_dir.'/binziptest/subdir1/subdir2');
938 @rmdir($updraft_dir.'/binziptest/subdir1');
939 @unlink($updraft_dir.'/binziptest/test.zip');
940 @rmdir($updraft_dir.'/binziptest');
941 }
942
943 // This function is purely for timing - we just want to know the maximum run-time; not whether we have achieved anything during it
944 public function record_still_alive() {
945 // Update the record of maximum detected runtime on each run
946 $time_passed = $this->jobdata_get('run_times');
947 if (!is_array($time_passed)) $time_passed = array();
948
949 $time_this_run = microtime(true)-$this->opened_log_time;
950 $time_passed[$this->current_resumption] = $time_this_run;
951 $this->jobdata_set('run_times', $time_passed);
952
953 $resume_interval = $this->jobdata_get('resume_interval');
954 if ($time_this_run + 30 > $resume_interval) {
955 $new_interval = ceil($time_this_run + 30);
956 set_site_transient('updraft_initial_resume_interval', (int)$new_interval, 8*86400);
957 $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");
958 $this->jobdata_set('resume_interval', $new_interval);
959 }
960
961 }
962
963 public function something_useful_happened() {
964
965 $this->record_still_alive();
966
967 if (!$this->something_useful_happened) {
968 $useful_checkin = $this->jobdata_get('useful_checkin');
969 if (empty($useful_checkin) || $this->current_resumption > $useful_checkin) $this->jobdata_set('useful_checkin', $this->current_resumption);
970 }
971
972 $this->something_useful_happened = true;
973
974 if ($this->current_resumption >= 9 && false == $this->newresumption_scheduled) {
975 $this->log("This is resumption ".$this->current_resumption.", but meaningful activity is still taking place; so a new one will be scheduled");
976 // We just use max here to make sure we get a number at all
977 $resume_interval = max($this->jobdata_get('resume_interval'), 75);
978 // Don't consult the minimum here
979 // if (!is_numeric($resume_interval) || $resume_interval<300) { $resume_interval = 300; }
980 $schedule_for = time()+$resume_interval;
981 $this->newresumption_scheduled = $schedule_for;
982 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($this->current_resumption + 1, $this->nonce));
983 } else {
984 $this->reschedule_if_needed();
985 }
986 }
987
988 public function option_filter_get($which) {
989 global $wpdb;
990 $row = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $which));
991 // Has to be get_row instead of get_var because of funkiness with 0, false, null values
992 return (is_object($row)) ? $row->option_value : false;
993 }
994
995 // 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
996 public function get_backupable_file_entities($include_others = true, $full_info = false) {
997
998 $wp_upload_dir = wp_upload_dir();
999
1000 if ($full_info) {
1001 $arr = array(
1002 'plugins' => array('path' => WP_PLUGIN_DIR, 'description' => __('Plugins','updraftplus')),
1003 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes','updraftplus')),
1004 'uploads' => array('path' => $wp_upload_dir['basedir'], 'description' => __('Uploads','updraftplus'))
1005 );
1006 } else {
1007 $arr = array(
1008 'plugins' => WP_PLUGIN_DIR,
1009 'themes' => WP_CONTENT_DIR.'/themes',
1010 'uploads' => $wp_upload_dir['basedir']
1011 );
1012 }
1013
1014 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
1015
1016 // We then add 'others' on to the end
1017 if ($include_others) {
1018 if ($full_info) {
1019 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others','updraftplus'));
1020 } else {
1021 $arr['others'] = WP_CONTENT_DIR;
1022 }
1023 }
1024
1025 // Entries that should be added after 'others'
1026 $arr = apply_filters('updraft_backupable_file_entities_final', $arr, $full_info);
1027
1028 return $arr;
1029
1030 }
1031
1032 # 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)
1033 public function filter_updraft_backup_history($v) {
1034 global $wpdb;
1035 $row = $wpdb->get_row( $wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", 'updraft_backup_history' ) );
1036 if (is_object($row )) return maybe_unserialize($row->option_value);
1037 return false;
1038 }
1039
1040 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
1041 switch ($errno) {
1042 case 1: $e_type = 'E_ERROR'; break;
1043 case 2: $e_type = 'E_WARNING'; break;
1044 case 4: $e_type = 'E_PARSE'; break;
1045 case 8: $e_type = 'E_NOTICE'; break;
1046 case 16: $e_type = 'E_CORE_ERROR'; break;
1047 case 32: $e_type = 'E_CORE_WARNING'; break;
1048 case 64: $e_type = 'E_COMPILE_ERROR'; break;
1049 case 128: $e_type = 'E_COMPILE_WARNING'; break;
1050 case 256: $e_type = 'E_USER_ERROR'; break;
1051 case 512: $e_type = 'E_USER_WARNING'; break;
1052 case 1024: $e_type = 'E_USER_NOTICE'; break;
1053 case 2048: $e_type = 'E_STRICT'; break;
1054 case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break;
1055 case 8192: $e_type = 'E_DEPRECATED'; break;
1056 case 16384: $e_type = 'E_USER_DEPRECATED'; break;
1057 case 30719: $e_type = 'E_ALL'; break;
1058 default: $e_type = "E_UNKNOWN ($errno)"; break;
1059 }
1060
1061 if (!is_string($errstr)) $errstr = serialize($errstr);
1062
1063 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
1064
1065 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
1066
1067 }
1068
1069 public function php_error($errno, $errstr, $errfile, $errline) {
1070 if (0 == error_reporting()) return true;
1071 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
1072 $this->log($logline);
1073 # Pass it up the chain
1074 return false;
1075 }
1076
1077 public function backup_resume($resumption_no, $bnonce) {
1078
1079 set_error_handler(array($this, 'php_error'), E_ALL & ~E_STRICT);
1080
1081 $this->current_resumption = $resumption_no;
1082
1083 // 15 minutes
1084 @set_time_limit(900);
1085 @ignore_user_abort(true);
1086
1087 $runs_started = array();
1088 $time_now = microtime(true);
1089
1090 add_filter('pre_option_updraft_backup_history', array($this, 'filter_updraft_backup_history'));
1091
1092 // Restore state
1093 $resumption_extralog = '';
1094 $prev_resumption = $resumption_no - 1;
1095 $last_successful_resumption = -1;
1096 $job_type = 'backup';
1097
1098 if ($resumption_no > 0) {
1099
1100 $this->nonce = $bnonce;
1101 $this->backup_time = $this->jobdata_get('backup_time');
1102
1103 $this->job_time_ms = $this->jobdata_get('job_time_ms');
1104 # 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)
1105 $warnings = $this->jobdata_get('warnings');
1106 $this->logfile_open($bnonce);
1107 // 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
1108 if (is_array($warnings)) {
1109 foreach ($warnings as $warning) {
1110 $this->errors[] = array('level' => 'warning', 'message' => $warning);
1111 }
1112 }
1113
1114 $runs_started = $this->jobdata_get('runs_started');
1115 if (!is_array($runs_started)) $runs_started=array();
1116 $time_passed = $this->jobdata_get('run_times');
1117 if (!is_array($time_passed)) $time_passed = array();
1118 foreach ($time_passed as $run => $passed) {
1119 if (isset($runs_started[$run]) && $runs_started[$run] + $time_passed[$run] + 30 > $time_now) {
1120 $this->terminate_due_to_activity('check-in', round($time_now,1), round($runs_started[$run] + $time_passed[$run],1));
1121 }
1122 }
1123
1124 for ($i = 0; $i<=$prev_resumption; $i++) {
1125 if (isset($time_passed[$i])) $last_successful_resumption = $i;
1126 }
1127
1128 if (isset($time_passed[$prev_resumption])) {
1129 $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 1)."s";
1130 } else {
1131 $this->no_checkin_last_time = true;
1132 }
1133
1134
1135 # This is just a simple test to catch restorations of old backup sets where the backup includes a resumption of the backup job
1136 if ($time_now - $this->backup_time > 172800 && true == apply_filters('updraftplus_check_obsolete_backup', true, $time_now)) {
1137 $this->log("This backup task began over 2 days ago: aborting ($time_now, ".$this->backup_time.")");
1138 die;
1139 }
1140
1141 }
1142
1143 $this->last_successful_resumption = $last_successful_resumption;
1144
1145 $runs_started[$resumption_no] = $time_now;
1146 if (!empty($this->backup_time)) $this->jobdata_set('runs_started', $runs_started);
1147
1148 // Schedule again, to run in 5 minutes again, in case we again fail
1149 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
1150 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 100);
1151
1152 $btime = $this->backup_time;
1153
1154 $job_type = $this->jobdata_get('job_type');
1155
1156 do_action('updraftplus_resume_backup_'.$job_type);
1157
1158 $updraft_dir = $this->backups_dir_location();
1159
1160 $time_ago = time()-$btime;
1161
1162 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
1163
1164 // 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.
1165 // 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.
1166 if (($resumption_no >= 1 && 'finished' == $this->jobdata_get('jobstatus')) || ('backup' == $job_type && !empty($this->backup_is_already_complete))) {
1167 $this->log('Terminate: This backup job is already finished.');
1168 die;
1169 }
1170
1171 if ($resumption_no > 0 && isset($runs_started[$prev_resumption])) {
1172 $our_expected_start = $runs_started[$prev_resumption] + $resume_interval;
1173 # If the previous run increased the resumption time, then it is timed from the end of the previous run, not the start
1174 if (isset($time_passed[$prev_resumption]) && $time_passed[$prev_resumption]>0) $our_expected_start += $time_passed[$prev_resumption];
1175 $our_expected_start = apply_filters('updraftplus_expected_start', $our_expected_start, $job_type);
1176 # More than 12 minutes late?
1177 if ($time_now > $our_expected_start + 720) {
1178 $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));
1179 $this->log(__('Your website is visited infrequently and UpdraftPlus is not getting the resources it hoped for; please read this page:', 'updraftplus').' http://updraftplus.com/faqs/why-am-i-getting-warnings-about-my-site-not-having-enough-visitors/', 'warning', 'infrequentvisits');
1180 }
1181 }
1182
1183 $this->jobdata_set('current_resumption', $resumption_no);
1184
1185 $first_run = apply_filters('updraftplus_filerun_firstrun', 0);
1186
1187 // We just do this once, as we don't want to be in permanent conflict with the overlap detector
1188 if ($resumption_no >= $first_run + 8 && $resumption_no < $first_run + 15 && $resume_interval >= 300) {
1189
1190 // $time_passed is set earlier
1191 list($max_time, $timings_string, $run_times_known) = $this->max_time_passed($time_passed, $resumption_no - 1, $first_run);
1192
1193 # Do this on resumption 8, or the first time that we have 6 data points
1194 if (($first_run + 8 == $resumption_no && $run_times_known >= 6) || (6 == $run_times_known && !empty($time_passed[$prev_resumption]))) {
1195 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
1196 // 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
1197 if ($max_time + 52 < $resume_interval) {
1198 $resume_interval = round($max_time + 52);
1199 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
1200 $this->jobdata_set('resume_interval', $resume_interval);
1201 }
1202 }
1203
1204 }
1205
1206 // A different argument than before is needed otherwise the event is ignored
1207 $next_resumption = $resumption_no+1;
1208 if ($next_resumption < $first_run + 10) {
1209 if (true === $this->jobdata_get('one_shot')) {
1210 $this->log('We are in "one shot" mode - no resumptions will be scheduled');
1211 } else {
1212 $schedule_resumption = true;
1213 }
1214 } else {
1215 // 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
1216 $useful_checkin = $this->jobdata_get('useful_checkin');
1217 $last_resumption = $resumption_no-1;
1218
1219 if (empty($useful_checkin) || $useful_checkin < $last_resumption) {
1220 $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));
1221 } else {
1222 $schedule_resumption = true;
1223 }
1224 }
1225
1226 // Sanity check
1227 if (empty($this->backup_time)) {
1228 $this->log('The backup_time parameter appears to be empty (usually caused by resuming an already-complete backup).');
1229 return false;
1230 }
1231
1232 if (isset($schedule_resumption)) {
1233 $schedule_for = time()+$resume_interval;
1234 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
1235 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
1236 $this->newresumption_scheduled = $schedule_for;
1237 }
1238
1239 $backup_files = $this->jobdata_get('backup_files');
1240
1241 global $updraftplus_backup;
1242 // Bring in all the backup routines
1243 require_once(UPDRAFTPLUS_DIR.'/backup.php');
1244 $updraftplus_backup = new UpdraftPlus_Backup($backup_files, apply_filters('updraftplus_files_altered_since', -1, $job_type));
1245
1246 $undone_files = array();
1247
1248 if ('no' == $backup_files) {
1249 $this->log("This backup run is not intended for files - skipping");
1250 $our_files = array();
1251 } else {
1252
1253 // This should be always called; if there were no files in this run, it returns us an empty array
1254 $backup_array = $updraftplus_backup->resumable_backup_of_files($resumption_no);
1255
1256 // This save, if there was something, is then immediately picked up again
1257 if (is_array($backup_array)) {
1258 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
1259 $this->save_backup_history($backup_array);
1260 }
1261
1262 // Switch of variable name is purely vestigial
1263 $our_files = $backup_array;
1264 if (!is_array($our_files)) $our_files = array();
1265
1266 }
1267
1268 $backup_databases = $this->jobdata_get('backup_database');
1269
1270 if (!is_array($backup_databases)) $backup_databases = array('wp' => $backup_databases);
1271
1272 foreach ($backup_databases as $whichdb => $backup_database) {
1273
1274 if (is_array($backup_database)) {
1275 $dbinfo = $backup_database['dbinfo'];
1276 $backup_database = $backup_database['status'];
1277 } else {
1278 $dbinfo = array();
1279 }
1280
1281 $tindex = ('wp' == $whichdb) ? 'db' : 'db'.$whichdb;
1282
1283 if ('begun' == $backup_database || 'finished' == $backup_database || 'encrypted' == $backup_database) {
1284
1285 if ('wp' == $whichdb) {
1286 $db_descrip = 'WordPress DB';
1287 } else {
1288 if (!empty($dbinfo) && is_array($dbinfo) && !empty($dbinfo['host'])) {
1289 $db_descrip = "External DB $whichdb - ".$dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'];
1290 } else {
1291 $db_descrip = "External DB $whichdb - details appear to be missing";
1292 }
1293 }
1294
1295 if ('begun' == $backup_database) {
1296 if ($resumption_no > 0) {
1297 $this->log("Resuming creation of database dump ($db_descrip)");
1298 } else {
1299 $this->log("Beginning creation of database dump ($db_descrip)");
1300 }
1301 } elseif ('encrypted' == $backup_database) {
1302 $this->log("Database dump ($db_descrip): Creation and encryption were completed already");
1303 } else {
1304 $this->log("Database dump ($db_descrip): Creation was completed already");
1305 }
1306
1307 if ('wp' != $whichdb && (empty($dbinfo) || !is_array($dbinfo) || empty($dbinfo['host']))) {
1308 unset($backup_databases[$whichdb]);
1309 $this->jobdata_set('backup_database', $backup_databases);
1310 continue;
1311 }
1312
1313 $db_backup = $updraftplus_backup->backup_db($backup_database, $whichdb, $dbinfo);
1314
1315 if(is_array($our_files) && is_string($db_backup)) $our_files[$tindex] = $db_backup;
1316
1317 if ('encrypted' != $backup_database) {
1318 $backup_databases[$whichdb] = array('status' => 'finished', 'dbinfo' => $dbinfo);
1319 $this->jobdata_set('backup_database', $backup_databases);
1320 }
1321 } elseif ('no' == $backup_database) {
1322 $this->log("No database backup ($whichdb) - not part of this run");
1323 } else {
1324 $this->log("Unrecognised data when trying to ascertain if the database ($whichdb) was backed up (".serialize($backup_database).")");
1325 }
1326
1327 // Save this to our history so we can track backups for the retain feature
1328 $this->log("Saving backup history");
1329 // 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.
1330 $this->save_backup_history($our_files);
1331
1332 // Potentially encrypt the database if it is not already
1333 if ('no' != $backup_database && isset($our_files[$tindex]) && !preg_match("/\.crypt$/", $our_files[$tindex])) {
1334 $our_files[$tindex] = $updraftplus_backup->encrypt_file($our_files[$tindex]);
1335 // No need to save backup history now, as it will happen in a few lines time
1336 if (preg_match("/\.crypt$/", $our_files[$tindex])) {
1337 $backup_databases[$whichdb] = array('status' => 'encrypted', 'dbinfo' => $dbinfo);
1338 $this->jobdata_set('backup_database', $backup_databases);
1339 }
1340 }
1341
1342 if ('no' != $backup_database && isset($our_files[$tindex]) && file_exists($updraft_dir.'/'.$our_files[$tindex])) {
1343 $our_files[$tindex.'-size'] = filesize($updraft_dir.'/'.$our_files[$tindex]);
1344 $this->save_backup_history($our_files);
1345 }
1346
1347 }
1348
1349 $backupable_entities = $this->get_backupable_file_entities(true);
1350
1351 $checksums = array('sha1' => array());
1352
1353 # Queue files for upload
1354 foreach ($our_files as $key => $files) {
1355 // Only continue if the stored info was about a dump
1356 if (!isset($backupable_entities[$key]) && ('db' != substr($key, 0, 2) || '-size' == substr($key, -5, 5))) continue;
1357 if (is_string($files)) $files = array($files);
1358 foreach ($files as $findex => $file) {
1359 $sha = $this->jobdata_get('sha1-'.$key.$findex);
1360 if ($sha) $checksums['sha1'][$key.$findex] = $sha;
1361 $sha = $this->jobdata_get('sha1-'.$key.$findex.'.crypt');
1362 if ($sha) $checksums['sha1'][$key.$findex.".crypt"] = $sha;
1363 if ($this->is_uploaded($file)) {
1364 $this->log("$file: $key: This file has already been successfully uploaded");
1365 } elseif (is_file($updraft_dir.'/'.$file)) {
1366 if (!in_array($file, $undone_files)) {
1367 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
1368 $undone_files[$key.$findex] = $file;
1369 } else {
1370 $this->log("$file: $key: This file was already queued for upload (this condition should never be seen)");
1371 }
1372 } else {
1373 $this->log("$file: $key: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem ($updraft_dir/$file)");
1374 $this->uploaded_file($file, true);
1375 }
1376 }
1377 }
1378 $our_files['checksums'] = $checksums;
1379
1380 # Save again (now that we have checksums)
1381 $this->save_backup_history($our_files);
1382 do_action('updraft_final_backup_history', $our_files);
1383
1384 // We finished; so, low memory was not a problem
1385 $this->log_removewarning('lowram');
1386
1387 if (0 == count($undone_files)) {
1388 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1389 $this->log("There were no more files that needed uploading; backup job is complete");
1390 // No email, as the user probably already got one if something else completed the run
1391 $this->backup_finish($next_resumption, true, false, $resumption_no);
1392 restore_error_handler();
1393 return;
1394 } else {
1395 $this->log("Requesting upload of the files that have not yet been successfully uploaded (".count($undone_files).")");
1396 $updraftplus_backup->cloud_backup($undone_files);
1397 }
1398
1399 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1400 if (is_array($our_files)) $this->save_last_backup($our_files);
1401 $this->backup_finish($next_resumption, true, true, $resumption_no);
1402
1403 restore_error_handler();
1404
1405 }
1406
1407 public function max_time_passed($time_passed, $upto, $first_run) {
1408 $max_time = 0;
1409 $timings_string = "";
1410 $run_times_known=0;
1411 for ($i=$first_run; $i<=$upto; $i++) {
1412 $timings_string .= "$i:";
1413 if (isset($time_passed[$i])) {
1414 $timings_string .= round($time_passed[$i], 1).' ';
1415 $run_times_known++;
1416 if ($time_passed[$i] > $max_time) $max_time = round($time_passed[$i]);
1417 } else {
1418 $timings_string .= '? ';
1419 }
1420 }
1421 return array($max_time, $timings_string, $run_times_known);
1422 }
1423
1424 public function jobdata_getarray($non) {
1425 return get_site_option("updraft_jobdata_".$non, array());
1426 }
1427
1428 // This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
1429 private function jobdata_set_multi() {
1430 if (!is_array($this->jobdata)) $this->jobdata = array();
1431
1432 $args = func_num_args();
1433
1434 for ($i=1; $i<=$args/2; $i++) {
1435 $key = func_get_arg($i*2-2);
1436 $value = func_get_arg($i*2-1);
1437 $this->jobdata[$key] = $value;
1438 }
1439 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1440 }
1441
1442 public function jobdata_set($key, $value) {
1443 if (!is_array($this->jobdata)) {
1444 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce);
1445 if (!is_array($this->jobdata)) $this->jobdata = array();
1446 }
1447 $this->jobdata[$key] = $value;
1448 update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1449 }
1450
1451 public function jobdata_delete($key) {
1452 if (!is_array($this->jobdata)) {
1453 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce);
1454 if (!is_array($this->jobdata)) $this->jobdata = array();
1455 }
1456 unset($this->jobdata[$key]);
1457 update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1458 }
1459
1460 public function get_job_option($opt) {
1461 // These are meant to be read-only
1462 if (empty($this->jobdata['option_cache']) || !is_array($this->jobdata['option_cache'])) {
1463 if (!is_array($this->jobdata)) $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
1464 $this->jobdata['option_cache'] = array();
1465 }
1466 return (isset($this->jobdata['option_cache'][$opt])) ? $this->jobdata['option_cache'][$opt] : UpdraftPlus_Options::get_updraft_option($opt);
1467 }
1468
1469 public function jobdata_get($key, $default = null) {
1470 if (!is_array($this->jobdata)) {
1471 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
1472 if (!is_array($this->jobdata)) return $default;
1473 }
1474 return (isset($this->jobdata[$key])) ? $this->jobdata[$key] : $default;
1475 }
1476
1477 private function ensure_semaphore_exists($semaphore) {
1478 // Make sure the options for semaphores exist
1479 global $wpdb;
1480 $results = $wpdb->get_results("
1481 SELECT option_id
1482 FROM $wpdb->options
1483 WHERE option_name IN ('updraftplus_locked_$semaphore', 'updraftplus_unlocked_$semaphore')
1484 ");
1485 // Use of update_option() is correct here - since it is what is used in class-semaphore.php
1486 if (!count($results)) {
1487 update_option('updraftplus_unlocked_'.$semaphore, '1');
1488 update_option('updraftplus_last_lock_time_'.$semaphore, current_time('mysql', 1));
1489 update_option('updraftplus_semaphore_'.$semaphore, '0');
1490 }
1491 }
1492
1493 public function backup_files() {
1494 # Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
1495 $this->boot_backup(true, false);
1496 }
1497
1498 public function backup_database() {
1499 # Note that nothing will happen if the file backup had the same schedule
1500 $this->boot_backup(false, true);
1501 }
1502
1503 public function backup_all($options) {
1504 $skip_cloud = empty($options['nocloud']) ? false : true;
1505 $this->boot_backup(1, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
1506 }
1507
1508 public function backupnow_files($options) {
1509 $skip_cloud = empty($options['nocloud']) ? false : true;
1510 $this->boot_backup(1, 0, false, false, ($skip_cloud) ? 'none' : false, $options);
1511 }
1512
1513 public function backupnow_database($options) {
1514 $skip_cloud = empty($options['nocloud']) ? false : true;
1515 $this->boot_backup(0, 1, false, false, ($skip_cloud) ? 'none' : false, $options);
1516 }
1517
1518 // This procedure initiates a backup run
1519 // $backup_files/$backup_database: true/false = yes/no (over-write allowed); 1/0 = yes/no (force)
1520 public function boot_backup($backup_files, $backup_database, $restrict_files_to_override = false, $one_shot = false, $service = false, $options = array()) {
1521
1522 @ignore_user_abort(true);
1523 @set_time_limit(900);
1524
1525 // Generate backup information
1526 $this->backup_time_nonce();
1527 // The current_resumption is consulted within logfile_open()
1528 $this->current_resumption = 0;
1529 $this->logfile_open($this->nonce);
1530
1531 if (!is_file($this->logfile_name)) {
1532 $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.');
1533 $this->log(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.','updraftplus'), 'error');
1534 return false;
1535 }
1536
1537 // Some house-cleaning
1538 $this->clean_temporary_files();
1539 // Log some information that may be helpful
1540 $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').")");
1541
1542 if (false === $one_shot && is_bool($backup_database)) {
1543 # If the files and database schedules are the same, and if this the file one, then we rope in database too.
1544 # On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
1545 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' )) {
1546 $backup_database = ($backup_files == true) ? true : false;
1547 }
1548 $this->log("Processed schedules. Tasks now: Backup files: $backup_files Backup DB: $backup_database");
1549 }
1550
1551 $semaphore = (($backup_files) ? 'f' : '') . (($backup_database) ? 'd' : '');
1552 $this->ensure_semaphore_exists($semaphore);
1553
1554 if (false == apply_filters('updraftplus_boot_backup', true, $backup_files, $backup_database, $one_shot)) {
1555 $updraftplus->log("Backup aborted (via filter)");
1556 return false;
1557 }
1558
1559 if (!is_string($service) && !is_array($service)) $service = UpdraftPlus_Options::get_updraft_option('updraft_service');
1560 $service = $this->just_one($service);
1561 if (is_string($service)) $service = array($service);
1562 if (!is_array($service)) $service = array('none');
1563
1564 $option_cache = array();
1565 foreach ($service as $serv) {
1566 if ('' == $serv || 'none' == $serv) continue;
1567 include_once(UPDRAFTPLUS_DIR.'/methods/'.$serv.'.php');
1568 $cclass = 'UpdraftPlus_BackupModule_'.$serv;
1569 $obj = new $cclass;
1570 if (method_exists($cclass, 'get_credentials')) {
1571 $opts = $obj->get_credentials();
1572 if (is_array($opts)) {
1573 foreach ($opts as $opt) $option_cache[$opt] = UpdraftPlus_Options::get_updraft_option($opt);
1574 }
1575 }
1576 }
1577 $option_cache = apply_filters('updraftplus_job_option_cache', $option_cache);
1578
1579 # If nothing to be done, then just finish
1580 if (!$backup_files && !$backup_database) return $this->backup_finish(1, false, false, 0);
1581
1582 require_once(UPDRAFTPLUS_DIR.'/includes/class-semaphore.php');
1583 $this->semaphore = UpdraftPlus_Semaphore::factory();
1584 $this->semaphore->lock_name = $semaphore;
1585 $this->log('Requesting semaphore lock ('.$semaphore.')');
1586 if (!$this->semaphore->lock()) {
1587 $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)');
1588 return;
1589 }
1590
1591 // Allow the resume interval to be more than 300 if last time we know we went beyond that - but never more than 600
1592 $resume_interval = (int)min(max(300, get_site_transient('updraft_initial_resume_interval')), 600);
1593 # 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)
1594 delete_site_transient('updraft_initial_resume_interval');
1595
1596 $job_file_entities = array();
1597 if ($backup_files) {
1598 $possible_backups = $this->get_backupable_file_entities(true);
1599 foreach ($possible_backups as $youwhat => $whichdir) {
1600 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))) {
1601 // The 0 indicates the zip file index
1602 $job_file_entities[$youwhat] = array(
1603 'index' => 0
1604 );
1605 }
1606 }
1607 }
1608
1609 $followups_allowed = (((!$one_shot && defined('DOING_CRON') && DOING_CRON)) || (defined('UPDRAFTPLUS_FOLLOWUPS_ALLOWED') && UPDRAFTPLUS_FOLLOWUPS_ALLOWED));
1610
1611 $initial_jobdata = array(
1612 'resume_interval', $resume_interval,
1613 'job_type', 'backup',
1614 'jobstatus', 'begun',
1615 'backup_time', $this->backup_time,
1616 'job_time_ms', $this->job_time_ms,
1617 'service', $service,
1618 'split_every', max(intval(UpdraftPlus_Options::get_updraft_option('updraft_split_every', 500)), UPDRAFTPLUS_SPLIT_MIN),
1619 'maxzipbatch', 26214400, #25Mb
1620 'job_file_entities', $job_file_entities,
1621 'option_cache', $option_cache,
1622 'uploaded_lastreset', 9,
1623 'one_shot', $one_shot,
1624 'followsups_allowed', $followups_allowed
1625 );
1626
1627 if ($one_shot) update_site_option('updraft_oneshotnonce', $this->nonce);
1628
1629 // Save what *should* be done, to make it resumable from this point on
1630 if ($backup_database) {
1631 $dbs = apply_filters('updraft_backup_databases', array('wp' => 'begun'));
1632 if (is_array($dbs)) {
1633 foreach ($dbs as $key => $db) {
1634 if ('wp' != $key && (!is_array($db) || empty($db['dbinfo']) || !is_array($db['dbinfo']) || empty($db['dbinfo']['host']))) unset($dbs[$key]);
1635 }
1636 }
1637 } else {
1638 $dbs = "no";
1639 }
1640
1641 array_push($initial_jobdata, 'backup_database', $dbs);
1642 array_push($initial_jobdata, 'backup_files', (($backup_files) ? 'begun' : 'no'));
1643
1644 if (is_array($options) && !empty($options['label'])) array_push($initial_jobdata, 'label', $options['label']);
1645
1646 // Use of jobdata_set_multi saves around 200ms
1647 call_user_func_array(array($this, 'jobdata_set_multi'), apply_filters('updraftplus_initial_jobdata', $initial_jobdata));
1648
1649 // Everything is set up; now go
1650 $this->backup_resume(0, $this->nonce);
1651
1652 if ($one_shot) delete_site_option('updraft_oneshotnonce');
1653
1654 }
1655
1656 private function backup_finish($cancel_event, $do_cleanup, $allow_email, $resumption_no) {
1657
1658 if (!empty($this->semaphore)) $this->semaphore->unlock();
1659
1660 $delete_jobdata = false;
1661
1662 // 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)
1663
1664 // 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.
1665 if (0 == $this->error_count()) {
1666 if ($do_cleanup) {
1667 $this->log("There were no errors in the uploads, so the 'resume' event ($cancel_event) is being unscheduled");
1668 # 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)
1669 $this->jobdata_set('jobstatus', 'finished');
1670 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
1671 # 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
1672 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+1, $this->nonce));
1673 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+2, $this->nonce));
1674 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+3, $this->nonce));
1675 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event+4, $this->nonce));
1676 $delete_jobdata = true;
1677 }
1678 } else {
1679 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
1680 $this->jobdata_set('jobstatus', 'resumingforerrors');
1681 }
1682
1683 // Send the results email if appropriate, which means:
1684 // - The caller allowed it (which is not the case in an 'empty' run)
1685 // - And: An email address was set (which must be so in email mode)
1686 // And one of:
1687 // - Debug mode
1688 // - There were no errors (which means we completed and so this is the final run - time for the final report)
1689 // - It was the tenth resumption; everything failed
1690
1691 $send_an_email = false;
1692 # Save the jobdata's state for the reporting - because it might get changed (e.g. incremental backup is scheduled)
1693 $jobdata_as_was = $this->jobdata;
1694
1695 // Make sure that the final status is shown
1696 if (0 == $this->error_count()) {
1697 $send_an_email = true;
1698 if (0 == $this->error_count('warning')) {
1699 $final_message = __('The backup apparently succeeded and is now complete', 'updraftplus');
1700 # 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
1701 if ('The backup apparently succeeded and is now complete' != $final_message) {
1702 $this->log('The backup apparently succeeded and is now complete');
1703 }
1704 } else {
1705 $final_message = __('The backup apparently succeeded (with warnings) and is now complete','updraftplus');
1706 if ('The backup apparently succeeded (with warnings) and is now complete' != $final_message) {
1707 $this->log('The backup apparently succeeded (with warnings) and is now complete');
1708 }
1709 }
1710 if ($do_cleanup) $delete_jobdata = apply_filters('updraftplus_backup_complete', $delete_jobdata);
1711 } elseif (false == $this->newresumption_scheduled) {
1712 $send_an_email = true;
1713 $final_message = __('The backup attempt has finished, apparently unsuccessfully', 'updraftplus');
1714 } else {
1715 // There are errors, but a resumption will be attempted
1716 $final_message = __('The backup has not finished; a resumption is scheduled', 'updraftplus');
1717 }
1718
1719 // Now over-ride the decision to send an email, if needed
1720 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
1721 $send_an_email = true;
1722 $this->log("An email has been scheduled for this job, because we are in debug mode");
1723 }
1724
1725 $email = UpdraftPlus_Options::get_updraft_option('updraft_email');
1726
1727 // If there's no email address, or the set was empty, that is the final over-ride: don't send
1728 if (!$allow_email) {
1729 $send_an_email = false;
1730 $this->log("No email will be sent - this backup set was empty.");
1731 } elseif (empty($email)) {
1732 $send_an_email = false;
1733 $this->log("No email will/can be sent - the user has not configured an email address.");
1734 }
1735
1736 global $updraftplus_backup;
1737 if ($send_an_email) $updraftplus_backup->send_results_email($final_message, $jobdata_as_was);
1738
1739 # Make sure this is the final message logged (so it remains on the dashboard)
1740 $this->log($final_message);
1741
1742 @fclose($this->logfile_handle);
1743
1744 // This is left until last for the benefit of the front-end UI, which then gets maximum chance to display the 'finished' status
1745 if ($delete_jobdata) delete_site_option('updraft_jobdata_'.$this->nonce);
1746
1747 }
1748
1749 public function error_count($level = 'error') {
1750 $count = 0;
1751 foreach ($this->errors as $err) {
1752 if (('error' == $level && (is_string($err) || is_wp_error($err))) || (is_array($err) && $level == $err['level']) ) { $count++; }
1753 }
1754 return $count;
1755 }
1756
1757 public function list_errors() {
1758 echo '<ul style="list-style: disc inside;">';
1759 foreach ($this->errors as $err) {
1760 if (is_wp_error($err)) {
1761 foreach ($err->get_error_messages() as $msg) {
1762 echo '<li>'.htmlspecialchars($msg).'<li>';
1763 }
1764 } elseif (is_array($err) && 'error' == $err['level']) {
1765 echo "<li>".htmlspecialchars($err['message'])."</li>";
1766 } elseif (is_string($err)) {
1767 echo "<li>".htmlspecialchars($err)."</li>";
1768 } else {
1769 print "<li>".print_r($err,true)."</li>";
1770 }
1771 }
1772 echo '</ul>';
1773 }
1774
1775 private function save_last_backup($backup_array) {
1776 $success = ($this->error_count() == 0) ? 1 : 0;
1777 $last_backup = array('backup_time'=>$this->backup_time, 'backup_array'=>$backup_array, 'success'=>$success, 'errors'=>$this->errors, 'backup_nonce' => $this->nonce);
1778 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup, false);
1779 }
1780
1781 # $handle must be either false or a WPDB class (or extension thereof). Other options are not yet fully supported.
1782 public function check_db_connection($handle = false, $logit = false, $reschedule = false) {
1783
1784 $type = false;
1785 if (false === $handle || is_a($handle, 'wpdb')) {
1786 $type='wpdb';
1787 } elseif (is_resource($handle)) {
1788 # Expected: string(10) "mysql link"
1789 $type=get_resource_type($handle);
1790 } elseif (is_object($handle) && is_a($handle, 'mysqli')) {
1791 $type='mysqli';
1792 }
1793
1794 if (false === $type) return -1;
1795
1796 $db_connected = -1;
1797
1798 if ('mysql link' == $type || 'mysqli' == $type) {
1799 if ('mysql link' == $type && @mysql_ping($handle)) return true;
1800 if ('mysqli' == $type && @mysqli_ping($handle)) return true;
1801
1802 for ( $tries = 1; $tries <= 5; $tries++ ) {
1803 # to do, if ever needed
1804 // if ( $this->db_connect( false ) ) return true;
1805 // sleep( 1 );
1806 }
1807
1808 } elseif ('wpdb' == $type) {
1809 if (false === $handle || (is_object($handle) && 'wpdb' == get_class($handle))) {
1810 global $wpdb;
1811 $handle = $wpdb;
1812 }
1813 if (method_exists($handle, 'check_connection')) {
1814 if (!$handle->check_connection(false)) {
1815 if ($logit) $this->log("The database went away, and could not be reconnected to");
1816 # Almost certainly a no-op
1817 if ($reschedule) $this->reschedule(60);
1818 $db_connected = false;
1819 } else {
1820 $db_connected = true;
1821 }
1822 }
1823 }
1824
1825 return $db_connected;
1826
1827 }
1828
1829 // This should be called whenever a file is successfully uploaded
1830 public function uploaded_file($file, $force = false) {
1831
1832 global $updraftplus_backup;
1833
1834 $db_connected = $this->check_db_connection(false, true, true);
1835
1836 $service = (empty($updraftplus_backup->current_service)) ? '' : $updraftplus_backup->current_service;
1837 $shash = $service.'-'.md5($file);
1838
1839 $this->jobdata_set("uploaded_".$shash, 'yes');
1840
1841 if ($force || !empty($updraftplus_backup->last_service)) {
1842 $hash = md5($file);
1843 $this->log("Recording as successfully uploaded: $file ($hash)");
1844 $this->jobdata_set('uploaded_lastreset', $this->current_resumption);
1845 $this->jobdata_set("uploaded_".$hash, 'yes');
1846 } else {
1847 $this->log("Recording as successfully uploaded: $file (".$updraftplus_backup->current_service.", more services to follow)");
1848 }
1849
1850 $upload_status = $this->jobdata_get('uploading_substatus');
1851 if (is_array($upload_status) && isset($upload_status['i'])) {
1852 $upload_status['i']++;
1853 $upload_status['p']=0;
1854 $this->jobdata_set('uploading_substatus', $upload_status);
1855 }
1856
1857 # 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
1858 if (false === $db_connected) {
1859 $updraftplus->record_still_alive();
1860 die;
1861 }
1862
1863 // Delete local files immediately if the option is set
1864 // Where we are only backing up locally, only the "prune" function should do deleting
1865 if (!empty($updraftplus_backup->last_service) && ($this->jobdata_get('service') !== '' && ((is_array($this->jobdata_get('service')) && count($this->jobdata_get('service')) >0) || (is_string($this->jobdata_get('service')) && $this->jobdata_get('service') !== 'none')))) {
1866 $this->delete_local($file);
1867 }
1868 }
1869
1870 public function is_uploaded($file, $service = '') {
1871 $hash = $service.(('' == $service) ? '' : '-').md5($file);
1872 return ($this->jobdata_get("uploaded_$hash") === "yes") ? true : false;
1873 }
1874
1875 private function delete_local($file) {
1876 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local')) {
1877 $log = "Deleting local file: $file: ";
1878 $fullpath = $this->backups_dir_location().'/'.$file;
1879 $deleted = unlink($fullpath);
1880 $this->log($log.(($deleted) ? 'OK' : 'failed'));
1881 return $deleted;
1882 }
1883 return true;
1884 }
1885
1886 // This function is not needed for backup success, according to the design, but it helps with efficient scheduling
1887 private function reschedule_if_needed() {
1888 // If nothing is scheduled, then return
1889 if (empty($this->newresumption_scheduled)) return;
1890 $time_now = time();
1891 $time_away = $this->newresumption_scheduled - $time_now;
1892 // 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)
1893 if ($time_away >1 && $time_away <= 45) {
1894 $this->log('The scheduled resumption is within 45 seconds - will reschedule');
1895 // Push 45 seconds into the future
1896 // $this->reschedule(60);
1897 // Increase interval generally by 45 seconds, on the assumption that our prior estimates were innaccurate (i.e. not just 45 seconds *this* time)
1898 $this->increase_resume_and_reschedule(45);
1899 }
1900 }
1901
1902 public function reschedule($how_far_ahead) {
1903 // Reschedule - remove presently scheduled event
1904 $next_resumption = $this->current_resumption + 1;
1905 wp_clear_scheduled_hook('updraft_backup_resume', array($next_resumption, $this->nonce));
1906 // Add new event
1907 # This next line may be too cautious; but until 14-Aug-2014, it was 300. Also, note that on the current coding of increase_resume_and_reschedule(), any time that we arrive through there, the value is already at least 300. So, this minimum check only kicks in if we came through another route.
1908 if ($how_far_ahead < 180) $how_far_ahead=180;
1909 $schedule_for = time() + $how_far_ahead;
1910 $this->log("Rescheduling resumption $next_resumption: moving to $how_far_ahead seconds from now ($schedule_for)");
1911 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $this->nonce));
1912 $this->newresumption_scheduled = $schedule_for;
1913 }
1914
1915 private function increase_resume_and_reschedule($howmuch = 120, $force_schedule = false) {
1916
1917 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 300);
1918
1919 if (empty($this->newresumption_scheduled) && $force_schedule) {
1920 $this->log("A new resumption will be scheduled to prevent the job ending");
1921 }
1922
1923 $new_resume = $resume_interval + $howmuch;
1924 # 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
1925 if ($this->opened_log_time > 100 && microtime(true)-$this->opened_log_time > $new_resume) {
1926 $new_resume = ceil(microtime(true)-$this->opened_log_time)+45;
1927 $howmuch = $new_resume-$resume_interval;
1928 }
1929
1930 # 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.
1931 # Actually, let's not try this yet. I think it is safe, but think there is a more conservative solution available.
1932 #$how_far_ahead = min($new_resume, 600);
1933 $how_far_ahead = $new_resume;
1934 # If it is very long-running, then that would normally be known soon.
1935 # 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.
1936 if (1 >= $this->current_resumption && $new_resume > 720) $how_far_ahead = 600;
1937
1938 if (!empty($this->newresumption_scheduled) || $force_schedule) $this->reschedule($how_far_ahead);
1939 $this->jobdata_set('resume_interval', $new_resume);
1940
1941 $this->log("To decrease the likelihood of overlaps, increasing resumption interval to: $resume_interval + $howmuch = $new_resume");
1942 }
1943
1944 // For detecting another run, and aborting if one was found
1945 public function check_recent_modification($file) {
1946 if (file_exists($file)) {
1947 $time_mod = (int)@filemtime($file);
1948 $time_now = time();
1949 if ($time_mod>100 && ($time_now-$time_mod)<30) {
1950 $this->terminate_due_to_activity($file, $time_now, $time_mod);
1951 }
1952 }
1953 }
1954
1955 public function get_exclude($whichone) {
1956 if ('uploads' == $whichone) {
1957 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE));
1958 } elseif ('others' == $whichone) {
1959 $exclude = explode(',', UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
1960 } else {
1961 $exclude = apply_filters('updraftplus_include_'.$whichone.'_exclude', array());
1962 }
1963 return (empty($exclude) || !is_array($exclude)) ? array() : $exclude;
1964 }
1965
1966 public function really_is_writable($dir) {
1967 // Suppress warnings, since if the user is dumping warnings to screen, then invalid JavaScript results and the screen breaks.
1968 if (!@is_writable($dir)) return false;
1969 // Found a case - GoDaddy server, Windows, PHP 5.2.17 - where is_writable returned true, but writing failed
1970 $rand_file = "$dir/test-".md5(rand().time()).".txt";
1971 while (file_exists($rand_file)) {
1972 $rand_file = "$dir/test-".md5(rand().time()).".txt";
1973 }
1974 $ret = @file_put_contents($rand_file, 'testing...');
1975 @unlink($rand_file);
1976 return ($ret > 0);
1977 }
1978
1979 public function backup_uploads_dirlist($logit = false) {
1980 # Create an array of directories to be skipped
1981 # Make the values into the keys
1982 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_uploads_exclude', UPDRAFT_DEFAULT_UPLOADS_EXCLUDE);
1983 if ($logit) $this->log("Exclusion option setting (uploads): ".$exclude);
1984 $skip = array_flip(preg_split("/,/", $exclude));
1985 $wp_upload_dir = wp_upload_dir();
1986 $uploads_dir = $wp_upload_dir['basedir'];
1987 return $this->compile_folder_list_for_backup($uploads_dir, array(), $skip);
1988 }
1989
1990 public function backup_others_dirlist($logit = false) {
1991 # Create an array of directories to be skipped
1992 # Make the values into the keys
1993 $exclude = UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE);
1994 if ($logit) $this->log("Exclusion option setting (others): ".$exclude);
1995 $skip = array_flip(preg_split("/,/", $exclude));
1996 $file_entities = $this->get_backupable_file_entities(false);
1997
1998 # Keys = directory names to avoid; values = the label for that directory (used only in log files)
1999 #$avoid_these_dirs = array_flip($file_entities);
2000 $avoid_these_dirs = array();
2001 foreach ($file_entities as $type => $dirs) {
2002 if (is_string($dirs)) {
2003 $avoid_these_dirs[$dirs] = $type;
2004 } elseif (is_array($dirs)) {
2005 foreach ($dirs as $dir) {
2006 $avoid_these_dirs[$dir] = $type;
2007 }
2008 }
2009 }
2010 return $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $avoid_these_dirs, $skip);
2011 }
2012
2013 // Add backquotes to tables and db-names in SQL queries. Taken from phpMyAdmin.
2014 public function backquote($a_name) {
2015 if (!empty($a_name) && $a_name != '*') {
2016 if (is_array($a_name)) {
2017 $result = array();
2018 reset($a_name);
2019 while(list($key, $val) = each($a_name))
2020 $result[$key] = '`'.$val.'`';
2021 return $result;
2022 } else {
2023 return '`'.$a_name.'`';
2024 }
2025 } else {
2026 return $a_name;
2027 }
2028 }
2029
2030 public function strip_dirslash($string) {
2031 return preg_replace('#/+(,|$)#', '$1', $string);
2032 }
2033
2034 public function remove_empties($list) {
2035 if (!is_array($list)) return $list;
2036 foreach ($list as $ind => $entry) {
2037 if (empty($entry)) unset($list[$ind]);
2038 }
2039 return $list;
2040 }
2041
2042 // 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.
2043 public function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
2044
2045 // 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.
2046
2047 $dirlist = array();
2048 $added = 0;
2049
2050 $this->log('Looking for candidates to back up in: '.$backup_from_inside_dir);
2051 $updraft_dir = $this->backups_dir_location();
2052
2053 if (is_file($backup_from_inside_dir)) {
2054 array_push($dirlist, $backup_from_inside_dir);
2055 $added++;
2056 $this->log("finding files: $backup_from_inside_dir: adding to list ($added)");
2057 } elseif ($handle = opendir($backup_from_inside_dir)) {
2058
2059 while (false !== ($entry = readdir($handle))) {
2060 // $candidate: full path; $entry = one-level
2061 $candidate = $backup_from_inside_dir.'/'.$entry;
2062 if ($entry != "." && $entry != "..") {
2063 if (isset($avoid_these_dirs[$candidate])) {
2064 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
2065 } elseif ($candidate == $updraft_dir) {
2066 $this->log("finding files: $entry: skipping: this is the updraft directory");
2067 } elseif (isset($skip_these_dirs[$entry])) {
2068 $this->log("finding files: $entry: skipping: excluded by options");
2069 } else {
2070 $add_to_list = true;
2071 // Now deal with entries in $skip_these_dirs ending in * or starting with *
2072 foreach ($skip_these_dirs as $skip => $sind) {
2073 if ('*' == substr($skip, -1, 1) && '*' == substr($skip, 0, 1) && strlen($skip) > 2) {
2074 if (strpos($entry, substr($skip, 1, strlen($skip-2))) !== false) {
2075 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2076 $add_to_list = false;
2077 }
2078 } elseif ('*' == substr($skip, -1, 1) && strlen($skip) > 1) {
2079 if (substr($entry, 0, strlen($skip)-1) == substr($skip, 0, strlen($skip)-1)) {
2080 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2081 $add_to_list = false;
2082 }
2083 } elseif ('*' == substr($skip, 0, 1) && strlen($skip) > 1) {
2084 if (strlen($entry) >= strlen($skip)-1 && substr($entry, (strlen($skip)-1)*-1) == substr($skip, 1)) {
2085 $this->log("finding files: $entry: skipping: excluded by options (glob)");
2086 $add_to_list = false;
2087 }
2088 }
2089 }
2090 if ($add_to_list) {
2091 array_push($dirlist, $candidate);
2092 $added++;
2093 $skip_dblog = ($added > 50 && 0 != $added % 100);
2094 $this->log("finding files: $entry: adding to list ($added)", 'notice', false, $skip_dblog);
2095 }
2096 }
2097 }
2098 }
2099 @closedir($handle);
2100 } else {
2101 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
2102 $this->log(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir, 'error');
2103 }
2104
2105 return $dirlist;
2106
2107 }
2108
2109 private function save_backup_history($backup_array) {
2110 if(is_array($backup_array)) {
2111 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
2112 $backup_history = (is_array($backup_history)) ? $backup_history : array();
2113 $backup_array['nonce'] = $this->nonce;
2114 $backup_array['service'] = $this->jobdata_get('service');
2115 if ('' != ($label = $this->jobdata_get('label', ''))) $backup_array['label'] = $label;
2116 $backup_history[$this->backup_time] = $backup_array;
2117 UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history, false);
2118 } else {
2119 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
2120 $this->log(__('Could not save backup history because we have no backup array. Backup probably failed.','updraftplus'), 'error');
2121 }
2122 }
2123
2124 public function is_db_encrypted($file) {
2125 return preg_match('/\.crypt$/i', $file);
2126 }
2127
2128 public function get_backup_history($timestamp = false) {
2129 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
2130 // In fact, it looks like the line below actually *introduces* a race condition
2131 //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
2132 // global $wpdb;
2133 // $backup_history = @unserialize($wpdb->get_var($wpdb->prepare("SELECT option_value from $wpdb->options WHERE option_name='updraft_backup_history'")));
2134 if(is_array($backup_history)) {
2135 krsort($backup_history); //reverse sort so earliest backup is last on the array. Then we can array_pop.
2136 } else {
2137 $backup_history = array();
2138 }
2139 if (!$timestamp) return $backup_history;
2140 return (isset($backup_history[$timestamp])) ? $backup_history[$timestamp] : array();
2141 }
2142
2143 public function terminate_due_to_activity($file, $time_now, $time_mod) {
2144 # We check-in, to avoid 'no check in last time!' detectors firing
2145 $this->record_still_alive();
2146 $file_size = file_exists($file) ? round(filesize($file)/1024,1). 'Kb' : 'n/a';
2147 $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.");
2148 $this->increase_resume_and_reschedule(120, true);
2149 if (!defined('UPDRAFTPLUS_ALLOW_RECENT_ACTIVITY') || true != UPDRAFTPLUS_ALLOW_RECENT_ACTIVITY) die;
2150 }
2151
2152 # Replace last occurence
2153 public function str_lreplace($search, $replace, $subject) {
2154 $pos = strrpos($subject, $search);
2155 if($pos !== false) $subject = substr_replace($subject, $replace, $pos, strlen($search));
2156 return $subject;
2157 }
2158
2159 public function str_replace_once($needle, $replace, $haystack) {
2160 $pos = strpos($haystack,$needle);
2161 return ($pos !== false) ? substr_replace($haystack,$replace,$pos,strlen($needle)) : $haystack;
2162 }
2163
2164 /*
2165 This function is both the backup scheduler and a filter callback for saving the option.
2166 It is called in the register_setting for the updraft_interval, which means when the
2167 admin settings are saved it is called.
2168 */
2169 public function schedule_backup($interval) {
2170 $previous_time = wp_next_scheduled('updraft_backup');
2171
2172 // Clear schedule so that we don't stack up scheduled backups
2173 wp_clear_scheduled_hook('updraft_backup');
2174 if ('manual' == $interval) return 'manual';
2175
2176 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval');
2177
2178 $valid_schedules = wp_get_schedules();
2179 if (empty($valid_schedules[$interval])) $interval = 'daily';
2180
2181 // 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.
2182 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
2183 $first_time = apply_filters('updraftplus_schedule_firsttime_files', $default_time);
2184
2185 wp_schedule_event($first_time, $interval, 'updraft_backup');
2186
2187 return $interval;
2188 }
2189
2190 public function schedule_backup_database($interval) {
2191 $previous_time = wp_next_scheduled('updraft_backup_database');
2192
2193 // Clear schedule so that we don't stack up scheduled backups
2194 wp_clear_scheduled_hook('updraft_backup_database');
2195 if ('manual' == $interval) return 'manual';
2196
2197 $previous_interval = UpdraftPlus_Options::get_updraft_option('updraft_interval_database');
2198
2199 $valid_schedules = wp_get_schedules();
2200 if (empty($valid_schedules[$interval])) $interval = 'daily';
2201
2202 // 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.
2203 $default_time = ($interval == $previous_interval && $previous_time>0) ? $previous_time : time()+120;
2204
2205 $first_time = apply_filters('updraftplus_schedule_firsttime_db', $default_time);
2206 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
2207
2208 return $interval;
2209 }
2210
2211 // Acts as a WordPress options filter
2212 public function googledrive_checkchange($google) {
2213 $opts = UpdraftPlus_Options::get_updraft_option('updraft_googledrive');
2214 if (!is_array($google)) return $opts;
2215 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
2216 if (!empty($opts['token']) && $old_client_id != $google['clientid']) {
2217 require_once(UPDRAFTPLUS_DIR.'/methods/googledrive.php');
2218 add_action('http_request_args', array($this, 'modify_http_options'));
2219 UpdraftPlus_BackupModule_googledrive::gdrive_auth_revoke(false);
2220 remove_action('http_request_args', array($this, 'modify_http_options'));
2221 $google['token'] = '';
2222 unset($opts['ownername']);
2223 }
2224 foreach ($google as $key => $value) { $opts[$key] = $value; }
2225 if (isset($opts['folder'])) {
2226 $opts['folder'] = apply_filters('updraftplus_options_googledrive_foldername', 'UpdraftPlus', $opts['folder']);
2227 unset($opts['parentid']);
2228 }
2229 return $opts;
2230 }
2231
2232 public function ftp_sanitise($ftp) {
2233 if (is_array($ftp) && !empty($ftp['host']) && preg_match('#ftp(es|s)?://(.*)#i', $ftp['host'], $matches)) {
2234 $ftp['host'] = untrailingslashit($matches[2]);
2235 }
2236 return $ftp;
2237 }
2238
2239 public function s3_sanitise($s3) {
2240 if (is_array($s3) && !empty($s3['path']) && '/' == substr($s3['path'], 0, 1)) {
2241 $s3['path'] = substr($s3['path'], 1);
2242 }
2243 return $s3;
2244 }
2245
2246 // Acts as a WordPress options filter
2247 public function bitcasa_checkchange($bitcasa) {
2248 $opts = UpdraftPlus_Options::get_updraft_option('updraft_bitcasa');
2249 if (!is_array($opts)) $opts = array();
2250 if (!is_array($bitcasa)) return $opts;
2251 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
2252 if (!empty($opts['token']) && $old_client_id != $bitcasa['clientid']) {
2253 unset($opts['token']);
2254 unset($opts['ownername']);
2255 }
2256 foreach ($bitcasa as $key => $value) { $opts[$key] = $value; }
2257 return $opts;
2258 }
2259
2260 // Acts as a WordPress options filter
2261 public function copycom_checkchange($copycom) {
2262 $opts = UpdraftPlus_Options::get_updraft_option('updraft_copycom');
2263 if (!is_array($opts)) $opts = array();
2264 if (!is_array($copycom)) return $opts;
2265 $old_client_id = (empty($opts['clientid'])) ? '' : $opts['clientid'];
2266 if (!empty($opts['token']) && $old_client_id != $copycom['clientid']) {
2267 unset($opts['token']);
2268 unset($opts['tokensecret']);
2269 unset($opts['ownername']);
2270 }
2271 foreach ($copycom as $key => $value) { $opts[$key] = $value; }
2272 return $opts;
2273 }
2274
2275 // Acts as a WordPress options filter
2276 public function dropbox_checkchange($dropbox) {
2277 $opts = UpdraftPlus_Options::get_updraft_option('updraft_dropbox');
2278 if (!is_array($opts)) $opts = array();
2279 if (!is_array($dropbox)) return $opts;
2280 foreach ($dropbox as $key => $value) { $opts[$key] = $value; }
2281 if (preg_match('#^https?://(www.)dropbox\.com/home/Apps/UpdraftPlus([^/]*)/(.*)$#i', $opts['folder'], $matches)) $opts['folder'] = $matches[3];
2282 return $opts;
2283 }
2284
2285 public function remove_local_directory($dir, $contents_only = false) {
2286 // PHP 5.3+ only
2287 //foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
2288 // $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
2289 //}
2290 //return rmdir($dir);
2291 $d = dir($dir);
2292 while (false !== ($entry = $d->read())) {
2293 if ('.' !== $entry && '..' !== $entry) {
2294 if (is_dir($dir.'/'.$entry)) {
2295 $this->remove_local_directory($dir.'/'.$entry, false);
2296 } else {
2297 @unlink($dir.'/'.$entry);
2298 }
2299 }
2300 }
2301 $d->close();
2302 return ($contents_only) ? true : rmdir($dir);
2303 }
2304
2305 // Returns without any trailing slash
2306 public function backups_dir_location() {
2307
2308 if (!empty($this->backup_dir)) return $this->backup_dir;
2309
2310 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
2311 # 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.
2312 if (preg_match('/^wp-content\/(.*)$/', $updraft_dir, $matches) && ABSPATH.'wp-content' === WP_CONTENT_DIR) {
2313 UpdraftPlus_Options::update_updraft_option('updraft_dir', $matches[1]);
2314 $updraft_dir = WP_CONTENT_DIR.'/'.$matches[1];
2315 }
2316 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
2317 $updraft_dir = ($updraft_dir) ? $updraft_dir : $default_backup_dir;
2318
2319 // Do a test for a relative path
2320 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
2321 # Legacy - file paths stored related to ABSPATH
2322 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')) {
2323 $updraft_dir = ABSPATH.$updraft_dir;
2324 } else {
2325 # File paths stored relative to WP_CONTENT_DIR
2326 $updraft_dir = trailingslashit(WP_CONTENT_DIR).$updraft_dir;
2327 }
2328 }
2329
2330 // Check for the existence of the dir and prevent enumeration
2331 // index.php is for a sanity check - make sure that we're not somewhere unexpected
2332 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')) {
2333 @mkdir($updraft_dir, 0775, true);
2334 @file_put_contents($updraft_dir.'/index.html',"<html><body><a href=\"http://updraftplus.com\">WordPress backups by UpdraftPlus</a></body></html>");
2335 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess','deny from all');
2336 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");
2337 }
2338
2339 $this->backup_dir = $updraft_dir;
2340
2341 return $updraft_dir;
2342 }
2343
2344 private function spool_crypted_file($fullpath, $encryption) {
2345 if ('' == $encryption) $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
2346 if ('' == $encryption) {
2347 header('Content-type: text/plain');
2348 _e("Decryption failed. The database file is encrypted, but you have no encryption key entered.", 'updraftplus');
2349 $this->log('Decryption of database failed: the database file is encrypted, but you have no encryption key entered.', 'error');
2350 } else {
2351 $ciphertext = $this->decrypt($fullpath, $encryption);
2352 if ($ciphertext) {
2353 header('Content-type: application/x-gzip');
2354 header("Content-Disposition: attachment; filename=\"".substr(basename($fullpath), 0, -6)."\";");
2355 header("Content-Length: ".strlen($ciphertext));
2356 print $ciphertext;
2357 } else {
2358 header('Content-type: text/plain');
2359 echo __("Decryption failed. The most likely cause is that you used the wrong key.",'updraftplus')." ".__('The decryption key used:','updraftplus').' '.$encryption;
2360
2361 }
2362 }
2363 return true;
2364 }
2365
2366 public function spool_file($type, $fullpath, $encryption = "") {
2367 @set_time_limit(900);
2368
2369 if (file_exists($fullpath)) {
2370
2371 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
2372 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
2373
2374 $spooled = false;
2375 if ('.crypt' == substr($fullpath, -6, 6)) $spooled = $this->spool_crypted_file($fullpath, $encryption);
2376
2377 if (!$spooled) {
2378
2379 header("Content-Length: ".filesize($fullpath));
2380
2381 if ('.zip' == substr($fullpath, -4, 4)) {
2382 header('Content-type: application/zip');
2383 } elseif ('.tar' == substr($fullpath, -4, 4)) {
2384 header('Content-type: application/x-tar');
2385 } elseif ('.tar.gz' == substr($fullpath, -7, 7)) {
2386 header('Content-type: application/x-tgz');
2387 } elseif ('.tar.bz2' == substr($fullpath, -8, 8)) {
2388 header('Content-type: application/x-bzip-compressed-tar');
2389 } else {
2390 // When we sent application/x-gzip, we found a case where the server compressed it a second time
2391 header('Content-type: application/octet-stream');
2392 }
2393 header("Content-Disposition: attachment; filename=\"".basename($fullpath)."\";");
2394 # Prevent the file being read into memory
2395 @ob_end_flush();
2396 readfile($fullpath);
2397 }
2398 } else {
2399 echo __('File not found', 'updraftplus');
2400 }
2401 }
2402
2403 public function retain_range($input) {
2404 $input = (int)$input;
2405 return ($input > 0 && $input < 3650) ? $input : 1;
2406 }
2407
2408 public function replace_http_with_webdav($input) {
2409 if (!empty($input['url']) && 'http' == substr($input['url'], 0, 4)) $input['url'] = 'webdav'.substr($input['url'], 4);
2410 return $input;
2411 }
2412
2413 public function just_one_email($input, $required = false) {
2414 $x = $this->just_one($input, 'saveemails', (empty($input) && false === $required) ? '' : get_bloginfo('admin_email'));
2415 if (is_array($x)) {
2416 foreach ($x as $ind => $val) {
2417 if (empty($val)) unset($x[$ind]);
2418 }
2419 if (empty($x)) $x = '';
2420 }
2421 return $x;
2422 }
2423
2424 public function just_one($input, $filter = 'savestorage', $rinput = false) {
2425 $oinput = $input;
2426 if (false === $rinput) $rinput = (is_array($input)) ? array_pop($input) : $input;
2427 if (is_string($rinput) && false !== strpos($rinput, ',')) $rinput = substr($rinput, 0, strpos($rinput, ','));
2428 return apply_filters('updraftplus_'.$filter, $rinput, $oinput);
2429 }
2430
2431 function memory_check_current($memory_limit = false) {
2432 # Returns in megabytes
2433 if ($memory_limit == false) $memory_limit = ini_get('memory_limit');
2434 $memory_limit = rtrim($memory_limit);
2435 $memory_unit = $memory_limit[strlen($memory_limit)-1];
2436 if ((int)$memory_unit == 0 && $memory_unit !== '0') {
2437 $memory_limit = substr($memory_limit,0,strlen($memory_limit)-1);
2438 } else {
2439 $memory_unit = '';
2440 }
2441 switch($memory_unit) {
2442 case '':
2443 $memory_limit = floor($memory_limit/1048576);
2444 break;
2445 case 'K':
2446 case 'k':
2447 $memory_limit = floor($memory_limit/1024);
2448 break;
2449 case 'G':
2450 $memory_limit = $memory_limit*1024;
2451 break;
2452 case 'M':
2453 //assumed size, no change needed
2454 break;
2455 }
2456 return $memory_limit;
2457 }
2458
2459 function memory_check($memory, $check_using = false) {
2460 $memory_limit = $this->memory_check_current($check_using);
2461 return ($memory_limit >= $memory)?true:false;
2462 }
2463
2464 private function url_start($urls, $url, $https = false) {
2465 $proto = ($https) ? 'https' : 'http';
2466 return ($urls) ? "<a href=\"$proto://$url\">" : "";
2467 }
2468
2469 private function url_end($urls, $url, $https = false) {
2470 $proto = ($https) ? 'https' : 'http';
2471 return ($urls) ? '</a>' : " ($proto://$url)";
2472 }
2473
2474 public function get_updraftplus_rssfeed() {
2475 if (!function_exists('fetch_feed')) require(ABSPATH . WPINC . '/feed.php');
2476 return fetch_feed('http://feeds.feedburner.com/updraftplus/');
2477 }
2478
2479 public function get_wplang() {
2480 # See: https://core.trac.wordpress.org/changeset/29630
2481 global $wp_current_db_version;
2482 if ( $wp_current_db_version < 29630 ) {
2483 return (defined('WPLANG')) ? WPLANG : '';
2484 } else {
2485 return get_option('WPLANG', '');
2486 }
2487 }
2488
2489 public function wordshell_random_advert($urls) {
2490 if (defined('UPDRAFTPLUS_NOADS_B')) return "";
2491 $rad = rand(0, 8);
2492 switch ($rad) {
2493 case 0:
2494 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');
2495 break;
2496 case 1:
2497 $wplang = $this->get_wplang();
2498 if (strlen($wplang)>0 && !is_file(UPDRAFTPLUS_DIR.'/languages/updraftplus-'.$wplang.
2499 '.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/');
2500
2501 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);
2502 break;
2503 case 2:
2504 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');
2505 break;
2506 case 3:
2507 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');
2508 break;
2509 case 4:
2510 return $this->url_start($urls,'www.simbahosting.co.uk', true).__("Need high-quality WordPress hosting from WordPress specialists? (Including automatic backups and 1-click installer). Get it from the creators of UpdraftPlus.", 'updraftplus').$this->url_end($urls,'www.simbahosting.co.uk', true);
2511 break;
2512 case 5:
2513 if (!defined('UPDRAFTPLUS_NOADS_B')) {
2514 return $this->url_start($urls,'updraftplus.com').__("Need even more features and support? Check out UpdraftPlus Premium",'updraftplus').$this->url_end($urls,'updraftplus.com');
2515 } else {
2516 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.";
2517 }
2518 break;
2519 case 6:
2520 // 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/');
2521 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');
2522 break;
2523 case 7:
2524 return $this->url_start($urls,'updraftplus.com').__("Check out UpdraftPlus.Com for help, add-ons and support",'updraftplus').$this->url_end($urls,'updraftplus.com');
2525 break;
2526 // case 8:
2527 // 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);
2528 // break;
2529 case 8:
2530 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);
2531 break;
2532 }
2533 }
2534
2535 }
2536