PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.13.9
UpdraftPlus: WP Backup & Migration Plugin v1.13.9
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 / backup.php

backup.php in UpdraftPlus: WP Backup & Migration Plugin 1.13.9, at backup.php

3,165 lines 152.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
4 if (!class_exists('UpdraftPlus_PclZip')) require_once(UPDRAFTPLUS_DIR.'/class-zip.php');
5
6 // This file contains functions that are only needed/loaded when a backup is running (reduces memory usage on other pages)
7
8 class UpdraftPlus_Backup {
9
10 public $index = 0;
11
12 private $zipfiles_added;
13 private $zipfiles_added_thisrun = 0;
14 public $zipfiles_dirbatched;
15 public $zipfiles_batched;
16 public $zipfiles_skipped_notaltered;
17 private $zip_split_every = 419430400; # 400MB
18 private $zip_last_ratio = 1;
19 private $whichone;
20 private $zip_basename = '';
21 private $backup_basename = '';
22 private $zipfiles_lastwritetime;
23 // 0 = unknown; false = failed
24 public $binzip = 0;
25
26 private $dbhandle;
27 private $dbhandle_isgz;
28
29 # Array of entities => times
30 private $altered_since = -1;
31 # Time for the current entity
32 private $makezip_if_altered_since = -1;
33
34 private $excluded_extensions = false;
35
36 private $use_zip_object = 'UpdraftPlus_ZipArchive';
37 public $debug = false;
38
39 public $updraft_dir;
40 private $blog_name;
41 private $wpdb_obj;
42 private $job_file_entities = array();
43
44 private $first_run = 0;
45
46 // Record of zip files created
47 private $backup_files_array = array();
48
49 // Used for reporting
50 private $remotestorage_extrainfo = array();
51
52 // Used when deciding to use the 'store' or 'deflate' zip storage method
53 private $extensions_to_not_compress = array();
54
55 // Append to this any skipped tables
56 private $skipped_tables;
57
58 public function __construct($backup_files, $altered_since = -1) {
59
60 global $updraftplus;
61
62 // Get the blog name and rip out known-problematic characters. Remember that we may need to be able to upload this to any FTP server or cloud storage, where filename support may be unknown
63 $blog_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr(get_bloginfo(), 0, 32))));
64 if (!$blog_name || preg_match('#^_+$#', $blog_name)) {
65 // Try again...
66 $parsed_url = parse_url(home_url(), PHP_URL_HOST);
67 $parsed_subdir = untrailingslashit(parse_url(home_url(), PHP_URL_PATH));
68 if ($parsed_subdir && '/' != $parsed_subdir) $parsed_url .= str_replace(array('/', '\\'), '_', $parsed_subdir);
69 $blog_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/','', str_replace(' ','_', substr($parsed_url, 0, 32))));
70 if (!$blog_name || preg_match('#^_+$#', $blog_name)) $blog_name = 'WordPress_Backup';
71 }
72
73 // Allow an over-ride. Careful about introducing characters not supported by your filesystem or cloud storage.
74 $this->blog_name = apply_filters('updraftplus_blog_name', $blog_name);
75
76 # Decide which zip engine to begin with
77 $this->debug = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
78 $this->updraft_dir = $updraftplus->backups_dir_location();
79 $this->updraft_dir_realpath = realpath($this->updraft_dir);
80
81 add_action('updraft_report_remotestorage_extrainfo', array($this, 'report_remotestorage_extrainfo'), 10, 3);
82
83 if ('no' === $backup_files) {
84 $this->use_zip_object = 'UpdraftPlus_PclZip';
85 return;
86 }
87
88 $this->extensions_to_not_compress = array_unique(array_map('strtolower', array_map('trim', explode(',', UPDRAFTPLUS_ZIP_NOCOMPRESS))));
89
90 $this->altered_since = $altered_since;
91
92 // false means 'tried + failed'; whereas 0 means 'not yet tried'
93 // Disallow binzip on OpenVZ when we're not sure there's plenty of memory
94 if ($this->binzip === 0 && (!defined('UPDRAFTPLUS_PREFERPCLZIP') || UPDRAFTPLUS_PREFERPCLZIP != true) && (!defined('UPDRAFTPLUS_NO_BINZIP') || !UPDRAFTPLUS_NO_BINZIP) && $updraftplus->current_resumption <9) {
95
96 if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {
97 $meminfo = @file_get_contents('/proc/meminfo', false, null, -1, 200);
98 if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
99 $memory_mb = $matches[1]/1024;
100 # If the report is of a large amount, then we're probably getting the total memory on the hypervisor (this has been observed), and don't really know the VPS's memory
101 $vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." MB";
102 if ($memory_mb < 1024 || $memory_mb > 8192) {
103 $openvz_lowmem = true;
104 $vz_log .= " (will not use BinZip)";
105 }
106 $updraftplus->log($vz_log);
107 }
108 }
109 if (empty($openvz_lowmem)) {
110 $updraftplus->log('Checking if we have a zip executable available');
111 $binzip = $updraftplus->find_working_bin_zip();
112 if (is_string($binzip)) {
113 $updraftplus->log("Zip engine: found/will use a binary zip: $binzip");
114 $this->binzip = $binzip;
115 $this->use_zip_object = 'UpdraftPlus_BinZip';
116 }
117 }
118 }
119
120 # In tests, PclZip was found to be 25% slower than ZipArchive
121 if ($this->use_zip_object != 'UpdraftPlus_PclZip' && empty($this->binzip) && ((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
122 global $updraftplus;
123 $updraftplus->log("Zip engine: ZipArchive is not available or is disabled (will use PclZip if needed)");
124 $this->use_zip_object = 'UpdraftPlus_PclZip';
125 }
126
127 }
128
129 public function report_remotestorage_extrainfo($service, $info_html, $info_plain) {
130 $this->remotestorage_extrainfo[$service] = array('pretty' => $info_html, 'plain' => $info_plain);
131 }
132
133 // Public, because called from the 'More Files' add-on
134 public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index, $first_linked_index = false) {
135 // Note: $create_from_dir can be an array or a string
136 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
137 $original_index = $index;
138 $this->index = $index;
139 $this->first_linked_index = (false === $first_linked_index) ? 0 : $first_linked_index;
140
141 $this->whichone = $whichone;
142
143 global $updraftplus;
144
145 $this->zip_split_every = max((int)$updraftplus->jobdata_get('split_every'), UPDRAFTPLUS_SPLIT_MIN)*1048576;
146
147 if ('others' != $whichone) $updraftplus->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576,1)." MB)");
148
149 if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
150 $flag_error = true;
151 $updraftplus->log("Does not exist: $create_from_dir");
152 if ('mu-plugins' == $whichone) {
153 if (!function_exists('get_mu_plugins')) require_once(ABSPATH.'wp-admin/includes/plugin.php');
154 $mu_plugins = get_mu_plugins();
155 if (count($mu_plugins) == 0) {
156 $updraftplus->log("There appear to be no mu-plugins to back up. Will not raise an error.");
157 $flag_error = false;
158 }
159 }
160 if ($flag_error) $updraftplus->log(sprintf(__("%s - could not back this entity up; the corresponding directory does not exist (%s)", 'updraftplus'), $whichone, $create_from_dir), 'error');
161 return false;
162 }
163
164 $itext = (empty($index)) ? '' : ($index+1);
165 $base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
166 $full_path = $this->updraft_dir.'/'.$base_path;
167 $time_now = time();
168
169 # This is compatible with filenames which indicate increments, as it is looking only for the current increment
170 if (file_exists($full_path)) {
171 # Gather any further files that may also exist
172 $files_existing = array();
173 while (file_exists($full_path)) {
174 $files_existing[] = $base_path;
175 $time_mod = (int)@filemtime($full_path);
176 $updraftplus->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod,1)." s)");
177 if ($time_mod>100 && ($time_now-$time_mod)<30) {
178 $updraftplus->terminate_due_to_activity($base_path, $time_now, $time_mod);
179 }
180 $index++;
181 # This is compatible with filenames which indicate increments, as it is looking only for the current increment
182 $base_path = $backup_file_basename.'-'.$whichone.($index+1).'.zip';
183 $full_path = $this->updraft_dir.'/'.$base_path;
184 }
185 }
186
187 // Temporary file, to be able to detect actual completion (upon which, it is renamed)
188
189 // New (Jun-13) - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
190 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-$whichone", 600);
191
192 // Firstly, make sure that the temporary file is not already being written to - which can happen if a resumption takes place whilst an old run is still active
193 $zip_name = $full_path.'.tmp';
194 $time_mod = (int)@filemtime($zip_name);
195 if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
196 $updraftplus->terminate_due_to_activity($zip_name, $time_now, $time_mod);
197 }
198 if (file_exists($zip_name)) {
199 $updraftplus->log("File exists ($zip_name), but was apparently not modified within the last 30 seconds, so we assume that any previous run has now terminated (time_mod=$time_mod, time_now=$time_now, diff=".($time_now-$time_mod).")");
200 }
201
202 // Now, check for other forms of temporary file, which would indicate that some activity is going on (even if it hasn't made it into the main zip file yet)
203 // Note: this doesn't catch PclZip temporary files
204 $d = dir($this->updraft_dir);
205 $match = '_'.$updraftplus->nonce."-".$whichone;
206 while (false !== ($e = $d->read())) {
207 if ('.' == $e || '..' == $e || !is_file($this->updraft_dir.'/'.$e)) continue;
208 $ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $e);
209 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
210 $pclzip_match = preg_match("/^pclzip-[a-z0-9]+.tmp$/", $e);
211 if ($time_now-filemtime($this->updraft_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $updraftplus->current_resumption && ($binzip_match || $pclzip_match)))) {
212 $updraftplus->terminate_due_to_activity($this->updraft_dir.'/'.$e, $time_now, filemtime($this->updraft_dir.'/'.$e));
213 }
214 }
215 @$d->close();
216 clearstatcache();
217
218 if (isset($files_existing)) {
219 # Because of zip-splitting, the mere fact that files exist is not enough to indicate that the entity is finished. For that, we need to also see that no subsequent file has been started.
220 # Q. What if the previous runner died in between zips, and it is our job to start the next one? A. The next temporary file is created before finishing the former zip, so we are safe (and we are also safe-guarded by the updated value of the index being stored in the database).
221 return $files_existing;
222 }
223
224 $this->log_account_space();
225
226 $this->zip_microtime_start = microtime(true);
227
228 # The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
229 $zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
230 if ($zipcode !== true) {
231 $updraftplus->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
232 $updraftplus->log(sprintf(__("Could not create %s zip. Consult the log file for more information.",'updraftplus'),$whichone), 'error');
233 # The caller is required to update $index from $this->index
234 return false;
235 } else {
236 $itext = (empty($this->index)) ? '' : ($this->index+1);
237 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
238 if (file_exists($full_path.'.tmp')) {
239 if (@filesize($full_path.'.tmp') === 0) {
240 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
241 @unlink($full_path.'.tmp');
242 } else {
243
244 $checksum_description = '';
245
246 $checksums = $updraftplus->which_checksums();
247
248 foreach ($checksums as $checksum) {
249
250 $cksum = hash_file($checksum, $full_path.'.tmp');
251 $updraftplus->jobdata_set($checksum.'-'.$whichone.$this->index, $cksum);
252 if ($checksum_description) $checksum_description .= ', ';
253 $checksum_description .= "$checksum: $cksum";
254
255 }
256
257 @rename($full_path.'.tmp', $full_path);
258 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
259 $kbsize = filesize($full_path)/1024;
260 $rate = round($kbsize/$timetaken, 1);
261 $updraftplus->log("Created $whichone zip (".$this->index.") - ".round($kbsize,1)." KB in ".round($timetaken,1)." s ($rate KB/s) ($checksum_description)");
262 // We can now remove any left-over temporary files from this job
263 }
264 } elseif ($this->index > $original_index) {
265 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed (2)");
266 # Added 12-Feb-2014 (to help multiple morefiles)
267 $this->index--;
268 } else {
269 $updraftplus->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
270 }
271 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-$whichone", 0);
272 }
273
274 // Remove cache list files as well, if there are any
275 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-$whichone", 0, true);
276
277 # Create the results array to send back (just the new ones, not any prior ones)
278 $files_existing = array();
279 $res_index = 0;
280 for ($i = $original_index; $i<= $this->index; $i++) {
281 $itext = (empty($i)) ? '' : ($i+1);
282 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
283 if (file_exists($full_path)) {
284 $files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
285 }
286 $res_index++;
287 }
288 return $files_existing;
289 }
290
291 // This method is for calling outside of a cloud_backup() context. It constructs a list of services for which prune operations should be attempted, and then calls prune_retained_backups() if necessary upon them.
292 public function do_prune_standalone() {
293 global $updraftplus;
294
295 $services = $updraftplus->just_one($updraftplus->jobdata_get('service'));
296 if (!is_array($services)) $services = array($services);
297
298 $prune_services = array();
299
300 foreach ($services as $ind => $service) {
301 if ($service == "none" || '' == $service) continue;
302
303 $objname = "UpdraftPlus_BackupModule_${service}";
304 if (!class_exists($objname) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php')) {
305 require_once(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php');
306 }
307 if (class_exists($objname)) {
308 $remote_obj = new $objname;
309 $pass_to_prune = null;
310 $prune_services[$service] = array($remote_obj, null);
311 } else {
312 $updraftplus->log("Could not prune from service $service: remote method not found");
313 }
314
315 }
316
317 if (!empty($prune_services)) $this->prune_retained_backups($prune_services);
318 }
319
320 // Dispatch to the relevant function
321 public function cloud_backup($backup_array) {
322
323 global $updraftplus;
324
325 $services = $updraftplus->just_one($updraftplus->jobdata_get('service'));
326 if (!is_array($services)) $services = array($services);
327
328 // We need to make sure that the loop below actually runs
329 if (empty($services)) $services = array('none');
330
331 $updraftplus->jobdata_set('jobstatus', 'clouduploading');
332
333 $updraftplus->register_wp_http_option_hooks();
334
335 $upload_status = $updraftplus->jobdata_get('uploading_substatus');
336 if (!is_array($upload_status) || !isset($upload_status['t'])) {
337 $upload_status = array('i' => 0, 'p' => 0, 't' => max(1, count($services))*count($backup_array));
338 $updraftplus->jobdata_set('uploading_substatus', $upload_status);
339 }
340
341 $do_prune = array();
342
343 # If there was no check-in last time, then attempt a different service first - in case a time-out on the attempted service leads to no activity and everything stopping
344 if (count($services) >1 && !empty($updraftplus->no_checkin_last_time)) {
345 $updraftplus->log('No check-in last time: will try a different remote service first');
346 array_push($services, array_shift($services));
347 // Make sure that the 'no worthwhile activity' detector isn't flumoxed by the starting of a new upload at 0%
348 if ($updraftplus->current_resumption > 9) $updraftplus->jobdata_set('uploaded_lastreset', $updraftplus->current_resumption);
349 if (1 == ($updraftplus->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
350 }
351
352 $errors_before_uploads = $updraftplus->error_count();
353
354 foreach ($services as $ind => $service) {
355 # Used for logging by record_upload_chunk()
356 $this->current_service = $service;
357 # Used when deciding whether to delete the local file
358 $this->last_service = ($ind+1 >= count($services) && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
359
360 $log_extra = ($this->last_service) ? ' (last)' : '';
361 $updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service.$log_extra);
362 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
363
364 $method_include = UPDRAFTPLUS_DIR.'/methods/'.$service.'.php';
365 if (file_exists($method_include)) require_once($method_include);
366
367 if ($service == "none" || '' == $service) {
368 $updraftplus->log("No remote despatch: user chose no remote backup service");
369 # Still want to mark as "uploaded", to signal that nothing more needs doing. (Important on incremental runs with no cloud storage).
370 foreach ($backup_array as $bind => $file) {
371 if ($updraftplus->is_uploaded($file)) {
372 $updraftplus->log("Already uploaded: $file");
373 } else {
374 $updraftplus->uploaded_file($file, true);
375 }
376 }
377 $this->prune_retained_backups(array("none" => array(null, null)));
378 } else {
379 $updraftplus->log("Beginning dispatch of backup to remote ($service)");
380 $sarray = array();
381 foreach ($backup_array as $bind => $file) {
382 if ($updraftplus->is_uploaded($file, $service)) {
383 $updraftplus->log("Already uploaded to $service: $file");
384 } else {
385 $sarray[$bind] = $file;
386 }
387 }
388 $objname = "UpdraftPlus_BackupModule_$service";
389 if (class_exists($objname)) {
390 $remote_obj = new $objname;
391 if (count($sarray)>0) {
392 $pass_to_prune = $remote_obj->backup($sarray);
393 $do_prune[$service] = array($remote_obj, $pass_to_prune);
394 } else {
395 // We still need to make sure that prune is run on this remote storage method, even if all entities were previously uploaded
396 $do_prune[$service] = array($remote_obj, null);
397 }
398 } else {
399 $updraftplus->log("Unexpected error: no class '$objname' was found ($method_include)");
400 $updraftplus->log(sprintf(__("Unexpected error: no class '%s' was found (your UpdraftPlus installation seems broken - try re-installing)", 'updraftplus'), $objname), 'error');
401 }
402 }
403 }
404
405 if (!empty($do_prune)) $this->prune_retained_backups($do_prune);
406
407 $updraftplus->register_wp_http_option_hooks(false);
408
409 }
410
411 private function group_backups($backup_history) {
412 return array(array('sets' => $backup_history, 'process_order' => 'keep_newest'));
413 }
414
415 /**
416 * Logs a message; with the message being logged to the database also only if that has not been done in the last 3 seconds. Useful for better overall performance on slow database servers with rapid logging.
417 *
418 * @uses UpdraftPlus::log()
419 *
420 * @param String $message - the message to log
421 * @param String $level - the log level
422 */
423 private function log_with_db_occasionally($message, $level = 'notice') {
424 global $updraftplus;
425 static $last_db = false;
426
427 if (time() > $last_db + 3) {
428 $last_db = time();
429 $skip_dblog = false;
430 } else {
431 $skip_dblog = true;
432 }
433
434 return $updraftplus->log($message, $level, false, $skip_dblog);
435 }
436
437 /**
438 * Prunes historical backups, according to the user's settings
439 *
440 * @param Array $services - a list of services to prune on. This must be an array (i.e. it is not flexible like some other places)
441 *
442 * @return void
443 */
444 public function prune_retained_backups($services) {
445
446 global $updraftplus, $wpdb;
447
448 if ($updraftplus->jobdata_get('remotesend_info') != '') {
449 $updraftplus->log("Prune old backups from local store: skipping, as this was a remote send operation");
450 return;
451 }
452
453 if (method_exists($wpdb, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
454 if (!$wpdb->check_connection(false)) {
455 $updraftplus->reschedule(60);
456 $updraftplus->log("It seems the database went away; scheduling a resumption and terminating for now");
457 $updraftplus->record_still_alive();
458 die;
459 }
460 }
461
462 // If they turned off deletion on local backups, then there is nothing to do
463 if (0 == UpdraftPlus_Options::get_updraft_option('updraft_delete_local') && 1 == count($services) && in_array('none', $services)) {
464 $updraftplus->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
465 return;
466 }
467
468 // $updraftplus->jobdata_set('jobstatus', 'pruning');
469 // $updraftplus->jobdata_set('prune', 'begun');
470 call_user_func_array(array($updraftplus, 'jobdata_set_multi'), array('jobstatus', 'pruning', 'prune', 'begun'));
471
472 // Number of backups to retain - files
473 $updraft_retain = UpdraftPlus_Options::get_updraft_option('updraft_retain', 2);
474 $updraft_retain = is_numeric($updraft_retain) ? $updraft_retain : 1;
475
476 // Number of backups to retain - db
477 $updraft_retain_db = UpdraftPlus_Options::get_updraft_option('updraft_retain_db', $updraft_retain);
478 $updraft_retain_db = is_numeric($updraft_retain_db) ? $updraft_retain_db : 1;
479
480 $updraftplus->log("Retain: beginning examination of existing backup sets; user setting: retain_files=$updraft_retain, retain_db=$updraft_retain_db");
481
482 // Returns an array, most recent first, of backup sets
483 $backup_history = UpdraftPlus_Backup_History::get_history();
484 $db_backups_found = 0;
485 $file_backups_found = 0;
486
487 $ignored_because_imported = array();
488
489 // Remove non-native (imported) backups, which are neither counted nor pruned. It's neater to do these in advance, and log only one line.
490 $functional_backup_history = $backup_history;
491 foreach ($functional_backup_history as $backup_time => $backup_to_examine) {
492 if (isset($backup_to_examine['native']) && false == $backup_to_examine['native']) {
493 $ignored_because_imported[] = $backup_time;
494 unset($functional_backup_history[$backup_time]);
495 }
496 }
497 if (!empty($ignored_because_imported)) {
498 $updraftplus->log("These backup set(s) were imported from a remote location, so will not be counted or pruned. Skipping: ".implode(', ', $ignored_because_imported));
499 }
500
501 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
502
503 $database_backups_found = array();
504
505 $file_entities_backups_found = array();
506 foreach ($backupable_entities as $entity => $info) {
507 $file_entities_backups_found[$entity] = 0;
508 }
509
510 if (false === ($backup_db_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'db'))) {
511 $backup_db_groups = $this->group_backups($functional_backup_history);
512 }
513 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (db): ".count($backup_db_groups));
514
515 $started_main_prune_loop_at = time();
516
517 foreach ($backup_db_groups as $group_id => $group) {
518
519 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
520
521 if (empty($group['sets']) || !is_array($group['sets'])) continue;
522 $sets = $group['sets'];
523
524 // Sort the groups into the desired "keep this first" order
525 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
526 if ('keep_oldest' == $process_order) ksort($sets);
527
528 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
529
530 foreach ($sets as $backup_datestamp => $backup_to_examine) {
531
532 $files_to_prune = array();
533 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
534
535 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
536 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
537 $this->log_with_db_occasionally(sprintf("Examining (for databases) backup set with group_id=$group_id, nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
538
539 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
540 $is_autobackup = !empty($backup_to_examine['autobackup']);
541
542 $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
543
544 $any_deleted_via_filter_yet = false;
545
546 // Databases
547 foreach ($backup_to_examine as $key => $data) {
548 if ('db' != strtolower(substr($key, 0, 2)) || '-size' == substr($key, -5, 5)) continue;
549
550 if (empty($database_backups_found[$key])) $database_backups_found[$key] = 0;
551
552 if ($nonce == $updraftplus->nonce) {
553 $this->log_with_db_occasionally("This backup set is the backup set just made, so will not be deleted.");
554 $database_backups_found[$key]++;
555 continue;
556 }
557
558 if ($is_autobackup) {
559 if ($any_deleted_via_filter_yet) {
560 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
561 $prune_it = true;
562 } elseif ($database_backups_found[$key] < $updraft_retain_db) {
563 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
564 continue;
565 } else {
566 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
567 $prune_it = true;
568 }
569 } else {
570 $prune_it = false;
571 }
572
573 if ($remote_sent) {
574 $prune_it = true;
575 $this->log_with_db_occasionally("$backup_datestamp: $key: was sent to remote site; will remove from local record (only)");
576 }
577
578 // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
579 $prune_it_before_filter = $prune_it;
580
581 if (!$is_autobackup) $prune_it = apply_filters('updraftplus_prune_or_not', $prune_it, 'db', $backup_datestamp, $key, $database_backups_found[$key], $rule, $group_id);
582
583 // Apply the final retention limit list (do not increase the 'retained' counter before seeing if the backup is being pruned for some other reason)
584 if (!$prune_it && !$is_autobackup) {
585
586 if ($database_backups_found[$key] + 1 > $updraft_retain_db) {
587 $prune_it = true;
588
589 $fname = is_string($data) ? $data : $data[0];
590 $this->log_with_db_occasionally("$backup_datestamp: $key: this set includes a database (".$fname."); db count is now ".$database_backups_found[$key]);
591
592 $this->log_with_db_occasionally("$backup_datestamp: $key: over retain limit ($updraft_retain_db); will delete this database");
593 }
594
595 }
596
597 if ($prune_it) {
598 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
599
600 if (!empty($data)) {
601 $size_key = $key.'-size';
602 $size = isset($backup_to_examine[$size_key]) ? $backup_to_examine[$size_key] : null;
603 foreach ($services as $service => $sd) {
604 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
605 }
606 }
607 unset($backup_to_examine[$key]);
608 $updraftplus->record_still_alive();
609 } elseif (!$is_autobackup) {
610 $database_backups_found[$key]++;
611 }
612
613 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history);
614 if (empty($backup_to_examine)) {
615 unset($functional_backup_history[$backup_datestamp]);
616 unset($backup_history[$backup_datestamp]);
617 $this->maybe_save_backup_history_and_reschedule($backup_history);
618 } else {
619 $functional_backup_history[$backup_datestamp] = $backup_to_examine;
620 $backup_history[$backup_datestamp] = $backup_to_examine;
621 }
622 }
623 }
624 }
625
626 if (false === ($backup_files_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'files'))) {
627 $backup_files_groups = $this->group_backups($functional_backup_history);
628 }
629
630 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (files): ".count($backup_files_groups));
631
632 // Now again - this time for the files
633 foreach ($backup_files_groups as $group_id => $group) {
634
635 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
636
637 if (empty($group['sets']) || !is_array($group['sets'])) continue;
638 $sets = $group['sets'];
639
640 // Sort the groups into the desired "keep this first" order
641 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
642 // Youngest - i.e. smallest epoch - first
643 if ('keep_oldest' == $process_order) ksort($sets);
644
645 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
646
647 foreach ($sets as $backup_datestamp => $backup_to_examine) {
648
649 $files_to_prune = array();
650 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
651
652 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
653 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
654 $this->log_with_db_occasionally(sprintf("Examining (for files) backup set with nonce=%s, datestamp=%s (%s)", $nonce, $backup_datestamp, gmdate('M d Y H:i:s', $backup_datestamp)));
655
656 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
657 $is_autobackup = !empty($backup_to_examine['autobackup']);
658
659 $remote_sent = (!empty($backup_to_examine['service']) && ((is_array($backup_to_examine['service']) && in_array('remotesend', $backup_to_examine['service'])) || 'remotesend' === $backup_to_examine['service'])) ? true : false;
660
661 $any_deleted_via_filter_yet = false;
662
663 $file_sizes = array();
664
665 // Files
666 foreach ($backupable_entities as $entity => $info) {
667 if (!empty($backup_to_examine[$entity])) {
668
669 // This should only be able to happen if you import backups with a future timestamp
670 if ($nonce == $updraftplus->nonce) {
671 $updraftplus->log("This backup set is the backup set just made, so will not be deleted.");
672 $file_entities_backups_found[$entity]++;
673 continue;
674 }
675
676 if ($is_autobackup) {
677 if ($any_deleted_via_filter_yet) {
678 $this->log_with_db_occasionally("This backup set was an automatic backup, but we have previously deleted a backup due to a limit, so it will be pruned (but not counted towards numerical limits).");
679 $prune_it = true;
680 } elseif ($file_entities_backups_found[$entity] < $updraft_retain) {
681 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have not yet reached any retain limits, so it will not be counted or pruned. Skipping.");
682 continue;
683 } else {
684 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an automatic backup, and we have already reached retain limits, so it will be pruned.");
685 $prune_it = true;
686 }
687 } else {
688 $prune_it = false;
689 }
690
691 if ($remote_sent) {
692 $prune_it = true;
693 }
694
695 // All non-auto backups must be run through this filter (in date order) regardless of the current state of $prune_it - so that filters are able to track state.
696 $prune_it_before_filter = $prune_it;
697 if (!$is_autobackup) $prune_it = apply_filters('updraftplus_prune_or_not', $prune_it, 'files', $backup_datestamp, $entity, $file_entities_backups_found[$entity], $rule, $group_id);
698
699 // The "more than maximum to keep?" counter should not be increased until we actually know that the set is being kept. Before verison 1.11.22, we checked this before running the filter, which resulted in the counter being increased for sets that got pruned via the filter (i.e. not kept) - and too many backups were thus deleted
700 if (!$prune_it && !$is_autobackup) {
701 if ($file_entities_backups_found[$entity] >= $updraft_retain) {
702 $this->log_with_db_occasionally("$entity: over retain limit ($updraft_retain); will delete this file entity");
703 $prune_it = true;
704 }
705 }
706
707 if ($prune_it) {
708 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
709 $prune_this = $backup_to_examine[$entity];
710 if (is_string($prune_this)) $prune_this = array($prune_this);
711
712 foreach ($prune_this as $k => $prune_file) {
713 if ($remote_sent) {
714 $updraftplus->log("$entity: $backup_datestamp: was sent to remote site; will remove from local record (only)");
715 }
716 $size_key = (0 == $k) ? $entity.'-size' : $entity.$k.'-size';
717 $size = (isset($backup_to_examine[$size_key])) ? $backup_to_examine[$size_key] : null;
718 $files_to_prune[] = $prune_file;
719 $file_sizes[] = $size;
720 }
721 unset($backup_to_examine[$entity]);
722
723 } elseif (!$is_autobackup) {
724 $file_entities_backups_found[$entity]++;
725 }
726 }
727 }
728
729 // Sending an empty array is not itself a problem - except that the remote storage method may not check that before setting up a connection, which can waste time: especially if this is done every time around the loop.
730 if (!empty($files_to_prune)) {
731 // Actually delete the files
732 foreach ($services as $service => $sd) {
733 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], $file_sizes);
734 $updraftplus->record_still_alive();
735 }
736 }
737
738 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history);
739 if (empty($backup_to_examine)) {
740 // unset($functional_backup_history[$backup_datestamp]);
741 unset($backup_history[$backup_datestamp]);
742 $this->maybe_save_backup_history_and_reschedule($backup_history);
743 } else {
744 // $functional_backup_history[$backup_datestamp] = $backup_to_examine;
745 $backup_history[$backup_datestamp] = $backup_to_examine;
746 }
747
748 // Loop over backup sets
749 }
750
751 // Look over backup groups
752 }
753
754 $updraftplus->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
755 UpdraftPlus_Backup_History::save_history($backup_history, false);
756
757 do_action('updraftplus_prune_retained_backups_finished');
758
759 $updraftplus->jobdata_set('prune', 'finished');
760
761 }
762
763 /**
764 * The purpose of this is to save the backup history periodically - for the benefit of setups where the pruning takes longer than the total allow run time (e.g. if the network communications to the remote storage have delays in, and there are a lot of sets to scan)
765 *
766 * @param Array $backup_history - the backup history to possible save
767 */
768 private function maybe_save_backup_history_and_reschedule($backup_history) {
769 static $last_saved_at = 0;
770 if (!$last_saved_at) $last_saved_at = time();
771 if (time() - $last_saved_at >= 10) {
772 global $updraftplus;
773 $updraftplus->log("Retain: saving new backup history, because at least 10 seconds have passed since the last save (sets now: ".count($backup_history).")");
774 UpdraftPlus_Backup_History::save_history($backup_history, false);
775 $updraftplus->something_useful_happened();
776 $last_saved_at = time();
777 }
778 }
779
780 private function remove_backup_set_if_empty($backup_to_examine, $backup_datestamp, $backupable_entities, $backup_history) {
781
782 global $updraftplus;
783
784 // Get new result, post-deletion; anything left in this set?
785 $contains_files = 0;
786 foreach ($backupable_entities as $entity => $info) {
787 if (isset($backup_to_examine[$entity])) {
788 $contains_files = 1;
789 break;
790 }
791 }
792
793 $contains_db = 0;
794 foreach ($backup_to_examine as $key => $data) {
795 if ('db' == strtolower(substr($key, 0, 2)) && '-size' != substr($key, -5, 5)) {
796 $contains_db = 1;
797 break;
798 }
799 }
800
801 // Delete backup set completely if empty, o/w just remove DB
802 // We search on the four keys which represent data, allowing other keys to be used to track other things
803 if (!$contains_files && !$contains_db) {
804 $updraftplus->log("This backup set is now empty; will remove from history");
805 if (isset($backup_to_examine['nonce'])) {
806 $fullpath = $this->updraft_dir."/log.".$backup_to_examine['nonce'].".txt";
807 if (is_file($fullpath)) {
808 $updraftplus->log("Deleting log file (log.".$backup_to_examine['nonce'].".txt)");
809 @unlink($fullpath);
810 } else {
811 $updraftplus->log("Corresponding log file (log.".$backup_to_examine['nonce'].".txt) not found - must have already been deleted");
812 }
813 } else {
814 $updraftplus->log("No nonce record found in the backup set, so cannot delete any remaining log file");
815 }
816 // unset($backup_history[$backup_datestamp]);
817 return false;
818 } else {
819 $updraftplus->log("This backup set remains non-empty (f=$contains_files/d=$contains_db); will retain in history");
820 return $backup_to_examine;
821 }
822
823 }
824
825 # $dofiles: An array of files (or a single string for one file)
826 private function prune_file($service, $dofiles, $method_object = null, $object_passback = null, $file_sizes = array()) {
827 global $updraftplus;
828 if (!is_array($dofiles)) $dofiles=array($dofiles);
829
830 if (!apply_filters('updraftplus_prune_file', true, $dofiles, $service, $method_object, $object_passback, $file_sizes)) {
831 $updraftplus->log("Prune: service=$service: skipped via filter");
832 }
833
834 foreach ($dofiles as $i => $dofile) {
835 if (empty($dofile)) continue;
836 $updraftplus->log("Delete file: $dofile, service=$service");
837 $fullpath = $this->updraft_dir.'/'.$dofile;
838 // delete it if it's locally available
839 if (file_exists($fullpath)) {
840 $updraftplus->log("Deleting local copy ($dofile)");
841 @unlink($fullpath);
842 }
843 }
844 // Despatch to the particular method's deletion routine
845 if (!is_null($method_object)) $method_object->delete($dofiles, $object_passback, $file_sizes);
846 }
847
848 # The jobdata is passed in instead of fetched, because the live jobdata may now differ from that which should be reported on (e.g. an incremental run was subsequently scheduled)
849 public function send_results_email($final_message, $jobdata) {
850
851 global $updraftplus;
852
853 $debug_mode = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
854
855 $sendmail_to = $updraftplus->just_one_email(UpdraftPlus_Options::get_updraft_option('updraft_email'));
856 if (is_string($sendmail_to)) $sendmail_to = array($sendmail_to);
857
858 $backup_files =$jobdata['backup_files'];
859 $backup_db = $jobdata['backup_database'];
860
861 if (is_array($backup_db)) $backup_db = $backup_db['wp'];
862 if (is_array($backup_db)) $backup_db = $backup_db['status'];
863
864 $backup_type = ('backup' == $jobdata['job_type']) ? __('Full backup', 'updraftplus') : __('Incremental', 'updraftplus');
865
866 $was_aborted = !empty($jobdata['aborted']);
867
868 if ($was_aborted) {
869 $backup_contains = __('The backup was aborted by the user', 'updraftplus');
870 } elseif ('finished' == $backup_files && ('finished' == $backup_db || 'encrypted' == $backup_db)) {
871 $backup_contains = __("Files and database", 'updraftplus')." ($backup_type)";
872 } elseif ('finished' == $backup_files) {
873 $backup_contains = ($backup_db == "begun") ? __("Files (database backup has not completed)", 'updraftplus') : __("Files only (database was not part of this particular schedule)", 'updraftplus');
874 $backup_contains .= " ($backup_type)";
875 } elseif ($backup_db == 'finished' || $backup_db == 'encrypted') {
876 $backup_contains = ($backup_files == "begun") ? __("Database (files backup has not completed)", 'updraftplus') : __("Database only (files were not part of this particular schedule)", 'updraftplus');
877 } else {
878 $updraftplus->log('Unknown/unexpected status: '.serialize($backup_files).'/'.serialize($backup_db));
879 $backup_contains = __("Unknown/unexpected error - please raise a support request", 'updraftplus');
880 }
881
882 $append_log = '';
883 $attachments = array();
884
885 $error_count = 0;
886
887 if ($updraftplus->error_count() > 0) {
888 $append_log .= __('Errors encountered:', 'updraftplus')."\r\n";
889 $attachments[0] = $updraftplus->logfile_name;
890 foreach ($updraftplus->errors as $err) {
891 if (is_wp_error($err)) {
892 foreach ($err->get_error_messages() as $msg) {
893 $append_log .= "* ".rtrim($msg)."\r\n";
894 }
895 } elseif (is_array($err) && 'error' == $err['level']) {
896 $append_log .= "* ".rtrim($err['message'])."\r\n";
897 } elseif (is_string($err)) {
898 $append_log .= "* ".rtrim($err)."\r\n";
899 }
900 $error_count++;
901 }
902 $append_log.="\r\n";
903 }
904 $warnings = (isset($jobdata['warnings'])) ? $jobdata['warnings'] : array();
905 if (is_array($warnings) && count($warnings) >0) {
906 $append_log .= __('Warnings encountered:', 'updraftplus')."\r\n";
907 $attachments[0] = $updraftplus->logfile_name;
908 foreach ($warnings as $err) {
909 $append_log .= "* ".rtrim($err)."\r\n";
910 }
911 $append_log.="\r\n";
912 }
913
914 if ($debug_mode && '' != $updraftplus->logfile_name && !in_array($updraftplus->logfile_name, $attachments)) {
915 $append_log .= "\r\n".__('The log file has been attached to this email.', 'updraftplus');
916 $attachments[0] = $updraftplus->logfile_name;
917 }
918
919 // We have to use the action in order to set the MIME type on the attachment - by default, WordPress just puts application/octet-stream
920
921 $subject = apply_filters('updraft_report_subject', sprintf(__('Backed up: %s', 'updraftplus'), get_bloginfo('name')).' (UpdraftPlus '.$updraftplus->version.') '.get_date_from_gmt(gmdate('Y-m-d H:i:s', time()), 'Y-m-d H:i'), $error_count, count($warnings));
922
923 # The class_exists() check here is a micro-optimization to prevent a possible HTTP call whose results may be disregarded by the filter
924 $feed = '';
925 if (!class_exists('UpdraftPlus_Addon_Reporting') && !defined('UPDRAFTPLUS_NOADS_B') && !defined('UPDRAFTPLUS_NONEWSFEED')) {
926 $updraftplus->log('Fetching RSS news feed');
927 $rss = $updraftplus->get_updraftplus_rssfeed();
928 $updraftplus->log('Fetched RSS news feed; result is a: '.get_class($rss));
929 if (is_a($rss, 'SimplePie')) {
930 $feed .= __('Email reports created by UpdraftPlus (free edition) bring you the latest UpdraftPlus.com news', 'updraftplus')." - ".sprintf(__('read more at %s', 'updraftplus'), 'https://updraftplus.com/news/')."\r\n\r\n";
931 foreach ($rss->get_items(0, 6) as $item) {
932 $feed .= '* ';
933 $feed .= $item->get_title();
934 $feed .= " (".$item->get_date('j F Y').")";
935 #$feed .= ' - '.$item->get_permalink();
936 $feed .= "\r\n";
937 }
938 }
939 $feed .= "\r\n\r\n";
940 }
941
942 $extra_messages = apply_filters('updraftplus_report_extramessages', array());
943 $extra_msg = '';
944 if (is_array($extra_messages)) {
945 foreach ($extra_messages as $msg) {
946 $extra_msg .= '<strong>'.$msg['key'].'</strong>: '.$msg['val']."\r\n";
947 }
948 }
949
950 foreach ($this->remotestorage_extrainfo as $service => $message) {
951 if (!empty($updraftplus->backup_methods[$service])) $extra_msg .= $updraftplus->backup_methods[$service].': '.$message['plain']."\r\n";
952 }
953
954 // Make it available to the filter
955 $jobdata['remotestorage_extrainfo'] = $this->remotestorage_extrainfo;
956
957 if (!class_exists('UpdraftPlus_Notices')) require_once(UPDRAFTPLUS_DIR.'/includes/updraftplus-notices.php');
958 global $updraftplus_notices;
959 $ws_advert = $updraftplus_notices->do_notice(false, 'report-plain', true);
960
961 $body = apply_filters('updraft_report_body',
962 __('Backup of:', 'updraftplus').' '.site_url()."\r\n".
963 "UpdraftPlus ".__('WordPress backup is complete', 'updraftplus').".\r\n".
964 __('Backup contains:', 'updraftplus')." $backup_contains\r\n".
965 __('Latest status:', 'updraftplus').' '.$final_message."\r\n".
966 $extra_msg.
967 "\r\n".
968 $feed.
969 $ws_advert."\r\n".
970 $append_log,
971 $final_message, $backup_contains, $updraftplus->errors, $warnings, $jobdata);
972
973 $this->attachments = apply_filters('updraft_report_attachments', $attachments);
974
975 if (count($this->attachments)>0) add_action('phpmailer_init', array($this, 'phpmailer_init'));
976
977 $attach_size = 0;
978 $unlink_files = array();
979
980 foreach ($this->attachments as $ind => $attach) {
981 if ($attach == $updraftplus->logfile_name && filesize($attach) > 6*1048576) {
982
983 $updraftplus->log("Log file is large (".round(filesize($attach)/1024, 1)." KB): will compress before e-mailing");
984
985 if (!$handle = fopen($attach, "r")) {
986 $updraftplus->log("Error: Failed to open log file for reading: ".$attach);
987 } else {
988 if (!$whandle = gzopen($attach.'.gz', 'w')) {
989 $updraftplus->log("Error: Failed to open log file for reading: ".$attach.".gz");
990 } else {
991 while (false !== ($line = @stream_get_line($handle, 131072, "\n"))) {
992 @gzwrite($whandle, $line."\n");
993 }
994 fclose($handle);
995 gzclose($whandle);
996 $this->attachments[$ind] = $attach.'.gz';
997 $unlink_files[] = $attach.'.gz';
998 }
999 }
1000 }
1001 $attach_size += filesize($this->attachments[$ind]);
1002 }
1003
1004 foreach ($sendmail_to as $ind => $mailto) {
1005
1006 if (false === apply_filters('updraft_report_sendto', true, $mailto, $error_count, count($warnings), $ind)) continue;
1007
1008 foreach (explode(',', $mailto) as $sendmail_addr) {
1009 $updraftplus->log("Sending email ('$backup_contains') report (attachments: ".count($attachments).", size: ".round($attach_size/1024, 1)." KB) to: ".substr($sendmail_addr, 0, 5)."...");
1010 try {
1011 wp_mail(trim($sendmail_addr), $subject, $body, array("X-UpdraftPlus-Backup-ID: ".$updraftplus->nonce));
1012 } catch (Exception $e) {
1013 $updraftplus->log("Exception occurred when sending mail (".get_class($e)."): ".$e->getMessage());
1014 }
1015 }
1016 }
1017
1018 foreach ($unlink_files as $file) @unlink($file);
1019
1020 do_action('updraft_report_finished');
1021 if (count($this->attachments)>0) remove_action('phpmailer_init', array($this, 'phpmailer_init'));
1022
1023 }
1024
1025 // The purpose of this function is to make sure that the options table is put in the database first, then the users table, then the site + blogs tables (if present - multisite), then the usermeta table; and after that the core WP tables - so that when restoring we restore the core tables first
1026 private function backup_db_sorttables($a_arr, $b_arr) {
1027
1028 $a = $a_arr['name'];
1029 $a_table_type = $a_arr['type'];
1030 $b = $b_arr['name'];
1031 $b_table_type = $b_arr['type'];
1032
1033 // Views must always go after tables (since they can depend upon them)
1034 if ('VIEW' == $a_table_type && 'VIEW' != $b_table_type) return 1;
1035 if ('VIEW' == $b_table_type && 'VIEW' != $a_table_type) return -1;
1036
1037 if ('wp' != $this->whichdb) return strcmp($a, $b);
1038
1039 global $updraftplus;
1040 if ($a == $b) return 0;
1041 $our_table_prefix = $this->table_prefix_raw;
1042 if ($a == $our_table_prefix.'options') return -1;
1043 if ($b == $our_table_prefix.'options') return 1;
1044 if ($a == $our_table_prefix.'site') return -1;
1045 if ($b == $our_table_prefix.'site') return 1;
1046 if ($a == $our_table_prefix.'blogs') return -1;
1047 if ($b == $our_table_prefix.'blogs') return 1;
1048 if ($a == $our_table_prefix.'users') return -1;
1049 if ($b == $our_table_prefix.'users') return 1;
1050 if ($a == $our_table_prefix.'usermeta') return -1;
1051 if ($b == $our_table_prefix.'usermeta') return 1;
1052
1053 if (empty($our_table_prefix)) return strcmp($a, $b);
1054
1055 try {
1056 $core_tables = array_merge($this->wpdb_obj->tables, $this->wpdb_obj->global_tables, $this->wpdb_obj->ms_global_tables);
1057 } catch (Exception $e) {
1058 $updraftplus->log($e->getMessage());
1059 }
1060
1061 if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'termmeta', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions');
1062
1063 global $updraftplus;
1064 $na = $updraftplus->str_replace_once($our_table_prefix, '', $a);
1065 $nb = $updraftplus->str_replace_once($our_table_prefix, '', $b);
1066 if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
1067 if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
1068 return strcmp($a, $b);
1069 }
1070
1071 private function log_account_space() {
1072 # Don't waste time if space is huge
1073 if (!empty($this->account_space_oodles)) return;
1074 global $updraftplus;
1075 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
1076 if (is_array($hosting_bytes_free)) {
1077 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
1078 $updraftplus->log(sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %"));
1079 }
1080 }
1081
1082 // Returns the basename up to and including the nonce (but not the entity)
1083 private function get_backup_file_basename_from_time($use_time) {
1084 global $updraftplus;
1085 return 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $use_time), 'Y-m-d-Hi').'_'.$this->blog_name.'_'.$updraftplus->nonce;
1086 }
1087
1088 private function find_existing_zips($dir, $match_nonce) {
1089 $zips = array();
1090 if ($handle = opendir($dir)) {
1091 while (false !== ($entry = readdir($handle))) {
1092 if ($entry != "." && $entry != "..") {
1093 if (preg_match('/^backup_(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})_.*_([0-9a-f]{12})-([\-a-z]+)([0-9]+)?\.zip$/i', $entry, $matches)) {
1094 if ($matches[6] !== $match_nonce) continue;
1095 $timestamp = mktime($matches[4], $matches[5], 0, $matches[2], $matches[3], $matches[1]);
1096 $entity = $matches[7];
1097 $index = empty($matches[8]) ? '0': $matches[8];
1098 $zips[$entity][$index] = array($timestamp, $entry);
1099 }
1100 }
1101 }
1102 }
1103 return $zips;
1104 }
1105
1106 // $files should be an array as returned by find_existing_zips()
1107 private function file_exists($files, $entity, $index = 0) {
1108 if (isset($files[$entity]) && isset($files[$entity][$index])) {
1109 $file = $files[$entity][$index];
1110 // Return the filename
1111 return $file[1];
1112 } else {
1113 return false;
1114 }
1115 }
1116
1117 // This function is resumable
1118 private function backup_dirs($job_status) {
1119
1120 global $updraftplus;
1121
1122 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1123
1124 $use_time = apply_filters('updraftplus_backup_time_thisrun', $updraftplus->backup_time);
1125 $backup_file_basename = $this->get_backup_file_basename_from_time($use_time);
1126
1127 $backup_array = array();
1128
1129 $possible_backups = $updraftplus->get_backupable_file_entities(true);
1130
1131 // Was there a check-in last time? If not, then reduce the amount of data attempted
1132 if ($job_status != 'finished' && $updraftplus->current_resumption >= 2) {
1133
1134 # NOTYET: Possible amendment to original algorithm; not just no check-in, but if the check in was very early (can happen if we get a very early checkin for some trivial operation, then attempt something too big)
1135
1136
1137 // 03-Sep-2015 - came across a case (HS#2052) where there apparently was a check-in 'last time', but no resumption was scheduled because the 'useful_checkin' jobdata was *not* last time - which must indicate dying at a very unfortunate/unlikely point in the code. As a result, the split was not auto-reduced. Consequently, we've added !$updraftplus->newresumption_scheduled as a condition on the first check here (it was already on the second), as if no resumption is scheduled then whatever checkin there was last time was only partial. This was on GoDaddy, for which a number of curious I/O event combinations have been seen in recent months - their platform appears to have some odd behaviour when PHP is killed off.
1138 // 04-Sep-2015 - move the '$updraftplus->current_resumption<=10' check to the inner loop (instead of applying to this whole section), as I see no reason for that restriction (case seen in HS#2064 where it was required on resumption 15)
1139 if (!empty($updraftplus->no_checkin_last_time) || !$updraftplus->newresumption_scheduled) {
1140 // Apr 2015: !$updraftplus->newresumption_scheduled added after seeing a log where there was no activity on resumption 9, and extra resumption 10 then tried the same operation.
1141 if ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2 || !$updraftplus->newresumption_scheduled) {
1142 $this->try_split = true;
1143 } elseif ($updraftplus->current_resumption<=10) {
1144 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
1145 if ((int)$maxzipbatch < 1) $maxzipbatch = 26214400;
1146
1147 $new_maxzipbatch = max(floor($maxzipbatch * 0.75), 20971520);
1148 if ($new_maxzipbatch < $maxzipbatch) {
1149 $updraftplus->log("No check-in was detected on the previous run - as a result, we are reducing the batch amount (old=$maxzipbatch, new=$new_maxzipbatch)");
1150 $updraftplus->jobdata_set('maxzipbatch', $new_maxzipbatch);
1151 $updraftplus->jobdata_set('maxzipbatch_ceiling', $new_maxzipbatch);
1152 }
1153 }
1154 }
1155 }
1156
1157 if($job_status != 'finished' && !$updraftplus->really_is_writable($this->updraft_dir)) {
1158 $updraftplus->log("Backup directory (".$this->updraft_dir.") is not writable, or does not exist");
1159 $updraftplus->log(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'updraftplus'), $this->updraft_dir), 'error');
1160 return array();
1161 }
1162
1163 $this->job_file_entities = $updraftplus->jobdata_get('job_file_entities');
1164 # This is just used for the visual feedback (via the 'substatus' key)
1165 $which_entity = 0;
1166 # e.g. plugins, themes, uploads, others
1167 # $whichdir might be an array (if $youwhat is 'more')
1168
1169 # Returns an array (keyed off the entity) of ($timestamp, $filename) arrays
1170 $existing_zips = $this->find_existing_zips($this->updraft_dir, $updraftplus->nonce);
1171
1172 foreach ($possible_backups as $youwhat => $whichdir) {
1173
1174 if (isset($this->job_file_entities[$youwhat])) {
1175
1176 $index = (int)$this->job_file_entities[$youwhat]['index'];
1177 if (empty($index)) $index=0;
1178 $indextext = (0 == $index) ? '' : (1+$index);
1179
1180 $zip_file = $this->updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.$indextext.'.zip';
1181
1182 # Split needed?
1183 $split_every = max((int)$updraftplus->jobdata_get('split_every'), 250);
1184 //if (file_exists($zip_file) && filesize($zip_file) > $split_every*1048576) {
1185 if (false != ($existing_file = $this->file_exists($existing_zips, $youwhat, $index)) && filesize($this->updraft_dir.'/'.$existing_file) > $split_every*1048576) {
1186 $index++;
1187 $this->job_file_entities[$youwhat]['index'] = $index;
1188 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1189 }
1190
1191 // Populate prior parts of array, if we're on a subsequent zip file
1192 if ($index > 0) {
1193 for ($i=0; $i<$index; $i++) {
1194 $itext = (0 == $i) ? '' : ($i+1);
1195 // Get the previously-stored filename if possible (which should be always); failing that, base it on the current run
1196
1197 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$i])) ? $this->backup_files_array[$youwhat][$i] : $backup_file_basename.'-'.$youwhat.$itext.'.zip';
1198
1199 $backup_array[$youwhat][$i] = $zip_file;
1200 $z = $this->updraft_dir.'/'.$zip_file;
1201 $itext = (0 == $i) ? '' : $i;
1202
1203 $fs_key = $youwhat.$itext.'-size';
1204 if (file_exists($z)) {
1205 $backup_array[$fs_key] = filesize($z);
1206 } elseif (isset($this->backup_files_array[$fs_key])) {
1207 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1208 }
1209 }
1210 }
1211
1212 // I am not certain that all the conditions in here are possible. But there's no harm.
1213 if ('finished' == $job_status) {
1214 // Add the final part of the array
1215 if ($index > 0) {
1216 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][$index])) ? $this->backup_files_array[$youwhat][$index] : $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1217
1218 //$fbase = $backup_file_basename.'-'.$youwhat.($index+1).'.zip';
1219 $z = $this->updraft_dir.'/'.$zip_file;
1220 $fs_key = $youwhat.$index.'-size';
1221 if (file_exists($z)) {
1222 $backup_array[$youwhat][$index] = $fbase;
1223 $backup_array[$fs_key] = filesize($z);
1224 } elseif (isset($this->backup_files_array[$fs_key])) {
1225 $backup_array[$youwhat][$index] = $fbase;
1226 $backup_array[$fs_key] = $this->backup_files_array[$fskey];
1227 }
1228 } else {
1229 $zip_file = (isset($this->backup_files_array[$youwhat]) && isset($this->backup_files_array[$youwhat][0])) ? $this->backup_files_array[$youwhat][0] : $backup_file_basename.'-'.$youwhat.'.zip';
1230
1231 $backup_array[$youwhat] = $zip_file;
1232 $fs_key=$youwhat.'-size';
1233
1234 if (file_exists($zip_file)) {
1235 $backup_array[$fs_key] = filesize($zip_file);
1236 } elseif (isset($this->backup_files_array[$fs_key])) {
1237 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1238 }
1239 }
1240 } else {
1241
1242 $which_entity++;
1243 $updraftplus->jobdata_set('filecreating_substatus', array('e' => $youwhat, 'i' => $which_entity, 't' => count($this->job_file_entities)));
1244
1245 if ('others' == $youwhat) $updraftplus->log("Beginning backup of other directories found in the content directory (index: $index)");
1246
1247 # Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
1248 $created = apply_filters('updraftplus_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename, $index);
1249 # If the filter did not lead to something being created, then use the default method
1250 if ($created === $whichdir) {
1251
1252 // http://www.phpconcept.net/pclzip/user-guide/53
1253 /* First parameter to create is:
1254 An array of filenames or dirnames,
1255 or
1256 A string containing the filename or a dirname,
1257 or
1258 A string containing a list of filename or dirname separated by a comma.
1259 */
1260
1261 if ('others' == $youwhat) {
1262 $dirlist = $updraftplus->backup_others_dirlist(true);
1263 } elseif ('uploads' == $youwhat) {
1264 $dirlist = $updraftplus->backup_uploads_dirlist(true);
1265 } else {
1266 $dirlist = $whichdir;
1267 if (is_array($dirlist)) $dirlist=array_shift($dirlist);
1268 }
1269
1270 if (count($dirlist)>0) {
1271 $created = $this->create_zip($dirlist, $youwhat, $backup_file_basename, $index);
1272 # Now, store the results
1273 if (!is_string($created) && !is_array($created)) $updraftplus->log("$youwhat: create_zip returned an error");
1274 } else {
1275 $updraftplus->log("No backup of $youwhat: there was nothing found to back up");
1276 }
1277 }
1278
1279 if ($created != $whichdir && (is_string($created) || is_array($created))) {
1280 if (is_string($created)) $created=array($created);
1281 foreach ($created as $findex => $fname) {
1282 $backup_array[$youwhat][$index] = $fname;
1283 $itext = ($index == 0) ? '' : $index;
1284 $index++;
1285 $backup_array[$youwhat.$itext.'-size'] = filesize($this->updraft_dir.'/'.$fname);
1286 }
1287 }
1288
1289 $this->job_file_entities[$youwhat]['index'] = $this->index;
1290 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1291
1292 }
1293 } else {
1294 $updraftplus->log("No backup of $youwhat: excluded by user's options");
1295 }
1296 }
1297
1298 return $backup_array;
1299 }
1300
1301 // This uses a saved status indicator; 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 saved status indicator just helps save resources.
1302 public function resumable_backup_of_files($resumption_no) {
1303 global $updraftplus;
1304 // Backup directories and return a numerically indexed array of file paths to the backup files
1305 $bfiles_status = $updraftplus->jobdata_get('backup_files');
1306 $this->backup_files_array = $updraftplus->jobdata_get('backup_files_array');;
1307 if (!is_array($this->backup_files_array)) $this->backup_files_array = array();
1308 if ('finished' == $bfiles_status) {
1309 $updraftplus->log("Creation of backups of directories: already finished");
1310 # Check for recent activity
1311 foreach ($this->backup_files_array as $files) {
1312 if (!is_array($files)) $files=array($files);
1313 foreach ($files as $file) $updraftplus->check_recent_modification($this->updraft_dir.'/'.$file);
1314 }
1315 } elseif ('begun' == $bfiles_status) {
1316 $this->first_run = apply_filters('updraftplus_filerun_firstrun', 0);
1317 if ($resumption_no > $this->first_run) {
1318 $updraftplus->log("Creation of backups of directories: had begun; will resume");
1319 } else {
1320 $updraftplus->log("Creation of backups of directories: beginning");
1321 }
1322 $updraftplus->jobdata_set('jobstatus', 'filescreating');
1323 $this->backup_files_array = $this->backup_dirs($bfiles_status);
1324 $updraftplus->jobdata_set('backup_files_array', $this->backup_files_array);
1325 $updraftplus->jobdata_set('backup_files', 'finished');
1326 $updraftplus->jobdata_set('jobstatus', 'filescreated');
1327 } else {
1328 # This is not necessarily a backup run which is meant to contain files at all
1329 $updraftplus->log('This backup run is not intended for files - skipping');
1330 return array();
1331 }
1332
1333 /*
1334 // DOES NOT WORK: there is no crash-safe way to do this here - have to be renamed at cloud-upload time instead
1335 $new_backup_array = array();
1336 foreach ($backup_array as $entity => $files) {
1337 if (!is_array($files)) $files=array($files);
1338 $outof = count($files);
1339 foreach ($files as $ind => $file) {
1340 $nval = $file;
1341 if (preg_match('/^(backup_[\-0-9]{15}_.*_[0-9a-f]{12}-[\-a-z]+)([0-9]+)?\.zip$/i', $file, $matches)) {
1342 $num = max((int)$matches[2],1);
1343 $new = $matches[1].$num.'of'.$outof.'.zip';
1344 if (file_exists($this->updraft_dir.'/'.$file)) {
1345 if (@rename($this->updraft_dir.'/'.$file, $this->updraft_dir.'/'.$new)) {
1346 $updraftplus->log(sprintf("Renaming: %s to %s", $file, $new));
1347 $nval = $new;
1348 }
1349 } elseif (file_exists($this->updraft_dir.'/'.$new)) {
1350 $nval = $new;
1351 }
1352 }
1353 $new_backup_array[$entity][$ind] = $nval;
1354 }
1355 }
1356 */
1357 return $this->backup_files_array;
1358 }
1359
1360 /* This function is resumable, using the following method:
1361 - Each table is written out to ($final_filename).table.tmp
1362 - When the writing finishes, it is renamed to ($final_filename).table
1363 - When all tables are finished, they are concatenated into the final file
1364 */
1365 // dbinfo is only used when whichdb != 'wp'; and the keys should be: user, pass, name, host, prefix
1366 public function backup_db($already_done = 'begun', $whichdb = 'wp', $dbinfo = array()) {
1367
1368 global $updraftplus, $wpdb;
1369
1370 $this->whichdb = $whichdb;
1371 $this->whichdb_suffix = ('wp' == $whichdb) ? '' : $whichdb;
1372
1373 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1374 if (!$updraftplus->opened_log_time) $updraftplus->logfile_open($updraftplus->nonce);
1375
1376 if ('wp' == $this->whichdb) {
1377 $this->wpdb_obj = $wpdb;
1378 # The table prefix after being filtered - i.e. what filters what we'll actually back up
1379 $this->table_prefix = $updraftplus->get_table_prefix(true);
1380 # The unfiltered table prefix - i.e. the real prefix that things are relative to
1381 $this->table_prefix_raw = $updraftplus->get_table_prefix(false);
1382 $dbinfo['host'] = DB_HOST;
1383 $dbinfo['name'] = DB_NAME;
1384 $dbinfo['user'] = DB_USER;
1385 $dbinfo['pass'] = DB_PASSWORD;
1386 } else {
1387 if (!is_array($dbinfo) || empty($dbinfo['host'])) return false;
1388 # The methods that we may use: check_connection (WP>=3.9), get_results, get_row, query
1389 $this->wpdb_obj = new UpdraftPlus_WPDB_OtherDB($dbinfo['user'], $dbinfo['pass'], $dbinfo['name'], $dbinfo['host']);
1390 if (!empty($this->wpdb_obj->error)) {
1391 $updraftplus->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : database connection attempt failed');
1392 $updraftplus->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : '.__('database connection attempt failed.', 'updraftplus').' '.__('Connection failed: check your access details, that the database server is up, and that the network connection is not firewalled.', 'updraftplus'), 'error');
1393 return $updraftplus->log_wp_error($this->wpdb_obj->error);
1394 }
1395 $this->table_prefix = $dbinfo['prefix'];
1396 $this->table_prefix_raw = $dbinfo['prefix'];
1397 }
1398
1399 $this->dbinfo = $dbinfo;
1400
1401 $errors = 0;
1402
1403 $use_time = apply_filters('updraftplus_backup_time_thisrun', $updraftplus->backup_time);
1404 $file_base = $this->get_backup_file_basename_from_time($use_time);
1405 $backup_file_base = $this->updraft_dir.'/'.$file_base;
1406
1407 if ('finished' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz';
1408 if ('encrypted' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz.crypt';
1409
1410 $updraftplus->jobdata_set('jobstatus', 'dbcreating'.$this->whichdb_suffix);
1411
1412 $binsqldump = $updraftplus->find_working_sqldump();
1413
1414 $total_tables = 0;
1415
1416 # 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
1417 if ('wp' == $whichdb && method_exists($this->wpdb_obj, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
1418 if (!$this->wpdb_obj->check_connection(false)) {
1419 $updraftplus->reschedule(60);
1420 $updraftplus->log("It seems the database went away; scheduling a resumption and terminating for now");
1421 $updraftplus->record_still_alive();
1422 die;
1423 }
1424 }
1425
1426 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
1427 $all_tables = $this->wpdb_obj->get_results("SHOW FULL TABLES", ARRAY_N);
1428
1429 if (empty($all_tables) && !empty($this->wpdb_obj->last_error)) {
1430 $all_tables = $this->wpdb_obj->get_results("SHOW TABLES", ARRAY_N);
1431 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
1432 } else {
1433 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
1434 }
1435
1436 # If this is not the WP database, then we do not consider it a fatal error if there are no tables
1437 if ('wp' == $whichdb && 0 == count($all_tables)) {
1438 $extra = ($updraftplus->newresumption_scheduled) ? ' - '.__('please wait for the rescheduled attempt', 'updraftplus') : '';
1439 $updraftplus->log("Error: No WordPress database tables found (SHOW TABLES returned nothing)".$extra);
1440 $updraftplus->log(__("No database tables found", 'updraftplus').$extra, 'error');
1441 die;
1442 }
1443
1444 // Put the options table first
1445 usort($all_tables, array($this, 'backup_db_sorttables'));
1446
1447 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
1448
1449 if (!$updraftplus->really_is_writable($this->updraft_dir)) {
1450 $updraftplus->log("The backup directory (".$this->updraft_dir.") could not be written to (could be account/disk space full, or wrong permissions).");
1451 $updraftplus->log($this->updraft_dir.": ".__('The backup directory is not writable (or disk space is full) - the database backup is expected to shortly fail.', 'updraftplus'), 'warning');
1452 # Why not just fail now? We saw a bizarre case when the results of really_is_writable() changed during the run.
1453 }
1454
1455 # This check doesn't strictly get all possible duplicates; it's only designed for the case that can happen when moving between deprecated Windows setups and Linux
1456 $this->duplicate_tables_exist = false;
1457 foreach ($all_table_names as $table) {
1458 if (strtolower($table) != $table && in_array(strtolower($table), $all_table_names)) {
1459 $this->duplicate_tables_exist = true;
1460 $updraftplus->log("Tables with names differing only based on case-sensitivity exist in the MySQL database: $table / ".strtolower($table));
1461 }
1462 }
1463 $how_many_tables = count($all_tables);
1464
1465 $stitch_files = array();
1466 $found_options_table = false;
1467 $is_multisite = is_multisite();
1468
1469 foreach ($all_tables as $ti) {
1470
1471 $table = $ti['name'];
1472 $table_type = $ti['type'];
1473
1474 $manyrows_warning = false;
1475 $total_tables++;
1476
1477 // Increase script execution time-limit to 15 min for every table.
1478 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1479 // The table file may already exist if we have produced it on a previous run
1480 $table_file_prefix = $file_base.'-db'.$this->whichdb_suffix.'-table-'.$table.'.table';
1481
1482 if ('wp' == $whichdb && (strtolower($this->table_prefix_raw.'options') == strtolower($table) || ($is_multisite && (strtolower($this->table_prefix_raw.'sitemeta') == strtolower($table) || strtolower($this->table_prefix_raw.'1_options') == strtolower($table))))) $found_options_table = true;
1483
1484 if (file_exists($this->updraft_dir.'/'.$table_file_prefix.'.gz')) {
1485 $stitched = count($stitch_files);
1486 $skip_dblog = (($stitched > 10 && 0 != $stitched % 20) || ($stitched > 100 && 0 != $stitched % 100));
1487 $updraftplus->log("Table $table: corresponding file already exists; moving on", 'notice', false, $skip_dblog);
1488 $stitch_files[] = $table_file_prefix;
1489 } else {
1490 // === is needed, otherwise 'false' matches (i.e. prefix does not match)
1491 if (empty($this->table_prefix) || ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 ) || ($this->duplicate_tables_exist == true && strpos($table, $this->table_prefix) === 0)) {
1492
1493 if (!apply_filters('updraftplus_backup_table', true, $table, $this->table_prefix, $whichdb, $dbinfo)) {
1494 $updraftplus->log("Skipping table (filtered): $table");
1495 if (empty($this->skipped_tables)) $this->skipped_tables = array();
1496
1497 // whichdb could be an int in which case to get the name of the database and the array key use the name from dbinfo
1498 if ('wp' !== $whichdb) {
1499 $key = $dbinfo['name'];
1500 } else {
1501 $key = $whichdb;
1502 }
1503
1504 if (empty($this->skipped_tables[$key])) $this->skipped_tables[$key] = '';
1505 if ('' != $this->skipped_tables[$key]) $this->skipped_tables[$key] .= ',';
1506 $this->skipped_tables[$key] .= $table;
1507
1508 $total_tables--;
1509 } else {
1510
1511 $db_temp_file = $this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz';
1512 $updraftplus->check_recent_modification($db_temp_file);
1513
1514 // Open file, store the handle
1515 $opened = $this->backup_db_open($db_temp_file, true);
1516 if (false === $opened) return false;
1517
1518 // Create the SQL statements
1519 $this->stow("# " . sprintf('Table: %s' ,$updraftplus->backquote($table)) . "\n");
1520 $updraftplus->jobdata_set('dbcreating_substatus', array('t' => $table, 'i' => $total_tables, 'a' => $how_many_tables));
1521
1522 $table_status = $this->wpdb_obj->get_row("SHOW TABLE STATUS WHERE Name='$table'");
1523 if (isset($table_status->Rows)) {
1524 $rows = $table_status->Rows;
1525 $updraftplus->log("Table $table: Total expected rows (approximate): ".$rows);
1526 $this->stow("# Approximate rows expected in table: $rows\n");
1527 if ($rows > UPDRAFTPLUS_WARN_DB_ROWS) {
1528 $manyrows_warning = true;
1529 $updraftplus->log(sprintf(__("Table %s has very many rows (%s) - we hope your web hosting company gives you enough resources to dump out that table in the backup", 'updraftplus'), $table, $rows).' '.__('If not, you will need to either remove data from this table, or contact your hosting company to request more resources.', 'updraftplus'), 'warning', 'manyrows_'.$this->whichdb_suffix.$table);
1530 }
1531 }
1532
1533 // Don't include the job data for any backups - so that when the database is restored, it doesn't continue an apparently incomplete backup
1534 if ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'sitemeta') == strtolower($table))) {
1535 $where = 'meta_key NOT LIKE "updraft_jobdata_%"';
1536 } elseif ('wp' == $this->whichdb && (!empty($this->table_prefix) && strtolower($this->table_prefix.'options') == strtolower($table))) {
1537 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1538 $updraft_jobdata = "'updraft_jobdata_%'";
1539 $site_transient_update = "'_site_transient_update_%'";
1540 } else {
1541 $updraft_jobdata = '"updraft_jobdata_%"';
1542 $site_transient_update = '"_site_transient_update_%"';
1543 }
1544
1545 $where = 'option_name NOT LIKE '.$updraft_jobdata.' AND option_name NOT LIKE '.$site_transient_update.'';
1546 } else {
1547 $where = '';
1548 }
1549
1550 // If no check-in last time, then we could in future try the other method (but - any point in retrying slow method on large tables??)
1551
1552 // New Jul 2014: This attempt to use bindump instead at a lower threshold is quite conservative - only if the last successful run was exactly two resumptions ago - may be useful to expand
1553 $bindump_threshold = (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption == 2)) ? 1000 : 8000;
1554
1555 $bindump = (isset($rows) && ($rows>$bindump_threshold || (defined('UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP') && UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP)) && is_string($binsqldump)) ? $this->backup_table_bindump($binsqldump, $table, $where) : false;
1556 if (true !== $bindump) $this->backup_table($table, $where, 'none', $table_type);
1557
1558 if (!empty($manyrows_warning)) $updraftplus->log_removewarning('manyrows_'.$this->whichdb_suffix.$table);
1559
1560 $this->close();
1561
1562 $updraftplus->log("Table $table: finishing file (${table_file_prefix}.gz - ".round(filesize($this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz')/1024, 1)." KB)", 'notice', false, false);
1563
1564 rename($db_temp_file, $this->updraft_dir.'/'.$table_file_prefix.'.gz');
1565 $updraftplus->something_useful_happened();
1566 $stitch_files[] = $table_file_prefix;
1567 }
1568 } else {
1569 $total_tables--;
1570 $updraftplus->log("Skipping table (lacks our prefix (".$this->table_prefix.")): $table");
1571 }
1572
1573 }
1574 }
1575
1576 if ('wp' == $whichdb) {
1577 if (!$found_options_table) {
1578 if ($is_multisite) {
1579 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('no options or sitemeta table was found', 'updraftplus'), 'warning', 'optstablenotfound');
1580 } else {
1581 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('the options table was not found', 'updraftplus'), 'warning', 'optstablenotfound');
1582 }
1583 $time_this_run = time()-$updraftplus->opened_log_time;
1584 if ($time_this_run > 2000) {
1585 # Have seen this happen; not sure how, but it was apparently deterministic; if the current process had been running for a long time, then apparently all database commands silently failed.
1586 # If we have been running that long, then the resumption may be far off; bring it closer
1587 $updraftplus->reschedule(60);
1588 $updraftplus->log("Have been running very long, and it seems the database went away; scheduling a resumption and terminating for now");
1589 $updraftplus->record_still_alive();
1590 die;
1591 }
1592 } else {
1593 $updraftplus->log_removewarning('optstablenotfound');
1594 }
1595 }
1596
1597 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
1598 $backup_final_file_name = $backup_file_base.'-db'.$this->whichdb_suffix.'.gz';
1599 $time_now = time();
1600 $time_mod = (int)@filemtime($backup_final_file_name);
1601 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
1602 $updraftplus->terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
1603 }
1604 if (file_exists($backup_final_file_name)) {
1605 $updraftplus->log("The final database file ($backup_final_file_name) exists, but was apparently not modified within the last 30 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.");
1606 }
1607
1608 // Finally, stitch the files together
1609 if (!function_exists('gzopen')) {
1610 if (function_exists('gzopen64')) {
1611 $updraftplus->log("PHP function is disabled; abort expected: gzopen - buggy Ubuntu PHP version; try this plugin to help: https://wordpress.org/plugins/wp-ubuntu-gzopen-fix/");
1612 } else {
1613 $updraftplus->log("PHP function is disabled; abort expected: gzopen");
1614 }
1615 }
1616
1617 if (false === $this->backup_db_open($backup_final_file_name, true)) return false;
1618
1619 $this->backup_db_header();
1620
1621 // 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
1622 $unlink_files = array();
1623
1624 $sind = 1;
1625 foreach ($stitch_files as $table_file) {
1626 $updraftplus->log("{$table_file}.gz ($sind/$how_many_tables): adding to final database dump");
1627 if (!$handle = gzopen($this->updraft_dir.'/'.$table_file.'.gz', "r")) {
1628 $updraftplus->log("Error: Failed to open database file for reading: ${table_file}.gz");
1629 $updraftplus->log(__("Failed to open database file for reading:", 'updraftplus').' '.$table_file.'.gz', 'error');
1630 $errors++;
1631 } else {
1632 while ($line = gzgets($handle, 65536)) { $this->stow($line); }
1633 gzclose($handle);
1634 $unlink_files[] = $this->updraft_dir.'/'.$table_file.'.gz';
1635 }
1636 $sind++;
1637 // Came across a database with 7600 tables... adding them all took over 500 seconds; and so when the resumption started up, no activity was detected
1638 if ($sind % 100 == 0) $updraftplus->something_useful_happened();
1639 }
1640
1641 if (defined('DB_CHARSET') && DB_CHARSET) {
1642 $this->stow("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n");
1643 }
1644
1645 $updraftplus->log($file_base.'-db'.$this->whichdb_suffix.'.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024, 1).' KB)');
1646 if (!$this->close()) {
1647 $updraftplus->log('An error occurred whilst closing the final database file');
1648 $updraftplus->log(__('An error occurred whilst closing the final database file', 'updraftplus'), 'error');
1649 $errors++;
1650 }
1651
1652 foreach ($unlink_files as $unlink_file) @unlink($unlink_file);
1653
1654 if ($errors > 0) {
1655 return false;
1656 } else {
1657 // We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
1658 $updraftplus->jobdata_set('jobstatus', 'dbcreated'.$this->whichdb_suffix);
1659
1660 $checksums = $updraftplus->which_checksums();
1661
1662 $checksum_description = '';
1663
1664 foreach ($checksums as $checksum) {
1665
1666 $cksum = hash_file($checksum, $backup_final_file_name);
1667 $updraftplus->jobdata_set($checksum.'-db'.(('wp' == $whichdb) ? '0' : $whichdb.'0'), $cksum);
1668 if ($checksum_description) $checksum_description .= ', ';
1669 $checksum_description .= "$checksum: $cksum";
1670
1671 }
1672
1673 $updraftplus->log("Total database tables backed up: $total_tables (".basename($backup_final_file_name).", size: ".filesize($backup_final_file_name).", $checksum)");
1674
1675 return basename($backup_final_file_name);
1676 }
1677
1678 }
1679
1680 private function backup_table_bindump($potsql, $table_name, $where) {
1681
1682 $microtime = microtime(true);
1683
1684 global $updraftplus;
1685
1686 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
1687 // Can't get binary mysqldump to make this transformation
1688 // $dump_as_table = ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
1689
1690 $pfile = md5(time().rand()).'.tmp';
1691 file_put_contents($this->updraft_dir.'/'.$pfile, "[mysqldump]\npassword=".$this->dbinfo['pass']."\n");
1692
1693 // Note: escapeshellarg() adds quotes around the string
1694 if ($where) $where="--where=".escapeshellarg($where);
1695
1696 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
1697 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $this->updraft_dir))." & ";
1698 } else {
1699 $exec = "cd ".escapeshellarg($this->updraft_dir)."; ";
1700 }
1701
1702 $exec .= "$potsql --defaults-file=$pfile $where --max_allowed_packet=1M --quote-names --add-drop-table --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --user=".escapeshellarg($this->dbinfo['user'])." --host=".escapeshellarg($this->dbinfo['host'])." ".$this->dbinfo['name']." ".escapeshellarg($table_name);
1703
1704 $ret = false;
1705 $any_output = false;
1706 $writes = 0;
1707 $handle = popen($exec, "r");
1708 if ($handle) {
1709 while (!feof($handle)) {
1710 $w = fgets($handle);
1711 if ($w) {
1712 $this->stow($w);
1713 $writes++;
1714 $any_output = true;
1715 }
1716 }
1717 $ret = pclose($handle);
1718 if ($ret != 0) {
1719 $updraftplus->log("Binary mysqldump: error (code: $ret)");
1720 // Keep counter of failures? Change value of binsqldump?
1721 } else {
1722 if ($any_output) {
1723 $updraftplus->log("Table $table_name: binary mysqldump finished (writes: $writes) in ".sprintf("%.02f",max(microtime(true)-$microtime,0.00001))." seconds");
1724 $ret = true;
1725 }
1726 }
1727 } else {
1728 $updraftplus->log("Binary mysqldump error: bindump popen failed");
1729 }
1730
1731 // Clean temporary files
1732 @unlink($this->updraft_dir.'/'.$pfile);
1733
1734 return $ret;
1735
1736 }
1737
1738 /**
1739 * Taken partially from phpMyAdmin and partially from
1740 * Alain Wolf, Zurich - Switzerland
1741 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1742 * Modified by Scott Merrill (http://www.skippy.net/)
1743 * to use the WordPress $wpdb object
1744 * @param String $table
1745 * @param String $where
1746 * @param String $segment
1747 * @param String $table_type
1748 * @return void
1749 */
1750 private function backup_table($table, $where = '', $segment = 'none', $table_type = 'BASE TABLE') {
1751 global $updraftplus;
1752
1753 $microtime = microtime(true);
1754 $total_rows = 0;
1755
1756 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
1757 $dump_as_table = ($this->duplicate_tables_exist == false && stripos($table, $this->table_prefix) === 0 && strpos($table, $this->table_prefix) !== 0) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
1758
1759 $table_structure = $this->wpdb_obj->get_results("DESCRIBE ".$updraftplus->backquote($table));
1760 if (!$table_structure) {
1761 //$updraftplus->log(__('Error getting table details','wp-db-backup') . ": $table", 'error');
1762 return false;
1763 }
1764
1765 if ('none' == $segment|| 0 == $segment) {
1766 // Add SQL statement to drop existing table
1767 $this->stow("\n# Delete any existing table ".$updraftplus->backquote($table)."\n\n");
1768 $this->stow("DROP TABLE IF EXISTS " . $updraftplus->backquote($dump_as_table) . ";\n");
1769
1770 if ('VIEW' == $table_type) {
1771 $this->stow("DROP VIEW IF EXISTS " . $updraftplus->backquote($dump_as_table) . ";\n");
1772 }
1773
1774 // Table structure
1775 // Comment in SQL-file
1776
1777 $description = ('VIEW' == $table_type) ? 'view' : 'table';
1778
1779 $this->stow("\n# Table structure of $description ".$updraftplus->backquote($table)."\n\n");
1780
1781 $create_table = $this->wpdb_obj->get_results("SHOW CREATE TABLE ".$updraftplus->backquote($table), ARRAY_N);
1782 if (false === $create_table) {
1783 $err_msg ='Error with SHOW CREATE TABLE for '.$table;
1784 //$updraftplus->log($err_msg, 'error');
1785 $this->stow("#\n# $err_msg\n#\n");
1786 }
1787 $create_line = $updraftplus->str_lreplace('TYPE=', 'ENGINE=', $create_table[0][1]);
1788
1789 # Remove PAGE_CHECKSUM parameter from MyISAM - was internal, undocumented, later removed (so causes errors on import)
1790 if (preg_match('/ENGINE=([^\s;]+)/', $create_line, $eng_match)) {
1791 $engine = $eng_match[1];
1792 if ('myisam' == strtolower($engine)) {
1793 $create_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $create_line, 1);
1794 }
1795 }
1796
1797 if ($dump_as_table !== $table) $create_line = $updraftplus->str_replace_once($table, $dump_as_table, $create_line);
1798
1799 $this->stow($create_line.' ;');
1800
1801 if (false === $table_structure) {
1802 $err_msg = sprintf("Error getting $description structure of %s", $table);
1803 $this->stow("#\n# $err_msg\n#\n");
1804 }
1805
1806 // Comment in SQL-file
1807 $this->stow("\n\n# ".sprintf("Data contents of $description %s", $updraftplus->backquote($table))."\n\n");
1808
1809 }
1810
1811 # Some tables have optional data, and should be skipped if they do not work
1812 $table_sans_prefix = substr($table, strlen($this->table_prefix_raw));
1813 $data_optional_tables = ('wp' == $this->whichdb) ? apply_filters('updraftplus_data_optional_tables', explode(',', UPDRAFTPLUS_DATA_OPTIONAL_TABLES)) : array();
1814 if (in_array($table_sans_prefix, $data_optional_tables)) {
1815 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2)) {
1816 $updraftplus->log("Table $table: Data skipped (previous attempts failed, and table is marked as non-essential)");
1817 return true;
1818 }
1819 }
1820
1821 // In UpdraftPlus, segment is always 'none'
1822 if ('VIEW' != $table_type && ('none' == $segment|| 0 <= $segment)) {
1823 $defs = array();
1824 $integer_fields = array();
1825 // $table_structure was from "DESCRIBE $table"
1826 foreach ($table_structure as $struct) {
1827 if ((0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint')) ||
1828 (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint')) ) {
1829 $defs[strtolower($struct->Field)] = (null === $struct->Default ) ? 'NULL' : $struct->Default;
1830 $integer_fields[strtolower($struct->Field)] = "1";
1831 }
1832 }
1833
1834 // Experimentation here shows that on large tables (we tested with 180,000 rows) on MyISAM, 1000 makes the table dump out 3x faster than the previous value of 100. After that, the benefit diminishes (increasing to 4000 only saved another 12%)
1835
1836 $increment = 1000;
1837 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 1)) {
1838 # This used to be fixed at 500; but we (after a long time) saw a case that looked like an out-of-memory even at this level. We must be careful about going too low, though - otherwise we increase the risks of timeouts.
1839 $increment = ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2) ? 350 : 500;
1840 }
1841
1842 if ('none' == $segment) {
1843 $row_start = 0;
1844 $row_inc = $increment;
1845 } else {
1846 $row_start = $segment * $increment;
1847 $row_inc = $increment;
1848 }
1849
1850 $search = array("\x00", "\x0a", "\x0d", "\x1a");
1851 $replace = array('\0', '\n', '\r', '\Z');
1852
1853 if ($where) $where = "WHERE $where";
1854
1855 do {
1856 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
1857
1858 $table_data = $this->wpdb_obj->get_results("SELECT * FROM ".$updraftplus->backquote($table)." $where LIMIT {$row_start}, {$row_inc}", ARRAY_A);
1859 $entries = 'INSERT INTO '.$updraftplus->backquote($dump_as_table).' VALUES ';
1860 // \x08\\x09, not required
1861 if($table_data) {
1862 $thisentry = "";
1863 foreach ($table_data as $row) {
1864 $total_rows++;
1865 $values = array();
1866 foreach ($row as $key => $value) {
1867 if (isset($integer_fields[strtolower($key)])) {
1868 // make sure there are no blank spots in the insert syntax,
1869 // yet try to avoid quotation marks around integers
1870 $value = (null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
1871 $values[] = ('' === $value) ? "''" : $value;
1872 } else {
1873 $values[] = (null === $value) ? 'NULL' : "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
1874 }
1875 }
1876 if ($thisentry) $thisentry .= ",\n ";
1877 $thisentry .= '('.implode(', ', $values).')';
1878 // Flush every 512KB
1879 if (strlen($thisentry) > 524288) {
1880 $this->stow(" \n".$entries.$thisentry.';');
1881 $thisentry = "";
1882 }
1883
1884 }
1885 if ($thisentry) $this->stow(" \n".$entries.$thisentry.';');
1886 $row_start += $row_inc;
1887 }
1888 } while (count($table_data) > 0 && 'none' == $segment);
1889 }
1890
1891 if ($segment == 'none' || $segment < 0) {
1892 // Create footer/closing comment in SQL-file
1893 $this->stow("\n# End of data contents of table ".$updraftplus->backquote($table)."\n\n");
1894 }
1895 $updraftplus->log("Table $table: Total rows added: $total_rows in ".sprintf("%.02f", max(microtime(true)-$microtime, 0.00001))." seconds");
1896
1897 }
1898
1899 /*END OF WP-DB-BACKUP BLOCK */
1900
1901 // Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
1902 public function encrypt_file($file) {
1903 global $updraftplus;
1904 $encryption = UpdraftPlus_Options::get_updraft_option('updraft_encryptionphrase');
1905 if (strlen($encryption) > 0) {
1906 $updraftplus->log("Attempting to encrypt backup file");
1907 $result = apply_filters('updraft_encrypt_file', null, $file, $encryption, $this->whichdb, $this->whichdb_suffix);
1908 if (null === $result) {
1909 // $updraftplus->log(sprintf(__("As previously warned (see: %s), encryption is no longer a feature of the free edition of UpdraftPlus", 'updraftplus'), 'https://updraftplus.com/next-updraftplus-release-ready-testing/ + https://updraftplus.com/shop/updraftplus-premium/'), 'warning', 'needpremiumforcrypt');
1910 // UpdraftPlus_Options::update_updraft_option('updraft_encryptionphrase', '');
1911 return basename($file);
1912 }
1913 return $result;
1914 } else {
1915 return basename($file);
1916 }
1917 }
1918
1919 public function close() {
1920 return ($this->dbhandle_isgz) ? gzclose($this->dbhandle) : fclose($this->dbhandle);
1921 }
1922
1923 // Open a file, store its filehandle
1924 public function backup_db_open($file, $allow_gz = true) {
1925 if (function_exists('gzopen') && $allow_gz == true) {
1926 $this->dbhandle = @gzopen($file, 'w');
1927 $this->dbhandle_isgz = true;
1928 } else {
1929 $this->dbhandle = @fopen($file, 'w');
1930 $this->dbhandle_isgz = false;
1931 }
1932 if(false === $this->dbhandle) {
1933 global $updraftplus;
1934 $updraftplus->log("ERROR: $file: Could not open the backup file for writing");
1935 $updraftplus->log($file.": ".__("Could not open the backup file for writing",'updraftplus'), 'error');
1936 }
1937 return $this->dbhandle;
1938 }
1939
1940 /**
1941 * Adds a line to the database backup
1942 *
1943 * @param String $query_line - the line to log
1944 *
1945 * @return Integer|Boolean - the number of octets written, or false for a failure (as returned by gzwrite() / fwrite)
1946 */
1947 public function stow($query_line) {
1948 if ($this->dbhandle_isgz) {
1949 if (false == ($ret = @gzwrite($this->dbhandle, $query_line))) {
1950 //$updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup').' '.$query_line.' '.$php_errormsg, 'error');
1951 }
1952 } else {
1953 if (false == ($ret = @fwrite($this->dbhandle, $query_line))) {
1954 //$updraftplus->log(__('There was an error writing a line to the backup script:','wp-db-backup').' '.$query_line.' '.$php_errormsg, 'error');
1955 }
1956 }
1957 return $ret;
1958 }
1959
1960 private function backup_db_header() {
1961
1962 global $wp_version, $updraftplus;
1963 include(ABSPATH.WPINC.'/version.php');
1964
1965 $mysql_version = $this->wpdb_obj->db_version();
1966 # (function_exists('mysql_get_server_info')) ? @mysql_get_server_info() : '?';
1967
1968 if ('wp' == $this->whichdb) {
1969 $wp_upload_dir = wp_upload_dir();
1970 $this->stow("# WordPress MySQL database backup\n");
1971 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
1972 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
1973 $this->stow("# Backup of: ".untrailingslashit(site_url())."\n");
1974 $this->stow("# Home URL: ".untrailingslashit(home_url())."\n");
1975 $this->stow("# Content URL: ".untrailingslashit(content_url())."\n");
1976 $this->stow("# Uploads URL: ".untrailingslashit($wp_upload_dir['baseurl'])."\n");
1977 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
1978 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
1979 $this->stow("# Site info: multisite=".(is_multisite() ? '1' : '0')."\n");
1980 $this->stow("# Site info: end\n");
1981 } else {
1982 $this->stow("# MySQL database backup (supplementary database ".$this->whichdb.")\n");
1983 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
1984 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
1985 $this->stow("# ".sprintf('External database: (%s)', $this->dbinfo['user'].'@'.$this->dbinfo['host'].'/'.$this->dbinfo['name'])."\n");
1986 $this->stow("# Backup created by: ".untrailingslashit(site_url())."\n");
1987 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
1988 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
1989 }
1990
1991 $label = $updraftplus->jobdata_get('label');
1992 if (!empty($label)) $this->stow("# Label: $label\n");
1993
1994 $this->stow("#\n");
1995 $this->stow("# Generated: ".date("l j. F Y H:i T")."\n");
1996 $this->stow("# Hostname: ".$this->dbinfo['host']."\n");
1997 $this->stow("# Database: ".$updraftplus->backquote($this->dbinfo['name'])."\n");
1998
1999 if (!empty($this->skipped_tables)) {
2000 if ('wp' == $this->whichdb) {
2001 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->whichdb]."\n");
2002 } elseif (isset($this->skipped_tables[$this->dbinfo['name']])) {
2003 $this->stow("# Skipped tables: " . $this->skipped_tables[$this->dbinfo['name']]."\n");
2004 }
2005 }
2006
2007 $this->stow("# --------------------------------------------------------\n");
2008
2009 if (@constant("DB_CHARSET")) {
2010 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
2011 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
2012 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
2013 $this->stow("/*!40101 SET NAMES " . DB_CHARSET . " */;\n");
2014 }
2015 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n\n");
2016 }
2017
2018 public function phpmailer_init($phpmailer) {
2019 global $updraftplus;
2020 if (empty($this->attachments) || !is_array($this->attachments)) return;
2021 foreach ($this->attachments as $attach) {
2022 $mime_type = (preg_match('/\.gz$/', $attach)) ? 'application/x-gzip' : 'text/plain';
2023 try {
2024 $phpmailer->AddAttachment($attach, '', 'base64', $mime_type);
2025 } catch (Exception $e) {
2026 $updraftplus->log("Exception occurred when adding attachment (".get_class($e)."): ".$e->getMessage());
2027 }
2028 }
2029 }
2030
2031 // This function recursively packs the zip, dereferencing symlinks but packing into a single-parent tree for universal unpacking
2032 // $exclude is passed by reference so that we can remove elements as they are matched - saves time checking against already-dealt-with objects
2033 private function makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, &$exclude) {
2034
2035 // $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
2036
2037 global $updraftplus;
2038
2039 // Only BinZip supports symlinks. This means that as a consistent outcome, the only think that can be done with directory symlinks is either a) potentially duplicate the data or b) skip it. Whilst with internal WP entities (e.g. plugins) we definitely want the data, in the case of user-selected directories, we assume the user knew what they were doing when they chose the directory - i.e. we can skip symlink-accessed data that's outside.
2040 if (is_link($fullpath) && is_dir($fullpath) && 'more' == $this->whichone) {
2041 $updraftplus->log("Directory symlink encounted in more files backup: $use_path_when_storing -> ".readlink($fullpath).": skipping");
2042 return true;
2043 }
2044
2045 // De-reference. Important to do to both, because on Windows only doing it to one can make them non-equal, where they were previously equal - something which we later rely upon
2046 $fullpath = realpath($fullpath);
2047 $original_fullpath = realpath($original_fullpath);
2048
2049 // Is the place we've ended up above the original base? That leads to infinite recursion
2050 if (($fullpath !== $original_fullpath && strpos($original_fullpath, $fullpath) === 0) || ($original_fullpath == $fullpath && ((1== $startlevels && strpos($use_path_when_storing, '/') !== false) || (2 == $startlevels && substr_count($use_path_when_storing, '/') >1)))) {
2051 $updraftplus->log("Infinite recursion: symlink led us to $fullpath, which is within $original_fullpath");
2052 $updraftplus->log(__("Infinite recursion: consult your log for more information",'updraftplus'), 'error');
2053 return false;
2054 }
2055
2056 # This is sufficient for the ones we have exclude options for - uploads, others, wpcore
2057 $stripped_storage_path = (1 == $startlevels) ? $use_path_when_storing : substr($use_path_when_storing, strpos($use_path_when_storing, '/') + 1);
2058 if (false !== ($fkey = array_search($stripped_storage_path, $exclude))) {
2059 $updraftplus->log("Entity excluded by configuration option: $stripped_storage_path");
2060 unset($exclude[$fkey]);
2061 return true;
2062 }
2063
2064 $if_altered_since = $this->makezip_if_altered_since;
2065
2066 if (is_file($fullpath)) {
2067 if (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($fullpath)) {
2068 $updraftplus->log("Entity excluded by configuration option (extension): ".basename($fullpath));
2069 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($fullpath)) {
2070 $updraftplus->log("Entity excluded by configuration option (prefix): ".basename($fullpath));
2071 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath)) {
2072 $updraftplus->log("Entity excluded by filter: ".basename($fullpath));
2073 } elseif (is_readable($fullpath)) {
2074 $mtime = filemtime($fullpath);
2075 $key = ($fullpath == $original_fullpath) ? ((2 == $startlevels) ? $use_path_when_storing : $this->basename($fullpath)) : $use_path_when_storing.'/'.$this->basename($fullpath);
2076 if ($mtime > 0 && $mtime > $if_altered_since) {
2077 $this->zipfiles_batched[$fullpath] = $key;
2078 $this->makezip_recursive_batchedbytes += @filesize($fullpath);
2079 #@touch($zipfile);
2080 } else {
2081 $this->zipfiles_skipped_notaltered[$fullpath] = $key;
2082 }
2083 } else {
2084 $updraftplus->log("$fullpath: unreadable file");
2085 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up (check the file permissions and ownership)", 'updraftplus'), $fullpath), 'warning');
2086 }
2087 } elseif (is_dir($fullpath)) {
2088 if ($fullpath == $this->updraft_dir_realpath) {
2089 $updraftplus->log("Skip directory (UpdraftPlus backup directory): $use_path_when_storing");
2090 return true;
2091 }
2092
2093 if (apply_filters('updraftplus_exclude_directory', false, $fullpath)) {
2094 $updraftplus->log("Skip filtered directory: $use_path_when_storing");
2095 return true;
2096 }
2097
2098 if (file_exists($fullpath.'/.donotbackup')) {
2099 $updraftplus->log("Skip directory (.donotbackup file found): $use_path_when_storing");
2100 return true;
2101 }
2102
2103 if (!isset($this->existing_files[$use_path_when_storing])) $this->zipfiles_dirbatched[] = $use_path_when_storing;
2104
2105 if (!$dir_handle = @opendir($fullpath)) {
2106 $updraftplus->log("Failed to open directory: $fullpath");
2107 $updraftplus->log(sprintf(__("Failed to open directory (check the file permissions and ownership): %s",'updraftplus'), $fullpath), 'error');
2108 return false;
2109 }
2110
2111 while (false !== ($e = readdir($dir_handle))) {
2112 if ('.' == $e || '..' == $e) continue;
2113
2114 if (is_link($fullpath.'/'.$e)) {
2115 $deref = realpath($fullpath.'/'.$e);
2116 if (is_file($deref)) {
2117 if (is_readable($deref)) {
2118 $use_stripped = $stripped_storage_path.'/'.$e;
2119 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
2120 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
2121 unset($exclude[$fkey]);
2122 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
2123 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
2124 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
2125 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
2126 } elseif (apply_filters('updraftplus_exclude_file', false, $deref)) {
2127 $updraftplus->log("Entity excluded by filter: $use_stripped");
2128 } else {
2129 $mtime = filemtime($deref);
2130 if ($mtime > 0 && $mtime > $if_altered_since) {
2131 $this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
2132 $this->makezip_recursive_batchedbytes += @filesize($deref);
2133 #@touch($zipfile);
2134 } else {
2135 $this->zipfiles_skipped_notaltered[$deref] = $use_path_when_storing.'/'.$e;
2136 }
2137 }
2138 } else {
2139 $updraftplus->log("$deref: unreadable file");
2140 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
2141 }
2142 } elseif (is_dir($deref)) {
2143
2144 // $link_target = readlink($deref);
2145 // $updraftplus->log("Symbolic link $use_path_when_storing/$e -> $link_target");
2146
2147 $this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2148 }
2149 } elseif (is_file($fullpath.'/'.$e)) {
2150 if (is_readable($fullpath.'/'.$e)) {
2151 $use_stripped = $stripped_storage_path.'/'.$e;
2152 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
2153 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
2154 unset($exclude[$fkey]);
2155 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
2156 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
2157 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
2158 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
2159 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath.'/'.$e)) {
2160 $updraftplus->log("Entity excluded by filter: $use_stripped");
2161 } else {
2162 $mtime = filemtime($fullpath.'/'.$e);
2163 if ($mtime > 0 && $mtime > $if_altered_since) {
2164 $this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2165 $this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);
2166 } else {
2167 $this->zipfiles_skipped_notaltered[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
2168 }
2169 }
2170 } else {
2171 $updraftplus->log("$fullpath/$e: unreadable file");
2172 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up", 'updraftplus'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
2173 }
2174 } elseif (is_dir($fullpath.'/'.$e)) {
2175 if ('wpcore' == $this->whichone && 'updraft' == $e && basename($use_path_when_storing) == 'wp-content' && (!defined('UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS') || !UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS)) {
2176 // This test, of course, won't catch everything - it just aims to make things better by default
2177 $updraftplus->log("Directory excluded for looking like a sub-site's internal UpdraftPlus directory (enable by defining UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS): ".$use_path_when_storing.'/'.$e);
2178 } else {
2179 // no need to add_empty_dir here, as it gets done when we recurse
2180 $this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
2181 }
2182 }
2183 }
2184 closedir($dir_handle);
2185 } else {
2186 $updraftplus->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
2187 }
2188
2189 return true;
2190
2191 }
2192
2193 private function get_excluded_extensions($exclude) {
2194 if (!is_array($exclude)) $exclude = array();
2195 $exclude_extensions = array();
2196 foreach ($exclude as $ex) {
2197 if (preg_match('/^ext:(.+)$/i', $ex, $matches)) {
2198 $exclude_extensions[] = strtolower($matches[1]);
2199 }
2200 }
2201
2202 if (defined('UPDRAFTPLUS_EXCLUDE_EXTENSIONS')) {
2203 $exclude_from_define = explode(',', UPDRAFTPLUS_EXCLUDE_EXTENSIONS);
2204 foreach ($exclude_from_define as $ex) {
2205 $exclude_extensions[] = strtolower(trim($ex));
2206 }
2207 }
2208
2209 return $exclude_extensions;
2210 }
2211
2212 private function get_excluded_prefixes($exclude) {
2213 if (!is_array($exclude)) $exclude = array();
2214 $exclude_prefixes = array();
2215 foreach ($exclude as $pref) {
2216 if (preg_match('/^prefix:(.+)$/i', $pref, $matches)) {
2217 $exclude_prefixes[] = strtolower($matches[1]);
2218 }
2219 }
2220
2221 return $exclude_prefixes;
2222 }
2223
2224 private function is_entity_excluded_by_extension($entity) {
2225 foreach ($this->excluded_extensions as $ext) {
2226 if (!$ext) continue;
2227 $eln = strlen($ext);
2228 if (strtolower(substr($entity, -$eln, $eln)) == $ext) return true;
2229 }
2230 return false;
2231 }
2232
2233 private function is_entity_excluded_by_prefix($entity) {
2234 $entity = basename($entity);
2235 foreach ($this->excluded_prefixes as $pref) {
2236 if (!$pref) continue;
2237 $eln = strlen($pref);
2238 if (strtolower(substr($entity, 0, $eln)) == $pref) return true;
2239 }
2240 return false;
2241 }
2242
2243 private function unserialize_gz_cache_file($file) {
2244 if (!$whandle = gzopen($file, 'r')) return false;
2245 global $updraftplus;
2246 $emptimes = 0;
2247 $var = '';
2248 while (!gzeof($whandle)) {
2249 $bytes = @gzread($whandle, 1048576);
2250 if (empty($bytes)) {
2251 $emptimes++;
2252 $updraftplus->log("Got empty gzread ($emptimes times)");
2253 if ($emptimes>2) return false;
2254 } else {
2255 $var .= $bytes;
2256 }
2257 }
2258 gzclose($whandle);
2259 return unserialize($var);
2260 }
2261
2262 // Caution: $source is allowed to be an array, not just a filename
2263 // $destination is the temporary file (ending in .tmp)
2264 private function make_zipfile($source, $backup_file_basename, $whichone, $retry_on_error = true) {
2265
2266 global $updraftplus;
2267
2268 $original_index = $this->index;
2269
2270 $itext = (empty($this->index)) ? '' : ($this->index+1);
2271 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2272 $destination = $this->updraft_dir.'/'.$destination_base;
2273
2274 // Legacy/redundant
2275 //if (empty($whichone) && is_string($whichone)) $whichone = basename($source);
2276
2277 // When to prefer PCL:
2278 // - We were asked to
2279 // - No zip extension present and no relevant method present
2280 // The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
2281
2282 // We need meta-info about $whichone
2283 $backupable_entities = $updraftplus->get_backupable_file_entities(true, false);
2284 # This is only used by one corner-case in BinZip
2285 #$this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
2286 $this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
2287
2288 $this->existing_files = array();
2289 # Used for tracking compression ratios
2290 $this->existing_files_rawsize = 0;
2291 $this->existing_zipfiles_size = 0;
2292
2293 // Enumerate existing files
2294 // Usually first_linked_index is zero; the exception being with more files, where previous zips' contents are irrelevant
2295 for ($j=$this->first_linked_index; $j<=$this->index; $j++) {
2296 $jtext = ($j == 0) ? '' : ($j+1);
2297 # This is, in a non-obvious way, compatible with filenames which indicate increments
2298 # $j does not need to start at zero; it should start at the index which the current entity split at. However, this is not directly known, and can only be deduced from examining the filenames. And, for other indexes from before the current increment, the searched-for filename won't exist (even if there is no cloud storage). So, this indirectly results in the desired outcome when we start from $j=0.
2299 $examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
2300
2301 // This comes from https://wordpress.org/support/topic/updraftplus-not-moving-all-files-to-remote-server - where it appears that the jobdata's record of the split was done (i.e. database write), but the *earlier* rename of the .tmp file was not done (i.e. I/O lost). i.e. In theory, this should be impossible; but, the sychnronicity apparently cannot be fully relied upon in some setups. The check for the index being one behind is being conservative - there's no inherent reason why it couldn't be done for other indexes.
2302 // Note that in this 'impossible' case, no backup data was being lost - the design still ensures that the on-disk backup is fine. The problem was a gap in the sequence numbering of the zip files, leading to user confusion.
2303 // Other examples of this appear to be in HS#1001 and #1047
2304 if ($j != $this->index && !file_exists($examine_zip)) {
2305 $alt_examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index - 1) ? '.tmp' : '');
2306 if ($alt_examine_zip != $examine_zip && file_exists($alt_examine_zip) && is_readable($alt_examine_zip) && filesize($alt_examine_zip)>0) {
2307 $updraftplus->log("Looked-for zip file not found; but non-zero .tmp zip was, despite not being current index ($j != ".$this->index." - renaming zip (assume previous resumption's IO was lost before kill)");
2308 if (rename($alt_examine_zip, $examine_zip)) {
2309 clearstatcache();
2310 } else {
2311 $updraftplus->log("Rename failed - backup zips likely to not have sequential numbers (does not affect backup integrity, but can cause user confusion)");
2312 }
2313 }
2314 }
2315
2316 // If the file exists, then we should grab its index of files inside, and sizes
2317 // Then, when we come to write a file, we should check if it's already there, and only add if it is not
2318 if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip)>0) {
2319 $this->existing_zipfiles_size += filesize($examine_zip);
2320 $zip = new $this->use_zip_object;
2321 if (true !== $zip->open($examine_zip)) {
2322 $updraftplus->log("Could not open zip file to examine (".$zip->last_error."); will remove: ".basename($examine_zip));
2323 @unlink($examine_zip);
2324 } else {
2325
2326 # Don't put this in the for loop, or the magic __get() method gets called and opens the zip file every time the loop goes round
2327 $numfiles = $zip->numFiles;
2328
2329 for ($i=0; $i < $numfiles; $i++) {
2330 $si = $zip->statIndex($i);
2331 $name = $si['name'];
2332 $this->existing_files[$name] = $si['size'];
2333 $this->existing_files_rawsize += $si['size'];
2334 }
2335
2336 @$zip->close();
2337 }
2338
2339 $updraftplus->log(basename($examine_zip).": Zip file already exists, with ".count($this->existing_files)." files");
2340
2341 # try_split is set if there have been no check-ins recently - or if it needs to be split anyway
2342 if ($j == $this->index) {
2343 if (isset($this->try_split)) {
2344 if (filesize($examine_zip) > 50*1048576) {
2345 # We could, as a future enhancement, save this back to the job data, if we see a case that needs it
2346 $this->zip_split_every = max(
2347 (int)$this->zip_split_every/2,
2348 UPDRAFTPLUS_SPLIT_MIN*1048576,
2349 min(filesize($examine_zip)-1048576, $this->zip_split_every)
2350 );
2351 $updraftplus->jobdata_set('split_every', (int)($this->zip_split_every/1048576));
2352 $updraftplus->log("No check-in on last two runs; bumping index and reducing zip split to: ".round($this->zip_split_every/1048576, 1)." MB");
2353 $do_bump_index = true;
2354 }
2355 unset($this->try_split);
2356 } elseif (filesize($examine_zip) > $this->zip_split_every) {
2357 $updraftplus->log(sprintf("Zip size is at/near split limit (%s MB / %s MB) - bumping index (from: %d)", filesize($examine_zip), round($this->zip_split_every/1048576, 1), $this->index));
2358 $do_bump_index = true;
2359 }
2360 }
2361
2362 } elseif (file_exists($examine_zip)) {
2363 $updraftplus->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
2364 @unlink($examine_zip);
2365 }
2366 }
2367
2368 $this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
2369
2370 $this->zipfiles_added = 0;
2371 $this->zipfiles_added_thisrun = 0;
2372 $this->zipfiles_dirbatched = array();
2373 $this->zipfiles_batched = array();
2374 $this->zipfiles_skipped_notaltered = array();
2375 $this->zipfiles_lastwritetime = time();
2376 $this->zip_basename = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone;
2377
2378 if (!empty($do_bump_index)) $this->bump_index();
2379
2380 $error_occurred = false;
2381
2382 # Store this in its original form
2383 $this->source = $source;
2384
2385 # Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
2386 $this->makezip_recursive_batchedbytes = 0;
2387 if (!is_array($source)) $source=array($source);
2388
2389 $exclude = $updraftplus->get_exclude($whichone);
2390
2391 $files_enumerated_at = $updraftplus->jobdata_get('files_enumerated_at');
2392 if (!is_array($files_enumerated_at)) $files_enumerated_at = array();
2393 $files_enumerated_at[$whichone] = time();
2394 $updraftplus->jobdata_set('files_enumerated_at', $files_enumerated_at);
2395
2396 $this->makezip_if_altered_since = (is_array($this->altered_since)) ? (isset($this->altered_since[$whichone]) ? $this->altered_since[$whichone] : -1) : -1;
2397
2398 // Reset
2399 $got_uploads_from_cache = false;
2400
2401 // Uploads: can/should we get it back from the cache?
2402 // || 'others' == $whichone
2403 if (('uploads' == $whichone || 'others' == $whichone) && function_exists('gzopen') && function_exists('gzread')) {
2404 $use_cache_files = false;
2405 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2406 // Cache file suffixes: -zfd.gz.tmp, -zfb.gz.tmp, -info.tmp, (possible)-zfs.gz.tmp
2407 if (file_exists($cache_file_base.'-zfd.gz.tmp') && file_exists($cache_file_base.'-zfb.gz.tmp') && file_exists($cache_file_base.'-info.tmp')) {
2408 // Cache files exist; shall we use them?
2409 $mtime = filemtime($cache_file_base.'-zfd.gz.tmp');
2410 // Require < 30 minutes old
2411 if (time() - $mtime < 1800) { $use_cache_files = true; }
2412 $any_failures = false;
2413 if ($use_cache_files) {
2414 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfd.gz.tmp');
2415 if (is_array($var)) {
2416 $this->zipfiles_dirbatched = $var;
2417 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfb.gz.tmp');
2418 if (is_array($var)) {
2419 $this->zipfiles_batched = $var;
2420 if (file_exists($cache_file_base.'-info.tmp')) {
2421 $var = maybe_unserialize(file_get_contents($cache_file_base.'-info.tmp'));
2422 if (is_array($var) && isset($var['makezip_recursive_batchedbytes'])) {
2423 $this->makezip_recursive_batchedbytes = $var['makezip_recursive_batchedbytes'];
2424 if (file_exists($cache_file_base.'-zfs.gz.tmp')) {~
2425 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfs.gz.tmp');
2426 if (is_array($var)) {
2427 $this->zipfiles_skipped_notaltered = $var;
2428 } else {
2429 $any_failures = true;
2430 }
2431 } else {
2432 $this->zipfiles_skipped_notaltered = array();
2433 }
2434 } else {
2435 $any_failures = true;
2436 }
2437 }
2438 } else {
2439 $any_failures = true;
2440 }
2441 } else {
2442 $any_failures = true;
2443 }
2444 if ($any_failures) {
2445 $updraftplus->log("Failed to recover file lists from existing cache files");
2446 // Reset it all
2447 $this->zipfiles_skipped_notaltered = array();
2448 $this->makezip_recursive_batchedbytes = 0;
2449 $this->zipfiles_batched = array();
2450 $this->zipfiles_dirbatched = array();
2451 } else {
2452 $updraftplus->log("File lists recovered from cache files; sizes: ".count($this->zipfiles_batched).", ".count($this->zipfiles_batched).", ".count($this->zipfiles_skipped_notaltered).")");
2453 $got_uploads_from_cache = true;
2454 }
2455 }
2456 }
2457 }
2458
2459 $time_counting_began = time();
2460
2461 $this->excluded_extensions = $this->get_excluded_extensions($exclude);
2462 $this->excluded_prefixes = $this->get_excluded_prefixes($exclude);
2463
2464 foreach ($source as $element) {
2465 #makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
2466 if ('uploads' == $whichone) {
2467 if (empty($got_uploads_from_cache)) {
2468 $dirname = dirname($element);
2469 $basename = $this->basename($element);
2470 $add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.$basename, $element, 2, $exclude);
2471 } else {
2472 $add_them = true;
2473 }
2474 } else {
2475 if (empty($got_uploads_from_cache)) {
2476 $add_them = $this->makezip_recursive_add($element, $this->basename($element), $element, 1, $exclude);
2477 } else {
2478 $add_them = true;
2479 }
2480 }
2481 if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
2482 }
2483
2484 $time_counting_ended = time();
2485
2486 // Cache the file scan, if it looks like it'll be useful
2487 // We use gzip to reduce the size as on hosts which limit disk I/O, the cacheing may make things worse
2488 // || 'others' == $whichone
2489 if (('uploads' == $whichone|| 'others' == $whichone) && !$error_occurred && function_exists('gzopen') && function_exists('gzwrite')) {
2490 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
2491
2492 // Just approximate - we're trying to avoid an otherwise-unpredictable PHP fatal error. Cacheing only happens if file enumeration took a long time - so presumably there are very many.
2493 $memory_needed_estimate = 0;
2494 foreach ($this->zipfiles_batched as $k => $v) { $memory_needed_estimate += strlen($k)+strlen($v)+12; }
2495
2496 // We haven't bothered to check if we just fetched the files from cache, as that shouldn't take a long time and so shouldn't trigger this
2497 // Let us suppose we need 15% overhead for gzipping
2498
2499 $memory_limit = ini_get('memory_limit');
2500 $memory_usage = round(@memory_get_usage(false)/1048576, 1);
2501 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);
2502
2503 if ($time_counting_ended-$time_counting_began > 20 && $updraftplus->verify_free_memory($memory_needed_estimate*0.15) && $whandle = gzopen($cache_file_base.'-zfb.gz.tmp', 'w')) {
2504 $updraftplus->log("File counting took a long time (".($time_counting_ended - $time_counting_began)."s); will attempt to cache results (memory_limit: $memory_limit (used: ${memory_usage}M | ${memory_usage2}M), estimated uncompressed bytes: ".round($memory_needed_estimate/1024, 1)." Kb)");
2505
2506 $buf = 'a:'.count($this->zipfiles_batched).':{';
2507 foreach ($this->zipfiles_batched as $file => $add_as) {
2508 $k = addslashes($file);
2509 $v = addslashes($add_as);
2510 $buf .= 's:'.strlen($k).':"'.$k.'";s:'.strlen($v).':"'.$v.'";';
2511 if (strlen($buf) > 1048576) {
2512 gzwrite($whandle, $buf, strlen($buf));
2513 $buf = '';
2514 }
2515 }
2516 $buf .= '}';
2517 $final = gzwrite($whandle, $buf);
2518 unset($buf);
2519
2520 // $serialised = serialize($this->zipfiles_batched);
2521 // $updraftplus->log("Actual uncompressed bytes: ".round(strlen($serialised)/1024, 1)." Kb");
2522 // if (!gzwrite($whandle, $serialised)) {
2523 if (!$final) {
2524 @unlink($cache_file_base.'-zfb.gz.tmp');
2525 @gzclose($whandle);
2526 } else {
2527 gzclose($whandle);
2528 if (!empty($this->zipfiles_skipped_notaltered)) {
2529 if ($shandle = gzopen($cache_file_base.'-zfs.gz.tmp', 'w')) {
2530 if (!gzwrite($shandle, serialize($this->zipfiles_skipped_notaltered))) {
2531 $aborted_on_skipped = true;
2532 }
2533 gzclose($shandle);
2534 } else {
2535 $aborted_on_skipped = true;
2536 }
2537 }
2538 if (!empty($aborted_on_skipped)) {
2539 @unlink($cache_file_base.'-zfs.gz.tmp');
2540 @unlink($cache_file_base.'-zfb.gz.tmp');
2541 } else {
2542 $info_array = array('makezip_recursive_batchedbytes' => $this->makezip_recursive_batchedbytes);
2543 if (!file_put_contents($cache_file_base.'-info.tmp', serialize($info_array))) {
2544 @unlink($cache_file_base.'-zfs.gz.tmp');
2545 @unlink($cache_file_base.'-zfb.gz.tmp');
2546 }
2547 if ($dhandle = gzopen($cache_file_base.'-zfd.gz.tmp', 'w')) {
2548 if (!gzwrite($dhandle, serialize($this->zipfiles_dirbatched))) {
2549 $aborted_on_dirbatched = true;
2550 }
2551 gzclose($dhandle);
2552 } else {
2553 $aborted_on_dirbatched = true;
2554 }
2555 if (!empty($aborted_on_dirbatched)) {
2556 @unlink($cache_file_base.'-zfs.gz.tmp');
2557 @unlink($cache_file_base.'-zfd.gz.tmp');
2558 @unlink($cache_file_base.'-zfb.gz.tmp');
2559 @unlink($cache_file_base.'-info.tmp');
2560 } else {
2561 // Success.
2562 }
2563 }
2564 }
2565 }
2566
2567 /*
2568 Class variables that get altered:
2569 zipfiles_batched
2570 makezip_recursive_batchedbytes
2571 zipfiles_skipped_notaltered
2572 zipfiles_dirbatched
2573
2574 Class variables that the result depends upon (other than the state of the filesystem):
2575 makezip_if_altered_since
2576 existing_files
2577 */
2578
2579 }
2580
2581 // Any not yet dispatched? Under our present scheme, at this point nothing has yet been despatched. And since the enumerating of all files can take a while, we can at this point do a further modification check to reduce the chance of overlaps.
2582 // This relies on us *not* touch()ing the zip file to indicate to any resumption 'behind us' that we're already here. Rather, we're relying on the combined facts that a) if it takes us a while to search the directory tree, then it should do for the one behind us too (though they'll have the benefit of cache, so could catch very fast) and b) we touch *immediately* after finishing the enumeration of the files to add.
2583 // $retry_on_error is here being used as a proxy for 'not the second time around, when there might be the remains of the file on the first time around'
2584 if ($retry_on_error) $updraftplus->check_recent_modification($destination);
2585 // Here we're relying on the fact that both PclZip and ZipArchive will happily operate on an empty file. Note that BinZip *won't* (for that, may need a new strategy - e.g. add the very first file on its own, in order to 'lay down a marker')
2586 if (empty($do_bump_index)) @touch($destination);
2587
2588 if (count($this->zipfiles_dirbatched) > 0 || count($this->zipfiles_batched) > 0) {
2589
2590 $updraftplus->log(sprintf("Total entities for the zip file: %d directories, %d files (%d skipped as non-modified), %s MB", count($this->zipfiles_dirbatched), count($this->zipfiles_batched), count($this->zipfiles_skipped_notaltered), round($this->makezip_recursive_batchedbytes/1048576,1)));
2591
2592 // No need to warn if we're going to retry anyway. (And if we get killed, the zip will be rescanned for its contents upon resumption).
2593 $warn_on_failures = ($retry_on_error) ? false : true;
2594 $add_them = $this->makezip_addfiles($warn_on_failures);
2595
2596 if (is_wp_error($add_them)) {
2597 foreach ($add_them->get_error_messages() as $msg) {
2598 $updraftplus->log("Error returned from makezip_addfiles: ".$msg);
2599 }
2600 $error_occurred = true;
2601 } elseif (false === $add_them) {
2602 $updraftplus->log("Error: makezip_addfiles returned false");
2603 $error_occurred = true;
2604 }
2605
2606 }
2607
2608 // Reset these variables because the index may have changed since we began
2609
2610 $itext = (empty($this->index)) ? '' : ($this->index+1);
2611 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
2612 $destination = $this->updraft_dir.'/'.$destination_base;
2613
2614 // ZipArchive::addFile sometimes fails - there's nothing when we expected something.
2615 // Did not used to have || $error_occured here. But it is better to retry, than to simply warn the user to check his logs.
2616 if (((file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'UpdraftPlus_ZipArchive' == $this->use_zip_object) || ($error_occurred && $retry_on_error)) {
2617 // This can be made more sophisticated if feedback justifies it. Currently we just switch to PclZip. But, it may have been a BinZip failure, so we could then try ZipArchive if that is available. If doing that, make sure that an infinite recursion isn't made possible.
2618 $updraftplus->log("makezip_addfiles(".$this->use_zip_object.") apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
2619 $saved_zip_object = $this->use_zip_object;
2620 $this->use_zip_object = 'UpdraftPlus_PclZip';
2621 $ret = $this->make_zipfile($source, $backup_file_basename, $whichone, false);
2622 $this->use_zip_object = $saved_zip_object;
2623 return $ret;
2624 }
2625
2626 // zipfiles_added > 0 means that $zip->close() has been called. i.e. An attempt was made to add something: something _should_ be there.
2627 // Why return true even if $error_occurred may be set? 1) Because in that case, a warning has already been logged. 2) Because returning false causes an error to be logged, which means it'll all be retried again. Also 3) this has been the pattern of the code for a long time, and the algorithm has been proven in the real-world: don't change what's not broken.
2628 // (file_exists($destination) || $this->index == $original_index) might be an alternative to $this->zipfiles_added > 0 - ? But, don't change what's not broken.
2629 if ($error_occurred == false || $this->zipfiles_added > 0) {
2630 return true;
2631 } else {
2632 $updraftplus->log("makezip failure: zipfiles_added=".$this->zipfiles_added.", error_occurred=".$error_occurred." (method=".$this->use_zip_object.")");
2633 return false;
2634 }
2635
2636 }
2637
2638 private function basename($element) {
2639 # This function is an ugly, conservative workaround for https://bugs.php.net/bug.php?id=62119. It does not aim to always work-around, but to ensure that nothing is made worse.
2640 $dirname = dirname($element);
2641 $basename_manual = preg_replace('#^[\\/]+#', '', substr($element, strlen($dirname)));
2642 $basename = basename($element);
2643 if ($basename_manual != $basename) {
2644 $locale = setlocale(LC_CTYPE, "0");
2645 if ('C' == $locale) {
2646 setlocale(LC_CTYPE, 'en_US.UTF8');
2647 $basename_new = basename($element);
2648 if ($basename_new == $basename_manual) $basename = $basename_new;
2649 setlocale(LC_CTYPE, $locale);
2650 }
2651 }
2652 return $basename;
2653 }
2654
2655 private function file_should_be_stored_without_compression($file) {
2656 if (!is_array($this->extensions_to_not_compress)) return false;
2657 foreach ($this->extensions_to_not_compress as $ext) {
2658 $ext_len = strlen($ext);
2659 if (strtolower(substr($file, -$ext_len, $ext_len)) == $ext) return true;
2660 }
2661 return false;
2662 }
2663
2664 // Q. Why don't we only open and close the zip file just once?
2665 // A. Because apparently PHP doesn't write out until the final close, and it will return an error if anything file has vanished in the meantime. So going directory-by-directory reduces our chances of hitting an error if the filesystem is changing underneath us (which is very possible if dealing with e.g. 1GB of files)
2666
2667 // We batch up the files, rather than do them one at a time. So we are more efficient than open,one-write,close.
2668 // To call into here, the array $this->zipfiles_batched must be populated (keys=paths, values=add-to-zip-as values). It gets reset upon exit from here.
2669 private function makezip_addfiles($warn_on_failures) {
2670
2671 global $updraftplus;
2672
2673 # Used to detect requests to bump the size
2674 $bump_index = false;
2675 $ret = true;
2676
2677 $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
2678
2679 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
2680 if ((int)$maxzipbatch < 1024) $maxzipbatch = 26214400;
2681
2682 // Short-circuit the null case, because we want to detect later if something useful happenned
2683 if (count($this->zipfiles_dirbatched) == 0 && count($this->zipfiles_batched) == 0) return true;
2684
2685 # If on PclZip, then if possible short-circuit to a quicker method (makes a huge time difference - on a folder of 1500 small files, 2.6s instead of 76.6)
2686 # This assumes that makezip_addfiles() is only called once so that we know about all needed files (the new style)
2687 # This is rather conservative - because it assumes zero compression. But we can't know that in advance.
2688 $force_allinone = false;
2689 if (0 == $this->index && $this->makezip_recursive_batchedbytes < $this->zip_split_every) {
2690 # So far, we only have a processor for this for PclZip; but that check can be removed - need to address the below items
2691 # TODO: Is this really what we want? Always go all-in-one for < 500MB???? Should be more conservative? Or, is it always faster to go all-in-one? What about situations where we might want to auto-split because of slowness - check that that is still working.
2692 # TODO: Test this new method for PclZip - are we still getting the performance gains? Test for ZipArchive too.
2693 if ('UpdraftPlus_PclZip' == $this->use_zip_object && ($this->makezip_recursive_batchedbytes < 512*1048576 || (defined('UPDRAFTPLUS_PCLZIP_FORCEALLINONE') && UPDRAFTPLUS_PCLZIP_FORCEALLINONE == true && 'UpdraftPlus_PclZip' == $this->use_zip_object))) {
2694 $updraftplus->log("Only one archive required (".$this->use_zip_object.") - will attempt to do in single operation (data: ".round($this->makezip_recursive_batchedbytes/1024, 1)." KB, split: ".round($this->zip_split_every/1024, 1)." KB)");
2695 // $updraftplus->log("PclZip, and only one archive required - will attempt to do in single operation (data: ".round($this->makezip_recursive_batchedbytes/1024, 1)." KB, split: ".round($this->zip_split_every/1024, 1)." KB)");
2696 $force_allinone = true;
2697 // if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
2698 // $zip = new PclZip($zipfile);
2699 // $remove_path = ($this->whichone == 'wpcore') ? untrailingslashit(ABSPATH) : WP_CONTENT_DIR;
2700 // $add_path = false;
2701 // // Remove prefixes
2702 // $backupable_entities = $updraftplus->get_backupable_file_entities(true);
2703 // if (isset($backupable_entities[$this->whichone])) {
2704 // if ('plugins' == $this->whichone || 'themes' == $this->whichone || 'uploads' == $this->whichone) {
2705 // $remove_path = dirname($backupable_entities[$this->whichone]);
2706 // # To normalise instead of removing (which binzip doesn't support, so we don't do it), you'd remove the dirname() in the above line, and uncomment the below one.
2707 // #$add_path = $this->whichone;
2708 // } else {
2709 // $remove_path = $backupable_entities[$this->whichone];
2710 // }
2711 // }
2712 // if ($add_path) {
2713 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path, PCLZIP_OPT_ADD_PATH, $add_path);
2714 // } else {
2715 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path);
2716 // }
2717 // if ($zipcode == 0) {
2718 // $updraftplus->log("PclZip Error: ".$zip->errorInfo(true), 'warning');
2719 // return $zip->errorCode();
2720 // } else {
2721 // $updraftplus->something_useful_happened();
2722 // return true;
2723 // }
2724 }
2725 }
2726
2727 // 05-Mar-2013 - added a new check on the total data added; it appears that things fall over if too much data is contained in the cumulative total of files that were addFile'd without a close-open cycle; presumably data is being stored in memory. In the case in question, it was a batch of MP3 files of around 100MB each - 25 of those equals 2.5GB!
2728
2729 $data_added_since_reopen = 0;
2730 # The following array is used only for error reporting if ZipArchive::close fails (since that method itself reports no error messages - we have to track manually what we were attempting to add)
2731 $files_zipadded_since_open = array();
2732
2733 $zip = new $this->use_zip_object;
2734 if (file_exists($zipfile)) {
2735 $opencode = $zip->open($zipfile);
2736 $original_size = filesize($zipfile);
2737 clearstatcache();
2738 } else {
2739 $create_code = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
2740 $opencode = $zip->open($zipfile, $create_code);
2741 $original_size = 0;
2742 }
2743
2744 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'),$zipfile, $zip->last_error));
2745 # TODO: This action isn't being called for the all-in-one case - should be, I think
2746 do_action("updraftplus_makezip_addfiles_prepack", $this, $this->whichone);
2747
2748 // Make sure all directories are created before we start creating files
2749 while ($dir = array_pop($this->zipfiles_dirbatched)) $zip->addEmptyDir($dir);
2750 $zipfiles_added_thisbatch = 0;
2751
2752 // Go through all those batched files
2753 foreach ($this->zipfiles_batched as $file => $add_as) {
2754
2755 if (!file_exists($file)) {
2756 $updraftplus->log("File has vanished from underneath us; dropping: ".$add_as);
2757 continue;
2758 }
2759
2760 $fsize = filesize($file);
2761
2762 if (@constant('UPDRAFTPLUS_SKIP_FILE_OVER_SIZE') && $fsize > UPDRAFTPLUS_SKIP_FILE_OVER_SIZE) {
2763 $updraftplus->log("File is larger than the user-configured (UPDRAFTPLUS_SKIP_FILE_OVER_SIZE) maximum (is: ".round($fsize/1024, 1)." KB); will skip: ".$add_as);
2764 continue;
2765 } elseif ($fsize > UPDRAFTPLUS_WARN_FILE_SIZE) {
2766 $updraftplus->log(sprintf(__('A very large file was encountered: %s (size: %s Mb)', 'updraftplus'), $add_as, round($fsize/1048576, 1)), 'warning', 'vlargefile_'.md5($this->whichone.'#'.$add_as));
2767 }
2768
2769 // Skips files that are already added
2770 if (!isset($this->existing_files[$add_as]) || $this->existing_files[$add_as] != $fsize) {
2771
2772 @touch($zipfile);
2773 $zip->addFile($file, $add_as);
2774 $zipfiles_added_thisbatch++;
2775
2776 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($add_as)) {
2777 if (false == ($set_compress = $zip->setCompressionName($add_as, ZipArchive::CM_STORE))) {
2778 $updraftplus->log("Zip: setCompressionName failed on: $add_as");
2779 }
2780 }
2781
2782 // N.B., Since makezip_addfiles() can get called more than once if there were errors detected, potentially $zipfiles_added_thisrun can exceed the total number of batched files (if they get processed twice).
2783 $this->zipfiles_added_thisrun++;
2784 $files_zipadded_since_open[] = array('file' => $file, 'addas' => $add_as);
2785
2786 $data_added_since_reopen += $fsize;
2787 /* Conditions for forcing a write-out and re-open:
2788 - more than $maxzipbatch bytes have been batched
2789 - more than 2.0 seconds have passed since the last time we wrote
2790 - that adding this batch of data is likely already enough to take us over the split limit (and if that happens, then do actually split - to prevent a scenario of progressively tinier writes as we approach but don't actually reach the limit)
2791 - more than 500 files batched (should perhaps intelligently lower this as the zip file gets bigger - not yet needed)
2792 */
2793
2794 # Add 10% margin. It only really matters when the OS has a file size limit, exceeding which causes failure (e.g. 2GB on 32-bit)
2795 # Since we don't test before the file has been created (so that zip_last_ratio has meaningful data), we rely on max_zip_batch being less than zip_split_every - which should always be the case
2796 $reaching_split_limit = ($this->zip_last_ratio > 0 && $original_size>0 && ($original_size + 1.1*$data_added_since_reopen*$this->zip_last_ratio) > $this->zip_split_every) ? true : false;
2797
2798 if (!$force_allinone && ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES || $reaching_split_limit || $data_added_since_reopen > $maxzipbatch || (time() - $this->zipfiles_lastwritetime) > 2)) {
2799
2800 @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
2801 $something_useful_sizetest = false;
2802
2803 if ($data_added_since_reopen > $maxzipbatch) {
2804 $something_useful_sizetest = true;
2805 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over ".round($maxzipbatch/1048576,1)." MB added on this batch (".round($data_added_since_reopen/1048576,1)." MB, ".count($this->zipfiles_batched)." files batched, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
2806 } elseif ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES) {
2807 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over ".UPDRAFTPLUS_MAXBATCHFILES." files added on this batch (".round($data_added_since_reopen/1048576,1)." MB, ".count($this->zipfiles_batched)." files batched, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
2808 } elseif (!$reaching_split_limit) {
2809 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): over 2.0 seconds have passed since the last write (".round($data_added_since_reopen/1048576,1)." MB, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") files added so far); re-opening (prior size: ".round($original_size/1024, 1).' KB)');
2810 } else {
2811 $updraftplus->log("Adding batch to zip file (".$this->use_zip_object."): possibly approaching split limit (".round($data_added_since_reopen/1048576,1)." MB, $zipfiles_added_thisbatch (".$this->zipfiles_added_thisrun.") files added so far); last ratio: ".round($this->zip_last_ratio,4)."; re-opening (prior size: ".round($original_size/1024, 1).' KB)');
2812 }
2813
2814 if (!$zip->close()) {
2815 // Though we will continue processing the files we've got, the final error code will be false, to allow a second attempt on the failed ones. This also keeps us consistent with a negative result for $zip->close() further down. We don't just retry here, because we have seen cases (with BinZip) where upon failure, the existing zip had actually been deleted. So, to be safe we need to re-scan the existing zips.
2816 $ret = false;
2817 $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
2818 }
2819
2820 $zipfiles_added_thisbatch = 0;
2821
2822 # This triggers a re-open, later
2823 unset($zip);
2824 $files_zipadded_since_open = array();
2825 // Call here, in case we've got so many big files that we don't complete the whole routine
2826 if (filesize($zipfile) > $original_size) {
2827
2828 # It is essential that this does not go above 1, even though in reality (and this can happen at the start, if just 1 file is added (e.g. due to >2.0s detection) the 'compressed' zip file may be *bigger* than the files stored in it. When that happens, if the ratio is big enough, it can then fire the "approaching split limit" detection (very) prematurely
2829 $this->zip_last_ratio = ($data_added_since_reopen > 0) ? min((filesize($zipfile) - $original_size)/$data_added_since_reopen, 1) : 1;
2830
2831 # We need a rolling update of this
2832 $original_size = filesize($zipfile);
2833
2834 # Move on to next zip?
2835 if ($reaching_split_limit || filesize($zipfile) > $this->zip_split_every) {
2836 $bump_index = true;
2837 # Take the filesize now because later we wanted to know we did clearstatcache()
2838 $bumped_at = round(filesize($zipfile)/1048576, 1);
2839 }
2840
2841 # Need to make sure that something_useful_happened() is always called
2842
2843 # How long since the current run began? If it's taken long (and we're in danger of not making it at all), or if that is forseeable in future because of general slowness, then we should reduce the parameters.
2844 if (!$something_useful_sizetest) {
2845 $updraftplus->something_useful_happened();
2846 } else {
2847
2848 // Do this as early as possible
2849 $updraftplus->something_useful_happened();
2850
2851 $time_since_began = max(microtime(true)- $this->zipfiles_lastwritetime, 0.000001);
2852 $normalised_time_since_began = $time_since_began*($maxzipbatch/$data_added_since_reopen);
2853
2854 // Don't measure speed until after ZipArchive::close()
2855 $rate = round($data_added_since_reopen/$time_since_began, 1);
2856
2857 $updraftplus->log(sprintf("A useful amount of data was added after this amount of zip processing: %s s (normalised: %s s, rate: %s KB/s)", round($time_since_began, 1), round($normalised_time_since_began, 1), round($rate/1024, 1)));
2858
2859 // We want to detect not only that we need to reduce the size of batches, but also the capability to increase them. This is particularly important because of ZipArchive()'s (understandable, given the tendency of PHP processes being terminated without notice) practice of first creating a temporary zip file via copying before acting on that zip file (so the information is atomic). Unfortunately, once the size of the zip file gets over 100MB, the copy operation beguns to be significant. By the time you've hit 500MB on many web hosts the copy is the majority of the time taken. So we want to do more in between these copies if possible.
2860
2861 /* "Could have done more" - detect as:
2862 - A batch operation would still leave a "good chunk" of time in a run
2863 - "Good chunk" means that the time we took to add the batch is less than 50% of a run time
2864 - We can do that on any run after the first (when at least one ceiling on the maximum time is known)
2865 - But in the case where a max_execution_time is long (so that resumptions are never needed), and we're always on run 0, we will automatically increase chunk size if the batch took less than 6 seconds.
2866 */
2867
2868 // At one stage we had a strategy of not allowing check-ins to have more than 20s between them. However, once the zip file got to a certain size, PHP's habit of copying the entire zip file first meant that it *always* went over 18s, and thence a drop in the max size was inevitable - which was bad, because with the copy time being something that only grew, the outcome was less data being copied every time
2869
2870 // Gather the data. We try not to do this unless necessary (may be time-sensitive)
2871 if ($updraftplus->current_resumption >= 1) {
2872 $time_passed = $updraftplus->jobdata_get('run_times');
2873 if (!is_array($time_passed)) $time_passed = array();
2874 list($max_time, $timings_string, $run_times_known) = $updraftplus->max_time_passed($time_passed, $updraftplus->current_resumption-1, $this->first_run);
2875 } else {
2876 $run_times_known = 0;
2877 $max_time = -1;
2878 }
2879
2880 if ($normalised_time_since_began<6 || ($updraftplus->current_resumption >=1 && $run_times_known >=1 && $time_since_began < 0.6*$max_time)) {
2881
2882 // How much can we increase it by?
2883 if ($normalised_time_since_began <6) {
2884 if ($run_times_known > 0 && $max_time >0) {
2885 $new_maxzipbatch = min(floor(max(
2886 $maxzipbatch*6/$normalised_time_since_began, $maxzipbatch*((0.6*$max_time)/$normalised_time_since_began))),
2887 200*1024*1024
2888 );
2889 } else {
2890 # Maximum of 200MB in a batch
2891 $new_maxzipbatch = min(floor($maxzipbatch*6/$normalised_time_since_began), 200*1024*1024);
2892 }
2893 } else {
2894 // Use up to 60% of available time
2895 $new_maxzipbatch = min(floor($maxzipbatch*((0.6*$max_time)/$normalised_time_since_began)), 200*1024*1024);
2896 }
2897
2898 # Throttle increases - don't increase by more than 2x in one go - ???
2899 # $new_maxzipbatch = floor(min(2*$maxzipbatch, $new_maxzipbatch));
2900 # Also don't allow anything that is going to be more than 18 seconds - actually, that's harmful because of the basically fixed time taken to copy the file
2901 # $new_maxzipbatch = floor(min(18*$rate ,$new_maxzipbatch));
2902
2903 # Don't go above the split amount (though we expect that to be higher anyway, unless sending via email)
2904 $new_maxzipbatch = min($new_maxzipbatch, $this->zip_split_every);
2905
2906 # Don't raise it above a level that failed on a previous run
2907 $maxzipbatch_ceiling = $updraftplus->jobdata_get('maxzipbatch_ceiling');
2908 if (is_numeric($maxzipbatch_ceiling) && $maxzipbatch_ceiling > 20*1024*1024 && $new_maxzipbatch > $maxzipbatch_ceiling) {
2909 $updraftplus->log("Was going to raise maxzipbytes to $new_maxzipbatch, but this is too high: a previous failure led to the ceiling being set at $maxzipbatch_ceiling, which we will use instead");
2910 $new_maxzipbatch = $maxzipbatch_ceiling;
2911 }
2912
2913 // Final sanity check
2914 if ($new_maxzipbatch > 1024*1024) $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
2915
2916 if ($new_maxzipbatch <= 1024*1024) {
2917 $updraftplus->log("Unexpected new_maxzipbatch value obtained (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
2918 } elseif ($new_maxzipbatch > $maxzipbatch) {
2919 $updraftplus->log("Performance is good - will increase the amount of data we attempt to batch (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
2920 } elseif ($new_maxzipbatch < $maxzipbatch) {
2921 // Ironically, we thought we were speedy...
2922 $updraftplus->log("Adjust: Reducing maximum amount of batched data (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, new_max_bytes=$new_maxzipbatch, old_max_bytes=$maxzipbatch)");
2923 } else {
2924 $updraftplus->log("Performance is good - but we will not increase the amount of data we batch, as we are already at the present limit (time=$time_since_began, normalised_time=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, max_bytes=$maxzipbatch)");
2925 }
2926
2927 if ($new_maxzipbatch > 1024*1024) $maxzipbatch = $new_maxzipbatch;
2928 }
2929
2930 // Detect excessive slowness
2931 // Don't do this until we're on at least resumption 7, as we want to allow some time for things to settle down and the maxiumum time to be accurately known (since reducing the batch size unnecessarily can itself cause extra slowness, due to PHP's usage of temporary zip files)
2932
2933 // We use a percentage-based system as much as possible, to avoid the various criteria being in conflict with each other (i.e. a run being both 'slow' and 'fast' at the same time, which is increasingly likely as max_time gets smaller).
2934
2935 if (!$updraftplus->something_useful_happened && $updraftplus->current_resumption >= 7) {
2936
2937 $updraftplus->something_useful_happened();
2938
2939 if ($run_times_known >= 5 && ($time_since_began > 0.8 * $max_time || $time_since_began + 7 > $max_time)) {
2940
2941 $new_maxzipbatch = max(floor($maxzipbatch*0.8), 20971520);
2942 if ($new_maxzipbatch < $maxzipbatch) {
2943 $maxzipbatch = $new_maxzipbatch;
2944 $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
2945 $updraftplus->log("We are within a small amount of the expected maximum amount of time available; the zip-writing thresholds will be reduced (time_passed=$time_since_began, normalised_time_passed=$normalised_time_since_began, max_time=$max_time, data points known=$run_times_known, old_max_bytes=$maxzipbatch, new_max_bytes=$new_maxzipbatch)");
2946 } else {
2947 $updraftplus->log("We are within a small amount of the expected maximum amount of time available, but the zip-writing threshold is already at its lower limit (20MB), so will not be further reduced (max_time=$max_time, data points known=$run_times_known, max_bytes=$maxzipbatch)");
2948 }
2949 }
2950
2951 } else {
2952 $updraftplus->something_useful_happened();
2953 }
2954 }
2955 $data_added_since_reopen = 0;
2956 } else {
2957 # ZipArchive::close() can take a very long time, which we want to know about
2958 $updraftplus->record_still_alive();
2959 }
2960
2961 clearstatcache();
2962 $this->zipfiles_lastwritetime = time();
2963 }
2964 } elseif (0 == $this->zipfiles_added_thisrun) {
2965 // Update lastwritetime, because otherwise the 2.0-second-activity detection can fire prematurely (e.g. if it takes >2.0 seconds to process the previously-written files, then the detector fires after 1 file. This then can have the knock-on effect of having something_useful_happened() called, but then a subsequent attempt to write out a lot of meaningful data fails, and the maximum batch is not then reduced.
2966 // Testing shows that calling time() 1000 times takes negligible time
2967 $this->zipfiles_lastwritetime = time();
2968 }
2969
2970 $this->zipfiles_added++;
2971
2972 // Don't call something_useful_happened() here - nothing necessarily happens until close() is called
2973 if (0 == $this->zipfiles_added % 100) {
2974 $skip_dblog = ($this->zipfiles_added_thisrun > 0 || 0 == $this->zipfiles_added % 1000) ? false : true;
2975 $updraftplus->log("Zip: ".basename($zipfile).": ".$this->zipfiles_added." files added (on-disk size: ".round(@filesize($zipfile)/1024, 1)." KB)", 'notice', false, $skip_dblog);
2976 }
2977
2978 if ($bump_index) {
2979 $updraftplus->log(sprintf("Zip size is at/near split limit (%s MB / %s MB) - bumping index (from: %d)", $bumped_at, round($this->zip_split_every/1048576, 1), $this->index));
2980 $bump_index = false;
2981 $this->bump_index();
2982 $zipfile = $this->zip_basename.($this->index+1).'.zip.tmp';
2983 }
2984
2985 if (empty($zip)) {
2986 $zip = new $this->use_zip_object;
2987
2988 if (file_exists($zipfile)) {
2989 $opencode = $zip->open($zipfile);
2990 $original_size = filesize($zipfile);
2991 clearstatcache();
2992 } else {
2993 $create_code = defined('ZIPARCHIVE::CREATE') ? ZIPARCHIVE::CREATE : 1;
2994 $opencode = $zip->open($zipfile, $create_code);
2995 $original_size = 0;
2996 }
2997
2998 if ($opencode !== true) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'), $zipfile, $zip->last_error));
2999 }
3000
3001 }
3002
3003 # Reset array
3004 $this->zipfiles_batched = array();
3005 $this->zipfiles_skipped_notaltered = array();
3006
3007 if (false == ($nret = $zip->close())) $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
3008
3009 do_action("updraftplus_makezip_addfiles_finished", $this, $this->whichone);
3010
3011 $this->zipfiles_lastwritetime = time();
3012 # May not exist if the last thing we did was bump
3013 if (file_exists($zipfile) && filesize($zipfile) > $original_size) $updraftplus->something_useful_happened();
3014
3015 # Move on to next archive?
3016 if (file_exists($zipfile) && filesize($zipfile) > $this->zip_split_every) {
3017 $updraftplus->log(sprintf("Zip size has gone over split limit (%s, %s) - bumping index (%d)", round(filesize($zipfile)/1048576,1), round($this->zip_split_every/1048576, 1), $this->index));
3018 $this->bump_index();
3019 }
3020
3021 clearstatcache();
3022
3023 return (false == $ret) ? false : $nret;
3024 }
3025
3026 private function record_zip_error($files_zipadded_since_open, $msg, $warn = true) {
3027 global $updraftplus;
3028
3029 if (!empty($updraftplus->cpanel_quota_readable)) {
3030 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
3031 if (is_array($hosting_bytes_free)) {
3032 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
3033 $quota_free_msg = sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
3034 $updraftplus->log($quota_free_msg);
3035 if ($hosting_bytes_free[3] < 1048576*50) {
3036 $quota_low = true;
3037 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
3038 $updraftplus->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);
3039 }
3040 }
3041 }
3042
3043 // Always warn of this
3044 if (strpos($msg, 'File Size Limit Exceeded') !== false && 'UpdraftPlus_BinZip' == $this->use_zip_object) {
3045 $updraftplus->log(sprintf(__('The zip engine returned the message: %s.', 'updraftplus'), 'File Size Limit Exceeded'). __('Go here for more information.', 'updraftplus').' https://updraftplus.com/what-should-i-do-if-i-see-the-message-file-size-limit-exceeded/', 'warning', 'zipcloseerror-filesizelimit');
3046 } elseif ($warn) {
3047 $warn_msg = __('A zip error occurred', 'updraftplus').' - ';
3048 if (!empty($quota_low)) {
3049 $warn_msg = sprintf(__('your web hosting account appears to be full; please see: %s', 'updraftplus'), 'https://updraftplus.com/faqs/how-much-free-disk-space-do-i-need-to-create-a-backup/');
3050 } else {
3051 $warn_msg .= __('check your log for more details.', 'updraftplus');
3052 }
3053 $updraftplus->log($warn_msg, 'warning', 'zipcloseerror-'.$this->whichone);
3054 }
3055
3056 $updraftplus->log("The attempt to close the zip file returned an error ($msg). List of files we were trying to add follows (check their permissions).");
3057
3058 foreach ($files_zipadded_since_open as $ffile) {
3059 $updraftplus->log("File: ".$ffile['addas']." (exists: ".(int) @file_exists($ffile['file']).", is_readable: ".(int) @is_readable($ffile['file'])." size: ".@filesize($ffile['file']).')', 'notice', false, true);
3060 }
3061 }
3062
3063 private function bump_index() {
3064 global $updraftplus;
3065 $youwhat = $this->whichone;
3066
3067 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
3068
3069 $itext = (0 == $this->index) ? '' : ($this->index+1);
3070 $full_path = $this->zip_basename.$itext.'.zip';
3071
3072 $checksums = $updraftplus->which_checksums();
3073
3074 $checksum_description = '';
3075
3076 foreach ($checksums as $checksum) {
3077
3078 $cksum = hash_file($checksum, $full_path.'.tmp');
3079 $updraftplus->jobdata_set($checksum.'-'.$youwhat.$this->index, $cksum);
3080 if ($checksum_description) $checksum_description .= ', ';
3081 $checksum_description .= "$checksum: $cksum";
3082
3083 }
3084
3085 $next_full_path = $this->zip_basename.($this->index+2).'.zip';
3086 // We touch the next zip before renaming the temporary file; this indicates that the backup for the entity is not *necessarily* finished
3087 touch($next_full_path.'.tmp');
3088
3089 if (file_exists($full_path.'.tmp') && filesize($full_path.'.tmp') > 0) {
3090 if (!rename($full_path.'.tmp', $full_path)) {
3091 $updraftplus->log("Rename failed for $full_path.tmp");
3092 } else {
3093 $updraftplus->something_useful_happened();
3094 }
3095 }
3096
3097 $kbsize = filesize($full_path)/1024;
3098 $rate = round($kbsize/$timetaken, 1);
3099 $updraftplus->log("Created ".$this->whichone." zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) (checksums: $checksum_description)");
3100 $this->zip_microtime_start = microtime(true);
3101
3102 // No need to add $itext here - we can just delete any temporary files for this zip
3103 $updraftplus->clean_temporary_files('_'.$updraftplus->nonce."-".$youwhat, 600);
3104
3105 $this->index++;
3106 $this->job_file_entities[$youwhat]['index'] = $this->index;
3107 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
3108 }
3109
3110 /**
3111 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
3112 *
3113 * @param Array $a - the array
3114 *
3115 * @return Array - with keys 'name' and 'type'
3116 */
3117 private function cb_get_name_base_type($a) {
3118 return array('name' => $a[0], 'type' => 'BASE TABLE');
3119 }
3120
3121 /**
3122 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
3123 *
3124 * @param Array $a - the array
3125 *
3126 * @return Array - keys are 'name' and 'type'
3127 */
3128 private function cb_get_name_type($a) {
3129 return array('name' => $a[0], 'type' => $a[1]);
3130 }
3131
3132 /**
3133 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
3134 *
3135 * @param Array $a - the array
3136 *
3137 * @return Mixed - the value with key (string)'name'
3138 */
3139 private function cb_get_name($a) {
3140 return $a['name'];
3141 }
3142 }
3143
3144 class UpdraftPlus_WPDB_OtherDB extends wpdb {
3145 /**
3146 * This adjusted bail() does two things: 1) Never dies and 2) logs in the UD log
3147 *
3148 * @param string $message Error message
3149 * @param string $error_code Error Code
3150 * @return boolean
3151 */
3152 public function bail($message, $error_code = '500') {
3153 global $updraftplus;
3154 if ('db_connect_fail' == $error_code) $message = 'Connection failed: check your access details, that the database server is up, and that the network connection is not firewalled.';
3155 $updraftplus->log("WPDB_OtherDB error: $message ($error_code)");
3156 // Now do the things that would have been done anyway
3157 if (class_exists('WP_Error')) {
3158 $this->error = new WP_Error($error_code, $message);
3159 } else {
3160 $this->error = $message;
3161 }
3162 return false;
3163 }
3164 }
3165