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

updraftplus.php in UpdraftPlus: WP Backup & Migration Plugin 1.7.18, at updraftplus.php

1,841 lines 85.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: UpdraftPlus - Backup/Restore
4 Plugin URI: http://updraftplus.com
5 Description: Backup and restore: take backups locally, or backup to Amazon S3, Dropbox, Google Drive, Rackspace, (S)FTP, WebDAV & email, on automatic schedules.
6 Author: UpdraftPlus.Com, DavidAnderson
7 Version: 1.7.18
8 Donate link: http://david.dw-perspective.org.uk/donate
9 License: GPLv3 or later
10 Text Domain: updraftplus
11 Author URI: http://updraftplus.com
12 */
13
14 /*
15 TODO - some of these are out of date/done, needs pruning
16 // Raise a warning for probably-too-large email attachments (make intelligent - i.e. record what's the largest succesfully despatched so far? Or is that not relevant, since that only tells us about the first link?)
17 // After Oct 15 2013: Remove page(s) from websites discussing W3TC
18 // Exempt UD itself from a plugins restore? (will options be out-of-sync? exempt options too?)
19 // Post restore/migrate, check updraft_dir, and reset if non-existent
20 // Auto-empty caches post-restore/post-migration (prevent support requests from people with state/wrong cacheing data)
21 // Show 'Migrate' instead of 'Restore' on the button if relevant
22 // Test with: http://wordpress.org/plugins/wp-db-driver/
23 // Backup notes
24 // Detect scheduler not running. Could detect possible causes? (e.g. parse .htaccess)
25 // The delete-em at the end needs to be made resumable
26 // Show running jobs at top. If there are some, and are overdue, kick off a few visits to wp-cron.php to help along.
27 // Incremental - can leverage some of the multi-zip work???
28 // Put in a help link to explain what WordPress core (including any additions to your WordPress root directory) does (was asked for support)
29 // mysqldump, if available, for faster database dumps. Need then to test compatibility with max_packet_size detection in restoration
30 // Check flow of activation on multisite
31 // Find a faster encryption method
32 // Multiple files in more-files
33 // On restore, raise a warning for ginormous zips
34 // Detect double-compressed files when they are uploaded (need a way to detect gz compression in general)
35 // Log migrations/restores, and have an option for auto-emailing the log
36 # Email backup method should be able to force split limit down to something manageable - or at least, should make the option display. (Put it in email class. Tweak the storage dropdown to not hide stuff also in expert class if expert is shown).
37 // What happens if you restore with a database that then changes the setting for updraft_dir ? Should be safe, as the setting is cached during a run: double-check.
38 // Import/slurp backups from other sites. See: http://www.skyverge.com/blog/extending-the-wordpress-xml-rpc-api/
39 // More sophisticated options for retaining/deleting (e.g. 4/day for X days, then 7/week for Z weeks, then 1/month for Y months)
40 // Unpack zips via AJAX? Do bit-by-bit to allow enormous opens a better chance? (have a huge one in Dropbox)
41 // Put in a maintenance-mode detector
42 // Detect CloudFlare output in attempts to connect - detecting cloudflare.com should be sufficient
43 // Bring multisite shop page up to date
44 // Re-do pricing + support packages
45 // Option to create S3 bucket with RRS
46 // More files: back up multiple directories, not just one
47 // Give a help page to go with the message: A zip error occurred - check your log for more details (reduce support requests)
48 // Exclude .git and .svn by default from wpcore
49 // Add option to add, not just replace entities on restore/migrate
50 // Warnings of unreadable files upon PclZip fall-back may need to be more visible - test
51 // Add warning to backup run at beginning if -old dirs exist
52 // Auto-alert if disk usage passes user-defined threshold / or an automatically computed one. Auto-alert if more backups are known than should be (usually a sign of incompleteness). Actually should just delete unknown backups over a certain age.
53 // Generic S3 provider: add page to site. S3-compatible storage providers: http://www.dragondisk.com/s3-storage-providers.html
54 // Importer - import backup sets from another WP site directly via HTTP
55 // Option to create new user for self post-restore
56 // Auto-disable certain cacheing/minifying plugins post-restore
57 // Enhance Google Drive support to not require registration, and to allow offline auth
58 // Add note post-DB backup: you will need to log in using details from newly-imported DB
59 // Make search+replace two-pass to deal with moving between exotic non-default moved-directory setups
60 // Get link - http://www.rackspace.com/knowledge_center/article/how-to-use-updraftplus-to-back-up-cloud-sites-to-cloud-files
61 // 'Delete from your webserver' should trigger a rescan if the backup was local-only
62 // Notify user only if backup fails
63 // Log file SHA1 after finishing creation
64 // Add more info to email - e.g. names + sizes + checksums of uploads + locations
65 // Encrypt archives - http://www.frostjedi.com/phpbb3/viewtopic.php?f=46&t=168508&p=391881&e=391881
66 // Option for additive restores - i.e. add content (themes, plugins,...) instead of replacing
67 // Testing framework - automated testing of all file upload / download / deletion methods
68 // Though Google Drive code users WP's native HTTP functions, it seems to not work if WP natively uses something other than curl
69 // Ginormous tables - need to make sure we "touch" the being-written-out-file (and double-check that we check for that) every 15 seconds - https://friendpaste.com/697eKEcWib01o6zT1foFIn
70 // With ginormous tables, log how many times they've been attempted: after 3rd attempt, log a warning and move on. But first, batch ginormous tables (resumable)
71 // Import single site into a multisite: http://codex.wordpress.org/Migrating_Multiple_Blogs_into_WordPress_3.0_Multisite, http://wordpress.org/support/topic/single-sites-to-multisite?replies=5, http://wpmu.org/import-export-wordpress-sites-multisite/
72 // Selective restores - some resources
73 // Automatically de-activate cacheing/minifying plugins upon restore (and inform user) to prevent unexpected clashes
74 // After restoring the themes, should check to see if the currently-active one still exists or not
75 // When you migrate/restore, if there is a .htaccess, warn/give option about it.
76 // 'Show log' should be done in a nice pop-out, with a button to download the raw
77 // delete_old_dirs() needs to use WP_Filesystem in a more user-friendly way when errors occur
78 // Bulk download of entire set at once (not have to click 7 times).
79 // Restoration should also clear all common cache locations (or just not back them up)
80 // Deal with gigantic database tables - e.g. those over a million rows on cheap hosting.
81 // When restoring core, need an option to retain database settings / exclude wp-config.php
82 // If migrating, warn about consequences of over-writing wp-config.php
83 // Produce a command-line version of the restorer (so that people with shell access are immune from server-enforced timeouts)
84 // Restorations should be logged also
85 // Migrator - list+download from remote, kick-off backup remotely
86 // April 20, 2015: This is the date when the Google Documents API is likely to stop working (https://developers.google.com/google-apps/documents-list/terms)
87 // Search for other TODO-s in the code
88 // Opt-in non-personal stats + link to aggregated results
89 // Stand-alone installer - take a look at this: http://wordpress.org/extend/plugins/duplicator/screenshots/
90 // More DB add-on (other non-WP tables; even other databases)
91 // Unlimited customers should be auto-emailed each time they add a site (security)
92 // Update all-features page at updraftplus.com (not updated after 1.5.5)
93 // Add in downloading in the 'Restore' modal, and remove the advice to do so manually.
94 // Save database encryption key inside backup history on per-db basis, so that if it changes we can still decrypt
95 // Switch to Google Drive SDK. Google folders. https://developers.google.com/drive/folder
96 // Convert S3.php to use WP's native HTTP functions
97 // AJAX-ify restoration
98 // Ability to re-scan existing cloud storage
99 // Dropbox uses one mcrypt function - port to phpseclib for more portability
100 // Store meta-data on which version of UD the backup was made with (will help if we ever introduce quirks that need ironing)
101 // Send the user an email upon their first backup with tips on what to do (e.g. support/improve) (include legacy check to not bug existing users)
102 // GoogleDrive +Rackspace folders
103 //Do an automated test periodically for the success of loop-back connections
104 //When a manual backup is run, use a timer to update the 'Download backups and logs' section, just like 'Last finished backup run'. Beware of over-writing anything that's in there from a resumable downloader.
105 //Change DB encryption to not require whole gzip in memory (twice)
106 //Add Box.Net, SugarSync, Me.Ga support??
107 //Make it easier to find add-ons
108 // Move the inclusion, cloud and retention data into the backup job (i.e. don't read current config, make it an attribute of each job). In fact, everything should be. So audit all code for where get_option is called inside a backup run: it shouldn't happen.
109 // Should we resume if the only errors were upon deletion (i.e. the backup itself was fine?) Presently we do, but it displays errors for the user to confuse them. Perhaps better to make pruning a separate scheuled task??
110 // Create a "Want Support?" button/console, that leads them through what is needed, and performs some basic tests...
111 // Chunking + resuming on SFTP
112 // Add-on to check integrity of backups
113 // Add-on to manage all your backups from a single dashboard
114 // Make disk space check more intelligent (currently hard-coded at 35Mb)
115 // Provide backup/restoration for UpdraftPlus's settings, to allow 'bootstrap' on a fresh WP install - some kind of single-use code which a remote UpdraftPlus can use to authenticate
116 // Multiple jobs
117 // Allow connecting to remote storage, scanning + populating backup history from it
118 // GoogleDrive in-dashboard download resumption loads the whole archive into memory - should instead either chunk or directly stream fo the file handle
119 // Multisite add-on should allow restoring of each blog individually
120 // Create single zip, containing even WordPress itself
121 // Remove the recurrence of admin notices when settings are saved due to _wp_referer
122
123 Encrypt filesystem, if memory allows (and have option for abort if not)
124 // New sub-module to verify that the backups are there, independently of backup thread
125 */
126
127 /*
128 Portions copyright 2011-13 David Anderson
129 Portions copyright 2010 Paul Kehrer
130 Other portions copyright as indicated authors in the relevant files
131
132 This program is free software; you can redistribute it and/or modify
133 it under the terms of the GNU General Public License as published by
134 the Free Software Foundation; either version 3 of the License, or
135 (at your option) any later version.
136
137 This program is distributed in the hope that it will be useful,
138 but WITHOUT ANY WARRANTY; without even the implied warranty of
139 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
140 GNU General Public License for more details.
141
142 You should have received a copy of the GNU General Public License
143 along with this program; if not, write to the Free Software
144 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
145 */
146
147 define('UPDRAFTPLUS_DIR', dirname(__FILE__));
148 define('UPDRAFTPLUS_URL', plugins_url('', __FILE__));
149 define('UPDRAFT_DEFAULT_OTHERS_EXCLUDE','upgrade,cache,updraft,backup*');
150
151 # The following can go in your wp-config.php
152 if (!defined('UPDRAFTPLUS_ZIP_EXECUTABLE')) define('UPDRAFTPLUS_ZIP_EXECUTABLE', "/usr/bin/zip,/bin/zip,/usr/local/bin/zip,/usr/sfw/bin/zip,/usr/xdg4/bin/zip,/opt/bin/zip");
153 # If any individual file size is greater than this, then a warning is given
154 if (!defined('UPDRAFTPLUS_WARN_FILE_SIZE')) define('UPDRAFTPLUS_WARN_FILE_SIZE', 1024*1024*250);
155 # On a test on a Pentium laptop, 100,000 rows needed ~ 1 minute to write out - so 150,000 is around the CPanel default of 90 seconds execution time.
156 if (!defined('UPDRAFTPLUS_WARN_DB_ROWS')) define('UPDRAFTPLUS_WARN_DB_ROWS', 150000);
157
158 # The smallest value (in megabytes) that the "split zip files at" setting is allowed to be set to
159 if (!defined('UPDRAFTPLUS_SPLIT_MIN')) define('UPDRAFTPLUS_SPLIT_MIN', 75);
160
161 // Load add-ons and various files that may or may not be present, depending on where the plugin was distributed
162 if (is_file(UPDRAFTPLUS_DIR.'/premium.php')) require_once(UPDRAFTPLUS_DIR.'/premium.php');
163 if (is_file(UPDRAFTPLUS_DIR.'/autoload.php')) require_once(UPDRAFTPLUS_DIR.'/autoload.php');
164 if (is_file(UPDRAFTPLUS_DIR.'/udaddons/updraftplus-addons.php')) include_once(UPDRAFTPLUS_DIR.'/udaddons/updraftplus-addons.php');
165
166 if (is_dir(UPDRAFTPLUS_DIR.'/addons') && $dir_handle = opendir(UPDRAFTPLUS_DIR.'/addons')) {
167 while (false !== ($e = readdir($dir_handle))) {
168 if (is_file(UPDRAFTPLUS_DIR.'/addons/'.$e) && preg_match('/\.php$/', $e)) {
169 include_once(UPDRAFTPLUS_DIR.'/addons/'.$e);
170 }
171 }
172 @closedir($dir_handle);
173 }
174
175 if (!isset($updraftplus)) $updraftplus = new UpdraftPlus();
176
177 if (!$updraftplus->memory_check(192)) {
178 // Experience appears to show that the memory limit is only likely to be hit (unless it is very low) by single files that are larger than available memory (when compressed)
179 # Add sanity checks - found someone who'd set WP_MAX_MEMORY_LIMIT to 256K !
180 if (!$updraftplus->memory_check($updraftplus->memory_check_current(WP_MAX_MEMORY_LIMIT))) {
181 $new = absint($updraftplus->memory_check_current(WP_MAX_MEMORY_LIMIT));
182 if ($new>32 && $new<100000) {
183 @ini_set('memory_limit', $new.'M'); //up the memory limit to the maximum WordPress is allowing for large backup files
184 }
185 }
186 }
187
188 if (!class_exists('UpdraftPlus_Options')) require_once(UPDRAFTPLUS_DIR.'/options.php');
189
190 class UpdraftPlus {
191
192 public $version;
193
194 public $plugin_title = 'UpdraftPlus Backup/Restore';
195
196 // Choices will be shown in the admin menu in the order used here
197 public $backup_methods = array (
198 "s3" => "Amazon S3",
199 "dropbox" => "Dropbox",
200 'cloudfiles' => 'Rackspace Cloud Files',
201 "googledrive" => "Google Drive",
202 "ftp" => "FTP",
203 'sftp' => 'SFTP / SCP',
204 'webdav' => 'WebDAV',
205 's3generic' => 'S3-Compatible (Generic)',
206 'dreamobjects' => 'DreamObjects',
207 "email" => "Email"
208 );
209
210 public $errors = array();
211 public $nonce;
212 public $logfile_name = "";
213 public $logfile_handle = false;
214 public $backup_time;
215 public $backup_time_ms;
216
217 public $opened_log_time;
218 private $backup_dir;
219
220 private $jobdata;
221
222 public $something_useful_happened = false;
223
224 // Used to schedule resumption attempts beyond the tenth, if needed
225 public $current_resumption;
226 private $newresumption_scheduled = false;
227
228 function __construct() {
229
230 // Initialisation actions - takes place on plugin load
231
232 if ($fp = fopen( __FILE__, 'r')) {
233 $file_data = fread( $fp, 1024 );
234 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
235 $this->version = $matches[1];
236 }
237 fclose($fp);
238 }
239
240 # Create admin page
241 add_action('init', array($this, 'handle_url_actions'));
242 // Run earlier than default - hence earlier than other components
243 add_action('admin_init', array($this,'admin_init'), 9);
244 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
245 add_action('admin_menu', array($this,'admin_init'), 9);
246 add_action('updraft_backup', array($this,'backup_files'));
247 add_action('updraft_backup_database', array($this,'backup_database'));
248 # backup_all is used by the manual "Backup Now" button
249 add_action('updraft_backup_all', array($this,'backup_all'));
250 # this is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
251 add_action('updraft_backup_resume', array($this,'backup_resume'), 10, 3);
252 # http://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules. Raised priority because some plugins wrongly over-write all prior schedule changes (including BackupBuddy!)
253 add_filter('cron_schedules', array($this,'modify_cron_schedules'), 30);
254 add_action('plugins_loaded', array($this, 'load_translations'));
255
256 register_deactivation_hook(__FILE__, array($this, 'deactivation'));
257
258 }
259
260 public function ensure_phpseclib($class = false, $class_path = false) {
261 if ($class && class_exists($class)) return;
262 if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
263 if ($class_path) require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/'.$class_path.'.php');
264 }
265
266 public function admin_init() {
267 // We are in the admin area: now load all that code
268 global $updraftplus_admin;
269 if (empty($updraftplus_admin)) require_once(UPDRAFTPLUS_DIR.'/admin.php');
270
271 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadlatestmodlog' && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
272
273 $updraft_dir = $this->backups_dir_location();
274
275 $log_file = '';
276 $mod_time = 0;
277
278 if ($handle = opendir($updraft_dir)) {
279 while (false !== ($entry = readdir($handle))) {
280 // The latter match is for files created internally by zipArchive::addFile
281 if (preg_match('/^log\.[a-z0-9]+\.txt$/i', $entry)) {
282 $mtime = filemtime($updraft_dir.'/'.$entry);
283 if ($mtime > $mod_time) {
284 $mod_time = $mtime;
285 $log_file = $updraft_dir.'/'.$entry;
286 }
287 }
288 }
289 @closedir($handle);
290 }
291
292 if ($mod_time >0) {
293 if (is_readable($log_file)) {
294 header('Content-type: text/plain');
295 readfile($log_file);
296 exit;
297 } else {
298 add_action('admin_notices', array($this,'show_admin_warning_unreadablelog') );
299 }
300 } else {
301 add_action('admin_notices', array($this,'show_admin_warning_nolog') );
302 }
303 }
304
305 }
306
307 public function add_curl_capath($handle) {
308 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) curl_setopt($handle, CURLOPT_CAINFO, UPDRAFTPLUS_DIR.'/includes/cacert.pem' );
309 }
310
311 // Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?page=updraftplus&action=updraftmethod-googledrive-auth
312 // Also handle action=downloadlog
313 public function handle_url_actions() {
314
315 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
316 // Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
317 if (isset($_SERVER['REQUEST_METHOD']) && 'GET' == $_SERVER['REQUEST_METHOD'] && UpdraftPlus_Options::user_can_manage() && isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && isset($_GET['action']) ) {
318 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php')) {
319 $method = $matches[1];
320 require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
321 $call_class = "UpdraftPlus_BackupModule_".$method;
322 $call_method = "action_".$matches[2];
323
324 add_action('http_api_curl', array($this, 'add_curl_capath'));
325 if (method_exists($call_class, $call_method)) call_user_func(array($call_class,$call_method));
326 remove_action('http_api_curl', array($this, 'add_curl_capath'));
327 } elseif ($_GET['action'] == 'downloadlog' && isset($_GET['updraftplus_backup_nonce']) && preg_match("/^[0-9a-f]{12}$/",$_GET['updraftplus_backup_nonce'])) {
328 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
329 $updraft_dir = $this->backups_dir_location();
330 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
331 if (is_readable($log_file)) {
332 header('Content-type: text/plain');
333 readfile($log_file);
334 exit;
335 } else {
336 add_action('admin_notices', array($this,'show_admin_warning_unreadablelog') );
337 }
338 } elseif ($_GET['action'] == 'downloadfile' && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-[\-a-z]+\.(gz\.crypt)$/i', $_GET['updraftplus_file'])) {
339 $updraft_dir = $this->backups_dir_location();
340 $spool_file = $updraft_dir.'/'.basename($_GET['updraftplus_file']);
341 if (is_readable($spool_file)) {
342 $dkey = (isset($_GET['decrypt_key'])) ? $_GET['decrypt_key'] : "";
343 $this->spool_file('db', $spool_file, $dkey);
344 exit;
345 } else {
346 add_action('admin_notices', array($this,'show_admin_warning_unreadablefile') );
347 }
348 }
349 }
350 }
351
352 public function get_table_prefix() {
353 global $wpdb;
354 if (!empty($wpdb->base_prefix)) return $wpdb->base_prefix;
355 return $wpdb->get_blog_prefix(0);
356 }
357
358 public function show_admin_warning_unreadablelog() {
359 global $updraftplus_admin;
360 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The log file could not be read.','updraftplus'));
361 }
362
363 public function show_admin_warning_nolog() {
364 global $updraftplus_admin;
365 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('No log files were found.','updraftplus'));
366 }
367
368 public function show_admin_warning_unreadablefile() {
369 global $updraftplus_admin;
370 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
371 }
372
373 function load_translations() {
374 // Tell WordPress where to find the translations
375 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages');
376 }
377
378 // Cleans up temporary files found in the updraft directory (and some in the site root - pclzip)
379 // Always cleans up temporary files over 12 hours old.
380 // With parameters, also cleans up those.
381 // Also cleans out old job data older than 12 hours old (immutable value)
382 public function clean_temporary_files($match = '', $older_than = 43200) {
383 # Clean out old job data
384 if ($older_than >10000) {
385 global $wpdb;
386 $all_jobs = $wpdb->get_results("SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'updraft_jobdata_%' LIMIT 1", ARRAY_A);
387 foreach ($all_jobs as $job) {
388 $val = maybe_unserialize($job['option_value']);
389 if (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) {
390 delete_option($job['option_name']);
391 }
392 }
393
394 }
395 $updraft_dir = $this->backups_dir_location();
396 $now_time=time();
397 if ($handle = opendir($updraft_dir)) {
398 while (false !== ($entry = readdir($handle))) {
399 // This match is for files created internally by zipArchive::addFile
400 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $entry);
401 // 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.
402 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry);
403 if ((preg_match("/$match\.tmp(\.gz)?$/i", $entry) || $ziparchive_match || $binzip_match) && is_file($updraft_dir.'/'.$entry)) {
404 // 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
405 if (($match && ($ziparchive_match || $binzip_match || 0 == $older_than) && $now_time-filemtime($updraft_dir.'/'.$entry) >= $older_than) || $now_time-filemtime($updraft_dir.'/'.$entry)>43200) {
406 $this->log("Deleting old temporary file: $entry");
407 @unlink($updraft_dir.'/'.$entry);
408 }
409 }
410 }
411 @closedir($handle);
412 }
413 # Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both
414 foreach (array(ABSPATH, ABSPATH.'wp-admin/') as $path) {
415 if ($handle = opendir($path)) {
416 while (false !== ($entry = readdir($handle))) {
417 # 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
418 if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) {
419 $this->log("Deleting old PclZip temporary file: $entry");
420 @unlink($path.$entry);
421 }
422 }
423 @closedir($handle);
424 }
425 }
426 }
427
428 public function backup_time_nonce() {
429 $this->backup_time_ms = microtime(true);
430 $this->backup_time = time();
431 $nonce = substr(md5(time().rand()), 20);
432 $this->nonce = $nonce;
433 }
434
435 public function logfile_open($nonce) {
436
437 //set log file name and open log file
438 $updraft_dir = $this->backups_dir_location();
439 $this->logfile_name = $updraft_dir."/log.$nonce.txt";
440
441 if (file_exists($this->logfile_name)) {
442 $seek_to = max((filesize($this->logfile_name) - 340), 1);
443 $handle = fopen($this->logfile_name, 'r');
444 if ($handle) {
445 # Returns 0 on success
446 if (0 === @fseek($handle, $seek_to)) {
447 $bytes_back = filesize($this->logfile_name) - $seek_to;
448 # Return to the end of the file
449 $read_recent = fread($handle, $bytes_back);
450 # Move to end of file - ought to be redundant
451 if (false !== strpos($read_recent, 'The backup apparently succeeded') && false !== strpos($read_recent, 'and is now complete')) {
452 $this->backup_is_already_complete = true;
453 }
454 }
455 fclose($handle);
456 }
457 }
458
459 $this->logfile_handle = fopen($this->logfile_name, 'a');
460
461 $this->opened_log_time = microtime(true);
462 $this->log('Opened log file at time: '.date('r'));
463 global $wp_version;
464 @include(ABSPATH.'wp-includes/version.php');
465
466 // Will need updating when WP stops being just plain MySQL
467 $mysql_version = (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
468
469 $safe_mode = $this->detect_safe_mode();
470
471 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
472 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
473 $logline = "UpdraftPlus: ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".php_uname().") MySQL: $mysql_version Server: ".$_SERVER["SERVER_SOFTWARE"]." safe_mode: $safe_mode max_execution_time: ".@ini_get("max_execution_time")." memory_limit: ".ini_get('memory_limit')." (used: ${memory_usage}M | ${memory_usage2}M) ZipArchive::addFile: ";
474
475 // method_exists causes some faulty PHP installations to segfault, leading to support requests
476 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
477 $logline .= 'Y';
478 } else {
479 $logline .= (method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
480 }
481
482 $w3oc = 'N';
483 if (0 === $this->current_resumption) {
484 if (defined('W3TC') && W3TC == true && function_exists('w3_instance')) {
485 $modules = w3_instance('W3_ModuleStatus');
486 if ($modules->is_enabled('objectcache')) {
487 $w3oc = 'Y';
488 }
489 }
490 $logline .= " W3TC/ObjectCache: $w3oc";
491 }
492
493 $this->log($logline);
494
495 if ('Y' === $w3oc) $this->log(__('W3 Total Cache\'s object cache is active. This is known to have a bug that messes with all scheduled tasks (including backup jobs).','updraftplus').' '.__('You should go to the W3 Total Cache settings page and turn it off.', 'updraftplus'), 'warning');
496
497
498 $disk_free_space = @disk_free_space($updraft_dir);
499 if ($disk_free_space === false) {
500 $this->log("Free space on disk containing Updraft's temporary directory: Unknown");
501 } else {
502 $this->log("Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576,1)." Mb");
503 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');
504 }
505 }
506
507 /* Logs the given line, adding (relative) time stamp and newline
508 Note these subtleties of log handling:
509 - 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.
510 - 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...
511 - ... 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
512 */
513 public function log($line, $level = 'notice', $uniq_id = false) {
514
515 if ('error' == $level || 'warning' == $level) {
516 if ('error' == $level && $this->error_count() == 0) $this->log("An error condition has occurred for the first time during this job");
517 $this->errors[] = array('level' => $level, 'message' => $line);
518 # Errors are logged separately
519 if ('error' == $level) return;
520 # It's a warning
521 $warnings = $this->jobdata_get('warnings');
522 if (!is_array($warnings)) $warnings=array();
523 if ($uniq_id) {
524 $warnings[$uniq_id] = $line;
525 } else {
526 $warnings[] = $line;
527 }
528 $this->jobdata_set('warnings', $warnings);
529 }
530
531 if ($this->logfile_handle) {
532 # Record log file times relative to the backup start, if possible
533 $rtime = (!empty($this->backup_time_ms)) ? microtime(true)-$this->backup_time_ms : microtime(true)-$this->opened_log_time;
534 fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") $line\n");
535 }
536
537 switch ($this->jobdata_get('job_type')) {
538 case 'download':
539 // Download messages are keyed on the job (since they could be running several), and type
540 // The values of the POST array were checked before
541 $findex = (!empty($_POST['findex'])) ? $_POST['findex'] : 0;
542 set_transient('ud_dlmess_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line." (".date('M d H:i:s').")", 3600);
543 break;
544 case 'restore':
545 #if ('debug' != $level) echo $line."\n";
546 break;
547 default:
548 if ('debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")");
549 break;
550 }
551
552 if (defined('UPDRAFTPLUS_CONSOLELOG')) print $line."\n";
553 if (defined('UPDRAFTPLUS_BROWSERLOG')) print htmlentities($line)."\n";
554 }
555
556 # 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()).
557 # 1st argument = the line to be logged (obligatory)
558 # Further arguments = parameters for sprintf()
559 public function log_e() {
560 $args = func_get_args();
561 # Get first argument
562 $pre_line = array_shift($args);
563 # Now run sprintf on it, using any remaining arguments
564 # TODO: Does not work - the line below passes an array; that's not right
565 $line = sprintf(__($pre_line, 'updraftplus'), $args);
566 echo $line.'<br>';
567 $this->log($line);
568 }
569
570 // 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
571 public function record_uploaded_chunk($percent, $extra, $file_path = false) {
572
573 // Touch the original file, which helps prevent overlapping runs
574 if ($file_path) touch($file_path);
575
576 // Log it
577 global $updraftplus_backup;
578 $log = ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded";
579 if ($extra) $log .= " ($extra)";
580 $this->log($log);
581 // If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
582 // 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
583 // 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
584 // 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
585
586 // 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)
587 if ($percent > 0.7 * ( $this->current_resumption - 9)) {
588 $this->something_useful_happened();
589 }
590
591 $upload_status = $this->jobdata_get('uploading_substatus');
592 if (is_array($upload_status)) {
593 $upload_status['p'] = $percent/100;
594 $this->jobdata_set('uploading_substatus', $upload_status);
595 }
596
597 }
598
599 function detect_safe_mode() {
600 return (@ini_get('safe_mode') && strtolower(@ini_get('safe_mode')) != "off") ? 1 : 0;
601 }
602
603 # We require -@ and -u -r to work - which is the usual Linux binzip
604 function find_working_bin_zip($logit = true) {
605 if ($this->detect_safe_mode()) return false;
606 // The hosting provider may have explicitly disabled the popen or proc_open functions
607 if (!function_exists('popen') || !function_exists('proc_open')) return false;
608 $updraft_dir = $this->backups_dir_location();
609 foreach (explode(',', UPDRAFTPLUS_ZIP_EXECUTABLE) as $potzip) {
610 if ($logit) $this->log("Testing: $potzip");
611 if (@is_executable($potzip)) {
612 # Test it, see if it is compatible with Info-ZIP
613 # If you have another kind of zip, then feel free to tell me about it
614 @mkdir($updraft_dir.'/binziptest/subdir1/subdir2', 0777, true);
615 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>');
616 @unlink($updraft_dir.'/binziptest/test.zip');
617 if (is_file($updraft_dir.'/binziptest/subdir1/subdir2/test.html')) {
618
619 $exec = "cd ".escapeshellarg($updraft_dir)."; $potzip -v -u -r binziptest/test.zip binziptest/subdir1";
620
621 $all_ok=true;
622 $handle = popen($exec, "r");
623 if ($handle) {
624 while (!feof($handle)) {
625 $w = fgets($handle);
626 if ($w && $logit) $this->log("Output: ".trim($w));
627 }
628 $ret = pclose($handle);
629 if ($ret !=0) {
630 if ($logit) $this->log("Binary zip: error (code: $ret)");
631 $all_ok = false;
632 }
633 } else {
634 if ($logit) $this->log("Error: popen failed");
635 $all_ok = false;
636 }
637
638 # Now test -@
639 if (true == $all_ok) {
640 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>');
641
642 $exec = $potzip." -v -@ binziptest/test.zip";
643
644 $all_ok=true;
645
646 $descriptorspec = array(
647 0 => array('pipe', 'r'),
648 1 => array('pipe', 'w'),
649 2 => array('pipe', 'w')
650 );
651 $handle = proc_open($exec, $descriptorspec, $pipes, $updraft_dir);
652 if (is_resource($handle)) {
653 if (!fwrite($pipes[0], "binziptest/subdir1/subdir2/test2.html\n")) {
654 @fclose($pipes[0]);
655 @fclose($pipes[1]);
656 @fclose($pipes[2]);
657 $all_ok = false;
658 } else {
659 fclose($pipes[0]);
660 while (!feof($pipes[1])) {
661 $w = fgets($pipes[1]);
662 if ($w && $logit) $this->log("Output: ".trim($w));
663 }
664 fclose($pipes[1]);
665
666 while (!feof($pipes[2])) {
667 $last_error = fgets($pipes[2]);
668 if (!empty($last_error) && $logit) $this->log("Error output: ".trim($w));
669 }
670 fclose($pipes[2]);
671
672 $ret = proc_close($handle);
673 if ($ret !=0) {
674 if ($logit) $this->log("Binary zip: error (code: $ret)");
675 $all_ok = false;
676 }
677
678 }
679
680 } else {
681 if ($logit) $this->log("Error: proc_open failed");
682 $all_ok = false;
683 }
684
685 }
686
687 // Do we now actually have a working zip? Need to test the created object using PclZip
688 // If it passes, then remove dirs and then return $potzip;
689 $found_first = false;
690 $found_second = false;
691 if ($all_ok && file_exists($updraft_dir.'/binziptest/test.zip')) {
692 if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
693 $zip = new PclZip($updraft_dir.'/binziptest/test.zip');
694 $foundit = 0;
695 if (($list = $zip->listContent()) != 0) {
696 foreach ($list as $obj) {
697 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test.html' == $obj['stored_filename'] && $obj['size']==127) $found_first=true;
698 if ($obj['filename'] && !empty($obj['stored_filename']) && 'binziptest/subdir1/subdir2/test2.html' == $obj['stored_filename'] && $obj['size']==134) $found_second=true;
699 }
700 }
701 }
702 $this->remove_binzip_test_files($updraft_dir);
703 if ($found_first && $found_second) {
704 if ($logit) $this->log("Working binary zip found: $potzip");
705 return $potzip;
706 }
707
708 }
709 $this->remove_binzip_test_files($updraft_dir);
710 }
711 }
712 return false;
713 }
714
715 function remove_binzip_test_files($updraft_dir) {
716 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test.html');
717 @unlink($updraft_dir.'/binziptest/subdir1/subdir2/test2.html');
718 @rmdir($updraft_dir.'/binziptest/subdir1/subdir2');
719 @rmdir($updraft_dir.'/binziptest/subdir1');
720 @unlink($updraft_dir.'/binziptest/test.zip');
721 @rmdir($updraft_dir.'/binziptest');
722 }
723
724 // This function is purely for timing - we just want to know the maximum run-time; not whether we have achieved anything during it
725 function record_still_alive() {
726 // Update the record of maximum detected runtime on each run
727 $time_passed = $this->jobdata_get('run_times');
728 if (!is_array($time_passed)) $time_passed = array();
729
730 $time_this_run = microtime(true)-$this->opened_log_time;
731 $time_passed[$this->current_resumption] = $time_this_run;
732 $this->jobdata_set('run_times', $time_passed);
733
734 $resume_interval = $this->jobdata_get('resume_interval');
735 if ($time_this_run + 30 > $resume_interval) {
736 $new_interval = ceil($time_this_run + 30);
737 set_transient('updraft_initial_resume_interval', (int)$new_interval, 8*86400);
738 $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");
739 $this->jobdata_set('resume_interval', $new_interval);
740 }
741
742 }
743
744 function something_useful_happened() {
745
746 $this->record_still_alive();
747
748 if (!$this->something_useful_happened) {
749 $useful_checkin = $this->jobdata_get('useful_checkin');
750 if (empty($useful_checkin) || $this->current_resumption > $useful_checkin) $this->jobdata_set('useful_checkin', $this->current_resumption);
751 }
752
753 $this->something_useful_happened = true;
754
755 if ($this->current_resumption >= 9 && $this->newresumption_scheduled == false) {
756 $this->log("This is resumption ".$this->current_resumption.", but meaningful activity is still taking place; so a new one will be scheduled");
757 // We just use max here to make sure we get a number at all
758 $resume_interval = max($this->jobdata_get('resume_interval'), 75);
759 // Don't consult the minimum here
760 // if (!is_numeric($resume_interval) || $resume_interval<300) { $resume_interval = 300; }
761 $schedule_for = time()+$resume_interval;
762 $this->newresumption_scheduled = $schedule_for;
763 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($this->current_resumption + 1, $this->nonce));
764 } else {
765 $this->reschedule_if_needed();
766 }
767 }
768
769 // 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
770 function get_backupable_file_entities($include_others = true, $full_info = false) {
771
772 $wp_upload_dir = wp_upload_dir();
773
774 if ($full_info) {
775 $arr = array(
776 'plugins' => array('path' => WP_PLUGIN_DIR, 'description' => __('Plugins','updraftplus')),
777 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes','updraftplus')),
778 'uploads' => array('path' => $wp_upload_dir['basedir'], 'description' => __('Uploads','updraftplus'))
779 );
780 } else {
781 $arr = array(
782 'plugins' => WP_PLUGIN_DIR,
783 'themes' => WP_CONTENT_DIR.'/themes',
784 'uploads' => $wp_upload_dir['basedir']
785 );
786 }
787
788 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
789
790 // We then add 'others' on to the end
791 if ($include_others) {
792 if ($full_info) {
793 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others','updraftplus'));
794 } else {
795 $arr['others'] = WP_CONTENT_DIR;
796 }
797 }
798
799 // Entries that should be added after 'others'
800 $arr = apply_filters('updraft_backupable_file_entities_final', $arr, $full_info);
801
802 return $arr;
803
804 }
805
806 # 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)
807 public function filter_pre_option_cron($v) {
808 global $wpdb;
809 $row = $wpdb->get_row( $wpdb->prepare("SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", 'cron' ) );
810 if (is_object($row )) return maybe_unserialize($row->option_value);
811 return false;
812 }
813
814 public function backup_resume($resumption_no, $bnonce) {
815
816 $this->current_resumption = $resumption_no;
817
818 // 15 minutes
819 @set_time_limit(900);
820
821 @ignore_user_abort(true);
822 // This is scheduled for 5 minutes after a backup job starts
823
824 $runs_started = array();
825 $time_now = microtime(true);
826
827 add_filter('pre_option_cron', array($this, 'filter_pre_option_cron'));
828
829 // Restore state
830 $resumption_extralog = '';
831 if ($resumption_no > 0) {
832 $this->nonce = $bnonce;
833 $this->backup_time = $this->jobdata_get('backup_time');
834 $this->backup_time_ms = $this->jobdata_get('backup_time_ms');
835 $this->logfile_open($bnonce);
836
837 $runs_started = $this->jobdata_get('runs_started');
838 if (!is_array($runs_started)) $runs_started=array();
839 $time_passed = $this->jobdata_get('run_times');
840 if (!is_array($time_passed)) $time_passed = array();
841 foreach ($time_passed as $run => $passed) {
842 if (isset($runs_started[$run]) && $runs_started[$run] + $time_passed[$run] + 30 > $time_now) {
843 $this->terminate_due_to_activity('check-in', round($time_now,1), round($runs_started[$run] + $time_passed[$run],1));
844 }
845 }
846
847 $prev_resumption = $resumption_no - 1;
848 if (isset($time_passed[$prev_resumption])) {
849 $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 1)."s";
850 } else {
851 $this->no_checkin_last_time = true;
852 }
853
854 }
855
856 $runs_started[$resumption_no] = $time_now;
857 if (!empty($this->backup_time)) $this->jobdata_set('runs_started', $runs_started);
858
859 // 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
860 $warnings = $this->jobdata_get('warnings');
861 if (is_array($warnings)) {
862 foreach ($warnings as $warning) {
863 $this->errors[] = array('level' => 'warning', 'message' => $warning);
864 }
865 }
866
867 // Schedule again, to run in 5 minutes again, in case we again fail
868 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
869 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 100);
870
871 if ($resumption_no > 0 && isset($runs_started[$prev_resumption])) {
872 $our_expected_start = $runs_started[$prev_resumption] + $resume_interval;
873 # More than 12 minutes late?
874 if ($time_now > $our_expected_start + 720) {
875 $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));
876 $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');
877 }
878 }
879
880 $btime = $this->backup_time;
881 $job_type = $this->jobdata_get('job_type');
882
883 $updraft_dir = $this->backups_dir_location();
884
885 $time_ago = time()-$btime;
886
887 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
888
889 // 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.
890 // 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.
891 if (($resumption_no >= 1 && 'finished' == $this->jobdata_get('jobstatus')) || (!empty($this->backup_is_already_complete))) {
892 $this->log('Terminate: This backup job is already finished.');
893 die;
894 }
895
896 // We just do this once, as we don't want to be in permanent conflict with the overlap detector
897 if ($resumption_no == 8) {
898 // $time_passed is set earlier
899 list($max_time, $timings_string, $run_times_known) = $this->max_time_passed($time_passed, 7);
900 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
901 // 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
902 if ($run_times_known >= 6 && ($max_time + 52 < $resume_interval)) {
903 $resume_interval = round($max_time + 52);
904 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
905 $this->jobdata_set('resume_interval', $resume_interval);
906 }
907 }
908
909 // A different argument than before is needed otherwise the event is ignored
910 $next_resumption = $resumption_no+1;
911 if ($next_resumption < 10) {
912 if ($this->jobdata_get('one_shot') === true) {
913 $this->log('We are in "one shot" mode - no resumptions will be scheduled');
914 } else {
915 $schedule_resumption = true;
916 }
917 } else {
918 // 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
919 $useful_checkin = $this->jobdata_get('useful_checkin');
920 $last_resumption = $resumption_no-1;
921
922 if (empty($useful_checkin) || $useful_checkin < $last_resumption) {
923 $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));
924 } else {
925 $schedule_resumption = true;
926 }
927 }
928 if (isset($schedule_resumption)) {
929 $schedule_for = time()+$resume_interval;
930 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
931 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
932 $this->newresumption_scheduled = $schedule_for;
933 }
934
935 // Sanity check
936 if (empty($this->backup_time)) {
937 # TODO: We no longer use object cache-ing - alter this message
938 $this->log('Abort this run: the backup_time parameter appears to be empty (this is either a one-time error caused by upgrading from a version earlier than 1.7.18 whilst a backup was in progress (which you can ignore), or caused by resuming an already-complete backup, or by your site having a faulty object cache active (e.g. W3 Total Cache\'s object cache))');
939 return false;
940 }
941
942 global $updraftplus_backup;
943 // Bring in all the zip routines
944 if (!is_a($updraftplus_backup, 'UpdraftPlus_Backup')) require_once(UPDRAFTPLUS_DIR.'/backup.php');
945
946 // This should be always called; if there were no files in this run, it returns us an empty array
947 $backup_array = $updraftplus_backup->resumable_backup_of_files($resumption_no);
948
949 // This save, if there was something, is then immediately picked up again
950 if (is_array($backup_array)) {
951 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
952 $this->save_backup_history($backup_array);
953 }
954
955 // Switch of variable name is purely vestigial
956 $our_files = $backup_array;
957 if (!is_array($our_files)) $our_files = array();
958
959 $undone_files = array();
960
961 $backup_database = $this->jobdata_get('backup_database');
962
963 // The value of jobdata['backup_database'] is read and written below (instead of using the existing variable) so that we can copy-and-paste this part as needed.
964 if ($backup_database == "begun" || $backup_database == 'finished' || $backup_database == 'encrypted') {
965 if ($backup_database == "begun") {
966 if ($resumption_no > 0) {
967 $this->log("Resuming creation of database dump");
968 } else {
969 $this->log("Beginning creation of database dump");
970 }
971 } elseif ($backup_database == 'encrypted') {
972 $this->log("Database dump: Creation and encryption were completed already");
973 } else {
974 $this->log("Database dump: Creation was completed already");
975 }
976
977 $db_backup = $updraftplus_backup->backup_db($backup_database);
978 if(is_array($our_files) && is_string($db_backup)) {
979 $our_files['db'] = $db_backup;
980 }
981
982 if ($backup_database != 'encrypted') $this->jobdata_set('backup_database', 'finished');
983 } elseif ('no' == $backup_database) {
984 $this->log('No database backup - not part of this run');
985 } else {
986 $this->log("Unrecognised data when trying to ascertain if the database was backed up ($backup_database)");
987 }
988
989 // Save this to our history so we can track backups for the retain feature
990 $this->log("Saving backup history");
991 // 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.
992 $this->save_backup_history($our_files);
993
994 // Potentially encrypt the database if it is not already
995 if (isset($our_files['db']) && !preg_match("/\.crypt$/", $our_files['db'])) {
996 $our_files['db'] = $updraftplus_backup->encrypt_file($our_files['db']);
997 // No need to save backup history now, as it will happen in a few lines time
998 if (preg_match("/\.crypt$/", $our_files['db'])) $this->jobdata_set('backup_database', 'encrypted');
999 }
1000
1001 if (isset($our_files['db']) && file_exists($updraft_dir.'/'.$our_files['db'])) {
1002 $our_files['db-size'] = filesize($updraft_dir.'/'.$our_files['db']);
1003 $this->save_backup_history($our_files);
1004 }
1005
1006 $backupable_entities = $this->get_backupable_file_entities(true);
1007
1008 # Queue files for upload
1009 foreach ($our_files as $key => $files) {
1010 // Only continue if the stored info was about a dump
1011 if (!isset($backupable_entities[$key]) && $key != 'db') continue;
1012 if (is_string($files)) $files = array($files);
1013 foreach ($files as $findex => $file) {
1014 if ($this->is_uploaded($file)) {
1015 $this->log("$file: $key: This file has already been successfully uploaded");
1016 } elseif (is_file($updraft_dir.'/'.$file)) {
1017 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
1018 $undone_files[$key.$findex] = $file;
1019 } else {
1020 $this->log("$file: $key: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem ($updraft_dir/$file)");
1021 $this->uploaded_file($file, null, true);
1022 }
1023 }
1024 }
1025
1026 if (count($undone_files) == 0) {
1027 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1028 $this->log("There were no more files that needed uploading; backup job is complete");
1029 // No email, as the user probably already got one if something else completed the run
1030 $this->backup_finish($next_resumption, true, false, $resumption_no);
1031 return;
1032 } else {
1033 $this->log("Requesting upload of the files that have not yet been successfully uploaded (".count($undone_files).")");
1034 $updraftplus_backup->cloud_backup($undone_files);
1035 }
1036
1037 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
1038 if (is_array($our_files)) $this->save_last_backup($our_files);
1039 $this->backup_finish($next_resumption, true, true, $resumption_no);
1040
1041 }
1042
1043 function max_time_passed($time_passed, $upto) {
1044 $max_time = 0;
1045 $timings_string = "";
1046 $run_times_known=0;
1047 for ($i=0; $i<=$upto; $i++) {
1048 $timings_string .= "$i:";
1049 if (isset($time_passed[$i])) {
1050 $timings_string .= round($time_passed[$i], 1).' ';
1051 $run_times_known++;
1052 if ($time_passed[$i] > $max_time) $max_time = round($time_passed[$i]);
1053 } else {
1054 $timings_string .= '? ';
1055 }
1056 }
1057 return array($max_time, $timings_string, $run_times_known);
1058 }
1059
1060 function backup_all() {
1061 $this->boot_backup(true,true);
1062 }
1063
1064 function backup_files() {
1065 # Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
1066 $this->boot_backup(true,false);
1067 }
1068
1069 function backup_database() {
1070 # Note that nothing will happen if the file backup had the same schedule
1071 $this->boot_backup(false,true);
1072 }
1073
1074 public function jobdata_getarray($non) {
1075 return get_site_option("updraft_jobdata_".$non, array());
1076 }
1077
1078 // This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
1079 private function jobdata_set_multi() {
1080 if (!is_array($this->jobdata)) $this->jobdata = array();
1081
1082 $args = func_num_args();
1083
1084 for ($i=1; $i<=$args/2; $i++) {
1085 $key = func_get_arg($i*2-2);
1086 $value = func_get_arg($i*2-1);
1087 $this->jobdata[$key] = $value;
1088 }
1089 if (!empty($this->nonce)) update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1090 }
1091
1092 public function jobdata_set($key, $value) {
1093 if (!is_array($this->jobdata)) {
1094 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce);
1095 if (!is_array($this->jobdata)) $this->jobdata = array();
1096 }
1097 $this->jobdata[$key] = $value;
1098 update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1099 }
1100
1101 public function jobdata_delete($key) {
1102 if (!is_array($this->jobdata)) {
1103 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce);
1104 if (!is_array($this->jobdata)) $this->jobdata = array();
1105 }
1106 unset($this->jobdata[$key]);
1107 update_site_option("updraft_jobdata_".$this->nonce, $this->jobdata);
1108 }
1109
1110 public function jobdata_get($key, $default = null) {
1111 if (!is_array($this->jobdata)) {
1112 $this->jobdata = get_site_option("updraft_jobdata_".$this->nonce, array());
1113 if (!is_array($this->jobdata)) return $default;
1114 }
1115 return (isset($this->jobdata[$key])) ? $this->jobdata[$key] : $default;
1116 }
1117
1118 // This procedure initiates a backup run
1119 public function boot_backup($backup_files, $backup_database, $restrict_files_to_override = false, $one_shot = false, $service = false) {
1120
1121 @ignore_user_abort(true);
1122 // 15 minutes
1123 @set_time_limit(900);
1124
1125 //generate backup information
1126 $this->backup_time_nonce();
1127 // The current_resumption is consulted within logfile_open()
1128 $this->current_resumption = 0;
1129 $this->logfile_open($this->nonce);
1130
1131 if (!is_file($this->logfile_name)) {
1132 $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.');
1133 $this->log(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.','updraftplus'), 'error');
1134 return false;
1135 }
1136
1137 // Some house-cleaning
1138 $this->clean_temporary_files();
1139 do_action('updraftplus_boot_backup');
1140
1141 // Log some information that may be helpful
1142 $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').")");
1143
1144 if (false === $one_shot) {
1145 # If the files and database schedules are the same, and if this the file one, then we rope in database too.
1146 # On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
1147 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' )) {
1148 $backup_database = ($backup_files == true) ? true : false;
1149 }
1150 $this->log("Processed schedules. Tasks now: Backup files: $backup_files Backup DB: $backup_database");
1151 }
1152
1153 if (!is_string($service) && !is_array($service)) $service = UpdraftPlus_Options::get_updraft_option('updraft_service');
1154 $service = $this->just_one($service);
1155
1156 # If nothing to be done, then just finish
1157 if (!$backup_files && !$backup_database) {
1158 $this->backup_finish(1, false, false, 0);
1159 return;
1160 }
1161
1162 // Allow the resume interval to be more than 300 if last time we know we went beyond that - but never more than 600
1163 $resume_interval = (int)min(max(300, get_transient('updraft_initial_resume_interval')), 600);
1164 # 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)
1165 delete_transient('updraft_initial_resume_interval');
1166
1167 $job_file_entities = array();
1168 if ($backup_files) {
1169 $possible_backups = $this->get_backupable_file_entities(true);
1170 foreach ($possible_backups as $youwhat => $whichdir) {
1171 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))) {
1172 // The 0 indicates the zip file index
1173 $job_file_entities[$youwhat] = array(
1174 'index' => 0
1175 );
1176 }
1177 }
1178 }
1179
1180 $initial_jobdata = array(
1181 'resume_interval', $resume_interval,
1182 'job_type', 'backup',
1183 'jobstatus', 'begun',
1184 'backup_time', $this->backup_time,
1185 'backup_time_ms', $this->backup_time_ms,
1186 'service', $service,
1187 'split_every', max(intval(UpdraftPlus_Options::get_updraft_option('updraft_split_every', 800)), UPDRAFTPLUS_SPLIT_MIN),
1188 'maxzipbatch', 26214400, #25Mb
1189 'job_file_entities', $job_file_entities,
1190 'one_shot', $one_shot
1191 );
1192
1193 if ($one_shot) update_site_option('updraft_oneshotnonce', $this->nonce);
1194
1195 // Save what *should* be done, to make it resumable from this point on
1196 array_push($initial_jobdata, 'backup_database', (($backup_database) ? 'begun' : 'no'));
1197 if ($backup_files) array_push($initial_jobdata, 'backup_files', 'begun');
1198
1199 // Use of jobdata_set_multi saves around 200ms
1200 call_user_func_array(array($this, 'jobdata_set_multi'), $initial_jobdata);
1201
1202 // Everything is set up; now go
1203 $this->backup_resume(0, $this->nonce);
1204
1205 if ($one_shot) delete_site_option('updraft_oneshotnonce');
1206
1207 }
1208
1209 function backup_finish($cancel_event, $do_cleanup, $allow_email, $resumption_no) {
1210
1211 $delete_jobdata = false;
1212
1213 // 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)
1214
1215 // 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.
1216 if ($this->error_count() == 0) {
1217 if ($do_cleanup) {
1218 $this->log("There were no errors in the uploads, so the 'resume' event ($cancel_event) is being unscheduled");
1219 # 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)
1220 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
1221 $this->jobdata_set('jobstatus', 'finished');
1222 $delete_jobdata = true;
1223 }
1224 } else {
1225 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
1226 $this->jobdata_set('jobstatus', 'resumingforerrors');
1227 }
1228
1229 // Send the results email if appropriate, which means:
1230 // - The caller allowed it (which is not the case in an 'empty' run)
1231 // - And: An email address was set (which must be so in email mode)
1232 // And one of:
1233 // - Debug mode
1234 // - There were no errors (which means we completed and so this is the final run - time for the final report)
1235 // - It was the tenth resumption; everything failed
1236
1237 $send_an_email = false;
1238
1239 // Make sure that the final status is shown
1240 if ($this->error_count() == 0) {
1241 $send_an_email = true;
1242 if ($this->error_count('warning') == 0) {
1243 $final_message = __('The backup apparently succeeded and is now complete','updraftplus');
1244 # 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
1245 if ('The backup apparently succeeded and is now complete' != $final_message) {
1246 $updraftplus->log('The backup apparently succeeded and is now complete');
1247 }
1248 } else {
1249 $final_message = __('The backup apparently succeeded (with warnings) and is now complete','updraftplus');
1250 if ('The backup apparently succeeded (with warnings) and is now complete' != $final_message) {
1251 $updraftplus->log('The backup apparently succeeded (with warnings) and is now complete');
1252 }
1253 }
1254 } elseif ($this->newresumption_scheduled == false) {
1255 $send_an_email = true;
1256 $final_message = __('The backup attempt has finished, apparently unsuccessfully', 'updraftplus');
1257 } else {
1258 // There are errors, but a resumption will be attempted
1259 $final_message = __('The backup has not finished; a resumption is scheduled', 'updraftplus');
1260 }
1261
1262 // Now over-ride the decision to send an email, if needed
1263 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
1264 $send_an_email = true;
1265 $this->log("An email has been scheduled for this job, because we are in debug mode");
1266 }
1267 // If there's no email address, or the set was empty, that is the final over-ride: don't send
1268 if (!$allow_email) {
1269 $send_an_email = false;
1270 $this->log("No email will be sent - this backup set was empty.");
1271 } elseif (UpdraftPlus_Options::get_updraft_option('updraft_email') == '') {
1272 $send_an_email = false;
1273 $this->log("No email will/can be sent - the user has not configured an email address.");
1274 }
1275
1276 global $updraftplus_backup;
1277 if ($send_an_email) $updraftplus_backup->send_results_email($final_message);
1278
1279 $this->log($final_message);
1280
1281 @fclose($this->logfile_handle);
1282
1283 // Don't delete the log file now; delete it upon rotation
1284 //if (!UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) @unlink($this->logfile_name);
1285
1286 // This is left until last for the benefit of the front-end UI, which then gets maximum chance to display the 'finished' status
1287 if ($delete_jobdata) delete_site_option('updraft_jobdata_'.$this->nonce);
1288
1289 }
1290
1291 public function error_count($level = 'error') {
1292 $count = 0;
1293 foreach ($this->errors as $err) {
1294 if (('error' == $level && (is_string($err) || is_wp_error($err))) || (is_array($err) && $level == $err['level']) ) { $count++; }
1295 }
1296 return $count;
1297 }
1298
1299 public function list_errors() {
1300 echo '<ul style="list-style: disc inside;">';
1301 foreach ($this->errors as $err) {
1302 if (is_wp_error($err)) {
1303 foreach ($err->get_error_messages() as $msg) {
1304 echo '<li>'.htmlspecialchars($msg).'<li>';
1305 }
1306 } elseif (is_array($err) && 'error' == $err['level']) {
1307 echo "<li>".htmlspecialchars($err['message'])."</li>";
1308 } elseif (is_string($err)) {
1309 echo "<li>".htmlspecialchars($err)."</li>";
1310 } else {
1311 print "<li>".print_r($err,true)."</li>";
1312 }
1313 }
1314 echo '</ul>';
1315 }
1316
1317 function save_last_backup($backup_array) {
1318 $success = ($this->error_count() == 0) ? 1 : 0;
1319
1320 $last_backup = array('backup_time'=>$this->backup_time, 'backup_array'=>$backup_array, 'success'=>$success, 'errors'=>$this->errors, 'backup_nonce' => $this->nonce);
1321
1322 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup);
1323 }
1324
1325 // This should be called whenever a file is successfully uploaded
1326 function uploaded_file($file, $id = false, $force = false) {
1327
1328 global $updraftplus_backup;
1329
1330 $service = (empty($updraftplus_backup->current_service)) ? '' : $updraftplus_backup->current_service;
1331 $shash = $service.'-'.md5($file);
1332 $this->jobdata_set("uploaded_".$shash, 'yes');
1333
1334 if ($force || $updraftplus_backup->last_service) {
1335 $hash = md5($file);
1336 $this->log("Recording as successfully uploaded: $file ($hash)");
1337 $this->jobdata_set("uploaded_".$hash, 'yes');
1338 } else {
1339 $this->log("Recording as successfully uploaded: $file (".$updraftplus_backup->current_service.", more services to follow)");
1340 }
1341
1342 $upload_status = $this->jobdata_get('uploading_substatus');
1343 if (is_array($upload_status) && isset($upload_status['i'])) {
1344 $upload_status['i']++;
1345 $upload_status['p']=0;
1346 $this->jobdata_set('uploading_substatus', $upload_status);
1347 }
1348
1349 if ($id) {
1350 $ids = UpdraftPlus_Options::get_updraft_option('updraft_file_ids', array() );
1351 $ids[$file] = $id;
1352 UpdraftPlus_Options::update_updraft_option('updraft_file_ids',$ids);
1353 $this->log("Stored file<->id correlation in database ($file <-> $id)");
1354 }
1355
1356 // Delete local files immediately if the option is set
1357 // Where we are only backing up locally, only the "prune" function should do deleting
1358 if ($updraftplus_backup->last_service && ($this->jobdata_get('service') !== '' && ((is_array($this->jobdata_get('service')) && count($this->jobdata_get('service')) >0) || (is_string($this->jobdata_get('service')) && $this->jobdata_get('service') !== 'none')))) {
1359 $this->delete_local($file);
1360 }
1361 }
1362
1363 function is_uploaded($file, $service = '') {
1364 $hash = $service.'-'.md5($file);
1365 return ($this->jobdata_get("uploaded_$hash") === "yes") ? true : false;
1366 }
1367
1368 function delete_local($file) {
1369 if(UpdraftPlus_Options::get_updraft_option('updraft_delete_local')) {
1370 $this->log("Deleting local file: $file");
1371 //need error checking so we don't delete what isn't successfully uploaded?
1372 $fullpath = $this->backups_dir_location().'/'.$file;
1373 return unlink($fullpath);
1374 }
1375 return true;
1376 }
1377
1378 // This function is not needed for backup success, according to the design, but it helps with efficient scheduling
1379 function reschedule_if_needed() {
1380 // If nothing is scheduled, then return
1381 if (empty($this->newresumption_scheduled)) return;
1382 $time_now = time();
1383 $time_away = $this->newresumption_scheduled - $time_now;
1384 // 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)
1385 if ($time_away >1 && $time_away <= 45) {
1386 $this->log('The scheduled resumption is within 45 seconds - will reschedule');
1387 // Push 45 seconds into the future
1388 // $this->reschedule(60);
1389 // Increase interval generally by 45 seconds, on the assumption that our prior estimates were innaccurate (i.e. not just 45 seconds *this* time)
1390 $this->increase_resume_and_reschedule(45);
1391 }
1392 }
1393
1394 function reschedule($how_far_ahead) {
1395 // Reschedule - remove presently scheduled event
1396 $next_resumption = $this->current_resumption + 1;
1397 wp_clear_scheduled_hook('updraft_backup_resume', array($next_resumption, $this->nonce));
1398 // Add new event
1399 if ($how_far_ahead < 300) $how_far_ahead=300;
1400 $schedule_for = time() + $how_far_ahead;
1401 $this->log("Rescheduling resumption $next_resumption: moving to $how_far_ahead seconds from now ($schedule_for)");
1402 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $this->nonce));
1403 $this->newresumption_scheduled = $schedule_for;
1404 }
1405
1406 function increase_resume_and_reschedule($howmuch = 120, $force_schedule = false) {
1407
1408 $resume_interval = max(intval($this->jobdata_get('resume_interval')), 300);
1409
1410 if (empty($this->newresumption_scheduled) && $force_schedule) {
1411 $this->log("A new resumption will be scheduled to prevent the job ending");
1412 }
1413
1414 $new_resume = $resume_interval + $howmuch;
1415 # 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
1416 if ($this->opened_log_time > 100 && microtime(true)-$this->opened_log_time > $new_resume) {
1417 $new_resume = ceil(microtime(true)-$this->opened_log_time)+45;
1418 $howmuch = $new_resume-$resume_interval;
1419 }
1420
1421 if (!empty($this->newresumption_scheduled) || $force_schedule) $this->reschedule($new_resume);
1422 $this->jobdata_set('resume_interval', $new_resume);
1423
1424 $this->log("To decrease the likelihood of overlaps, increasing resumption interval to: $resume_interval + $howmuch = $new_resume");
1425 }
1426
1427 // For detecting another run, and aborting if one was found
1428 function check_recent_modification($file) {
1429 if (file_exists($file)) {
1430 $time_mod = (int)@filemtime($file);
1431 $time_now = time();
1432 if ($time_mod>100 && ($time_now-$time_mod)<30) {
1433 $this->terminate_due_to_activity($file, $time_now, $time_mod);
1434 }
1435 }
1436 }
1437
1438 public function really_is_writable($dir) {
1439 // Suppress warnings, since if the user is dumping warnings to screen, then invalid JavaScript results and the screen breaks.
1440 if (!@is_writable($dir)) return false;
1441 // Found a case - GoDaddy server, Windows, PHP 5.2.17 - where is_writable returned true, but writing failed
1442 $rand_file = "$dir/test-".md5(rand().time()).".txt";
1443 while (file_exists($rand_file)) {
1444 $rand_file = "$dir/test-".md5(rand().time()).".txt";
1445 }
1446 $ret = @file_put_contents($rand_file, 'testing...');
1447 @unlink($rand_file);
1448 return ($ret > 0);
1449 }
1450
1451 function backup_others_dirlist() {
1452 # Create an array of directories to be skipped
1453 $others_skip = preg_split("/,/",UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
1454 # Make the values into the keys
1455 $others_skip = array_flip($others_skip);
1456
1457 $possible_backups_dirs = array_flip($this->get_backupable_file_entities(false));
1458
1459 $other_dirlist = $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $possible_backups_dirs, $others_skip);
1460
1461 return $other_dirlist;
1462
1463 }
1464
1465 /**
1466 * Add backquotes to tables and db-names in
1467 * SQL queries. Taken from phpMyAdmin.
1468 */
1469 public function backquote($a_name) {
1470 if (!empty($a_name) && $a_name != '*') {
1471 if (is_array($a_name)) {
1472 $result = array();
1473 reset($a_name);
1474 while(list($key, $val) = each($a_name))
1475 $result[$key] = '`' . $val . '`';
1476 return $result;
1477 } else {
1478 return '`' . $a_name . '`';
1479 }
1480 } else {
1481 return $a_name;
1482 }
1483 }
1484
1485 // 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.
1486 function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
1487
1488 // 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.
1489
1490 $dirlist = array();
1491
1492 $this->log('Looking for candidates to back up in: '.$backup_from_inside_dir);
1493
1494 $updraft_dir = $this->backups_dir_location();
1495 if ($handle = opendir($backup_from_inside_dir)) {
1496
1497 while (false !== ($entry = readdir($handle))) {
1498 // $candidate: full path; $entry = one-level
1499 $candidate = $backup_from_inside_dir.'/'.$entry;
1500 if ($entry != "." && $entry != "..") {
1501 if (isset($avoid_these_dirs[$candidate])) {
1502 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
1503 } elseif ($candidate == $updraft_dir) {
1504 $this->log("finding files: $entry: skipping: this is the updraft directory");
1505 } elseif (isset($skip_these_dirs[$entry])) {
1506 $this->log("finding files: $entry: skipping: excluded by options");
1507 } else {
1508 $add_to_list = true;
1509 // Now deal with entries in $skip_these_dirs ending in *
1510 foreach ($skip_these_dirs as $skip => $sind) {
1511 if ('*' == substr($skip, -1, 1)) {
1512 if (substr($entry, 0, strlen($skip)-1) == substr($skip, 0, strlen($skip)-1)) {
1513 $this->log("finding files: $entry: skipping: excluded by options (glob)");
1514 $add_to_list = false;
1515 }
1516 }
1517 }
1518 if ($add_to_list) {
1519 $this->log("finding files: $entry: adding to list");
1520 array_push($dirlist, $candidate);
1521 }
1522 }
1523 }
1524 }
1525 @closedir($handle);
1526 } else {
1527 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
1528 $this->log(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir, 'error');
1529 }
1530
1531 return $dirlist;
1532
1533 }
1534
1535 function save_backup_history($backup_array) {
1536 if(is_array($backup_array)) {
1537 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
1538 $backup_history = (is_array($backup_history)) ? $backup_history : array();
1539 $backup_array['nonce'] = $this->nonce;
1540 $backup_array['service'] = $this->jobdata_get('service');
1541 $backup_history[$this->backup_time] = $backup_array;
1542 UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history);
1543 } else {
1544 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
1545 $this->log(__('Could not save backup history because we have no backup array. Backup probably failed.','updraftplus'), 'error');
1546 }
1547 }
1548
1549 public function is_db_encrypted($file) {
1550 return preg_match('/\.crypt$/i', $file);
1551 }
1552
1553 public function get_backup_history($timestamp = false) {
1554 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
1555 // In fact, it looks like the line below actually *introduces* a race condition
1556 //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
1557 // global $wpdb;
1558 // $backup_history = @unserialize($wpdb->get_var($wpdb->prepare("SELECT option_value from $wpdb->options WHERE option_name='updraft_backup_history'")));
1559 if(is_array($backup_history)) {
1560 krsort($backup_history); //reverse sort so earliest backup is last on the array. Then we can array_pop.
1561 } else {
1562 $backup_history = array();
1563 }
1564 if (!$timestamp) return $backup_history;
1565 return (isset($backup_history[$timestamp])) ? $backup_history[$timestamp] : array();
1566 }
1567
1568 function terminate_due_to_activity($file, $time_now, $time_mod) {
1569 # We check-in, to avoid 'no check in last time!' detectors firing
1570 $this->record_still_alive();
1571 $file_size = file_exists($file) ? round(filesize($file)/1024,1). 'Kb' : 'n/a';
1572 $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.");
1573 $this->increase_resume_and_reschedule(120, true);
1574 die;
1575 }
1576
1577 # Replace last occurence
1578 public function str_lreplace($search, $replace, $subject) {
1579 $pos = strrpos($subject, $search);
1580 if($pos !== false) $subject = substr_replace($subject, $replace, $pos, strlen($search));
1581 return $subject;
1582 }
1583
1584 public function str_replace_once($needle, $replace, $haystack) {
1585 $pos = strpos($haystack,$needle);
1586 return ($pos !== false) ? substr_replace($haystack,$replace,$pos,strlen($needle)) : $haystack;
1587 }
1588
1589 /*
1590 this function is both the backup scheduler and ostensibly a filter callback for saving the option.
1591 it is called in the register_setting for the updraft_interval, which means when the admin settings
1592 are saved it is called. it returns the actual result from wp_filter_nohtml_kses (a sanitization filter)
1593 so the option can be properly saved.
1594 */
1595 public function schedule_backup($interval) {
1596 //clear schedule and add new so we don't stack up scheduled backups
1597 wp_clear_scheduled_hook('updraft_backup');
1598 switch($interval) {
1599 case 'every4hours':
1600 case 'every8hours':
1601 case 'twicedaily':
1602 case 'daily':
1603 case 'weekly':
1604 case 'fortnightly':
1605 case 'monthly':
1606 $first_time = apply_filters('updraftplus_schedule_firsttime_files', time()+30);
1607 wp_schedule_event($first_time, $interval, 'updraft_backup');
1608 break;
1609 }
1610 return wp_filter_nohtml_kses($interval);
1611 }
1612
1613 public function deactivation () {
1614 // wp_clear_scheduled_hook('updraftplus_weekly_ping');
1615 }
1616
1617 // Acts as a WordPress options filter
1618 public function googledrive_clientid_checkchange($client_id) {
1619 if (UpdraftPlus_Options::get_updraft_option('updraft_googledrive_token') != '' && UpdraftPlus_Options::get_updraft_option('updraft_googledrive_clientid') != $client_id) {
1620 require_once(UPDRAFTPLUS_DIR.'/methods/googledrive.php');
1621 add_action('http_api_curl', array($this, 'add_curl_capath'));
1622 UpdraftPlus_BackupModule_googledrive::gdrive_auth_revoke(true);
1623 remove_action('http_api_curl', array($this, 'add_curl_capath'));
1624
1625 }
1626 return $client_id;
1627 }
1628
1629 public function schedule_backup_database($interval) {
1630 //clear schedule and add new so we don't stack up scheduled backups
1631 wp_clear_scheduled_hook('updraft_backup_database');
1632 switch($interval) {
1633 case 'every4hours':
1634 case 'every8hours':
1635 case 'twicedaily':
1636 case 'daily':
1637 case 'weekly':
1638 case 'fortnightly':
1639 case 'monthly':
1640 $first_time = apply_filters('updraftplus_schedule_firsttime_db', time()+30);
1641 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
1642 break;
1643 }
1644 return wp_filter_nohtml_kses($interval);
1645 }
1646
1647 //wp-cron only has hourly, daily and twicedaily, so we need to add some of our own
1648 public function modify_cron_schedules($schedules) {
1649 $schedules['weekly'] = array( 'interval' => 604800, 'display' => 'Once Weekly' );
1650 $schedules['fortnightly'] = array( 'interval' => 1209600, 'display' => 'Once Each Fortnight' );
1651 $schedules['monthly'] = array( 'interval' => 2592000, 'display' => 'Once Monthly' );
1652 $schedules['every4hours'] = array( 'interval' => 14400, 'display' => 'Every 4 hours' );
1653 $schedules['every8hours'] = array( 'interval' => 28800, 'display' => 'Every 8 hours' );
1654 return $schedules;
1655 }
1656
1657 // Returns without any trailing slash
1658 public function backups_dir_location() {
1659
1660 if (!empty($this->backup_dir)) return $this->backup_dir;
1661
1662 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
1663 # 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.
1664 if (preg_match('/^wp-content\/(.*)$/', $updraft_dir, $matches) && ABSPATH.'wp-content' === WP_CONTENT_DIR) {
1665 UpdraftPlus_Options::update_updraft_option('updraft_dir', $matches[1]);
1666 $updraft_dir = WP_CONTENT_DIR.'/'.$matches[1];
1667 }
1668 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
1669 $updraft_dir = ($updraft_dir) ? $updraft_dir : $default_backup_dir;
1670
1671 // Do a test for a relative path
1672 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
1673 # Legacy - file paths stored related to ABSPATH
1674 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')) {
1675 $updraft_dir = ABSPATH.$updraft_dir;
1676 } else {
1677 # File paths stored relative to WP_CONTENT_DIR
1678 $updraft_dir = trailingslashit(WP_CONTENT_DIR).$updraft_dir;
1679 }
1680 }
1681
1682 // Check for the existence of the dir and prevent enumeration
1683 // index.php is for a sanity check - make sure that we're not somewhere unexpected
1684 if((!is_dir($updraft_dir) || !is_file($updraft_dir.'/index.html') || !is_file($updraft_dir.'/.htaccess')) && !is_file($updraft_dir.'/index.php')) {
1685 @mkdir($updraft_dir, 0775, true);
1686 @file_put_contents($updraft_dir.'/index.html',"<html><body><a href=\"http://updraftplus.com\">WordPress backups by UpdraftPlus</a></body></html>");
1687 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess','deny from all');
1688 }
1689
1690 $this->backup_dir = $updraft_dir;
1691
1692 return $updraft_dir;
1693 }
1694
1695 public function spool_file($type, $fullpath, $encryption = "") {
1696
1697 @set_time_limit(900);
1698
1699 if (file_exists($fullpath)) {
1700
1701 $file = basename($fullpath);
1702
1703 $len = filesize($fullpath);
1704
1705 $filearr = explode('.',$file);
1706
1707 $file_ext = array_pop($filearr);
1708 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
1709 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
1710 header("Content-Length: $len;");
1711
1712 if ($file_ext == 'crypt') {
1713 if ($encryption == "") $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
1714 if ($encryption == "") {
1715 header('Content-type: text/plain');
1716 _e("Decryption failed. The database file is encrypted, but you have no encryption key entered.",'updraftplus');
1717 $this->log('Decryption of database failed: the database file is encrypted, but you have no encryption key entered.', 'error');
1718 } else {
1719 $this->ensure_phpseclib('Crypt_Rijndael', 'Crypt/Rijndael');
1720 $rijndael = new Crypt_Rijndael();
1721 $rijndael->setKey($encryption);
1722 $ciphertext = $rijndael->decrypt(file_get_contents($fullpath));
1723 if ($ciphertext) {
1724 header('Content-type: application/x-gzip');
1725 header("Content-Disposition: attachment; filename=\"".substr($file,0,-6)."\";");
1726 print $ciphertext;
1727 } else {
1728 header('Content-type: text/plain');
1729 echo __("Decryption failed. The most likely cause is that you used the wrong key.",'updraftplus')." ".__('The decryption key used:','updraftplus').' '.$encryption;
1730
1731 }
1732 }
1733 } else {
1734 if ($file_ext == 'zip') {
1735 header('Content-type: application/zip');
1736 } else {
1737 // When we sent application/x-gzip, we found a case where the server compressed it a second time
1738 header('Content-type: application/octet-stream');
1739 }
1740 header("Content-Disposition: attachment; filename=\"$file\";");
1741 # Prevent the file being read into memory
1742 @ob_end_flush();
1743 readfile($fullpath);
1744 }
1745 // $this->delete_local($file);
1746 } else {
1747 echo __('File not found', 'updraftplus');
1748 }
1749 }
1750
1751 public function retain_range($input) {
1752 $input = (int)$input;
1753 return ($input > 0 && $input < 3650) ? $input : 1;
1754 }
1755
1756 public function just_one($input) {
1757 $oinput = $input;
1758 $rinput = (is_array($input)) ? array_pop($input) : $input;
1759 return apply_filters('updraftplus_savestorage', $rinput, $oinput);
1760 }
1761
1762 function memory_check_current($memory_limit = false) {
1763 # Returns in megabytes
1764 if ($memory_limit == false) $memory_limit = ini_get('memory_limit');
1765 $memory_limit = rtrim($memory_limit);
1766 $memory_unit = $memory_limit[strlen($memory_limit)-1];
1767 $memory_limit = substr($memory_limit,0,strlen($memory_limit)-1);
1768 switch($memory_unit) {
1769 case 'K':
1770 $memory_limit = floor($memory_limit/1024);
1771 break;
1772 case 'G':
1773 $memory_limit = $memory_limit*1024;
1774 break;
1775 case 'M':
1776 //assumed size, no change needed
1777 break;
1778 }
1779 return $memory_limit;
1780 }
1781
1782 function memory_check($memory, $check_using = false) {
1783 $memory_limit = $this->memory_check_current($check_using);
1784 return ($memory_limit >= $memory)?true:false;
1785 }
1786
1787 private function url_start($urls,$url) {
1788 return ($urls) ? '<a href="http://'.$url.'">' : "";
1789 }
1790
1791 private function url_end($urls,$url) {
1792 return ($urls) ? '</a>' : " (http://$url)";
1793 }
1794
1795 public function wordshell_random_advert($urls) {
1796 if (defined('UPDRAFTPLUS_NOADS3')) return "";
1797 $rad = rand(0,8);
1798 switch ($rad) {
1799 case 0:
1800 return $this->url_start($urls,'updraftplus.com')."Want more features or paid, guaranteed support? Check out UpdraftPlus.Com".$this->url_end($urls,'updraftplus.com');
1801 break;
1802 case 1:
1803 if (defined('WPLANG') && strlen(WPLANG)>0 && !is_file(UPDRAFTPLUS_DIR.'/languages/updraftplus-'.WPLANG.
1804 '.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/');
1805
1806 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');
1807 break;
1808 case 2:
1809 return $this->url_start($urls,'wordshell.net')."Check out WordShell".$this->url_end($urls,'www.wordshell.net')." - manage WordPress from the command line - huge time-saver";
1810 break;
1811 case 3:
1812 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');
1813 break;
1814 case 4:
1815 return $this->url_start($urls,'www.simbahosting.co.uk')."Need high-quality WordPress hosting from WordPress specialists? (Including automatic backups and 1-click installer). Get it from the creators of UpdraftPlus.".$this->url_end($urls,'www.simbahosting.co.uk');
1816 break;
1817 case 5:
1818 if (!defined('UPDRAFTPLUS_NOADS3')) {
1819 return $this->url_start($urls,'updraftplus.com').__("Need even more features and support? Check out UpdraftPlus Premium",'updraftplus').$this->url_end($urls,'updraftplus.com');
1820 } else {
1821 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.";
1822 }
1823 break;
1824 case 6:
1825 // 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/');
1826 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');
1827 break;
1828 case 7:
1829 return $this->url_start($urls,'updraftplus.com').__("Check out UpdraftPlus.Com for help, add-ons and support",'updraftplus').$this->url_end($urls,'updraftplus.com');
1830 break;
1831 case 8:
1832 return __("Want to say thank-you for UpdraftPlus?",'updraftplus').$this->url_start($urls,'updraftplus.com/shop/')." ".__("Please buy our very cheap 'no adverts' add-on.",'updraftplus').$this->url_end($urls,'updraftplus.com/shop/');
1833 break;
1834 }
1835 }
1836
1837 }
1838
1839
1840 ?>
1841