PageRenderTime 79ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

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

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