PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.8.13
UpdraftPlus: WP Backup & Migration Plugin v1.8.13
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.8.13, at updraftplus.php

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