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