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