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

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

1,819 lines 77.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: UpdraftPlus - Backup/Restore
4 Plugin URI: http://updraftplus.com
5 Description: Backup and restore: take backups locally, or backup to Amazon S3, Dropbox, Google Drive, Rackspace, (S)FTP, WebDAV & email, on automatic schedules.
6 Author: UpdraftPlus.Com, DavidAnderson
7 Version: 1.5.20
8 Donate link: http://david.dw-perspective.org.uk/donate
9 License: GPLv3 or later
10 Text Domain: updraftplus
11 Author URI: http://updraftplus.com
12 */
13
14 /*
15 TODO - some of these are out of date/done, needs pruning
16 // Add an appeal for translators to email me. If it a fails, use Google Translate Tools and appeal for native users to correct it.
17 // Automatically detect LiteSpeed and scan .htaccess and send them to the relevant FAQ
18 // Search for other TODO-s in the code
19 // Test in PHP 5.4
20 // Better Dropbox errors (see item in To-Do box)
21 // Count available time before doing a database restore
22 // Add in downloading in the 'Restore' modal, and remove the advice to do so manually.
23 // Provide an expert option to disable sslverify on WP HTTP requests. Mention in FAQs etc.
24 // Save database encryption key inside backup history on per-db basis, so that if it changes we can still decrypt
25 // Switch to Google Drive SDK. Google folders. https://developers.google.com/drive/folder
26 // GlotPress
27 // Convert S3.php to use WP's native HTTP functions
28 // Ability to re-scan existing cloud storage
29 // Migrator - search+replace the database
30 // Make mcrypt warning on dropbox more prominent - one customer missed it
31 // Store meta-data on which version of UD the backup was made with (will help if we ever introduce quirks that need ironing)
32 // Test restoration when uploads dir is /assets/ (e.g. with Shoestrap theme)
33 // 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)
34 //Allow use of /usr/bin/zip - since this can escape from PHP's memory limit. Can still batch as we do so, in order to monitor/measure progress
35 // Database decrypter
36 // Easier download of 'in progress' backup logs (not just last completed)
37 //Do an automated test periodically for the success of loop-back connections
38 //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.
39 //Change DB encryption to not require whole gzip in memory (twice)
40 //Add Rackspace, Box.Net, SugarSync and Microsoft Skydrive support??
41 //Make it easier to find add-ons
42 //The restorer has a hard-coded wp-content - fix
43 //?? On 'backup now', open up a Lightbox, count down 5 seconds, then start examining the log file (if it can be found)
44 //Should make clear in dashboard what is a non-fatal error (i.e. can be retried) - leads to unnecessary bug reports
45 // 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.
46 // 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??
47 // Warn the user if their zip-file creation is slooowww...
48 // Create a "Want Support?" button/console, that leads them through what is needed, and performs some basic tests...
49 // Chunking + resuming on SFTP
50 // Add-on to manage all your backups from a single dashboard
51 // Make disk space check more intelligent (currently hard-coded at 35Mb)
52 // 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
53 // Multiple jobs
54 // Allow connecting to remote storage, scanning + populating backup history from it
55 // GoogleDrive in-dashboard download resumption loads the whole archive into memory - should instead either chunk or directly stream fo the file handle
56 // Multisite add-on should allow restoring of each blog individually
57 // When looking for files to delete, is the current encryption setting used? Should not be.
58 // Create single zip, containing even WordPress itself
59 // When a new backup starts, AJAX-update the 'Last backup' display in the admin page.
60 // Remove the recurrence of admin notices when settings are saved due to _wp_referer
61
62 Encrypt filesystem, if memory allows (and have option for abort if not); split up into multiple zips when needed
63 // New sub-module to verify that the backups are there, independently of backup thread
64 */
65
66 /* Portions copyright 2010 Paul Kehrer
67 Portions copyright 2011-13 David Anderson
68 Other portions copyright as indicated authors in the relevant files
69
70 This program is free software; you can redistribute it and/or modify
71 it under the terms of the GNU General Public License as published by
72 the Free Software Foundation; either version 3 of the License, or
73 (at your option) any later version.
74
75 This program is distributed in the hope that it will be useful,
76 but WITHOUT ANY WARRANTY; without even the implied warranty of
77 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
78 GNU General Public License for more details.
79
80 You should have received a copy of the GNU General Public License
81 along with this program; if not, write to the Free Software
82 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
83 */
84
85 define('UPDRAFTPLUS_DIR', dirname(__FILE__));
86 define('UPDRAFTPLUS_URL', plugins_url('', __FILE__));
87 define('UPDRAFT_DEFAULT_OTHERS_EXCLUDE','upgrade,cache,updraft,index.php,backup,backups');
88 // This is used in various places, based on our assumption of the maximum time any job should take. May need lengthening in future if we get reports which show enormous sets hitting the limit.
89 // Also one section requires at least 1% progress each run, so on a 5-minute schedule, that equals just under 9 hours - then an extra allowance takes it just over
90 define('UPDRAFT_TRANSTIME', 3600*9+5);
91
92 // Load add-ons
93 if (is_file(UPDRAFTPLUS_DIR.'/premium.php')) require_once(UPDRAFTPLUS_DIR.'/premium.php');
94 if (is_file(UPDRAFTPLUS_DIR.'/autoload.php')) require_once(UPDRAFTPLUS_DIR.'/autoload.php');
95
96 if ($dir_handle = opendir(UPDRAFTPLUS_DIR.'/addons')) {
97 while ($e = readdir($dir_handle)) {
98 if (is_file(UPDRAFTPLUS_DIR.'/addons/'.$e) && preg_match('/\.php$/', $e)) {
99 include_once(UPDRAFTPLUS_DIR.'/addons/'.$e);
100 }
101 }
102 @closedir($dir_handle);
103 }
104
105 if (!isset($updraftplus)) $updraftplus = new UpdraftPlus();
106
107 if (!$updraftplus->memory_check(192)) {
108 // 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)
109 @ini_set('memory_limit', WP_MAX_MEMORY_LIMIT); //up the memory limit to the maximum WordPress is allowing for large backup files
110 }
111
112 if (!class_exists('UpdraftPlus_Options')) require_once(UPDRAFTPLUS_DIR.'/options.php');
113
114 set_include_path(get_include_path().PATH_SEPARATOR.UPDRAFTPLUS_DIR.'/includes/phpseclib');
115
116 class UpdraftPlus {
117
118 var $version;
119
120 var $plugin_title = 'UpdraftPlus Backup/Restore';
121
122 // Choices will be shown in the admin menu in the order used here
123 var $backup_methods = array (
124 "s3" => "Amazon S3",
125 "dropbox" => "Dropbox",
126 "googledrive" => "Google Drive",
127 "ftp" => "FTP",
128 'sftp' => 'SFTP',
129 'webdav' => 'WebDAV',
130 "email" => "Email"
131 );
132
133 var $dbhandle;
134 var $dbhandle_isgz;
135 var $errors = array();
136 var $nonce;
137 var $logfile_name = "";
138 var $logfile_handle = false;
139 var $backup_time;
140
141 var $opened_log_time;
142 var $backup_dir;
143
144 var $jobdata;
145
146 // Used to schedule resumption attempts beyond the tenth, if needed
147 var $current_resumption;
148 var $newresumption_scheduled = false;
149
150 function __construct() {
151
152 // Initialisation actions - takes place on plugin load
153
154 if ($fp = fopen( __FILE__, 'r')) {
155 $file_data = fread( $fp, 1024 );
156 if (preg_match("/Version: ([\d\.]+)(\r|\n)/", $file_data, $matches)) {
157 $this->version = $matches[1];
158 }
159 fclose($fp);
160 }
161
162 # Create admin page
163 add_action('init', array($this, 'handle_url_actions'));
164 // Run earlier than default - hence earlier than other components
165 add_action('admin_init', array($this,'admin_init'), 9);
166 // admin_menu runs earlier, and we need it because options.php wants to use $updraftplus_admin before admin_init happens
167 add_action('admin_menu', array($this,'admin_init'), 9);
168 add_action('updraft_backup', array($this,'backup_files'));
169 add_action('updraft_backup_database', array($this,'backup_database'));
170 # backup_all is used by the manual "Backup Now" button
171 add_action('updraft_backup_all', array($this,'backup_all'));
172 # this is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
173 add_action('updraft_backup_resume', array($this,'backup_resume'), 10, 3);
174 # http://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules
175 add_filter('cron_schedules', array($this,'modify_cron_schedules'));
176 add_action('plugins_loaded', array($this, 'load_translations'));
177
178 register_deactivation_hook(__FILE__, array($this, 'deactivation'));
179
180 }
181
182 function admin_init() {
183 // We are in the admin area: now load all that code
184 global $updraftplus_admin;
185 if (empty($updraftplus_admin)) require_once(UPDRAFTPLUS_DIR.'/admin.php');
186
187 if (isset($_GET['wpnonce']) && isset($_GET['page']) && isset($_GET['action']) && $_GET['page'] == 'updraftplus' && $_GET['action'] == 'downloadlatestmodlog' && wp_verify_nonce($_GET['wpnonce'], 'updraftplus_download')) {
188
189 $updraft_dir = $this->backups_dir_location();
190
191 $log_file = '';
192 $mod_time = 0;
193
194 if ($handle = opendir($updraft_dir)) {
195 while (false !== ($entry = readdir($handle))) {
196 // The latter match is for files created internally by zipArchive::addFile
197 if (preg_match('/^log\.[a-z0-9]+\.txt$/i', $entry)) {
198 $mtime = filemtime($updraft_dir.'/'.$entry);
199 if ($mtime > $mod_time) {
200 $mod_time = $mtime;
201 $log_file = $updraft_dir.'/'.$entry;
202 }
203 }
204 }
205 @closedir($handle);
206 }
207
208 if ($mod_time >0) {
209 if (is_readable($log_file)) {
210 header('Content-type: text/plain');
211 readfile($log_file);
212 exit;
213 } else {
214 add_action('admin_notices', array($this,'show_admin_warning_unreadablelog') );
215 }
216 } else {
217 add_action('admin_notices', array($this,'show_admin_warning_nolog') );
218 }
219 }
220
221 }
222
223 function add_curl_capath($handle) {
224 if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) curl_setopt($handle, CURLOPT_CAINFO, UPDRAFTPLUS_DIR.'/includes/cacert.pem' );
225 }
226
227 // Handle actions passed on to method plugins; e.g. Google OAuth 2.0 - ?page=updraftplus&action=updraftmethod-googledrive-auth
228 // Also handle action=downloadlog
229 function handle_url_actions() {
230
231 // First, basic security check: must be an admin page, with ability to manage options, with the right parameters
232 if ( UpdraftPlus_Options::user_can_manage() && isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && isset($_GET['action']) ) {
233 if (preg_match("/^updraftmethod-([a-z]+)-([a-z]+)$/", $_GET['action'], $matches) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$matches[1].'.php')) {
234 $method = $matches[1];
235 require_once(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php');
236 $call_class = "UpdraftPlus_BackupModule_".$method;
237 $call_method = "action_".$matches[2];
238
239 add_action('http_api_curl', array($this, 'add_curl_capath'));
240 if (method_exists($call_class, $call_method)) call_user_func(array($call_class,$call_method));
241 remove_action('http_api_curl', array($this, 'add_curl_capath'));
242
243 } elseif ($_GET['action'] == 'downloadlog' && isset($_GET['updraftplus_backup_nonce']) && preg_match("/^[0-9a-f]{12}$/",$_GET['updraftplus_backup_nonce'])) {
244 // No WordPress nonce is needed here or for the next, since the backup is already nonce-based
245 $updraft_dir = $this->backups_dir_location();
246 $log_file = $updraft_dir.'/log.'.$_GET['updraftplus_backup_nonce'].'.txt';
247 if (is_readable($log_file)) {
248 header('Content-type: text/plain');
249 readfile($log_file);
250 exit;
251 } else {
252 add_action('admin_notices', array($this,'show_admin_warning_unreadablelog') );
253 }
254 } elseif ($_GET['action'] == 'downloadfile' && isset($_GET['updraftplus_file']) && preg_match('/^backup_([\-0-9]{15})_.*_([0-9a-f]{12})-[\-a-z]+\.(gz\.crypt)$/i', $_GET['updraftplus_file'])) {
255 $updraft_dir = $this->backups_dir_location();
256 $spool_file = $updraft_dir.'/'.$_GET['updraftplus_file'];
257 if (is_readable($spool_file)) {
258 $dkey = (isset($_GET['decrypt_key'])) ? $_GET['decrypt_key'] : "";
259 $this->spool_file('db', $spool_file, $dkey);
260 exit;
261 } else {
262 add_action('admin_notices', array($this,'show_admin_warning_unreadablefile') );
263 }
264 }
265 }
266 }
267
268 function show_admin_warning_unreadablelog() {
269 global $updraftplus_admin;
270 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The log file could not be read.','updraftplus'));
271 }
272
273 function show_admin_warning_nolog() {
274 global $updraftplus_admin;
275 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('No log files were found.','updraftplus'));
276 }
277
278 function show_admin_warning_unreadablefile() {
279 global $updraftplus_admin;
280 $updraftplus_admin->show_admin_warning('<strong>'.__('UpdraftPlus notice:','updraftplus').'</strong> '.__('The given file could not be read.','updraftplus'));
281 }
282
283 function load_translations() {
284 // Tell WordPress where to find the translations
285 load_plugin_textdomain('updraftplus', false, basename(dirname(__FILE__)).'/languages');
286 }
287
288 // Cleans up temporary files found in the updraft directory
289 function clean_temporary_files() {
290 $updraft_dir = $this->backups_dir_location();
291 if ($handle = opendir($updraft_dir)) {
292 $now_time=time();
293 while (false !== ($entry = readdir($handle))) {
294 // The latter match is for files created internally by zipArchive::addFile
295 if ((preg_match('/\.tmp(\.gz)?$/i', $entry) || preg_match('/\.zip\.tmp\.([A-Za-z0-9]){6}?$/i', $entry)) && is_file($updraft_dir.'/'.$entry) && $now_time-filemtime($updraft_dir.'/'.$entry)>86400) {
296 $this->log("Deleting old temporary file: $entry");
297 @unlink($updraft_dir.'/'.$entry);
298 }
299 }
300 @closedir($handle);
301 }
302 }
303
304 function backup_time_nonce() {
305 $this->backup_time = time();
306 $nonce = substr(md5(time().rand()), 20);
307 $this->nonce = $nonce;
308 }
309
310 function logfile_open($nonce) {
311
312 //set log file name and open log file
313 $updraft_dir = $this->backups_dir_location();
314 $this->logfile_name = $updraft_dir. "/log.$nonce.txt";
315
316 // Use append mode in case it already exists
317 $this->logfile_handle = fopen($this->logfile_name, 'a');
318 $this->opened_log_time = microtime(true);
319 $this->log('Opened log file at time: '.date('r'));
320 global $wp_version;
321 $logline = "UpdraftPlus: ".$this->version." WP: ".$wp_version." PHP: ".phpversion()." (".php_uname().") Server: ".$_SERVER["SERVER_SOFTWARE"]." max_execution_time: ".@ini_get("max_execution_time")." memory_limit: ".ini_get('memory_limit')." ZipArchive::addFile : ";
322
323 // method_exists causes some faulty PHP installations to segfault, leading to support requests
324 if (version_compare(phpversion(), '5.2.0', '>=') && extension_loaded('zip')) {
325 $logline .= 'Y';
326 } else {
327 $logline .= (method_exists('ZipArchive', 'addFile')) ? "Y" : "N";
328 }
329 $this->log($logline);
330 $disk_free_space = @disk_free_space($updraft_dir);
331 $this->log("Free space on disk containing Updraft's temporary directory: ".round($disk_free_space/1048576,1)." Mb");
332 }
333
334 # Logs the given line, adding (relative) time stamp and newline
335 function log($line) {
336 if ($this->logfile_handle) fwrite($this->logfile_handle, sprintf("%08.03f", round(microtime(true)-$this->opened_log_time, 3))." (".$this->current_resumption.") $line\n");
337 if ('download' == $this->jobdata_get('job_type')) {
338 // Download messages are keyed on the job (since they could be running several), and transient
339 // The values of the POST array were checked before
340 set_transient('ud_dlmess_'.$_POST['timestamp'].'_'.$_POST['type'], $line." (".date('M d H:i:s').")", 3600);
341 } else {
342 UpdraftPlus_Options::update_updraft_option('updraft_lastmessage', $line." (".date('M d H:i:s').")");
343 }
344 if (defined('UPDRAFTPLUS_CONSOLELOG')) print $line."\n";
345 }
346
347 // 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
348 function record_uploaded_chunk($percent, $extra, $file_path = false) {
349
350 // Touch the original file, which helps prevent overlapping runs
351 if ($file_path) touch($file_path);
352
353 // Log it
354 $service = $this->jobdata_get('service');
355 $log = ucfirst($service)." chunked upload: $percent % uploaded";
356 if ($extra) $log .= " ($extra)";
357 $this->log($log);
358 // If we are on an 'overtime' resumption run, and we are still meainingfully uploading, then schedule a new resumption
359 // Our definition of meaningful is that we must maintain an overall average of at least 1% per run, after allowing 9 runs for everything else to get going
360 // i.e. Max 109 runs = 545 minutes = 9 hrs 05
361 // If they get 2 minutes on each run, and the file is 1Gb, then that equals 10.2Mb/120s = minimum 87Kb/s upload speed required
362
363 if ($percent > ( $this->current_resumption - 9)) {
364 $this->something_useful_happened();
365 }
366 }
367
368 function something_useful_happened() {
369
370 // First, update the record of maximum detected runtime on each run
371 $time_passed = $this->jobdata_get('run_times');
372 if (!is_array($time_passed)) $time_passed = array();
373 $time_passed[$this->current_resumption] = microtime(true)-$this->opened_log_time;
374 $this->jobdata_set('run_times', $time_passed);
375
376 if ($this->current_resumption >= 9 && $this->newresumption_scheduled == false) {
377 $this->log("This is resumption ".$this->current_resumption.", but meaningful activity is still taking place; so a new one will be scheduled");
378 $resume_interval = $this->jobdata_get('resume_interval');
379 if (!is_numeric($resume_interval) || $resume_interval<$this->minimum_resume_interval()) { $resume_interval = $this->minimum_resume_interval(); }
380 $schedule_for = time()+$resume_interval;
381 $this->newresumption_scheduled = $schedule_for;
382 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($this->current_resumption + 1, $this->nonce));
383 } else {
384 $this->reschedule_if_needed();
385 }
386 }
387
388 function minimum_resume_interval() {
389 // Bringing this down brings in more risk of undetectable overlaps than is worth it
390 return 300;
391 // $inter = (int)ini_get('max_execution_time');
392 // if (!$inter || $inter>300) $inter = 300;
393 // if ($inter<35) $inter=35;
394 // return $inter;
395 }
396
397 // 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
398 function get_backupable_file_entities($include_others = true, $full_info = false) {
399
400 $wp_upload_dir = wp_upload_dir();
401
402 if ($full_info) {
403 $arr = array(
404 'plugins' => array('path' => WP_PLUGIN_DIR, 'description' => __('Plugins','updraftplus')),
405 'themes' => array('path' => WP_CONTENT_DIR.'/themes', 'description' => __('Themes','updraftplus')),
406 'uploads' => array('path' => $wp_upload_dir['basedir'], 'description' => __('Uploads','updraftplus'))
407 );
408 } else {
409 $arr = array(
410 'plugins' => WP_PLUGIN_DIR,
411 'themes' => WP_CONTENT_DIR.'/themes',
412 'uploads' => $wp_upload_dir['basedir']
413 );
414 }
415
416 $arr = apply_filters('updraft_backupable_file_entities', $arr, $full_info);
417
418 // We always then add 'others' on to the end
419 if ($include_others) {
420 if ($full_info) {
421 $arr['others'] = array('path' => WP_CONTENT_DIR, 'description' => __('Others','updraftplus'));
422 } else {
423 $arr['others'] = WP_CONTENT_DIR;
424 }
425 }
426
427 return $arr;
428
429 }
430
431 function backup_resume($resumption_no, $bnonce) {
432
433 // 15 minutes
434 @set_time_limit(900);
435
436 @ignore_user_abort(true);
437 // This is scheduled for 5 minutes after a backup job starts
438
439 // Restore state
440 $resumption_extralog = '';
441 if ($resumption_no > 0) {
442 $this->nonce = $bnonce;
443 $this->backup_time = $this->jobdata_get('backup_time');
444 $this->logfile_open($bnonce);
445
446 $time_passed = $this->jobdata_get('run_times');
447 if (!is_array($time_passed)) $time_passed = array();
448
449 $prev_resumption = $resumption_no - 1;
450 if (isset($time_passed[$prev_resumption])) $resumption_extralog = ", previous check-in=".round($time_passed[$prev_resumption], 1)."s";
451
452 }
453
454 $btime = $this->backup_time;
455
456 $job_type = $this->jobdata_get('job_type');
457
458 $updraft_dir = $this->backups_dir_location();
459
460 $time_ago = time()-$btime;
461
462 $this->current_resumption = $resumption_no;
463 $this->log("Backup run: resumption=$resumption_no, nonce=$bnonce, begun at=$btime (${time_ago}s ago), job type=$job_type".$resumption_extralog);
464
465 // Schedule again, to run in 5 minutes again, in case we again fail
466 // The actual interval can be increased (for future resumptions) by other code, if it detects apparent overlapping
467 $resume_interval = $this->jobdata_get('resume_interval');
468 if (!is_numeric($resume_interval) || $resume_interval<$this->minimum_resume_interval()) $resume_interval = $this->minimum_resume_interval();
469
470 // We just do this once, as we don't want to be in permanent conflict with the overlap detector
471 if ($resumption_no == 8) {
472 $max_time = 0;
473 // $time_passed is set earlier
474 $timings_string = "";
475 $run_times_known=0;
476 for ($i=0; $i<=7; $i++) {
477 $timings_string .= "$i:";
478 if (isset($time_passed[$i])) {
479 $timings_string .= round($time_passed[$i], 1).' ';
480 $run_times_known++;
481 if ($time_passed[$i] > $max_time) $max_time = round($time_passed[$i]);
482 } else {
483 $timings_string .= '? ';
484 }
485 }
486 $this->log("Time passed on previous resumptions: $timings_string (known: $run_times_known, max: $max_time)");
487 if ($run_times_known >= 6 && ($max_time + 35 < $resume_interval)) {
488 $resume_interval = round($max_time + 35);
489 $this->log("Based on the available data, we are bringing the resumption interval down to: $resume_interval seconds");
490 $this->jobdata_set('resume_interval', $resume_interval);
491 }
492 }
493
494 // A different argument than before is needed otherwise the event is ignored
495 $next_resumption = $resumption_no+1;
496 if ($next_resumption < 10) {
497 $schedule_for = time()+$resume_interval;
498 $this->log("Scheduling a resumption ($next_resumption) after $resume_interval seconds ($schedule_for) in case this run gets aborted");
499 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $bnonce));
500 $this->newresumption_scheduled = $schedule_for;
501 } else {
502 $this->log(sprintf('The current run is resumption number %d - will not schedule a further attempt until we see something useful happening', $resumption_no));
503 }
504
505 // Sanity check
506 if (empty($this->backup_time)) {
507 $this->log('Abort this run: the backup_time parameter appears to be empty (this is usually caused by resuming an already-complete backup, or by your site having a faulty object cache active (e.g. W3 Total Cache\'s object cache))');
508 return false;
509 }
510
511 // This should be always called; if there were no files in this run, it returns us an empty array
512 $backup_array = $this->resumable_backup_of_files($resumption_no);
513
514 // This save, if there was something, is then immediately picked up again
515 if (is_array($backup_array)) {
516 $this->log('Saving backup status to database (elements: '.count($backup_array).")");
517 $this->save_backup_history($backup_array);
518 }
519
520 // Switch of variable name is purely vestigial
521 $our_files = $backup_array;
522 if (!is_array($our_files)) $our_files = array();
523
524 $undone_files = array();
525
526 $backup_database = $this->jobdata_get('backup_database');
527
528 // The transient is read and written below (instead of using the existing variable) so that we can copy-and-paste this part as needed.
529 if ($backup_database == "begun" || $backup_database == 'finished' || $backup_database == 'encrypted') {
530 if ($backup_database == "begun") {
531 if ($resumption_no > 0) {
532 $this->log("Resuming creation of database dump");
533 } else {
534 $this->log("Beginning creation of database dump");
535 }
536 } elseif ($backup_database == 'encrypted') {
537 $this->log("Database dump: Creation and encryption were completed already");
538 } else {
539 $this->log("Database dump: Creation was completed already");
540 }
541
542 $db_backup = $this->backup_db($backup_database);
543
544 if(is_array($our_files) && is_string($db_backup)) {
545 $our_files['db'] = $db_backup;
546 }
547
548 if ($backup_database != 'encrypted') $this->jobdata_set("backup_database", 'finished');
549 } else {
550 $this->log("Unrecognised data when trying to ascertain if the database was backed up ($backup_database)");
551 }
552
553 // Save this to our history so we can track backups for the retain feature
554 $this->log("Saving backup history");
555 // 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.
556 $this->save_backup_history($our_files);
557
558 // Potentially encrypt the database if it is not already
559 if (isset($our_files['db']) && !preg_match("/\.crypt$/", $our_files['db'])) {
560 $our_files['db'] = $this->encrypt_file($our_files['db']);
561 // No need to save backup history now, as it will happen in a few lines time
562 if (preg_match("/\.crypt$/", $our_files['db'])) $this->jobdata_set("backup_database", 'encrypted');
563 }
564
565 if (isset($our_files['db']) && file_exists($updraft_dir.'/'.$our_files['db'])) {
566 $our_files['db-size'] = filesize($updraft_dir.'/'.$our_files['db']);
567 $this->save_backup_history($our_files);
568 }
569
570 $backupable_entities = $this->get_backupable_file_entities(true);
571
572 foreach ($our_files as $key => $file) {
573
574 // Only continue if the stored info was about a dump
575 if (!isset($backupable_entities[$key]) && $key != 'db') continue;
576
577 $hash = md5($file);
578 $fullpath = $this->backups_dir_location().'/'.$file;
579 if ($this->jobdata_get("uploaded_$hash") === "yes") {
580 $this->log("$file: $key: This file has already been successfully uploaded");
581 } elseif (is_file($fullpath)) {
582 $this->log("$file: $key: This file has not yet been successfully uploaded: will queue");
583 $undone_files[$key] = $file;
584 } else {
585 $this->log("$file: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem");
586 $this->uploaded_file($file);
587 }
588 }
589
590 if (count($undone_files) == 0) {
591 $this->log("There were no more files that needed uploading; backup job is complete");
592 // No email, as the user probably already got one if something else completed the run
593 $this->backup_finish($next_resumption, true, false, $resumption_no);
594 return;
595 }
596
597 $this->log("Requesting backup of the files that were not successfully uploaded");
598 $this->cloud_backup($undone_files);
599
600 $this->log("Resume backup ($bnonce, $resumption_no): finish run");
601 if (is_array($our_files)) $this->save_last_backup($our_files);
602 $this->backup_finish($next_resumption, true, true, $resumption_no);
603
604 }
605
606 function backup_all() {
607 $this->boot_backup(true,true);
608 }
609
610 function backup_files() {
611 # Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
612 $this->boot_backup(true,false);
613 }
614
615 function backup_database() {
616 # Note that nothing will happen if the file backup had the same schedule
617 $this->boot_backup(false,true);
618 }
619
620 // This works with any amount of settings, but we provide also a jobdata_set for efficiency as normally there's only one setting
621 function jobdata_set_multi() {
622 if (!is_array($this->jobdata)) $this->jobdata = array();
623
624 $args = func_num_args();
625
626 for ($i=1; $i<=$args/2; $i++) {
627 $key = func_get_arg($i*2-2);
628 $value = func_get_arg($i*2-1);
629 $this->jobdata[$key] = $value;
630 }
631 if (!empty($this->nonce)) set_transient("updraft_jobdata_".$this->nonce, $this->jobdata, UPDRAFT_TRANSTIME);
632 }
633
634 function jobdata_set($key, $value) {
635 if (is_array($this->jobdata)) {
636 $this->jobdata[$key] = $value;
637 } else {
638 $this->jobdata = get_transient("updraft_jobdata_".$this->nonce);
639 if (!is_array($this->jobdata)) $this->jobdata = array($key => $value);
640 }
641 set_transient("updraft_jobdata_".$this->nonce, $this->jobdata, 14400);
642 }
643
644 function jobdata_get($key) {
645 if (!is_array($this->jobdata)) {
646 $this->jobdata = get_transient("updraft_jobdata_".$this->nonce);
647 if (!is_array($this->jobdata)) return false;
648 }
649 return (isset($this->jobdata[$key])) ? $this->jobdata[$key] : false;
650 }
651
652 // This uses a transient; its only purpose is to indicate *total* completion; there is no actual danger, just wasted time, in resuming when it was not needed. So the transient just helps save resources.
653 function resumable_backup_of_files($resumption_no) {
654 //backup directories and return a numerically indexed array of file paths to the backup files
655 $transient_status = $this->jobdata_get('backup_files');
656 if ($transient_status == 'finished') {
657 $this->log("Creation of backups of directories: already finished");
658 } elseif ($transient_status == "begun") {
659 if ($resumption_no>0) {
660 $this->log("Creation of backups of directories: had begun; will resume");
661 } else {
662 $this->log("Creation of backups of directories: beginning");
663 }
664 } else {
665 # This is not necessarily a backup run which is meant to contain files at all
666 $this->log("This backup run is not intended for files - skipping");
667 return array();
668 }
669 // We want this array, even if already finished
670 $backup_array = $this->backup_dirs($transient_status);
671 // This can get over-written later
672 $this->jobdata_set('backup_files', 'finished');
673 return $backup_array;
674 }
675
676 // This procedure initiates a backup run
677 function boot_backup($backup_files, $backup_database) {
678
679 @ignore_user_abort(true);
680 // 15 minutes
681 @set_time_limit(900);
682
683 //generate backup information
684 $this->backup_time_nonce();
685 $this->logfile_open($this->nonce);
686
687 if (!is_file($this->logfile_name)) {
688 $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.');
689 $this->error(__('Could not create files in the backup directory. Backup aborted - check your UpdraftPlus settings.','updraftplus'));
690 return false;
691 }
692
693 // Some house-cleaning
694 $this->clean_temporary_files();
695 do_action('updraftplus_boot_backup');
696
697 // Log some information that may be helpful
698 $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').")");
699
700 # If the files and database schedules are the same, and if this the file one, then we rope in database too.
701 # On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
702 if (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' ) {
703 $backup_database = ($backup_files == true) ? true : false;
704 }
705
706 $this->log("Processed schedules. Tasks now: Backup files: $backup_files Backup DB: $backup_database");
707
708 # If nothing to be done, then just finish
709 if (!$backup_files && !$backup_database) {
710 $this->backup_finish(1, false, false, 0);
711 return;
712 }
713
714 $resume_interval = $this->minimum_resume_interval();
715 // $max_execution_time = ini_get('max_execution_time');
716 // if ($max_execution_time >0 && $max_execution_time<300 && $resume_interval< $max_execution_time + 30) $resume_interval = $max_execution_time + 30;
717
718 $initial_jobdata = array(
719 'resume_interval', $resume_interval,
720 'job_type', 'backup',
721 'backup_time', $this->backup_time,
722 'service', UpdraftPlus_Options::get_updraft_option('updraft_service')
723 );
724
725 // Save what *should* be done, to make it resumable from this point on
726 if ($backup_database) array_push($initial_jobdata, 'backup_database', 'begun');
727 if ($backup_files) array_push($initial_jobdata, 'backup_files', 'begun');
728
729 // Use of jobdata_set_multi saves around 200ms
730 call_user_func_array(array($this, 'jobdata_set_multi'), $initial_jobdata);
731
732 // Everything is now set up; now go
733 $this->backup_resume(0, $this->nonce);
734
735 }
736
737 // Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
738 function encrypt_file($file) {
739 $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
740 if (strlen($encryption) > 0) {
741 $this->log("$file: applying encryption");
742 $encryption_error = 0;
743 $microstart = microtime(true);
744 require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/Crypt/Rijndael.php');
745 $rijndael = new Crypt_Rijndael();
746 $rijndael->setKey($encryption);
747 $updraft_dir = $this->backups_dir_location();
748 $file_size = @filesize($updraft_dir.'/'.$file)/1024;
749 if (false === file_put_contents($updraft_dir.'/'.$file.'.crypt' , $rijndael->encrypt(file_get_contents($updraft_dir.'/'.$file)))) {$encryption_error = 1;}
750 if (0 == $encryption_error) {
751 $time_taken = max(0.000001, microtime(true)-$microstart);
752 $this->log("$file: encryption successful: ".round($file_size,1)."Kb in ".round($time_taken,1)."s (".round($file_size/$time_taken, 1)."Kb/s)");
753 # Delete unencrypted file
754 @unlink($updraft_dir.'/'.$file);
755 return basename($file.'.crypt');
756 } else {
757 $this->log("Encryption error occurred when encrypting database. Encryption aborted.");
758 $this->error(__("Encryption error occurred when encrypting database. Encryption aborted.",'updraftplus'));
759 return basename($file);
760 }
761 } else {
762 return basename($file);
763 }
764 }
765
766 function backup_finish($cancel_event, $clear_nonce_transient, $allow_email, $resumption_no) {
767
768 // 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.
769 if (empty($this->errors)) {
770 if ($clear_nonce_transient) {
771 $this->log("There were no errors in the uploads, so the 'resume' event is being unscheduled");
772 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event, $this->nonce));
773 // TODO: Delete the job transient (is presently useful for debugging, and only lasts 4 hours)
774 }
775 } else {
776 $this->log("There were errors in the uploads, so the 'resume' event is remaining scheduled");
777 }
778
779 // Send the results email if appropriate, which means:
780 // - The caller allowed it (which is not the case in an 'empty' run)
781 // - And: An email address was set (which must be so in email mode)
782 // And one of:
783 // - Debug mode
784 // - There were no errors (which means we completed and so this is the final run - time for the final report)
785 // - It was the tenth resumption; everything failed
786
787 $send_an_email = false;
788
789 // Make sure that the final status is shown
790 if (empty($this->errors)) {
791 $send_an_email = true;
792 $final_message = __("The backup apparently succeeded and is now complete",'updraftplus');
793 } elseif ($this->newresumption_scheduled == false) {
794 $send_an_email = true;
795 $final_message = __("The backup attempt has finished, apparently unsuccessfully",'updraftplus');
796 } else {
797 // There are errors, but a resumption will be attempted
798 $final_message = __("The backup has not finished; a resumption is scheduled within 5 minutes",'updraftplus');
799 }
800
801 // Now over-ride the decision to send an email, if needed
802 if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) {
803 $send_an_email = true;
804 $this->log("An email has been scheduled for this job, because we are in debug mode");
805 }
806 // If there's no email address, or the set was empty, that is the final over-ride: don't send
807 if (!$allow_email) {
808 $send_an_email = false;
809 $this->log("No email will be sent - this backup set was empty.");
810 } elseif (UpdraftPlus_Options::get_updraft_option('updraft_email') == '') {
811 $send_an_email = false;
812 $this->log("No email will/can be sent - the user has not configured an email address.");
813 }
814
815 if ($send_an_email) $this->send_results_email($final_message);
816
817 $this->log($final_message);
818
819 @fclose($this->logfile_handle);
820
821 // Don't delete the log file now; delete it upon rotation
822 //if (!UpdraftPlus_Options::get_updraft_option('updraft_debug_mode')) @unlink($this->logfile_name);
823
824 }
825
826 function send_results_email($final_message) {
827
828 $debug_mode = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
829
830 $sendmail_to = UpdraftPlus_Options::get_updraft_option('updraft_email');
831
832 $backup_files = $this->jobdata_get('backup_files');
833 $backup_db = $this->jobdata_get("backup_database");
834
835 if ($backup_files == 'finished' && ( $backup_db == 'finished' || $backup_db == 'encrypted' ) ) {
836 $backup_contains = "Files and database";
837 } elseif ($backup_files == 'finished') {
838 $backup_contains = ($backup_db == "begun") ? "Files (database backup has not completed)" : "Files only (database was not part of this particular schedule)";
839 } elseif ($backup_db == 'finished' || $backup_db == 'encrypted') {
840 $backup_contains = ($backup_files == "begun") ? "Database (files backup has not completed)" : "Database only (files were not part of this particular schedule)";
841 } else {
842 $backup_contains = "Unknown/unexpected error - please raise a support request";
843 }
844
845 $this->log("Sending email ('$backup_contains') report to: ".substr($sendmail_to, 0, 5)."...");
846
847 $append_log = ($debug_mode && $this->logfile_name != "") ? "\r\nLog contents:\r\n".file_get_contents($this->logfile_name) : "" ;
848
849 wp_mail($sendmail_to,__('Backed up', 'updraftplus').': '.get_bloginfo('name').' (UpdraftPlus '.$this->version.') '.date('Y-m-d H:i',time()),'Site: '.site_url()."\r\nUpdraftPlus: ".__('WordPress backup is complete','updraftplus').".\r\n".__('Backup contains','updraftplus').': '.$backup_contains."\r\n".__('Latest status', 'updraftplus').": $final_message\r\n\r\n".$this->wordshell_random_advert(0)."\r\n".$append_log);
850
851 }
852
853 function save_last_backup($backup_array) {
854 $success = (empty($this->errors)) ? 1 : 0;
855
856 $last_backup = array('backup_time'=>$this->backup_time, 'backup_array'=>$backup_array, 'success'=>$success, 'errors'=>$this->errors, 'backup_nonce' => $this->nonce);
857
858 UpdraftPlus_Options::update_updraft_option('updraft_last_backup', $last_backup);
859 }
860
861 // This should be called whenever a file is successfully uploaded
862 function uploaded_file($file, $id = false) {
863 $hash = md5($file);
864 $this->log("Recording as successfully uploaded: $file ($hash)");
865 $this->jobdata_set("uploaded_$hash", "yes");
866 if ($id) {
867 $ids = UpdraftPlus_Options::get_updraft_option('updraft_file_ids', array() );
868 $ids[$file] = $id;
869 UpdraftPlus_Options::update_updraft_option('updraft_file_ids',$ids);
870 $this->log("Stored file<->id correlation in database ($file <-> $id)");
871 }
872 // Delete local files immediately if the option is set
873 // Where we are only backing up locally, only the "prune" function should do deleting
874 if ($this->jobdata_get('service') != '' && $this->jobdata_get('service') != 'none') $this->delete_local($file);
875 }
876
877 // Dispatch to the relevant function
878 function cloud_backup($backup_array) {
879
880 $service = $this->jobdata_get('service');
881 $this->log("Cloud backup selection: ".$service);
882 @set_time_limit(900);
883
884 $method_include = UPDRAFTPLUS_DIR.'/methods/'.$service.'.php';
885 add_action('http_api_curl', array($this, 'add_curl_capath'));
886 if (file_exists($method_include)) require_once($method_include);
887 remove_action('http_api_curl', array($this, 'add_curl_capath'));
888
889 if ($service == "none") {
890 $this->log("No remote despatch: user chose no remote backup service");
891 } else {
892 $this->log("Beginning dispatch of backup to remote");
893 }
894
895 $objname = "UpdraftPlus_BackupModule_${service}";
896 if (method_exists($objname, "backup")) {
897 // New style - external, allowing more plugability
898 $remote_obj = new $objname;
899 $remote_obj->backup($backup_array);
900 } elseif ($service == "none") {
901 $this->prune_retained_backups("none", null, null);
902 }
903 }
904
905 function prune_file($service, $dofile, $method_object = null, $object_passback = null ) {
906 $this->log("Delete this file: $dofile, service=$service");
907 $fullpath = $this->backups_dir_location().'/'.$dofile;
908 // delete it if it's locally available
909 if (file_exists($fullpath)) {
910 $this->log("Deleting local copy ($fullpath)");
911 @unlink($fullpath);
912 }
913
914 // Despatch to the particular method's deletion routine
915 if (!is_null($method_object)) $method_object->delete($dofile, $object_passback);
916 }
917
918 // Carries out retain behaviour. Pass in a valid S3 or FTP object and path if relevant.
919 function prune_retained_backups($service, $backup_method_object = null, $backup_passback = null) {
920
921 // If they turned off deletion on local backups, then there is nothing to do
922 if (UpdraftPlus_Options::get_updraft_option('updraft_delete_local') == 0 && $service == 'none') {
923 $this->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
924 return;
925 }
926
927 $this->log("Retain: beginning examination of existing backup sets");
928
929 // Number of backups to retain - files
930 $updraft_retain = UpdraftPlus_Options::get_updraft_option('updraft_retain', 1);
931 $updraft_retain = (is_numeric($updraft_retain)) ? $updraft_retain : 1;
932 $this->log("Retain files: user setting: number to retain = $updraft_retain");
933
934 // Number of backups to retain - db
935 $updraft_retain_db = UpdraftPlus_Options::get_updraft_option('updraft_retain_db', $updraft_retain);
936 $updraft_retain_db = (is_numeric($updraft_retain_db)) ? $updraft_retain_db : 1;
937 $this->log("Retain db: user setting: number to retain = $updraft_retain_db");
938
939 // Returns an array, most recent first, of backup sets
940 $backup_history = $this->get_backup_history();
941 $db_backups_found = 0;
942 $file_backups_found = 0;
943 $this->log("Number of backup sets in history: ".count($backup_history));
944
945 $backupable_entities = $this->get_backupable_file_entities(true);
946
947 foreach ($backup_history as $backup_datestamp => $backup_to_examine) {
948 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
949 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
950 $this->log("Examining backup set with datestamp: $backup_datestamp");
951
952 if (isset($backup_to_examine['db'])) {
953 $db_backups_found++;
954 $this->log("$backup_datestamp: this set includes a database (".$backup_to_examine['db']."); db count is now $db_backups_found");
955 if ($db_backups_found > $updraft_retain_db) {
956 $this->log("$backup_datestamp: over retain limit ($updraft_retain_db); will delete this database");
957 $dofile = $backup_to_examine['db'];
958 if (!empty($dofile)) $this->prune_file($service, $dofile, $backup_method_object, $backup_passback);
959 unset($backup_to_examine['db']);
960 }
961 }
962
963 $contains_files = false;
964 foreach ($backupable_entities as $entity => $info) {
965 if (isset($backup_to_examine[$entity])) {
966 $contains_files = true;
967 break;
968 }
969 }
970
971 if ($contains_files) {
972 $file_backups_found++;
973 $this->log("$backup_datestamp: this set includes files; fileset count is now $file_backups_found");
974 if ($file_backups_found > $updraft_retain) {
975 $this->log("$backup_datestamp: over retain limit ($updraft_retain); will delete this file set");
976
977 foreach ($backupable_entities as $entity => $info) {
978 if (!empty($backup_to_examine[$entity])) {
979 $this->prune_file($service, $backup_to_examine[$entity], $backup_method_object, $backup_passback);
980 }
981 unset($backup_to_examine[$entity]);
982 }
983
984 }
985 }
986
987 // Get new result, post-deletion
988 $contains_files = false;
989 foreach ($backupable_entities as $entity => $info) {
990 if (isset($backup_to_examine[$entity])) {
991 $contains_files = true;
992 break;
993 }
994 }
995
996 // Delete backup set completely if empty, o/w just remove DB
997 // We search on the four keys which represent data, allowing other keys to be used to track other things
998 if (!$contains_files && !isset($backup_to_examine['db']) ) {
999 $this->log("$backup_datestamp: this backup set is now empty; will remove from history");
1000 unset($backup_history[$backup_datestamp]);
1001 if (isset($backup_to_examine['nonce'])) {
1002 $fullpath = $this->backups_dir_location().'/log.'.$backup_to_examine['nonce'].'.txt';
1003 if (is_file($fullpath)) {
1004 $this->log("$backup_datestamp: deleting log file (log.".$backup_to_examine['nonce'].".txt)");
1005 @unlink($fullpath);
1006 } else {
1007 $this->log("$backup_datestamp: corresponding log file not found - must have already been deleted");
1008 }
1009 } else {
1010 $this->log("$backup_datestamp: no nonce record found in the backup set, so cannot delete any remaining log file");
1011 }
1012 } else {
1013 $this->log("$backup_datestamp: this backup set remains non-empty; will retain in history");
1014 $backup_history[$backup_datestamp] = $backup_to_examine;
1015 }
1016 }
1017 $this->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
1018 UpdraftPlus_Options::update_updraft_option('updraft_backup_history',$backup_history);
1019 }
1020
1021 function delete_local($file) {
1022 if(UpdraftPlus_Options::get_updraft_option('updraft_delete_local')) {
1023 $this->log("Deleting local file: $file");
1024 //need error checking so we don't delete what isn't successfully uploaded?
1025 $fullpath = $this->backups_dir_location().'/'.$file;
1026 return unlink($fullpath);
1027 }
1028 return true;
1029 }
1030
1031 // This function is not needed for backup success, according to the design, but it helps with efficient scheduling
1032 function reschedule_if_needed() {
1033 // If nothing is scheduled, then return
1034 if (empty($this->newresumption_scheduled)) return;
1035 $time_now = time();
1036 $time_away = $this->newresumption_scheduled - $time_now;
1037 // 30 is chosen because it is also used to detect recent activity on files (file mod times)
1038 if ($time_away >1 && $time_away <= 30) {
1039 $this->log('The scheduled resumption is within 30 seconds - will reschedule');
1040 // Push 30 seconds into the future
1041 // $this->reschedule(60);
1042 // Increase interval generally by 30 seconds, on the assumption that our prior estimates were innaccurate (i.e. not just 30 seconds *this* time)
1043 $this->increase_resume_and_reschedule(30);
1044 }
1045 }
1046
1047 function reschedule($how_far_ahead) {
1048 // Reschedule - remove presently scheduled event
1049 $next_resumption = $this->current_resumption + 1;
1050 wp_clear_scheduled_hook('updraft_backup_resume', array($next_resumption, $this->nonce));
1051 // Add new event
1052 if ($how_far_ahead < $this->minimum_resume_interval()) $how_far_ahead=$this->minimum_resume_interval();
1053 $schedule_for = time() + $how_far_ahead;
1054 $this->log("Rescheduling resumption $next_resumption: moving to $how_far_ahead seconds from now ($schedule_for)");
1055 wp_schedule_single_event($schedule_for, 'updraft_backup_resume', array($next_resumption, $this->nonce));
1056 $this->newresumption_scheduled = $schedule_for;
1057 }
1058
1059 function increase_resume_and_reschedule($howmuch = 120, $force_schedule = false) {
1060
1061 $resume_interval = $this->jobdata_get('resume_interval');
1062 if (!is_numeric($resume_interval) || $resume_interval < $this->minimum_resume_interval()) $resume_interval = $this->minimum_resume_interval();
1063
1064 if (empty($this->newresumption_scheduled) && $force_schedule) {
1065 $this->log("A new resumption will be scheduled to prevent the job ending");
1066 }
1067
1068 if (!empty($this->newresumption_scheduled) || $force_schedule) $this->reschedule($resume_interval+$howmuch);
1069 $this->jobdata_set('resume_interval', $resume_interval+$howmuch);
1070
1071 $this->log("To decrease the likelihood of overlaps, increasing resumption interval to: $resume_interval + $howmuch = ".($resume_interval+$howmuch));
1072 }
1073
1074 // For detecting another run, and aborting if one was found
1075 function check_recent_modification($file) {
1076 if (file_exists($file)) {
1077 $time_mod = (int)@filemtime($file);
1078 $time_now = time();
1079 if ($time_mod>100 && ($time_now-$time_mod)<30) {
1080 $this->terminate_due_to_activity($file, $time_now, $time_mod);
1081 }
1082 }
1083 }
1084
1085 // This function is resumable
1086 function backup_dirs($transient_status) {
1087
1088 if(!$this->backup_time) $this->backup_time_nonce();
1089
1090 $updraft_dir = $this->backups_dir_location();
1091 if(!is_writable($updraft_dir)) {
1092 $this->log("Backup directory ($updraft_dir) is not writable, or does not exist");
1093 $this->error(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'updraftplus'), $updraft_dir));
1094 return array();
1095 }
1096
1097 // Bring in all the zip routines
1098 require_once(UPDRAFTPLUS_DIR.'/backup.php');
1099 global $updraftplus_backup;
1100
1101 //get the blog name and rip out all non-alphanumeric chars other than _
1102 $blog_name = str_replace(' ','_',substr(get_bloginfo(), 0, 96));
1103 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', $blog_name);
1104 if(!$blog_name) $blog_name = 'non_alpha_name';
1105
1106 $backup_file_basename = 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $this->backup_time), 'Y-m-d-Hi').'_'.$blog_name.'_'.$this->nonce;
1107
1108 $backup_array = array();
1109
1110 $possible_backups = $this->get_backupable_file_entities(false);
1111
1112 # Plugins, themes, uploads
1113 foreach ($possible_backups as $youwhat => $whichdir) {
1114
1115 // TODO: Should store what is wanted in the job description
1116 if (UpdraftPlus_Options::get_updraft_option("updraft_include_$youwhat", apply_filters("updraftplus_defaultoption_include_$youwhat", true))) {
1117
1118 $zip_file = $updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.'.zip';
1119
1120 $this->check_recent_modification($zip_file);
1121
1122 if ($transient_status == 'finished') {
1123 $backup_array[$youwhat] = $backup_file_basename.'-'.$youwhat.'.zip';
1124 if (file_exists($zip_file)) $backup_array[$youwhat.'-size'] = filesize($zip_file);
1125 } else {
1126 # Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
1127 $created = apply_filters('updraftplus_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename);
1128 # If the filter did not lead to something being created, then use the default method
1129 if ($created == $whichdir) {
1130 $created = $updraftplus_backup->create_zip($whichdir, $youwhat, $updraft_dir, $backup_file_basename);
1131 }
1132 if ($created) {
1133 $backup_array[$youwhat] = $created;
1134 $backup_array[$youwhat.'-size'] = filesize($updraft_dir.'/'.$created);
1135 }
1136 }
1137 } else {
1138 $this->log("No backup of $youwhat: excluded by user's options");
1139 }
1140 }
1141
1142 # Others - needs special/separate handling, since its purpose is to mop up everything else
1143 if (UpdraftPlus_Options::get_updraft_option('updraft_include_others', true)) {
1144
1145 $zip_file = $updraft_dir.'/'.$backup_file_basename.'-others.zip';
1146 $this->check_recent_modification($zip_file);
1147
1148 if ($transient_status == 'finished') {
1149 $backup_array['others'] = $backup_file_basename.'-others.zip';
1150 if (file_exists($zip_file)) $backup_array['others-size'] = filesize($zip_file);
1151 } else {
1152 $this->log("Beginning backup of other directories found in the content directory");
1153
1154 // http://www.phpconcept.net/pclzip/user-guide/53
1155 /* First parameter to create is:
1156 An array of filenames or dirnames,
1157 or
1158 A string containing the filename or a dirname,
1159 or
1160 A string containing a list of filename or dirname separated by a comma.
1161 */
1162
1163 # Create an array of directories to be skipped
1164 $others_skip = preg_split("/,/",UpdraftPlus_Options::get_updraft_option('updraft_include_others_exclude', UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
1165 # Make the values into the keys
1166 $others_skip = array_flip($others_skip);
1167
1168 $possible_backups_dirs = array_flip($possible_backups);
1169
1170 $other_dirlist = $this->compile_folder_list_for_backup(WP_CONTENT_DIR, $possible_backups_dirs, $others_skip);
1171
1172 if (count($other_dirlist)>0) {
1173 $created = $updraftplus_backup->create_zip($other_dirlist, 'others', $updraft_dir, $backup_file_basename);
1174 if ($created) {
1175 $backup_array['others'] = $created;
1176 $backup_array['others-size'] = filesize($updraft_dir.'/'.$created);
1177 }
1178 } else {
1179 $this->log("No backup of other directories: there was nothing found to back up");
1180 }
1181
1182 # If we are not already finished
1183 }
1184 } else {
1185 $this->log("No backup of other directories: excluded by user's options");
1186 }
1187 return $backup_array;
1188 }
1189
1190 // 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 (avoid is potentially dangerous; skip is just a preference). They are allowed to overlap.
1191 function compile_folder_list_for_backup($backup_from_inside_dir, $avoid_these_dirs, $skip_these_dirs) {
1192
1193 $dirlist = array();
1194
1195 $this->log('Looking for candidates to back up in: '.$backup_from_inside_dir);
1196
1197 $updraft_dir = $this->backups_dir_location();
1198 if ($handle = opendir($backup_from_inside_dir)) {
1199
1200 while (false !== ($entry = readdir($handle))) {
1201 $candidate = $backup_from_inside_dir.'/'.$entry;
1202 if ($entry != "." && $entry != "..") {
1203 if (isset($avoid_these_dirs[$candidate])) {
1204 $this->log("finding files: $entry: skipping: this is the ".$avoid_these_dirs[$candidate]." directory");
1205 } elseif ($candidate == $updraft_dir) {
1206 $this->log("finding files: $entry: skipping: this is the updraft directory");
1207 } elseif (isset($skip_these_dirs[$entry])) {
1208 $this->log("finding files: $entry: skipping: excluded by options");
1209 } else {
1210 $this->log("finding files: $entry: adding to list");
1211 array_push($dirlist, $candidate);
1212 }
1213 }
1214 }
1215 @closedir($handle);
1216 } else {
1217 $this->log('ERROR: Could not read the directory: '.$backup_from_inside_dir);
1218 $this->error(__('Could not read the directory', 'updraftplus').': '.$backup_from_inside_dir);
1219 }
1220
1221 return $dirlist;
1222
1223 }
1224
1225 function save_backup_history($backup_array) {
1226 if(is_array($backup_array)) {
1227 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
1228 $backup_history = (is_array($backup_history)) ? $backup_history : array();
1229 $backup_array['nonce'] = $this->nonce;
1230 $backup_array['service'] = $this->jobdata_get('service');
1231 $backup_history[$this->backup_time] = $backup_array;
1232 UpdraftPlus_Options::update_updraft_option('updraft_backup_history', $backup_history);
1233 } else {
1234 $this->log('Could not save backup history because we have no backup array. Backup probably failed.');
1235 $this->error(__('Could not save backup history because we have no backup array. Backup probably failed.','updraftplus'));
1236 }
1237 }
1238
1239 function get_backup_history() {
1240 $backup_history = UpdraftPlus_Options::get_updraft_option('updraft_backup_history');
1241 // In fact, it looks like the line below actually *introduces* a race condition
1242 //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
1243 // global $wpdb;
1244 // $backup_history = @unserialize($wpdb->get_var($wpdb->prepare("SELECT option_value from $wpdb->options WHERE option_name='updraft_backup_history'")));
1245 if(is_array($backup_history)) {
1246 krsort($backup_history); //reverse sort so earliest backup is last on the array. Then we can array_pop.
1247 } else {
1248 $backup_history = array();
1249 }
1250 return $backup_history;
1251 }
1252
1253 // Open a file, store its filehandle
1254 function backup_db_open($file, $allow_gz = true) {
1255 if (function_exists('gzopen') && $allow_gz == true) {
1256 $this->dbhandle = @gzopen($file, 'w');
1257 $this->dbhandle_isgz = true;
1258 } else {
1259 $this->dbhandle = @fopen($file, 'w');
1260 $this->dbhandle_isgz = false;
1261 }
1262 if(!$this->dbhandle) {
1263 $this->log("ERROR: $file: Could not open the backup file for writing");
1264 $this->error("$file: ".__("Could not open the backup file for writing",'updraftplus'));
1265 }
1266 }
1267
1268 function backup_db_header() {
1269
1270 //Begin new backup of MySql
1271 $this->stow("# " . 'WordPress MySQL database backup' . "\n");
1272 $this->stow("# " . 'Created by UpdraftPlus (http://updraftplus.com)' . "\n");
1273 $this->stow("#\n");
1274 $this->stow("# " . sprintf(__('Generated: %s','wp-db-backup'),date("l j. F Y H:i T")) . "\n");
1275 $this->stow("# " . sprintf(__('Hostname: %s','wp-db-backup'),DB_HOST) . "\n");
1276 $this->stow("# " . sprintf(__('Database: %s','wp-db-backup'),$this->backquote(DB_NAME)) . "\n");
1277 $this->stow("# --------------------------------------------------------\n");
1278
1279 if (defined("DB_CHARSET")) {
1280 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
1281 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
1282 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
1283 $this->stow("/*!40101 SET NAMES " . DB_CHARSET . " */;\n");
1284 }
1285 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n");
1286 }
1287
1288 /* This function is resumable, using the following method:
1289 - Each table is written out to ($final_filename).table.tmp
1290 - When the writing finishes, it is renamed to ($final_filename).table
1291 - When all tables are finished, they are concatenated into the final file
1292 */
1293 function backup_db($already_done = "begun") {
1294
1295 // Get the file prefix
1296 $updraft_dir = $this->backups_dir_location();
1297
1298 if(!$this->backup_time) $this->backup_time_nonce();
1299 if (!$this->opened_log_time) $this->logfile_open($this->nonce);
1300
1301 // Get the blog name and rip out all non-alphanumeric chars other than _
1302 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr(get_bloginfo(), 0, 96)));
1303 if (!$blog_name) $blog_name = 'non_alpha_name';
1304
1305 $file_base = 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $this->backup_time), 'Y-m-d-Hi').'_'.$blog_name.'_'.$this->nonce;
1306 $backup_file_base = $updraft_dir.'/'.$file_base;
1307
1308 if ('finished' == $already_done) return basename($backup_file_base.'-db.gz');
1309 if ('encrypted' == $already_done) return basename($backup_file_base.'-db.gz.crypt');
1310
1311 $total_tables = 0;
1312
1313 global $table_prefix, $wpdb;
1314
1315 $all_tables = $wpdb->get_results("SHOW TABLES", ARRAY_N);
1316 $all_tables = array_map(create_function('$a', 'return $a[0];'), $all_tables);
1317
1318 if (!is_writable($updraft_dir)) {
1319 $this->log("The backup directory ($updraft_dir) is not writable.");
1320 $this->error("$updraft_dir: ".__('The backup directory is not writable.','updraftplus'));
1321 return false;
1322 }
1323
1324 $stitch_files = array();
1325
1326 foreach ($all_tables as $table) {
1327 $total_tables++;
1328 // Increase script execution time-limit to 15 min for every table.
1329 if ( !@ini_get('safe_mode') || strtolower(@ini_get('safe_mode')) == "off") @set_time_limit(15*60);
1330 // The table file may already exist if we have produced it on a previous run
1331 $table_file_prefix = $file_base.'-db-table-'.$table.'.table';
1332 if (file_exists($updraft_dir.'/'.$table_file_prefix.'.gz')) {
1333 $this->log("Table $table: corresponding file already exists; moving on");
1334 } else {
1335 // Open file, store the handle
1336 $this->backup_db_open($updraft_dir.'/'.$table_file_prefix.'.tmp.gz', true);
1337 # === is needed, otherwise 'false' matches (i.e. prefix does not match)
1338 if ( strpos($table, $table_prefix) === 0 ) {
1339 // Create the SQL statements
1340 $this->stow("# --------------------------------------------------------\n");
1341 $this->stow("# " . sprintf(__('Table: %s','wp-db-backup'),$this->backquote($table)) . "\n");
1342 $this->stow("# --------------------------------------------------------\n");
1343 $this->backup_table($table);
1344 } else {
1345 $this->stow("# --------------------------------------------------------\n");
1346 $this->stow("# " . sprintf(__('Skipping non-WP table: %s','wp-db-backup'),$this->backquote($table)) . "\n");
1347 $this->stow("# --------------------------------------------------------\n");
1348 }
1349 // Close file
1350 $this->close($this->dbhandle);
1351 $this->log("Table $table: finishing file (${table_file_prefix}.gz)");
1352 rename($updraft_dir.'/'.$table_file_prefix.'.tmp.gz', $updraft_dir.'/'.$table_file_prefix.'.gz');
1353 }
1354 $stitch_files[] = $table_file_prefix;
1355 }
1356
1357 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
1358 $backup_final_file_name = $backup_file_base.'-db.gz';
1359 $time_now = time();
1360 $time_mod = (int)@filemtime($backup_final_file_name);
1361 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<20) {
1362 $this->terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
1363 } elseif (file_exists($backup_final_file_name)) {
1364 $this->log("The final database file ($backup_final_file_name) exists, but was apparently not modified within the last 20 seconds (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod)."). Thus we assume that another UpdraftPlus terminated; thus we will continue.");
1365 }
1366
1367 // Finally, stitch the files together
1368 $this->backup_db_open($backup_final_file_name, true);
1369 $this->backup_db_header();
1370
1371 // We delay the unlinking because if two runs go concurrently and fail to detect each other (should not happen, but there's no harm in assuming the detection failed) then that leads to files missing from the db dump
1372 $unlink_files = array();
1373
1374 foreach ($stitch_files as $table_file) {
1375 $this->log("{$table_file}.gz: adding to final database dump");
1376 if (!$handle = gzopen($updraft_dir.'/'.$table_file.'.gz', "r")) {
1377 $this->log("Error: Failed to open database file for reading: ${table_file}.gz");
1378 $this->error(" Failed to open database file for reading: ${table_file}.gz");
1379 } else {
1380 while ($line = gzgets($handle, 2048)) { $this->stow($line); }
1381 gzclose($handle);
1382 $unlink_files[] = $updraft_dir.'/'.$table_file.'.gz';
1383 }
1384 }
1385
1386 if (defined("DB_CHARSET")) {
1387 $this->stow("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n");
1388 $this->stow("/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n");
1389 $this->stow("/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n");
1390 }
1391
1392 $this->log($file_base.'-db.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024,1).' Kb)');
1393 $this->close($this->dbhandle);
1394
1395 foreach ($unlink_files as $unlink_file) @unlink($unlink_file);
1396
1397 if (count($this->errors)) {
1398 return false;
1399 } else {
1400 # We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
1401 $this->log("Total database tables backed up: $total_tables");
1402 return basename($backup_file_base.'-db.gz');
1403 }
1404
1405 } //wp_db_backup
1406
1407 function terminate_due_to_activity($file, $time_now, $time_mod) {
1408 $file_size = filesize($file);
1409 $this->log("Terminate: the final database file ($file) exists, and was modified within the last 20 seconds (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod).", size=$file_size). This likely means that another UpdraftPlus run is at work; so we will exit.");
1410 $this->increase_resume_and_reschedule(120, true);
1411 die;
1412 }
1413
1414 /**
1415 * Taken partially from phpMyAdmin and partially from
1416 * Alain Wolf, Zurich - Switzerland
1417 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1418 * Modified by Scott Merrill (http://www.skippy.net/)
1419 * to use the WordPress $wpdb object
1420 * @param string $table
1421 * @param string $segment
1422 * @return void
1423 */
1424 function backup_table($table, $segment = 'none') {
1425 global $wpdb;
1426
1427 $microtime = microtime(true);
1428
1429 $total_rows = 0;
1430
1431 $table_structure = $wpdb->get_results("DESCRIBE $table");
1432 if (! $table_structure) {
1433 //$this->error(__('Error getting table details','wp-db-backup') . ": $table");
1434 return false;
1435 }
1436
1437 if(($segment == 'none') || ($segment == 0)) {
1438 // Add SQL statement to drop existing table
1439 $this->stow("\n\n");
1440 $this->stow("#\n");
1441 $this->stow("# " . sprintf(__('Delete any existing table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1442 $this->stow("#\n");
1443 $this->stow("\n");
1444 $this->stow("DROP TABLE IF EXISTS " . $this->backquote($table) . ";\n");
1445
1446 // Table structure
1447 // Comment in SQL-file
1448 $this->stow("\n\n");
1449 $this->stow("#\n");
1450 $this->stow("# " . sprintf(__('Table structure of table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1451 $this->stow("#\n");
1452 $this->stow("\n");
1453
1454 $create_table = $wpdb->get_results("SHOW CREATE TABLE $table", ARRAY_N);
1455 if (false === $create_table) {
1456 $err_msg = sprintf(__('Error with SHOW CREATE TABLE for %s.','wp-db-backup'), $table);
1457 //$this->error($err_msg);
1458 $this->stow("#\n# $err_msg\n#\n");
1459 }
1460 $this->stow($create_table[0][1] . ' ;');
1461
1462 if (false === $table_structure) {
1463 $err_msg = sprintf('Error getting table structure of %s', $table);
1464 $this->stow("#\n# $err_msg\n#\n");
1465 }
1466
1467 // Comment in SQL-file
1468 $this->stow("\n\n#\n# " . sprintf('Data contents of table %s',$this->backquote($table)) . "\n#\n");
1469 }
1470
1471 // In UpdraftPlus, segment is always 'none'
1472 if(($segment == 'none') || ($segment >= 0)) {
1473 $defs = array();
1474 $integer_fields = array();
1475 // $table_structure was from "DESCRIBE $table"
1476 foreach ($table_structure as $struct) {
1477 if ( (0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint')) ||
1478 (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint')) ) {
1479 $defs[strtolower($struct->Field)] = ( null === $struct->Default ) ? 'NULL' : $struct->Default;
1480 $integer_fields[strtolower($struct->Field)] = "1";
1481 }
1482 }
1483
1484 if($segment == 'none') {
1485 $row_start = 0;
1486 $row_inc = 100;
1487 } else {
1488 $row_start = $segment * 100;
1489 $row_inc = 100;
1490 }
1491
1492 do {
1493 if ( !@ini_get('safe_mode') || strtolower(@ini_get('safe_mode')) == "off") @set_time_limit(15*60);
1494
1495 $table_data = $wpdb->get_results("SELECT * FROM $table LIMIT {$row_start}, {$row_inc}", ARRAY_A);
1496 $entries = 'INSERT INTO ' . $this->backquote($table) . ' VALUES ';
1497 // \x08\\x09, not required
1498 $search = array("\x00", "\x0a", "\x0d", "\x1a");
1499 $replace = array('\0', '\n', '\r', '\Z');
1500 if($table_data) {
1501 $thisentry = "";
1502 foreach ($table_data as $row) {
1503 $total_rows++;
1504 $values = array();
1505 foreach ($row as $key => $value) {
1506 if (isset($integer_fields[strtolower($key)])) {
1507 // make sure there are no blank spots in the insert syntax,
1508 // yet try to avoid quotation marks around integers
1509 $value = ( null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
1510 $values[] = ( '' === $value ) ? "''" : $value;
1511 } else {
1512 $values[] = "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
1513 }
1514 }
1515 if ($thisentry) $thisentry .= ",\n ";
1516 $thisentry .= '('.implode(', ', $values).')';
1517 // Flush every 512Kb
1518 if (strlen($thisentry) > 524288) {
1519 $this->stow(" \n".$entries.$thisentry.';');
1520 $thisentry = "";
1521 }
1522
1523 }
1524 if ($thisentry) $this->stow(" \n".$entries.$thisentry.';');
1525 $row_start += $row_inc;
1526 }
1527 } while((count($table_data) > 0) and ($segment=='none'));
1528 }
1529
1530 if(($segment == 'none') || ($segment < 0)) {
1531 // Create footer/closing comment in SQL-file
1532 $this->stow("\n");
1533 $this->stow("#\n");
1534 $this->stow("# " . sprintf(__('End of data contents of table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1535 $this->stow("# --------------------------------------------------------\n");
1536 $this->stow("\n");
1537 }
1538 $this->log("Table $table: Total rows added: $total_rows in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1539
1540 } // end backup_table()
1541
1542 function stow($query_line) {
1543 if ($this->dbhandle_isgz) {
1544 if(! @gzwrite($this->dbhandle, $query_line)) {
1545 //$this->error(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg);
1546 }
1547 } else {
1548 if(false === @fwrite($this->dbhandle, $query_line)) {
1549 //$this->error(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg);
1550 }
1551 }
1552 }
1553
1554 function close($handle) {
1555 if ($this->dbhandle_isgz) {
1556 gzclose($handle);
1557 } else {
1558 fclose($handle);
1559 }
1560 }
1561
1562 function error($error) {
1563 if (count($this->errors) == 0) $this->log("An error condition has occurred for the first time on this run");
1564 $this->errors[] = $error;
1565 return true;
1566 }
1567
1568 /**
1569 * Add backquotes to tables and db-names in
1570 * SQL queries. Taken from phpMyAdmin.
1571 */
1572 function backquote($a_name) {
1573 if (!empty($a_name) && $a_name != '*') {
1574 if (is_array($a_name)) {
1575 $result = array();
1576 reset($a_name);
1577 while(list($key, $val) = each($a_name))
1578 $result[$key] = '`' . $val . '`';
1579 return $result;
1580 } else {
1581 return '`' . $a_name . '`';
1582 }
1583 } else {
1584 return $a_name;
1585 }
1586 }
1587
1588 /*END OF WP-DB-BACKUP BLOCK */
1589
1590 /*
1591 this function is both the backup scheduler and ostensibly a filter callback for saving the option.
1592 it is called in the register_setting for the updraft_interval, which means when the admin settings
1593 are saved it is called. it returns the actual result from wp_filter_nohtml_kses (a sanitization filter)
1594 so the option can be properly saved.
1595 */
1596 function schedule_backup($interval) {
1597 //clear schedule and add new so we don't stack up scheduled backups
1598 wp_clear_scheduled_hook('updraft_backup');
1599 switch($interval) {
1600 case 'every4hours':
1601 case 'every8hours':
1602 case 'twicedaily':
1603 case 'daily':
1604 case 'weekly':
1605 case 'fortnightly':
1606 case 'monthly':
1607 $first_time = apply_filters('updraftplus_schedule_start_files', time()+30);
1608 wp_schedule_event($first_time, $interval, 'updraft_backup');
1609 break;
1610 }
1611 return wp_filter_nohtml_kses($interval);
1612 }
1613
1614 function deactivation () {
1615 wp_clear_scheduled_hook('updraftplus_weekly_ping');
1616 }
1617
1618 // Acts as a WordPress options filter
1619 function googledrive_clientid_checkchange($client_id) {
1620 if (UpdraftPlus_Options::get_updraft_option('updraft_googledrive_token') != '' && UpdraftPlus_Options::get_updraft_option('updraft_googledrive_clientid') != $client_id) {
1621 require_once(UPDRAFTPLUS_DIR.'/methods/googledrive.php');
1622 add_action('http_api_curl', array($this, 'add_curl_capath'));
1623 UpdraftPlus_BackupModule_googledrive::gdrive_auth_revoke(true);
1624 remove_action('http_api_curl', array($this, 'add_curl_capath'));
1625
1626 }
1627 return $client_id;
1628 }
1629
1630 function schedule_backup_database($interval) {
1631 //clear schedule and add new so we don't stack up scheduled backups
1632 wp_clear_scheduled_hook('updraft_backup_database');
1633 switch($interval) {
1634 case 'every4hours':
1635 case 'every8hours':
1636 case 'twicedaily':
1637 case 'daily':
1638 case 'weekly':
1639 case 'fortnightly':
1640 case 'monthly':
1641 $first_time = apply_filters('updraftplus_schedule_start_db', time()+30);
1642 wp_schedule_event($first_time, $interval, 'updraft_backup_database');
1643 break;
1644 }
1645 return wp_filter_nohtml_kses($interval);
1646 }
1647
1648 //wp-cron only has hourly, daily and twicedaily, so we need to add some of our own
1649 function modify_cron_schedules($schedules) {
1650 $schedules['weekly'] = array( 'interval' => 604800, 'display' => 'Once Weekly' );
1651 $schedules['fortnightly'] = array( 'interval' => 1209600, 'display' => 'Once Each Fortnight' );
1652 $schedules['monthly'] = array( 'interval' => 2592000, 'display' => 'Once Monthly' );
1653 $schedules['every4hours'] = array( 'interval' => 14400, 'display' => 'Every 4 hours' );
1654 $schedules['every8hours'] = array( 'interval' => 28800, 'display' => 'Every 8 hours' );
1655 return $schedules;
1656 }
1657
1658 function backups_dir_location() {
1659
1660 if (!empty($this->backup_dir)) return $this->backup_dir;
1661
1662 $updraft_dir = untrailingslashit(UpdraftPlus_Options::get_updraft_option('updraft_dir'));
1663 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
1664 $updraft_dir = ($updraft_dir)?$updraft_dir:$default_backup_dir;
1665
1666 // Do a test for a relative path
1667 if ('/' != substr($updraft_dir, 0, 1) && "\\" != substr($updraft_dir, 0, 1) && !preg_match('/^[a-zA-Z]:/', $updraft_dir)) {
1668 $updraft_dir = ABSPATH.$updraft_dir;
1669 }
1670
1671 //if the option isn't set, default it to /backups inside the upload dir
1672 //check for the existence of the dir and an enumeration preventer.
1673 // index.php is for a sanity check - make sure that we're not somewhere unexpected
1674 if((!is_dir($updraft_dir) || !is_file($updraft_dir.'/index.html') || !is_file($updraft_dir.'/.htaccess')) && !is_file($updraft_dir.'/index.php')) {
1675 @mkdir($updraft_dir, 0775, true);
1676 @file_put_contents($updraft_dir.'/index.html',"<html><body><a href=\"http://updraftplus.com\">WordPress backups by UpdraftPlus</a></body></html>");
1677 if (!is_file($updraft_dir.'/.htaccess')) @file_put_contents($updraft_dir.'/.htaccess','deny from all');
1678 }
1679
1680 $this->backup_dir = $updraft_dir;
1681
1682 return $updraft_dir;
1683 }
1684
1685 function spool_file($type, $fullpath, $encryption = "") {
1686
1687 @set_time_limit(900);
1688
1689 if (file_exists($fullpath)) {
1690
1691 $file = basename($fullpath);
1692
1693 $len = filesize($fullpath);
1694
1695 $filearr = explode('.',$file);
1696 // //we've only got zip and gz...for now
1697 $file_ext = array_pop($filearr);
1698 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
1699 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
1700 header("Content-Length: $len;");
1701
1702 if ($file_ext == 'crypt') {
1703 if ($encryption == "") $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
1704 if ($encryption == "") {
1705 header('Content-type: text/plain');
1706 _e("Decryption failed. The database file is encrypted, but you have no encryption key entered.",'updraftplus');
1707 $this->error('Decryption of database failed: the database file is encrypted, but you have no encryption key entered.');
1708 } else {
1709 require_once(UPDRAFTPLUS_DIR.'/includes/phpseclib/Crypt/Rijndael.php');
1710 $rijndael = new Crypt_Rijndael();
1711 $rijndael->setKey($encryption);
1712 $ciphertext = $rijndael->decrypt(file_get_contents($fullpath));
1713 if ($ciphertext) {
1714 header('Content-type: application/x-gzip');
1715 header("Content-Disposition: attachment; filename=\"".substr($file,0,-6)."\";");
1716 print $ciphertext;
1717 } else {
1718 header('Content-type: text/plain');
1719 echo __("Decryption failed. The most likely cause is that you used the wrong key.",'updraftplus')." ".__('The decryption key used:','updraftplus').' '.$encryption;
1720
1721 }
1722 }
1723 } else {
1724 if ($file_ext == 'zip') {
1725 header('Content-type: application/zip');
1726 } else {
1727 header('Content-type: application/x-gzip');
1728 }
1729 header("Content-Disposition: attachment; filename=\"$file\";");
1730 readfile($fullpath);
1731 }
1732 // $this->delete_local($file);
1733 } else {
1734 echo __('File not found', 'updraftplus');
1735 }
1736 }
1737
1738 function retain_range($input) {
1739 $input = (int)$input;
1740 return ($input > 0 && $input < 3650) ? $input : 1;
1741 }
1742
1743 function memory_check_current() {
1744 # Returns in megabytes
1745 $memory_limit = ini_get('memory_limit');
1746 $memory_unit = $memory_limit[strlen($memory_limit)-1];
1747 $memory_limit = substr($memory_limit,0,strlen($memory_limit)-1);
1748 switch($memory_unit) {
1749 case 'K':
1750 $memory_limit = $memory_limit/1024;
1751 break;
1752 case 'G':
1753 $memory_limit = $memory_limit*1024;
1754 break;
1755 case 'M':
1756 //assumed size, no change needed
1757 break;
1758 }
1759 return $memory_limit;
1760 }
1761
1762 function memory_check($memory) {
1763 $memory_limit = $this->memory_check_current();
1764 return ($memory_limit >= $memory)?true:false;
1765 }
1766
1767 function url_start($urls,$url) {
1768 return ($urls) ? '<a href="http://'.$url.'">' : "";
1769 }
1770
1771 function url_end($urls,$url) {
1772 return ($urls) ? '</a>' : " (http://$url)";
1773 }
1774
1775 function wordshell_random_advert($urls) {
1776 if (defined('UPDRAFTPLUS_NOADS')) return "";
1777 $rad = rand(0,8);
1778 switch ($rad) {
1779 case 0:
1780 return $this->url_start($urls,'updraftplus.com')."Want more features or paid, guaranteed support? Check out UpdraftPlus.Com".$this->url_end($urls,'updraftplus.com');
1781 break;
1782 case 1:
1783 if (defined('WPLANG') && strlen(WPLANG)>0 && !is_file(UPDRAFTPLUS_DIR.'/languages/updraftplus-'.WPLANG.
1784 '.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/');
1785
1786 return __('Find UpdraftPlus useful?','updraftplus').' '.$this->url_start($urls,'david.dw-perspective.org.uk/donate').__("Please make a donation", 'updraftplus').$this->url_end($urls,'david.dw-perspective.org.uk/donate');
1787 case 2:
1788 return $this->url_start($urls,'wordshell.net')."Check out WordShell".$this->url_end($urls,'www.wordshell.net')." - manage WordPress from the command line - huge time-saver";
1789 break;
1790 case 3:
1791 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');
1792 break;
1793 case 4:
1794 return $this->url_start($urls,'www.simbahosting.co.uk')."Need high-quality WordPress hosting from WordPress specialists? (Including automatic backups and 1-click installer). Get it from the creators of UpdraftPlus.".$this->url_end($urls,'www.simbahosting.co.uk');
1795 break;
1796 case 5:
1797 if (!defined('UPDRAFTPLUS_NOADS')) {
1798 return $this->url_start($urls,'updraftplus.com').__("Need even more features and support? Check out UpdraftPlus Premium",'updraftplus').$this->url_end($urls,'updraftplus.com');
1799 } else {
1800 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.";
1801 }
1802 break;
1803 case 6:
1804 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/');
1805 break;
1806 case 7:
1807 return $this->url_start($urls,'updraftplus.com').__("Check out UpdraftPlus.Com for help, add-ons and support",'updraftplus').$this->url_end($urls,'updraftplus.com');
1808 break;
1809 case 8:
1810 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/');
1811 break;
1812 }
1813 }
1814
1815 }
1816
1817
1818 ?>
1819