PageRenderTime 5ms CodeModel.GetById 43ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 1ms

/wp-content/plugins/updraftplus/updraftplus.php

https://github.com/tjworks/mongoing
PHP | 2610 lines | 1792 code | 376 blank | 442 comment | 666 complexity | e74b0c70da6434e710abff8c9bd3b97a MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1<?php
  2/*
  3Plugin Name: UpdraftPlus - Backup/Restore
  4Plugin URI: http://updraftplus.com
  5Description: Backup and restore: take backups locally, or backup to Amazon S3, Dropbox, Google Drive, Rackspace, (S)FTP, WebDAV & email, on automatic schedules.
  6Author: UpdraftPlus.Com, DavidAnderson
  7Version: 1.9.13
  8Donate link: http://david.dw-perspective.org.uk/donate
  9License: GPLv3 or later
 10Text Domain: updraftplus
 11Domain Path: /languages
 12Author URI: http://updraftplus.com
 13*/
 14
 15/*
 16TODO - some of these are out of date/done, needs pruning
 17// On free version, add note to restore page/to "delete-old-dirs" section
 18// Make SFTP chunked (there is a new stream wrapper)
 19// Store/show current Dropbox account
 20// 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.
 21// Renewal links should redirect to login and redirect to relevant page after
 22// Alert user if they enter http(s):(etc) as their Dropbox path - seen one user do it
 23// Schedule a task to report on failure
 24// Copy.Com, Box
 25// Switch 'Backup Now' to call the WP action via AJAX instead of via Cron - then test on hosts who deny all cron (e.g. Heart)
 26// Get something to parse the 'Backups in progress' data, and if the 'next resumption' is far negative, and if also cron jobs appear to be not running, then call the action directly.
 27// If ionice is available, then use it to limit I/O usage
 28// Check the timestamps used in filenames - they should be UTC
 29// Get user to confirm if they check both the search/replace and wp-config boxes
 30// Tweak the display so that users seeing resumption messages don't think it's stuck
 31// A search/replace console without needing to restore
 32// On restore, check for some 'standard' PHP modules (prevents support requests related to them) -e.g. GD, Curl
 33// Recognise known huge non-core tables on restore, and postpone them to the end (AJAX method?)
 34// Add a cart notice if people have DBSF=quantity1
 35// 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)
 36// Include in email report the list of "more" directories: http://updraftplus.com/forums/support-forum-group1/paid-support-forum-forum2/wordpress-multi-sites-thread121/
 37// Integrate jstree for a nice files-chooser; use https://wordpress.org/plugins/dropbox-photo-sideloader/ to see how it's done
 38// Verify that attempting to bring back a MS backup on a non-MS install warns the user
 39// Pre-schedule resumptions that we know will be scheduled later
 40// Change add-ons screen, to be less confusing for people who haven't yet updated but have connected
 41// 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.
 42// Post restore, do an AJAX get for the site; if this results in a 500, then auto-turn-on WP_DEBUG
 43// Place in maintenance mode during restore - ?
 44// 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
 45// 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
 46// 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.
 47// Free/premium comparison page
 48// Complete the tweak to bring the delete-old-dirs within a dialog (just needed to deal wtih case of needing credentials more elegantly).
 49// Add note to support page requesting that non-English be translated
 50// More locking: lock the resumptions too (will need to manage keys to make sure junk data is not left behind)
 51// 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?
 52// Deal with missing plugins/themes/uploads directory when installing
 53// Bring down interval if we are already in upload time (since zip delays are no longer possible). See: options-general-11-23.txt
 54// Add FAQ - can I get it to save automatically to my computer?
 55// Pruner assumes storage is same as current - ?
 56// Include blog feed in basic email report
 57// 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).
 58// 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).
 59// Dates in the progress box are apparently untranslated
 60// Add-on descriptions are not internationalised
 61// Nicer in-dashboard log: show log + option to download; also (if 'reporting' add-on available) show the HTML report from that
 62// Take a look at logfile-to-examine.txt (stored), and the pattern of detection of zipfile contents
 63// http://www.phpclasses.org/package/8269-PHP-Send-MySQL-database-backup-files-to-Ubuntu-One.html
 64// Put the -old directories in updraft_dir instead of present location. Prevents file perms issues, and also will be automatically excluded from backups.
 65// Test restores via cloud service for small $??? (Relevant: http://browshot.com/features) (per-day? per-install?)
 66// Warn/prevent if trying to migrate between sub-domain/sub-folder based multisites
 67// Don't perform pruning when doing auto-backup?
 68// Post-migrate, notify the user if on Apache but without mod_rewrite (has been seen in the wild)
 69// Pre-check the search/replace box if migration detected
 70// Can some tables be omitted from the search/replace on a migrate? i.e. Special knowledge?
 71// Put a 'what do I get if I upgrade?' link into the mix
 72// Add to admin bar (and make it something that can be turned off)
 73// If migrated database from somewhere else, then add note about revising UD settings
 74// Strategy for what to do if the updraft_dir contains untracked backups. Automatically rescan?
 75// MySQL manual: See Section 8.2.2.1, Speed of INSERT Statements.
 76// Exempt UD itself from a plugins restore? (will options be out-of-sync? exempt options too?)
 77// Post restore/migrate, check updraft_dir, and reset if non-existent
 78// Auto-empty caches post-restore/post-migration (prevent support requests from people with state/wrong cacheing data)
 79// Show 'Migrate' instead of 'Restore' on the button if relevant
 80// Test with: http://wordpress.org/plugins/wp-db-driver/
 81// Backup notes
 82// Automatically re-count folder usage after doing a delete
 83// Switch zip engines earlier if no progress - see log.cfd793337563_hostingfails.txt
 84// 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)
 85// Incremental - can leverage some of the multi-zip work???
 86// Put in a help link to explain what WordPress core (including any additions to your WordPress root directory) does (was asked for support)
 87// More databases
 88// Multiple files in more-files
 89// On multisite, the settings should be in the network panel. Connection settings need migrating into site options.
 90// On restore, raise a warning for ginormous zips
 91// Detect double-compressed files when they are uploaded (need a way to detect gz compression in general)
 92// Log migrations/restores, and have an option for auto-emailing the log
 93# 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).
 94// 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.
 95// Multi-site manager at updraftplus.com
 96// Import/slurp backups from other sites. See: http://www.skyverge.com/blog/extending-the-wordpress-xml-rpc-api/
 97// 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)
 98// Unpack zips via AJAX? Do bit-by-bit to allow enormous opens a better chance? (have a huge one in Dropbox)
 99// Put in a maintenance-mode detector
100// Add update warning if they've got an add-on but not connected account
101// Detect CloudFlare output in attempts to connect - detecting cloudflare.com should be sufficient
102// Bring multisite shop page up to date
103// Re-do pricing + support packages
104// More files: back up multiple directories, not just one
105// Give a help page to go with the message: A zip error occurred - check your log for more details (reduce support requests)
106// Exclude .git and .svn by default from wpcore
107// Add option to add, not just replace entities on restore/migrate
108// Add warning to backup run at beginning if -old dirs exist
109// 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.
110// Generic S3 provider: add page to site. S3-compatible storage providers: http://www.dragondisk.com/s3-storage-providers.html
111// Importer - import backup sets from another WP site directly via HTTP
112// Option to create new user for self post-restore
113// Auto-disable certain cacheing/minifying plugins post-restore
114// Add note post-DB backup: you will need to log in using details from newly-imported DB
115// Make search+replace two-pass to deal with moving between exotic non-default moved-directory setups
116// Get link - http://www.rackspace.com/knowledge_center/article/how-to-use-updraftplus-to-back-up-cloud-sites-to-cloud-files
117// 'Delete from your webserver' should trigger a rescan if the backup was local-only
118// Option for additive restores - i.e. add content (themes, plugins,...) instead of replacing
119// Testing framework - automated testing of all file upload / download / deletion methods
120// 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
121// 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)
122// 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/
123// Selective restores - some resources
124// When you migrate/restore, if there is a .htaccess, warn/give option about it.
125// 'Show log' should be done in a nice pop-out, with a button to download the raw
126// delete_old_dirs() needs to use WP_Filesystem in a more user-friendly way when errors occur
127// Bulk download of entire set at once (not have to click 7 times).
128// Restoration should also clear all common cache locations (or just not back them up)
129// Deal with gigantic database tables - e.g. those over a million rows on cheap hosting.
130// When restoring core, need an option to retain database settings / exclude wp-config.php
131// If migrating, warn about consequences of over-writing wp-config.php
132// Produce a command-line version of the restorer (so that people with shell access are immune from server-enforced timeouts)
133// Restorations should be logged also
134// Migrator - list+download from remote, kick-off backup remotely
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// AJAX-ify restoration
143// Warn Premium users before de-activating not to update whilst inactive
144// Ability to re-scan existing cloud storage
145// Dropbox uses one mcrypt function - port to phpseclib for more portability
146// Store meta-data on which version of UD the backup was made with (will help if we ever introduce quirks that need ironing)
147// 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)
148// Rackspace folders
149//Do an automated test periodically for the success of loop-back connections
150//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.
151//Change DB encryption to not require whole gzip in memory (twice) http://www.frostjedi.com/phpbb3/viewtopic.php?f=46&t=168508&p=391881&e=391881
152//Add YouSendIt/Hightail, Copy.Com, Box.Net, SugarSync, Me.Ga support??
153//Make it easier to find add-ons
154// On restore, move in data, not the whole directory (gives more flexibility on file permissions)
155// 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.
156// 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??
157// Create a "Want Support?" button/console, that leads them through what is needed, and performs some basic tests...
158// Add-on to check integrity of backups
159// Add-on to manage all your backups from a single dashboard
160// 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
161// Multiple schedules
162// Allow connecting to remote storage, scanning + populating backup history from it
163// Multisite add-on should allow restoring of each blog individually
164// Remove the recurrence of admin notices when settings are saved due to _wp_referer
165// New sub-module to verify that the backups are there, independently of backup thread
166*/
167
168/*
169Portions copyright 2011-14 David Anderson
170Portions copyright 2010 Paul Kehrer
171Other portions copyright as indicated authors in the relevant files
172
173This program is free software; you can redistribute it and/or modify
174it under the terms of the GNU General Public License as published by
175the Free Software Foundation; either version 3 of the License, or
176(at your option) any later version.
177
178This program is distributed in the hope that it will be useful,
179but WITHOUT ANY WARRANTY; without even the implied warranty of
180MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
181GNU General Public License for more details.
182
183You should have received a copy of the GNU General Public License
184along with this program; if not, write to the Free Software
185Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
186*/
187
188define('UPDRAFTPLUS_DIR', dirname(__FILE__));
189define('UPDRAFTPLUS_URL', plugins_url('', __FILE__));
190define('UPDRAFT_DEFAULT_OTHERS_EXCLUDE','upgrade,cache,updraft,backup*,*backups');
191define('UPDRAFT_DEFAULT_UPLOADS_EXCLUDE','backup*,*backups,backwpup*,wp-clone');
192
193# The following can go in your wp-config.php
194# 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).
195if (!defined('UPDRAFTPLUS_DATA_OPTIONAL_TABLES')) define('UPDRAFTPLUS_DATA_OPTIONAL_TABLES', 'bwps_log,statpress,slim_stats,redirection_logs,Counterize,Counterize_Referers,Counterize_UserAgents');
196if (!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");
197if (!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");
198# If any individual file size is greater than this, then a warning is given
199if (!defined('UPDRAFTPLUS_WARN_FILE_SIZE')) define('UPDRAFTPLUS_WARN_FILE_SIZE', 1024*1024*250);
200# 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.
201if (!defined('UPDRAFTPLUS_WARN_DB_ROWS')) define('UPDRAFTPLUS_WARN_DB_ROWS', 150000);
202
203# The smallest value (in megabytes) that the "split zip files at" setting is allowed to be set to
204if (!defined('UPDRAFTPLUS_SPLIT_MIN')) define('UPDRAFTPLUS_SPLIT_MIN', 25);
205
206# The maximum number of files to batch at one time when writing to the backup archive. You'd only be likely to want to raise (not lower) this.
207if (!defined('UPDRAFTPLUS_MAXBATCHFILES')) define('UPDRAFTPLUS_MAXBATCHFILES', 500);
208
209// Load add-ons and various files that may or may not be present, depending on where the plugin was distributed
210if (is_file(UPDRAFTPLUS_DIR.'/premium.php')) require_once(UPDRAFTPLUS_DIR.'/premium.php');
211if (is_file(UPDRAFTPLUS_DIR.'/autoload.php')) require_once(UPDRAFTPLUS_DIR.'/autoload.php');
212if (is_file(UPDRAFTPLUS_DIR.'/udaddons/updraftplus-addons.php')) include_once(UPDRAFTPLUS_DIR.'/udaddons/updraftplus-addons.php');
213
214$updraftplus_have_addons = 0;
215if (is_dir(UPDRAFTPLUS_DIR.'/addons') && $dir_handle = opendir(UPDRAFTPLUS_DIR.'/addons')) {
216	while (false !== ($e = readdir($dir_handle))) {
217		if (is_file(UPDRAFTPLUS_DIR.'/addons/'.$e) && preg_match('/\.php$/', $e)) {
218			$header = file_get_contents(UPDRAFTPLUS_DIR.'/addons/'.$e, false, null, -1, 1024);
219			$phprequires = (preg_match("/RequiresPHP: (\d[\d\.]+)/", $header, $matches)) ? $matches[1] : false;
220			$phpinclude = (preg_match("/IncludePHP: (\S+)/", $header, $matches)) ? $matches[1] : false;
221			if (false === $phprequires || version_compare(PHP_VERSION, $phprequires, '>=')) {
222				$updraftplus_have_addons++;
223				if ($phpinclude) require_once(UPDRAFTPLUS_DIR.'/'.$phpinclude);
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
234if (!$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
245if (!class_exists('UpdraftPlus_Options')) require_once(UPDRAFTPLUS_DIR.'/options.php');
246
247class 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		'bitcasa' => 'Bitcasa',
263		's3generic' => 'S3-Compatible (Generic)',
264		'openstack' => 'OpenStack (Swift)',
265		'dreamobjects' => 'DreamObjects',
266		'email' => 'Email'
267	);
268
269	public $errors = array();
270	public $nonce;
271	public $logfile_name = "";
272	public $logfile_handle = false;
273	public $backup_time;
274	public $job_time_ms;
275
276	public $opened_log_time;
277	private $backup_dir;
278
279	private $jobdata;
280
281	public $something_useful_happened = false;
282	public $have_addons = false;
283
284	// Used to schedule resumption attempts beyond the tenth, if needed
285	public $current_resumption;
286	public $newresumption_scheduled = false;
287
288	public function __construct() {
289
290		// Initialisation actions - takes place on plugin load
291
292		if ($fp = fopen(__FILE__, 'r')) {
293			$file_data = fread( $fp, 1024 );
294			if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
295				$this->version = $matches[1];
296			}
297			fclose($fp);
298		}
299
300		# Create admin page
301		add_action('init', array($this, 'handle_url_actions'));
302		// Run earlier than default - hence earlier than other components
303		// admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
304		add_action(apply_filters('updraft_admin_menu_hook', 'admin_menu'), array($this, 'admin_menu'), 9);
305		# Not a mistake: admin-ajax.php calls only admin_init and not admin_menu
306		add_action('admin_init', array($this, 'admin_menu'), 9);
307		add_action('updraft_backup', array($this, 'backup_files'));
308		add_action('updraft_backup_database', array($this, 'backup_database'));
309		add_action('updraft_backupnow_backup', array($this, 'backupnow_files'));
310		add_action('updraft_backupnow_backup_database', array($this, 'backupnow_database'));
311		add_action('updraft_backupnow_backup_all', array($this, 'backup_all'));
312		# backup_all as an action is legacy (Oct 2013) - there may be some people who wrote cron scripts to use it
313		add_action('updraft_backup_all', array($this, 'backup_all'));
314		# this is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
315		add_action('updraft_backup_resume', array($this, 'backup_resume'), 10, 3);
316		# http://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules. Raised priority because some plugins wrongly over-write all prior schedule changes (including BackupBuddy!)
317		add_filter('cron_schedules', array($this, 'modify_cron_schedules'), 30);
318		add_action('plugins_loaded', array($this, 'load_translations'));
319
320		# Prevent iThemes Security from telling people that they have no backups (and advertising them another product on that basis!)
321		add_filter('itsec_has_external_backup', array($this, 'return_true'), 999);
322		add_filter('itsec_external_backup_link', array($this, 'itsec_external_backup_link'), 999);
323		add_filter('itsec_scheduled_external_backup', array($this, 'itsec_scheduled_external_backup'), 999);
324
325		register_deactivation_hook(__FILE__, array($this, 'deactivation'));
326
327	}
328
329	public function itsec_scheduled_external_backup($x) { return (!wp_next_scheduled('updraft_backup')) ? false : true; }
330	public function itsec_external_backup_link($x) { return UpdraftPlus_Options::admin_page_url().'?page=updraftplus'; }
331	public function return_true($x) { return true; }
332
333	public function ensure_phpseclib($class = false, $class_path = false) {
334		if ($class && class_exists($class)) return;
335		if (false === strpos(get_include_path(), UPDRAFTPLUS_DIR.'/includes/phpseclib')) set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
336		if ($class_path) require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/'.$class_path.'.php');
337	}
338
339	// Returns the number of bytes free, if it can be detected; otherwise, false
340	// Presently, we only detect CPanel. If you know of others, then feel free to contribute!
341	public function get_hosting_disk_quota_free() {
342		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;
343
344		$perl = (@is_executable('/usr/local/cpanel/3rdparty/bin/perl')) ? '/usr/local/cpanel/3rdparty/bin/perl' : '/usr/local/bin/perl';
345
346		$exec = "UPDRAFTPLUSKEY=updraftplus $perl ".UPDRAFTPLUS_DIR."/includes/get-cpanel-quota-usage.pl";
347
348		$handle = @popen($exec, 'r');
349		if (!is_resource($handle)) return false;
350
351		$found = false;
352		$lines = 0;
353		while (false === $found && !feof($handle) && $lines<100) {
354			$lines++;
355			$w = fgets($handle);
356			# Used, limit, remain
357			if (preg_match('/RESULT: (\d+) (\d+) (\d+) /', $w, $matches)) { $found = true; }
358		}
359		$ret = pclose($handle);
360		if (false === $found ||$ret != 0) return false;
361
362		if ((int)$matches[2]<100 || ($matches[1] + $matches[3] != $matches[2])) return false;
363
364		return $matches;
365	}
366
367	// This function may get called multiple times, so write accordingly
368	public function admin_menu() {
369		// We are in the admin area: now load all that code
370		global $updraftplus_admin;
371		if (empty($updraftplus_admin)) require_once(UPDRAFTPLUS_DIR.'/admin.php');
372
373		if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadlatestmodlog' && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
374
375			$updraft_dir = $this->backups_dir_location();
376
377			$log_file = '';
378			$mod_time = 0;
379
380			if ($handle = @opendir($updraft_dir)) {
381				while (false !== ($entry = readdir($handle))) {
382					// The latter match is for files created internally by zipArchive::addFile
383					if (preg_match('/^log\.[a-z0-9]+\.txt$/i', $entry)) {
384						$mtime = filemtime($updraft_dir.'/'.$entry);
385						if ($mtime > $mod_time) {
386							$mod_time = $mtime;
387							$log_file = $updraft_dir.'/'.$entry;
388						}
389					}
390				}
391				@closedir($handle);
392			}
393
394			if ($mod_time >0) {
395				if (is_readable($log_file)) {
396					header('Content-type: text/plain');
397					readfile($log_file);
398					exit;
399				} else {
400					add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
401				}
402			} else {
403				add_action('all_admin_notices', array($this,'show_admin_warning_nolog') );
404			}
405		}
406
407	}
408
409	public function add_curl_capath($handle) {
410		if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) curl_setopt($handle, CURLOPT_CAINFO, UPDRAFTPLUS_DIR.'/includes/cacert.pem' );
411	}
412
413	// Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?action=updraftmethod-googledrive-auth&page=updraftplus
414	// Nov 2013: Google's new cloud console, for reasons as yet unknown, only allows you to enter a redirect_uri with a single URL parameter... thus, we put page second, and re-add it if necessary. Apr 2014: Bitcasa already do this, so perhaps it is part of the OAuth2 standard or best practice somewhere.
415	// Also handle action=downloadlog
416	public function handle_url_actions() {
417
418		// First, basic security check: must be an admin page, with ability to manage options, with the right parameters
419		// Also, only on GET because WordPress on the options page repeats parameters sometimes when POST-ing via the _wp_referer field
420		if (isset($_SERVER['REQUEST_METHOD']) && 'GET' == $_SERVER['REQUEST_METHOD'] && isset($_GET['action'])) {
421			if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php') && UpdraftPlus_Options::user_can_manage()) {
422				$_GET['page'] = 'updraftplus';
423				$_REQUEST['page'] = 'updraftplus';
424				$method = $matches[1];
425				require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
426				$call_class = "UpdraftPlus_BackupModule_".$method;
427				$call_method = "action_".$matches[2];
428				$backup_obj = new $call_class;
429				add_action('http_api_curl', array($this, 'add_curl_capath'));
430				try {
431					if (method_exists($backup_obj, $call_method)) {
432						call_user_func(array($backup_obj, $call_method));
433					} elseif (method_exists($backup_obj, 'action_handler')) {
434						call_user_func(array($backup_obj, 'action_handler'), $matches[2]);
435					}
436				} catch (Exception $e) {
437					$this->log(sprintf(__("%s error: %s", 'updraftplus'), $method, $e->getMessage().' ('.$e->getCode().')', 'error'));
438				}
439				remove_action('http_api_curl', array($this, 'add_curl_capath'));
440			} 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()) {
441				// No WordPress nonce is needed here or for the next, since the backup is already nonce-based
442				$updraft_dir = $this->backups_dir_location();
443				$log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
444				if (is_readable($log_file)) {
445					header('Content-type: text/plain');
446					readfile($log_file);
447					exit;
448				} else {
449					add_action('all_admin_notices', array($this,'show_admin_warning_unreadablelog') );
450				}
451			} elseif (isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadfile' && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-db([0-9]+)?+\.(gz\.crypt)$/i', $_GET['updraftplus_file']) && UpdraftPlus_Options::user_can_manage()) {
452				$updraft_dir = $this->backups_dir_location();
453				$spool_file = $updraft_dir.'/'.basename($_GET['updraftplus_file']);
454				if (is_readable($spool_file)) {
455					$dkey = (isset($_GET['decrypt_key'])) ? $_GET['decrypt_key'] : "";
456					$this->spool_file('db', $spool_file, $dkey);
457					exit;
458				} else {
459					add_action('all_admin_notices', array($this,'show_admin_warning_unreadablefile') );
460				}
461			}
462		}
463	}
464
465	public function get_table_prefix($allow_override = false) {
466		global $wpdb;
467		if (is_multisite() && !defined('MULTISITE')) {
468			# 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.
469			$prefix = $wpdb->base_prefix;
470		} else {
471			$prefix = $wpdb->get_blog_prefix(0);
472		}
473		return ($allow_override) ? apply_filters('updraftplus_get_table_prefix', $prefix) : $prefix;
474	}
475
476	public function show_admin_warning_unreadablelog() {
477		global $updraftplus_admin;
478		$updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The log file could not be read.','updraftplus'));
479	}
480
481	public function show_admin_warning_nolog() {
482		global $updraftplus_admin;
483		$updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('No log files were found.','updraftplus'));
484	}
485
486	public function show_admin_warning_unreadablefile() {
487		global $updraftplus_admin;
488		$updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
489	}
490
491	public function load_translations() {
492		// Tell WordPress where to find the translations
493		load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages/');
494		# The Google Analyticator plugin does something horrible: loads an old version of the Google SDK on init, always - which breaks us
495		if ((defined('DOING_CRON') && DOING_CRON) || (isset($_GET['page']) && $_GET['page'] == 'updraftplus')) {
496			remove_action('init', 'ganalyticator_stats_init');
497			# Appointments+ does the same; but providers a cleaner way to disable it
498			define('APP_GCAL_DISABLE', true);
499		}
500	}
501
502	// Cleans up temporary files found in the updraft directory (and some in the site root - pclzip)
503	// Always cleans up temporary files over 12 hours old.
504	// With parameters, also cleans up those.
505	// Also cleans out old job data older than 12 hours old (immutable value)
506	public function clean_temporary_files($match = '', $older_than = 43200) {
507		# Clean out old job data
508		if ($older_than >10000) {
509			global $wpdb;
510			$all_jobs = $wpdb->get_results("SELECT option_name, option_value FROM $wpdb->options WHERE option_name LIKE 'updraft_jobdata_%'", ARRAY_A);
511			foreach ($all_jobs as $job) {
512				$val = maybe_unserialize($job['option_value']);
513				# TODO: Can simplify this after a while (now all jobs use job_time_ms) - 1 Jan 2014
514				# TODO: This will need changing when incremental backups are introduced
515				if (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) {
516					delete_option($job['option_name']);
517				} elseif (!empty($val['job_time_ms']) && time() > $val['job_time_ms'] + 86400) {
518					delete_option($job['option_name']);
519				} elseif (empty($val['backup_time_ms']) && empty($val['job_time_ms']) && !empty($val['job_type']) && $val['job_type'] != 'backup') {
520					delete_option($job['option_name']);
521				}
522			}
523			
524		}
525		$updraft_dir = $this->backups_dir_location();
526		$now_time=time();
527		if ($handle = opendir($updraft_dir)) {
528			while (false !== ($entry = readdir($handle))) {
529				// This match is for files created internally by zipArchive::addFile
530				$ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $entry);
531				// 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.
532				$binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry);
533				# Temporary files from the database dump process - not needed, as is caught by the catch-all
534				# $table_match = preg_match("/${match}-table-(.*)\.table(\.tmp)?\.gz$/i", $entry);
535				# The gz goes in with the txt, because we *don't* want to reap the raw .txt files
536				if ((preg_match("/$match\.(tmp|table|txt\.gz)(\.gz)?$/i", $entry) || $ziparchive_match || $binzip_match) && is_file($updraft_dir.'/'.$entry)) {
537					// 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
538					if (($match && ($ziparchive_match || $binzip_match || 0 == $older_than) && $now_time-filemtime($updraft_dir.'/'.$entry) >= $older_than) || $now_time-filemtime($updraft_dir.'/'.$entry)>43200) {
539						$this->log("Deleting old temporary file: $entry");
540						@unlink($updraft_dir.'/'.$entry);
541					}
542				}
543			}
544			@closedir($handle);
545		}
546		# Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both
547		foreach (array(ABSPATH, ABSPATH.'wp-admin/') as $path) {
548			if ($handle = opendir($path)) {
549				while (false !== ($entry = readdir($handle))) {
550					# 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
551					if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) {
552						$this->log("Deleting old PclZip temporary file: $entry");
553						@unlink($path.$entry);
554					}
555				}
556				@closedir($handle);
557			}
558		}
559	}
560
561	public function backup_time_nonce($nonce = false) {
562		$this->job_time_ms = microtime(true);
563		$this->backup_time = time();
564		if (false === $nonce) $nonce = substr(md5(time().rand()), 20);
565		$this->nonce = $nonce;
566	}
567
568	public function logfile_open($nonce) {
569
570		//set log file name and open log file
571		$updraft_dir = $this->backups_dir_location();
572		$this->logfile_name =  $updraft_dir."/log.$nonce.txt";
573
574		if (file_exists($this->logfile_name)) {
575			$seek_to = max((filesize($this->logfile_name) - 340), 1);
576			$handle = fopen($this->logfile_name, 'r');
577			if (is_resource($handle)) {
578				# Returns 0 on success
579				if (0 === @fseek($handle, $seek_to)) {
580					$bytes_back = filesize($this->logfile_name) - $seek_to;
581					# Return to the end of the file
582					$read_recent = fread($handle, $bytes_back);
583					# Move to end of file - ought to be redundant
584					if (false !== strpos($read_recent, 'The backup apparently succeeded') && false !== strpos($read_recent, 'and is now complete')) {
585						$this->backup_is_already_complete = true;
586					}
587				}
588				fclose($handle);
589			}
590		}
591
592		$this->logfile_handle = fopen($this->logfile_name, 'a');
593
594		$this->opened_log_time = microtime(true);
595		$this->log('Opened log file at time: '.date('r').' on '.site_url());
596		global $wp_version;
597		@include(ABSPATH.'wp-includes/version.php');
598
599		// Will need updating when WP stops being just plain MySQL
600		$mysql_version = (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
601
602		$safe_mode = $this->detect_safe_mode();
603
604		$memory_limit = ini_get('memory_limit');
605		$memory_usage = round(@memory_get_usage(false)/1048576, 1);
606		$memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
607
608		# Attempt to raise limit to avoid false positives
609		@set_time_limit(900);
610		$max_execution_time = (int)@ini_get("max_execution_time");
611
612		$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: ";
613
614		// method_exists causes some faulty PHP installations to segfault, leading to support requests
615		if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
616			$logline .= 'Y';
617		} else {
618			$logline .= (class_exists('ZipArchive') && method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
619		}
620
621		$w3oc = 'N';
622		if (0 === $this->current_resumption) {
623			$memlim = $this->memory_check_current();
624			if ($memlim<65) {
625				$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');
626			}
627			if ($max_execution_time>0 && $max_execution_time<20) {
628				$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');
629			}
630			if (defined('W3TC') && W3TC == true && function_exists('w3_instance')) {
631				$modules = w3_instance('W3_ModuleStatus');
632				if ($modules->is_enabled('objectcache')) {
633					$w3oc = 'Y';
634				}
635			}
636			$logline .= " W3TC/ObjectCache: $w3oc";
637		}
638
639		$this->log($logline);
640
641		$hosting_bytes_free = $this->get_hosting_disk_quota_free();
642		if (is_array($hosting_bytes_free)) {
643			$perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
644			$quota_free = ' / '.sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." Mb", "$perc %");
645			if ($hosting_bytes_free[3] < 1048576*50) {
646				$quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
647				$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);
648			}
649		} else {
650			$quota_free = '';
651		}
652
653		$disk_free_space = @disk_free_space($updraft_dir);
654		if ($disk_free_space === false) {
655			$this->log("Free space on disk containing Updraft's temporary directory: Unknown".$quota_free);
656		} else {
657			$this->log("Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576,1)." Mb".$quota_free);
658			$disk_free_mb = round($disk_free_space/1048576, 1);
659			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);
660		}
661
662	}
663
664	/* Logs the given line, adding (relative) time stamp and newline
665	Note these subtleties of log handling:
666	- 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.
667	- 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...
668	- ... 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
669	$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
670	*/
671
672	public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) {
673
674		if ('error' == $level || 'warning' == $level) {
675			if ('error' == $level && 0 == $this->error_count()) $this->log('An error condition has occurred for the first time during this job');
676			if ($uniq_id) {
677				$this->errors[$uniq_id] = array('level' => $level, 'message' => $line);
678			} else {
679				$this->errors[] = array('level' => $level, 'message' => $line);
680			}
681			# Errors are logged separately
682			if ('error' == $level) return;
683			# It's a warning
684			$warnings = $this->jobdata_get('warnings');
685			if (!is_array($warnings)) $warnings=array();
686			if ($uniq_id) {
687				$warnings[$uniq_id] = $line;
688			} else {
689				$warnings[] = $line;
690			}
691			$this->jobdata_set('warnings', $warnings);
692		}
693
694		do_action('updraftplus_logline', $line, $this->nonce, $level, $uniq_id);
695
696		if ($this->logfile_handle) {
697			# Record log file times relative to the backup start, if possible
698			$rtime = (!empty($this->job_time_ms)) ? microtime(true)-$this->job_time_ms : microtime(true)-$this->opened_log_time;
699			fwrite($this->logfile_handle, sprintf("%08.03f", round($rtime, 3))." (".$this->current_resumption.") ".(('notice' != $level) ? '['.ucfirst($level).'] ' : '').$line."\n");
700		}
701
702		switch ($this->jobdata_get('job_type')) {
703			case 'download':
704				// Download messages are keyed on the job (since they could be running several), and type
705				// The values of the POST array were checked before
706				$findex = (!empty($_POST['findex'])) ? $_POST['findex'] : 0;
707
708				$this->jobdata_set('dlmessage_'.$_POST['timestamp'].'_'.$_POST['type'].'_'.$findex, $line);
709
710				break;
711			case 'restore':
712				#if ('debug' != $level) echo $line."\n";
713				break;
714			default:
715				if (!$skip_dblog && 'debug' != $level) UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date_i18n('M d H:i:s').")", false);
716				break;
717		}
718
719		if (defined('UPDRAFTPLUS_CONSOLELOG')) print $line."\n";
720		if (defined('UPDRAFTPLUS_BROWSERLOG')) print htmlentities($line)."<br>\n";
721	}
722
723	public function log_removewarning($uniq_id) {
724		$warnings = $this->jobdata_get('warnings');
725		if (!is_array($warnings)) $warnings=array();
726		unset($warnings[$uniq_id]);
727		$this->jobdata_set('warnings', $warnings);
728		unset($this->errors[$uniq_id]);
729	}
730
731	# For efficiency, you can also feed false or a string into this function
732	public function log_wp_error($err, $echo = false, $logerror = false) {
733		if (false === $err) return false;
734		if (is_string($err)) {
735			$this->log("Error message: $err");
736			if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($err))."<br>";
737			if ($logerror) $this->log($err, 'error');
738			return false;
739		}
740		foreach ($err->get_error_messages() as $msg) {
741			$this->log("Error message: $msg");
742			if ($echo) echo sprintf(__('Error: %s', 'updraftplus'), htmlspecialchars($msg))."<br>";
743			if ($logerror) $this->log($msg, 'error');
744		}
745		$codes = $err->get_error_codes();
746		if (is_array($codes)) {
747			foreach ($codes as $code) {
748				$data = $err->get_error_data($code);
749				if (!empty($data)) {
750					$ll = (is_string($data)) ? $data : serialize($data);
751					$this->log("Error data (".$code."): ".$ll);
752				}
753			}
754		}
755		# Returns false so that callers can return with false more efficiently if they wish
756		return false;
757	}
758
759	public function get_max_packet_size() {
760		global $wpdb, $updraftplus;
761		$mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
762		# Default to 1Mb
763		$mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
764		# 32Mb
765		if ($mp < 33554432) {
766			$save = $wpdb->show_errors(false);
767			$req = $wpdb->query("SET GLOBAL max_allowed_packet=33554432");
768			$wpdb->show_errors($save);
769			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).")");
770			$mp = (int)$wpdb->get_var("SELECT @@session.max_allowed_packet");
771			# Default to 1Mb
772			$mp = (is_numeric($mp) && $mp > 0) ? $mp : 1048576;
773		}
774		$updraftplus->log("Max packet size: ".round($mp/1048576, 1)." Mb");
775		return $mp;
776	}
777
778	# 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()).
779	# 1st argument = the line to be logged (obligatory)
780	# Further arguments = parameters for sprintf()
781	public function log_e() {
782		$args = func_get_args();
783		# Get first argument
784		$pre_line = array_shift($args);
785		# Log it whilst still in English
786		if (is_wp_error($pre_line)) {
787			$this->log_wp_error($pre_line);
788		} else {
789			# Now run (v)sprintf on it, using any remaining arguments. vsprintf = sprintf but takes an array instead of individual arguments
790			$this->log(vsprintf($pre_line, $args));
791			echo vsprintf(__($pre_line, 'updraftplus'), $args).'<br>';
792		}
793	}
794
795	// 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
796	public function record_uploaded_chunk($percent, $extra = '', $file_path = false) {
797
798		// Touch the original file, which helps prevent overlapping runs
799		if ($file_path) touch($file_path);
800
801		// 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)
802		if ($percent > 0.7 * ($this->current_resumption - max($this->jobdata_get('uploaded_lastreset'), 9))) $this->something_useful_happened();
803
804		// Log it
805		global $updraftplus_backup;
806		$log = (!empty($updraftplus_backup->current_service)) ? ucfirst($updraftplus_backup->current_service)." chunked upload: $percent % uploaded" : '';
807		if ($log) $this->log($log.(($extra) ? " ($extra)" : ''));
808		// If we are on an 'overtime' resumption run, and we are still meaningfully uploading, then schedule a new resumption
809		// 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
810		// 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
811		// 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
812
813		$upload_status = $this->jobdata_get('uploading_substatus');
814		if (is_array($upload_status)) {
815			$upload_status['p'] = $percent/100;
816			$this->jobdata_set('uploading_substatus', $upload_status);
817		}
818
819	}
820
821	function chunked_upload($caller, $file, $cloudpath, $logname, $chunk_size, $uploaded_size) {
822
823		$fullpath = $this->backups_dir_location().'/'.$file;
824		$orig_file_size = filesize($fullpath);
825		if ($uploaded_size >= $orig_file_size) return true;
826
827		$fp = @fopen($fullpath, 'rb');
828		if (!$fp) {
829			$this->log("$logname: failed to open file: $fullpath");
830			$this->log("$file: ".sprintf(__('%s Error: Failed to open local file','updraftplus'), $logname), 'error');
831			return false;
832		}
833
834		$chunks = floor($orig_file_size / $chunk_size);
835		// There will be a remnant unless the file size was exactly on a 5Mb boundary
836		if ($orig_file_size % $chunk_size > 0 ) $chunks++;
837
838		$this->log("$logname upload: $file (chunks: $chunks) -> $cloudpath ($uploaded_size)");
839
840		if ($chunks < 2) {
841			return 1;
842		} else {
843			$errors_so_far = 0;
844			for ($i = 1 ; $i <= $chunks; $i++) {
845
846				$upload_start = ($i-1)*$chunk_size;
847				// The file size -1 equals the byte offset of the final byte
848				$upload_end = min($i*$chunk_size-1, $orig_file_size-1);
849				// Don't forget the +1; otherwise the last byte is omitted
850				$upload_size = $upload_end - $upload_start + 1;
851
852				fseek($fp, $upload_start);
853
854				$uploaded = $caller->chunked_upload($file, $fp, $i, $upload_size, $upload_start, $upload_end);
855
856				if ($uploaded) {
857					$perc = round(100*((($i-1) * $chunk_size) + $upload_size)/max($orig_file_size, 1), 1);
858					# $perc = round(100*$i/$chunks,1); # Takes no notice of last chunk likely being smaller
859					$this->record_uploaded_chunk($perc, $i, $fullpath);
860				} else {
861					$errors_so_far++;
862					if ($errors_so_far>=3) return false;
863				}
864			}
865			if ($errors_so_far) return false;
866
867			// All chunks are uploaded - now combine the chunks
868			$ret = true;
869			if (method_exists($caller, 'chunked_upload_finish')) {
870				$ret = $caller->chunked_upload_finish($file);
871				if (!$ret) {
872					$this->log("$logname - failed to re-assemb…

Large files files are truncated, but you can click here to view the full file