PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.22.19
UpdraftPlus: WP Backup & Migration Plugin v1.22.19
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.22.19, at backup.php

4,583 lines 220.0 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
5 if (!class_exists('UpdraftPlus_PclZip')) require_once(UPDRAFTPLUS_DIR.'/includes/class-zip.php');
6
7 /**
8 * This file contains code that is only needed/loaded when a backup is running
9 */
10 class UpdraftPlus_Backup {
11
12 private $index = 0;
13
14 private $manifest_path;
15
16 private $zipfiles_added;
17
18 private $zipfiles_added_thisrun = 0;
19
20 public $zipfiles_dirbatched;
21
22 public $zipfiles_batched;
23
24 public $zipfiles_skipped_notaltered;
25
26 private $makezip_recursive_batchedbytes;
27
28 private $zip_split_every = 419430400; // 400MB
29
30 private $zip_last_ratio = 1;
31
32 private $whichone;
33
34 private $zip_basename = '';
35
36 private $zipfiles_lastwritetime;
37
38 // 0 = unknown; false = failed
39 public $binzip = 0;
40
41 private $dbhandle;
42
43 private $dbhandle_isgz;
44
45 private $whichdb;
46
47 private $whichdb_suffix;
48
49 // Array of entities => times
50 private $altered_since = -1;
51
52 // Time for the current entity
53 private $makezip_if_altered_since = -1;
54
55 private $excluded_extensions = false;
56
57 private $excluded_wildcards = false;
58
59 private $excluded_prefixes = false;
60
61 private $use_zip_object = 'UpdraftPlus_ZipArchive';
62
63 public $debug = false;
64
65 public $updraft_dir;
66
67 private $site_name;
68
69 private $wpdb_obj;
70
71 private $job_file_entities = array();
72
73 private $first_run = 0;
74
75 // Record of zip files created
76 private $backup_files_array = array();
77
78 // Used when deciding to use the 'store' or 'deflate' zip storage method
79 private $extensions_to_not_compress = array();
80
81 // Append to this any skipped tables
82 private $skipped_tables;
83
84 // When initialised, a boolean
85 public $last_storage_instance;
86
87 // The absolute upper limit that will be considered for a zip batch (in bytes)
88 private $zip_batch_ceiling;
89
90 private $backup_excluded_patterns = array();
91
92 // Bytes of uncompressed data written since last open
93 private $db_current_raw_bytes = 0;
94
95 private $table_prefix;
96
97 private $table_prefix_raw;
98
99 private $many_rows_warning = false;
100
101 private $expected_rows = false;
102
103 // @var Boolean
104 private $try_split = false;
105
106 private $zip_microtime_start;
107
108 public $current_service;
109
110 private $existing_files;
111
112 private $existing_files_rawsize;
113
114 private $existing_zipfiles_size;
115
116 private $dbinfo;
117
118 private $duplicate_tables_exist = false;
119
120 private $first_linked_index;
121
122 // private $source;
123
124 /**
125 * Class constructor
126 *
127 * @param Array|String $backup_files - files to backup, or (string)'no'
128 * @param Integer $altered_since - only backup files altered since this time (UNIX epoch time)
129 */
130 public function __construct($backup_files, $altered_since = -1) {
131
132 global $updraftplus;
133
134 $this->site_name = $this->get_site_name();
135
136 // Decide which zip engine to begin with
137 $this->debug = UpdraftPlus_Options::get_updraft_option('updraft_debug_mode');
138 $this->updraft_dir = $updraftplus->backups_dir_location();
139
140
141 require_once(UPDRAFTPLUS_DIR.'/includes/class-database-utility.php');
142
143 if ('no' === $backup_files) {
144 $this->use_zip_object = 'UpdraftPlus_PclZip';
145 return;
146 }
147
148 $this->extensions_to_not_compress = array_unique(array_map('strtolower', array_map('trim', explode(',', UPDRAFTPLUS_ZIP_NOCOMPRESS))));
149
150 $this->backup_excluded_patterns = array(
151 array(
152 // all in one wp migration pattern: WP_PLUGIN_DIR/all-in-one-wp-migration/storage/*/*.wpress, `ai1wm-backups` folder in wp-content is already implicitly handled on the UDP settings with a `*backups` predefined exclusion rule for `others` directory
153 'directory' => realpath(WP_PLUGIN_DIR).DIRECTORY_SEPARATOR.'all-in-one-wp-migration'.DIRECTORY_SEPARATOR.'storage',
154 'regex' => '/.+\.wpress$/is',
155 ),
156 );
157
158 $this->altered_since = $altered_since;
159
160 $resumptions_since_last_successful = $updraftplus->current_resumption - $updraftplus->last_successful_resumption;
161
162 // false means 'tried + failed'; whereas 0 means 'not yet tried'
163 // Disallow binzip on OpenVZ when we're not sure there's plenty of memory
164 if (0 === $this->binzip && (!defined('UPDRAFTPLUS_PREFERPCLZIP') || !UPDRAFTPLUS_PREFERPCLZIP) && (!defined('UPDRAFTPLUS_NO_BINZIP') || !UPDRAFTPLUS_NO_BINZIP) && ($updraftplus->current_resumption < 9 || $resumptions_since_last_successful < 2)) {
165
166 if (@file_exists('/proc/user_beancounters') && @file_exists('/proc/meminfo') && @is_readable('/proc/meminfo')) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
167 $meminfo = @file_get_contents('/proc/meminfo', false, null, 0, 200);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
168 if (is_string($meminfo) && preg_match('/MemTotal:\s+(\d+) kB/', $meminfo, $matches)) {
169 $memory_mb = $matches[1]/1024;
170 // 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
171 $vz_log = "OpenVZ; reported memory: ".round($memory_mb, 1)." MB";
172 if ($memory_mb < 1024 || $memory_mb > 8192) {
173 $openvz_lowmem = true;
174 $vz_log .= " (will not use BinZip)";
175 }
176 $updraftplus->log($vz_log);
177 }
178 }
179 if (empty($openvz_lowmem)) {
180 $updraftplus->log('Checking if we have a zip executable available');
181 $binzip = $updraftplus->find_working_bin_zip();
182 if (is_string($binzip)) {
183 $updraftplus->log("Zip engine: found/will use a binary zip: $binzip");
184 $this->binzip = $binzip;
185 $this->use_zip_object = 'UpdraftPlus_BinZip';
186 }
187 }
188 }
189
190 // In tests, PclZip was found to be 25% slower than ZipArchive
191 if ('UpdraftPlus_PclZip' != $this->use_zip_object && empty($this->binzip) && ((defined('UPDRAFTPLUS_PREFERPCLZIP') && UPDRAFTPLUS_PREFERPCLZIP == true) || !class_exists('ZipArchive') || !class_exists('UpdraftPlus_ZipArchive') || (!extension_loaded('zip') && !method_exists('ZipArchive', 'AddFile')))) {
192 global $updraftplus;
193 $updraftplus->log("Zip engine: ZipArchive (a.k.a. php-zip) is not available or is disabled (will use PclZip (much slower) if needed)");
194 $this->use_zip_object = 'UpdraftPlus_PclZip';
195 }
196
197 $this->zip_batch_ceiling = (defined('UPDRAFTPLUS_ZIP_BATCH_CEILING') && UPDRAFTPLUS_ZIP_BATCH_CEILING > 104857600) ? UPDRAFTPLUS_ZIP_BATCH_CEILING : 200 * 1048576;
198
199 add_filter('updraftplus_exclude_file', array($this, 'backup_exclude_file'), 10, 2);
200
201 }
202
203 /**
204 * Get a site name suitable for use in the backup filename
205 *
206 * @return String
207 */
208 private function get_site_name() {
209 // 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
210 $site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr(get_bloginfo(), 0, 32))));
211 if (!$site_name || preg_match('#^_+$#', $site_name)) {
212 // Try again...
213 $parsed_url = parse_url(home_url(), PHP_URL_HOST);
214 $parsed_subdir = untrailingslashit(parse_url(home_url(), PHP_URL_PATH));
215 if ($parsed_subdir && '/' != $parsed_subdir) $parsed_url .= str_replace(array('/', '\\'), '_', $parsed_subdir);
216 $site_name = str_replace('__', '_', preg_replace('/[^A-Za-z0-9_]/', '', str_replace(' ', '_', substr($parsed_url, 0, 32))));
217 if (!$site_name || preg_match('#^_+$#', $site_name)) $site_name = 'WordPress_Backup';
218 }
219
220 // Allow an over-ride. Careful about introducing characters not supported by your filesystem or cloud storage.
221 return apply_filters('updraftplus_blog_name', $site_name);
222 }
223
224 /**
225 * Public, because called from the 'More Files' add-on
226 *
227 * @param String|Array $create_from_dir Directory/ies to create the zip
228 * @param String $whichone Entity being backed up (e.g. 'plugins', 'uploads')
229 * @param String $backup_file_basename Name of backup file
230 * @param Integer $index Index of zip in the sequence
231 * @param Integer|Boolean $first_linked_index First linked index in the sequence, or false
232 *
233 * @return Boolean|Array - list of files, or false for failure
234 */
235 public function create_zip($create_from_dir, $whichone, $backup_file_basename, $index, $first_linked_index = false) {
236 // Note: $create_from_dir can be an array or a string
237
238 if (function_exists('set_time_limit')) set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);
239
240 $original_index = $index;
241
242 $this->index = $index;
243 $this->first_linked_index = (false === $first_linked_index) ? 0 : $first_linked_index;
244 $this->whichone = $whichone;
245
246 global $updraftplus;
247
248 $this->zip_split_every = max((int) $updraftplus->jobdata_get('split_every'), UPDRAFTPLUS_SPLIT_MIN)*1048576;
249
250 if ('others' != $whichone) $updraftplus->log("Beginning creation of dump of $whichone (split every: ".round($this->zip_split_every/1048576, 1)." MB)");
251
252 if (is_string($create_from_dir) && !file_exists($create_from_dir)) {
253 $flag_error = true;
254 $updraftplus->log("Does not exist: $create_from_dir");
255 if ('mu-plugins' == $whichone) {
256 if (!function_exists('get_mu_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
257 $mu_plugins = get_mu_plugins();
258 if (count($mu_plugins) == 0) {
259 $updraftplus->log("There appear to be no mu-plugins to backup. Will not raise an error.");
260 $flag_error = false;
261 }
262 }
263 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');
264 return false;
265 }
266
267 $itext = empty($index) ? '' : $index+1;
268 $base_path = $backup_file_basename.'-'.$whichone.$itext.'.zip';
269 $full_path = $this->updraft_dir.'/'.$base_path;
270 $time_now = time();
271
272 // This is compatible with filenames which indicate increments, as it is looking only for the current increment
273 if (file_exists($full_path) || $updraftplus->is_uploaded($base_path)) {
274 // Gather any further files that may also exist
275 $files_existing = array();
276 while (file_exists($full_path) || $updraftplus->is_uploaded($base_path)) {
277 $files_existing[] = $base_path;
278 if (file_exists($full_path)) {
279 $time_mod = (int) @filemtime($full_path);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
280 $updraftplus->log($base_path.": this file has already been created (age: ".round($time_now-$time_mod, 1)." s)");
281 if ($time_mod > 100 && ($time_now - $time_mod) < 30) {
282 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($base_path, $time_now, $time_mod);
283 }
284 }
285 $index++;
286 // This is compatible with filenames which indicate increments, as it is looking only for the current increment
287 $base_path = $backup_file_basename.'-'.$whichone.($index+1).'.zip';
288 $full_path = $this->updraft_dir.'/'.$base_path;
289 }
290 }
291
292 // Temporary file, to be able to detect actual completion (upon which, it is renamed)
293
294 // Jun-13 - be more aggressive in removing temporary files from earlier attempts - anything >=600 seconds old of this kind
295 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 600);
296
297 // 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
298 $zip_name = $full_path.'.tmp';
299 $time_mod = file_exists($zip_name) ? filemtime($zip_name) : 0;
300 if (file_exists($zip_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
301 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($zip_name, $time_now, $time_mod);
302 }
303
304 if (file_exists($zip_name)) {
305 $updraftplus->log("File exists ($zip_name), but was 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).")");
306 }
307
308 // 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)
309 // Note: this doesn't catch PclZip temporary files
310 $d = dir($this->updraft_dir);
311 $match = '_'.$updraftplus->file_nonce."-".$whichone;
312 while (false !== ($e = $d->read())) {
313 if ('.' == $e || '..' == $e || !is_file($this->updraft_dir.'/'.$e)) continue;
314 $ziparchive_match = preg_match("/$match(?:[0-9]*)\.zip\.tmp\.[A-Za-z0-9]+$/i", $e);
315 $binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $e);
316 $pclzip_match = preg_match("/^pclzip-[a-z0-9]+.(?:gz|tmp)$/", $e);
317 if ($time_now-filemtime($this->updraft_dir.'/'.$e) < 30 && ($ziparchive_match || (0 != $updraftplus->current_resumption && ($binzip_match || $pclzip_match)))) {
318 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($this->updraft_dir.'/'.$e, $time_now, filemtime($this->updraft_dir.'/'.$e));
319 }
320 }
321 @$d->close();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
322 clearstatcache();
323
324 if (isset($files_existing)) {
325 // 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.
326 // 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).
327 return $files_existing;
328 }
329
330 $this->log_account_space();
331
332 $this->zip_microtime_start = microtime(true);
333
334 // The paths in the zip should then begin with '$whichone', having removed WP_CONTENT_DIR from the front
335 $zipcode = $this->make_zipfile($create_from_dir, $backup_file_basename, $whichone);
336 if (true !== $zipcode) {
337 $updraftplus->log("ERROR: Zip failure: Could not create $whichone zip (".$this->index." / $index)");
338 $updraftplus->log(sprintf(__("Could not create %s zip. Consult the log file for more information.", 'updraftplus'), $whichone), 'error');
339 // The caller is required to update $index from $this->index
340 return false;
341 } else {
342 $itext = empty($this->index) ? '' : $this->index+1;
343 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
344 if (file_exists($full_path.'.tmp')) {
345 if (@filesize($full_path.'.tmp') === 0) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
346 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed");
347 @unlink($full_path.'.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
348 } else {
349
350 $checksum_description = '';
351
352 $checksums = $updraftplus->which_checksums();
353
354 foreach ($checksums as $checksum) {
355
356 $cksum = hash_file($checksum, $full_path.'.tmp');
357 $updraftplus->jobdata_set($checksum.'-'.$whichone.$this->index, $cksum);
358 if ($checksum_description) $checksum_description .= ', ';
359 $checksum_description .= "$checksum: $cksum";
360
361 }
362
363 @rename($full_path.'.tmp', $full_path);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
364 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
365 $kbsize = filesize($full_path)/1024;
366 $rate = round($kbsize/$timetaken, 1);
367 $updraftplus->log("Created $whichone zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) ($checksum_description)");
368 // We can now remove any left-over temporary files from this job
369 }
370 } elseif ($this->index > $original_index) {
371 $updraftplus->log("Did not create $whichone zip (".$this->index.") - not needed (2)");
372 // Added 12-Feb-2014 (to help multiple morefiles)
373 $this->index--;
374 } else {
375 $updraftplus->log("Looked-for $whichone zip (".$this->index.") was not found (".basename($full_path).".tmp)", 'warning');
376 }
377 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0);
378 $this->maybe_cloud_backup(basename($full_path), $whichone, $this->index);
379 }
380
381 // Remove cache list files as well, if there are any
382 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-$whichone", 0, true);
383
384 // Create the results array to send back (just the new ones, not any prior ones)
385 $files_existing = array();
386 $res_index = $original_index;
387 for ($i = $original_index; $i<= $this->index; $i++) {
388 $itext = empty($i) ? '' : ($i+1);
389 $full_path = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$itext.'.zip';
390 if (file_exists($full_path) || $updraftplus->is_uploaded($backup_file_basename.'-'.$whichone.$itext.'.zip')) {
391 $files_existing[$res_index] = $backup_file_basename.'-'.$whichone.$itext.'.zip';
392 }
393 $res_index++;
394 }
395 return $files_existing;
396 }
397
398 /**
399 * 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.
400 */
401 public function do_prune_standalone() {
402 global $updraftplus;
403
404 $services = (array) $updraftplus->just_one($updraftplus->jobdata_get('service'));
405
406 $prune_services = array();
407
408 foreach ($services as $service) {
409 if ('none' === $service || '' == $service) continue;
410
411 $objname = "UpdraftPlus_BackupModule_${service}";
412 if (!class_exists($objname) && file_exists(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php')) {
413 include_once(UPDRAFTPLUS_DIR.'/methods/'.$service.'.php');
414 }
415 if (class_exists($objname)) {
416 $remote_obj = new $objname;
417 $prune_services[$service]['all'] = array($remote_obj, null);
418 } else {
419 $updraftplus->log("Could not prune from service $service: remote method not found");
420 }
421
422 }
423
424 if (!empty($prune_services)) $this->prune_retained_backups($prune_services);
425 }
426
427 /**
428 * This function will check if backup archives exist and have a usable manifest if so it will atempt to send them for upload
429 *
430 * @param String $file - the name of the zip file
431 * @param String $whichone - the entity type
432 * @param Integer $index - the entity index
433 *
434 * @return void
435 */
436 private function maybe_cloud_backup($file, $whichone, $index) {
437
438 global $updraftplus;
439
440 // Check if the feature is enabled
441 if (defined('UPDRAFTPLUS_UPLOAD_AFTER_CREATE') && UPDRAFTPLUS_UPLOAD_AFTER_CREATE) {
442
443 if ($updraftplus->is_uploaded($file) || !is_file($this->updraft_dir.'/'.$file)) return;
444
445 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
446 $undone_files = array();
447
448 if (!isset($backupable_entities[$whichone]) && ('db' != substr($whichone, 0, 2))) return;
449
450 $manifest = $this->updraft_dir.'/'.$file.'.list.tmp';
451
452 if (!file_exists($manifest)) return;
453
454 $manifest_contents = json_decode(file_get_contents($manifest), true);
455
456 if (empty($manifest_contents) || empty($manifest_contents['files'])) return;
457
458 $undone_files[$whichone.$index] = $file;
459
460 $this->cloud_backup($undone_files, 'partialclouduploading');
461
462 // reset the jobstatus as we may have just uploaded a backup set
463 $updraftplus->jobdata_set('jobstatus', 'filescreating');
464 $updraftplus->jobdata_delete('uploading_substatus');
465 }
466 }
467
468 /**
469 * Dispatch to the relevant function
470 *
471 * @param Array $backup_array - List of archives for the backup
472 * @param String $stage - The stage we are uploading at (clouduploading, partialclouduploading)
473 */
474 public function cloud_backup($backup_array, $stage = 'clouduploading') {
475
476 global $updraftplus;
477
478 $services = (array) $updraftplus->just_one($updraftplus->jobdata_get('service'));
479 $remote_storage_instances = $updraftplus->jobdata_get('remote_storage_instances', array());
480
481 // We need to make sure that the loop below actually runs
482 if (empty($services)) $services = array('none');
483
484 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_enabled_storage_objects_and_ids($services, $remote_storage_instances);
485
486 $total_instances_count = 0;
487
488 foreach ($storage_objects_and_ids as $service) {
489 if ($service['object']->supports_feature('multi_options')) $total_instances_count += count($service['instance_settings']);
490 }
491
492 $updraftplus->jobdata_set('jobstatus', $stage);
493
494 $updraftplus->register_wp_http_option_hooks();
495
496 $upload_status = $updraftplus->jobdata_get('uploading_substatus');
497 if (!is_array($upload_status) || !isset($upload_status['t'])) {
498 $upload_status = array('i' => 0, 'p' => 0, 't' => max(1, $total_instances_count)*count($backup_array));
499 $updraftplus->jobdata_set('uploading_substatus', $upload_status);
500 }
501
502 $do_prune = array();
503
504 // 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
505 if (count($services) >1 && $updraftplus->no_checkin_last_time) {
506 $updraftplus->log('No check-in last time: will try a different remote service first');
507 array_push($services, array_shift($services));
508 // Make sure that the 'no worthwhile activity' detector isn't flumoxed by the starting of a new upload at 0%
509 if ($updraftplus->current_resumption > 9) $updraftplus->jobdata_set('uploaded_lastreset', $updraftplus->current_resumption);
510 if (1 == ($updraftplus->current_resumption % 2) && count($services)>2) array_push($services, array_shift($services));
511 }
512
513 $errors_before_uploads = $updraftplus->error_count();
514
515 foreach ($services as $ind => $service) {
516 try {
517 $instance_id_count = 0;
518 $total_instance_ids = ('none' !== $service && '' !== $service && $storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) ? count($storage_objects_and_ids[$service]['instance_settings']) : 1;
519
520 // Used for logging by record_upload_chunk()
521 $this->current_service = $service;
522
523 // Used when deciding whether to delete the local file
524 $this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
525 $log_extra = $this->last_storage_instance ? ' (last)' : '';
526 $updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
527 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
528
529 if ('none' == $service || '' == $service) {
530 $updraftplus->log('No remote despatch: user chose no remote backup service');
531 // Still want to mark as "uploaded", to signal that nothing more needs doing. (Important on incremental runs with no cloud storage).
532 foreach ($backup_array as $file) {
533 if ($updraftplus->is_uploaded($file)) {
534 $updraftplus->log("Already uploaded: $file");
535 } else {
536 $updraftplus->uploaded_file($file, true);
537 }
538 $fullpath = $this->updraft_dir.'/'.$file;
539 if (file_exists($fullpath.'.list.tmp')) {
540 $updraftplus->log("Deleting zip manifest ({$file}.list.tmp)");
541 unlink($fullpath.'.list.tmp');
542 }
543 }
544 $this->prune_retained_backups(array('none' => array('all' => array(null, null))));
545 } elseif (!empty($storage_objects_and_ids[$service]['object']) && !$storage_objects_and_ids[$service]['object']->supports_feature('multi_options')) {
546 $remote_obj = $storage_objects_and_ids[$service]['object'];
547
548 $do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, ''));
549 } elseif (!empty($storage_objects_and_ids[$service]['instance_settings'])) {
550 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $instance_id => $options) {
551
552 if ($instance_id_count > 0) {
553 $this->last_storage_instance = ($ind+1 >= count($services) && $instance_id_count+1 >= $total_instance_ids && $errors_before_uploads == $updraftplus->error_count()) ? true : false;
554 $log_extra = $this->last_storage_instance ? ' (last)' : '';
555 $updraftplus->log("Cloud backup selection (".($ind+1)."/".count($services)."): ".$service." with instance (".($instance_id_count+1)."/".$total_instance_ids.")".$log_extra);
556 }
557
558 // Used for logging by record_upload_chunk()
559 $this->current_instance = $instance_id;
560
561 if (!isset($options['instance_enabled'])) $options['instance_enabled'] = 1;
562
563 // if $remote_storage_instances is not empty then we are looping over a list of instances the user wants to backup to so we want to ignore if the instance is enabled or not
564 if (1 == $options['instance_enabled'] || !empty($remote_storage_instances)) {
565 $remote_obj = $storage_objects_and_ids[$service]['object'];
566 $remote_obj->set_options($options, true, $instance_id);
567 $do_prune = array_merge_recursive($do_prune, $this->upload_cloud($remote_obj, $service, $backup_array, $instance_id));
568 } else {
569 $updraftplus->log("This instance id ($instance_id) is set as inactive.");
570 }
571
572 $instance_id_count++;
573 }
574 }
575 } catch (Exception $e) {
576 $log_message = 'Exception ('.get_class($e).') occurred during backup uploads to the '.$service.'. Exception Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
577 $updraftplus->log($log_message);
578 error_log($log_message);
579 $updraftplus->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
580 // @codingStandardsIgnoreLine
581 } catch (Error $e) {
582 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during backup uploads to the '.$service.'. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
583 $updraftplus->log($log_message);
584 error_log($log_message);
585 $updraftplus->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
586 }
587 }
588
589 if (!empty($do_prune) && 'clouduploading' == $stage) $this->prune_retained_backups($do_prune);
590
591 $updraftplus->register_wp_http_option_hooks(false);
592
593 }
594
595 /**
596 * This method will start the upload of the backups to the chosen remote storage method and return an array of files to be pruned and their location.
597 *
598 * @param Object $remote_obj - the remote storage object
599 * @param String $service - the name of the service we are uploading to
600 * @param Array $backup_array - an array that contains the backup files we want to upload
601 * @param String $instance_id - the instance id we are using
602 * @return Array - an array with information about what files to prune and where they are located
603 */
604 private function upload_cloud($remote_obj, $service, $backup_array, $instance_id) {
605
606 global $updraftplus;
607
608 $do_prune = array();
609
610 if ('' == $instance_id) {
611 $updraftplus->log("Beginning dispatch of backup to remote ($service)");
612 } else {
613 $updraftplus->log("Beginning dispatch of backup to remote ($service) (instance identifier $instance_id)");
614 }
615
616 $errors_before_uploads = $updraftplus->error_count();
617
618 $sarray = array();
619 foreach ($backup_array as $bind => $file) {
620 if ($updraftplus->is_uploaded($file, $service, $instance_id)) {
621 if ('' == $instance_id) {
622 $updraftplus->log("Already uploaded to $service: $file", 'notice', false, true);
623 } else {
624 $updraftplus->log("Already uploaded to $service / $instance_id: $file", 'notice', false, true);
625 }
626 // If this is the last instance and this file has already been uploaded then we need to mark it as complete in order to get the local file cleaned up.
627 if (!empty($this->last_storage_instance)) $updraftplus->uploaded_file($file);
628 } else {
629 $sarray[$bind] = $file;
630 }
631 }
632
633 if (count($sarray) > 0) {
634 $pass_to_prune = $remote_obj->backup($sarray);
635 if ('remotesend' != $service) {
636 $do_prune[$service][$instance_id] = array($remote_obj, $pass_to_prune);
637 } else {
638 $do_prune[$service]['default'] = array($remote_obj, $pass_to_prune);
639 }
640
641 // Check there are no errors in the uploads, if none then call upload_completed() if it exists otherwise mark as complete
642 if ($errors_before_uploads == $updraftplus->error_count()) {
643 if (is_callable(array($remote_obj, 'upload_completed'))) {
644 $result = $remote_obj->upload_completed();
645 if ($result) $updraftplus->mark_upload_complete($service);
646 } else {
647 $updraftplus->mark_upload_complete($service);
648 }
649 }
650 } else {
651 // We still need to make sure that prune is run on this remote storage method, even if all entities were previously uploaded
652 $do_prune[$service]['all'] = array($remote_obj, null);
653 }
654
655 return $do_prune;
656 }
657
658 /**
659 * Group the backup history into sets for retention processing and indicate the retention rule to apply to each group. This is a 'default' function which just puts them all in together.
660 *
661 * @param Array $backup_history
662 *
663 * @return Array
664 */
665 private function group_backups($backup_history) {
666 return array(array('sets' => $backup_history, 'process_order' => 'keep_newest'));
667 }
668
669 /**
670 * 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.
671 *
672 * @uses UpdraftPlus::log()
673 *
674 * @param String $message - the message to log
675 * @param String $level - the log level
676 */
677 private function log_with_db_occasionally($message, $level = 'notice') {
678 global $updraftplus;
679 static $last_db = false;
680
681 if (time() > $last_db + 3) {
682 $last_db = time();
683 $skip_dblog = false;
684 } else {
685 $skip_dblog = true;
686 }
687
688 return $updraftplus->log($message, $level, false, $skip_dblog);
689 }
690
691 /**
692 * Prunes historical backups, according to the user's settings
693 *
694 * @param Array $services - An associative array with list of services as key and remote object and boolean flag as values to prune on. This must be an array (i.e. it is not flexible like some other places)
695 *
696 * @return void
697 */
698 public function prune_retained_backups($services) {
699
700 global $updraftplus, $wpdb;
701
702 if ('' != $updraftplus->jobdata_get('remotesend_info')) {
703 $updraftplus->log("Prune old backups from local store: skipping, as this was a remote send operation");
704 return;
705 }
706
707 if (method_exists($wpdb, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
708 if (!$wpdb->check_connection(false)) {
709 UpdraftPlus_Job_Scheduler::reschedule(60);
710 $updraftplus->log('It seems the database went away; scheduling a resumption and terminating for now');
711 UpdraftPlus_Job_Scheduler::record_still_alive();
712 die;
713 }
714 }
715
716 // If they turned off deletion on local backups, then there is nothing to do
717 if (!UpdraftPlus_Options::get_updraft_option('updraft_delete_local', 1) && 1 == count($services) && array_key_exists('none', $services)) {
718 $updraftplus->log("Prune old backups from local store: nothing to do, since the user disabled local deletion and we are using local backups");
719 return;
720 }
721
722 $updraftplus->jobdata_set_multi(array('jobstatus' => 'pruning', 'prune' => 'begun'));
723
724 // Number of backups to retain - files
725 $updraft_retain = UpdraftPlus_Options::get_updraft_option('updraft_retain', 2);
726 $updraft_retain = is_numeric($updraft_retain) ? $updraft_retain : 1;
727
728 // Number of backups to retain - db
729 $updraft_retain_db = UpdraftPlus_Options::get_updraft_option('updraft_retain_db', $updraft_retain);
730 $updraft_retain_db = is_numeric($updraft_retain_db) ? $updraft_retain_db : 1;
731
732 $updraftplus->log("Retain: beginning examination of existing backup sets; user setting: retain_files=$updraft_retain, retain_db=$updraft_retain_db");
733
734 // Returns an array, most recent first, of backup sets
735 $backup_history = UpdraftPlus_Backup_History::get_history();
736
737 $ignored_because_imported = array();
738
739 // Remove non-native (imported) backups, which are neither counted nor pruned. It's neater to do these in advance, and log only one line.
740 $functional_backup_history = $backup_history;
741 foreach ($functional_backup_history as $backup_time => $backup_to_examine) {
742 if (isset($backup_to_examine['native']) && false == $backup_to_examine['native']) {
743 $ignored_because_imported[] = $backup_time;
744 unset($functional_backup_history[$backup_time]);
745 }
746 }
747 if (!empty($ignored_because_imported)) {
748 $updraftplus->log("These backup set(s) were imported from a remote location, so will not be counted or pruned. Skipping: ".implode(', ', $ignored_because_imported));
749 }
750
751 $backupable_entities = $updraftplus->get_backupable_file_entities(true);
752
753 $database_backups_found = array();
754
755 $file_entities_backups_found = array();
756 foreach ($backupable_entities as $entity => $info) {
757 $file_entities_backups_found[$entity] = 0;
758 }
759
760 if (false === ($backup_db_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'db'))) {
761 $backup_db_groups = $this->group_backups($functional_backup_history);
762 }
763 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (db): ".count($backup_db_groups));
764
765 foreach ($backup_db_groups as $group_id => $group) {
766
767 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
768
769 if (empty($group['sets']) || !is_array($group['sets'])) continue;
770 $sets = $group['sets'];
771
772 // Sort the groups into the desired "keep this first" order
773 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
774 if ('keep_oldest' == $process_order) ksort($sets);
775
776 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
777
778 foreach ($sets as $backup_datestamp => $backup_to_examine) {
779
780 $files_to_prune = array();
781 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
782
783 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
784 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
785 $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)));
786
787 // "Always Keep" Backups should be counted in the count of how many have been retained for purposes of the "how many to retain" count... but if that count is already matched, it's not a problem
788 $is_always_keep = !empty($backup_to_examine['always_keep']);
789
790 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
791 $is_autobackup = !empty($backup_to_examine['autobackup']);
792
793 $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;
794
795 $any_deleted_via_filter_yet = false;
796
797 // Databases
798 foreach ($backup_to_examine as $key => $data) {
799 if ('db' != strtolower(substr($key, 0, 2)) || '-size' == substr($key, -5, 5)) continue;
800
801 if (empty($database_backups_found[$key])) $database_backups_found[$key] = 0;
802
803 if ($nonce == $updraftplus->nonce || $nonce == $updraftplus->file_nonce) {
804 $this->log_with_db_occasionally("This backup set is the backup set just made, so will not be deleted.");
805 $database_backups_found[$key]++;
806 continue;
807 }
808
809 if ($is_always_keep) {
810 if ($database_backups_found[$key] < $updraft_retain) {
811 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, and we have not yet reached any retain limits, so it should be counted in the count of how many have been retained for purposes of the 'how many to retain' count. It will not be pruned. Skipping.");
812 $database_backups_found[$key]++;
813 } else {
814 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, so it will not be pruned. Skipping.");
815 }
816 continue;
817 }
818
819 if ($is_autobackup) {
820 if ($any_deleted_via_filter_yet) {
821 $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).");
822 $prune_it = true;
823 } elseif ($database_backups_found[$key] < $updraft_retain_db) {
824 $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.");
825 continue;
826 } else {
827 $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.");
828 $prune_it = true;
829 }
830 } else {
831 $prune_it = false;
832 }
833
834 if ($remote_sent) {
835 $prune_it = true;
836 $this->log_with_db_occasionally("$backup_datestamp: $key: was sent to remote site; will remove from local record (only)");
837 }
838
839 // 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.
840 $prune_it_before_filter = $prune_it;
841
842 if (!$is_autobackup) $prune_it = apply_filters('updraftplus_prune_or_not', $prune_it, 'db', $backup_datestamp, $key, $database_backups_found[$key], $rule, $group_id);
843
844 // Apply the final retention limit list (do not increase the 'retained' counter before seeing if the backup is being pruned for some other reason)
845 if (!$prune_it && !$is_autobackup) {
846
847 if ($database_backups_found[$key] + 1 > $updraft_retain_db) {
848 $prune_it = true;
849
850 $fname = is_string($data) ? $data : $data[0];
851 $this->log_with_db_occasionally("$backup_datestamp: $key: this set includes a database (".$fname."); db count is now ".$database_backups_found[$key]);
852
853 $this->log_with_db_occasionally("$backup_datestamp: $key: over retain limit ($updraft_retain_db); will delete this database");
854 }
855
856 }
857
858 if ($prune_it) {
859 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
860
861 if (!empty($data)) {
862 $size_key = $key.'-size';
863 $size = isset($backup_to_examine[$size_key]) ? $backup_to_examine[$size_key] : null;
864 foreach ($services as $service => $instance_ids_to_prune) {
865 foreach ($instance_ids_to_prune as $instance_id_to_prune => $sd) {
866 if ('none' != $service && '' != $service && $sd[0]->supports_feature('multi_options')) {
867 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($service));
868 if ('all' == $instance_id_to_prune) {
869 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $saved_instance_id => $options) {
870 $sd[0]->set_options($options, false, $saved_instance_id);
871 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
872 }
873 } else {
874 $opts = $storage_objects_and_ids[$service]['instance_settings'][$instance_id_to_prune];
875 $sd[0]->set_options($opts, false, $instance_id_to_prune);
876 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
877 }
878 } else {
879 $this->prune_file($service, $data, $sd[0], $sd[1], array($size));
880 }
881 }
882 }
883 }
884 unset($backup_to_examine[$key]);
885 UpdraftPlus_Job_Scheduler::record_still_alive();
886 } elseif (!$is_autobackup) {
887 $database_backups_found[$key]++;
888 }
889
890 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backupable_entities);
891 if (empty($backup_to_examine)) {
892 unset($functional_backup_history[$backup_datestamp]);
893 unset($backup_history[$backup_datestamp]);
894 $this->maybe_save_backup_history_and_reschedule($backup_history);
895 } else {
896 $functional_backup_history[$backup_datestamp] = $backup_to_examine;
897 $backup_history[$backup_datestamp] = $backup_to_examine;
898 }
899 }
900 }
901 }
902
903 if (false === ($backup_files_groups = apply_filters('updraftplus_group_backups_for_pruning', false, $functional_backup_history, 'files'))) {
904 $backup_files_groups = $this->group_backups($functional_backup_history);
905 }
906
907 $updraftplus->log("Number of backup sets in history: ".count($backup_history)."; groups (files): ".count($backup_files_groups));
908
909 // Now again - this time for the files
910 foreach ($backup_files_groups as $group_id => $group) {
911
912 // N.B. The array returned by UpdraftPlus_Backup_History::get_history() is already sorted, with most-recent first
913
914 if (empty($group['sets']) || !is_array($group['sets'])) continue;
915 $sets = $group['sets'];
916
917 // Sort the groups into the desired "keep this first" order
918 $process_order = (!empty($group['process_order']) && 'keep_oldest' == $group['process_order']) ? 'keep_oldest' : 'keep_newest';
919 // Youngest - i.e. smallest epoch - first
920 if ('keep_oldest' == $process_order) ksort($sets);
921
922 $rule = !empty($group['rule']) ? $group['rule'] : array('after-howmany' => 0, 'after-period' => 0, 'every-period' => 1, 'every-howmany' => 1);
923
924 foreach ($sets as $backup_datestamp => $backup_to_examine) {
925
926 $files_to_prune = array();
927 $nonce = empty($backup_to_examine['nonce']) ? '???' : $backup_to_examine['nonce'];
928
929 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
930 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
931 $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)));
932
933 // "Always Keep" Backups should be counted in the count of how many have been retained for purposes of the "how many to retain" count... but if that count is already matched, it's not a problem
934 $is_always_keep = !empty($backup_to_examine['always_keep']);
935
936 // Auto-backups are only counted or deleted once we have reached the retain limit - before that, they are skipped
937 $is_autobackup = !empty($backup_to_examine['autobackup']);
938
939 $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;
940
941 $any_deleted_via_filter_yet = false;
942
943 $file_sizes = array();
944
945 // Files
946 foreach ($backupable_entities as $entity => $info) {
947 if (!empty($backup_to_examine[$entity])) {
948
949 // This should only be able to happen if you import backups with a future timestamp
950 if ($nonce == $updraftplus->nonce || $nonce == $updraftplus->file_nonce) {
951 $updraftplus->log("This backup set is the backup set just made, so will not be deleted.");
952 $file_entities_backups_found[$entity]++;
953 continue;
954 }
955
956 if ($is_always_keep) {
957 if ($file_entities_backups_found[$entity] < $updraft_retain) {
958 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, and we have not yet reached any retain limits, so it should be counted in the count of how many have been retained for purposes of the 'how many to retain' count. It will not be pruned. Skipping.");
959 $file_entities_backups_found[$entity]++;
960 } else {
961 $this->log_with_db_occasionally("This backup set ($backup_datestamp) was an 'Always Keep' backup, so it will not be pruned. Skipping.");
962 }
963 continue;
964 }
965
966 if ($is_autobackup) {
967 if ($any_deleted_via_filter_yet) {
968 $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).");
969 $prune_it = true;
970 } elseif ($file_entities_backups_found[$entity] < $updraft_retain) {
971 $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.");
972 continue;
973 } else {
974 $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.");
975 $prune_it = true;
976 }
977 } else {
978 $prune_it = false;
979 }
980
981 if ($remote_sent) {
982 $prune_it = true;
983 }
984
985 // 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.
986 $prune_it_before_filter = $prune_it;
987 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);
988
989 // 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
990 if (!$prune_it && !$is_autobackup) {
991 if ($file_entities_backups_found[$entity] >= $updraft_retain) {
992 $this->log_with_db_occasionally("$entity: over retain limit ($updraft_retain); will delete this file entity");
993 $prune_it = true;
994 }
995 }
996
997 if ($prune_it) {
998 if (!$prune_it_before_filter) $any_deleted_via_filter_yet = true;
999 $prune_this = $backup_to_examine[$entity];
1000 if (is_string($prune_this)) $prune_this = array($prune_this);
1001
1002 foreach ($prune_this as $k => $prune_file) {
1003 if ($remote_sent) {
1004 $updraftplus->log("$entity: $backup_datestamp: was sent to remote site; will remove from local record (only)");
1005 }
1006 $size_key = (0 == $k) ? $entity.'-size' : $entity.$k.'-size';
1007 $size = (isset($backup_to_examine[$size_key])) ? $backup_to_examine[$size_key] : null;
1008 $files_to_prune[] = $prune_file;
1009 $file_sizes[] = $size;
1010 }
1011 unset($backup_to_examine[$entity]);
1012
1013 } elseif (!$is_autobackup) {
1014 $file_entities_backups_found[$entity]++;
1015 }
1016 }
1017 }
1018
1019 // 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.
1020 if (!empty($files_to_prune)) {
1021 // Actually delete the files
1022 foreach ($services as $service => $instance_ids_to_prune) {
1023 foreach ($instance_ids_to_prune as $instance_id_to_prune => $sd) {
1024 if ("none" != $service && '' != $service && $sd[0]->supports_feature('multi_options')) {
1025 $storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array($service));
1026 if ('all' == $instance_id_to_prune) {
1027 foreach ($storage_objects_and_ids[$service]['instance_settings'] as $saved_instance_id => $options) {
1028 $sd[0]->set_options($options, false, $saved_instance_id);
1029 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
1030 }
1031 } else {
1032 $opts = $storage_objects_and_ids[$service]['instance_settings'][$instance_id_to_prune];
1033 $sd[0]->set_options($opts, false, $instance_id_to_prune);
1034 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
1035 }
1036 } else {
1037 $this->prune_file($service, $files_to_prune, $sd[0], $sd[1], array($size));
1038 }
1039 UpdraftPlus_Job_Scheduler::record_still_alive();
1040 }
1041 }
1042 }
1043
1044 $backup_to_examine = $this->remove_backup_set_if_empty($backup_to_examine, $backupable_entities);
1045 if (empty($backup_to_examine)) {
1046 unset($backup_history[$backup_datestamp]);
1047 $this->maybe_save_backup_history_and_reschedule($backup_history);
1048 } else {
1049 $backup_history[$backup_datestamp] = $backup_to_examine;
1050 }
1051
1052 // Loop over backup sets
1053 }
1054
1055 // Look over backup groups
1056 }
1057
1058 $updraftplus->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
1059 UpdraftPlus_Backup_History::save_history($backup_history, false);
1060
1061 do_action('updraftplus_prune_retained_backups_finished');
1062
1063 $updraftplus->jobdata_set('prune', 'finished');
1064
1065 }
1066
1067 /**
1068 * 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)
1069 *
1070 * @param Array $backup_history - the backup history to possible save
1071 */
1072 private function maybe_save_backup_history_and_reschedule($backup_history) {
1073 static $last_saved_at = 0;
1074 if (!$last_saved_at) $last_saved_at = time();
1075 if (time() - $last_saved_at >= 10) {
1076 global $updraftplus;
1077 $updraftplus->log("Retain: saving new backup history, because at least 10 seconds have passed since the last save (sets now: ".count($backup_history).")");
1078 UpdraftPlus_Backup_History::save_history($backup_history, false);
1079 UpdraftPlus_Job_Scheduler::something_useful_happened();
1080 $last_saved_at = time();
1081 }
1082 }
1083
1084 /**
1085 * Examine a backup set; if it is empty (no files or DB), then remove the associated log file
1086 *
1087 * @param Array $backup_to_examine - backup set
1088 * @param Array $backupable_entities - compare with this list of backup entities
1089 *
1090 * @return Array|Boolean - if it was empty, false is returned
1091 */
1092 private function remove_backup_set_if_empty($backup_to_examine, $backupable_entities) {
1093
1094 global $updraftplus;
1095
1096 // Get new result, post-deletion; anything left in this set?
1097 $contains_files = 0;
1098 foreach ($backupable_entities as $entity => $info) {
1099 if (isset($backup_to_examine[$entity])) {
1100 $contains_files = 1;
1101 break;
1102 }
1103 }
1104
1105 $contains_db = 0;
1106 foreach ($backup_to_examine as $key => $data) {
1107 if ('db' == strtolower(substr($key, 0, 2)) && '-size' != substr($key, -5, 5)) {
1108 $contains_db = 1;
1109 break;
1110 }
1111 }
1112
1113 // Delete backup set completely if empty, o/w just remove DB
1114 // We search on the four keys which represent data, allowing other keys to be used to track other things
1115 if (!$contains_files && !$contains_db) {
1116 $updraftplus->log("This backup set is now empty; will remove from history");
1117 if (isset($backup_to_examine['nonce'])) {
1118 $fullpath = $this->updraft_dir."/log.".$backup_to_examine['nonce'].".txt";
1119 if (is_file($fullpath)) {
1120 $updraftplus->log("Deleting log file (log.".$backup_to_examine['nonce'].".txt)");
1121 @unlink($fullpath);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1122 } else {
1123 $updraftplus->log("Corresponding log file (log.".$backup_to_examine['nonce'].".txt) not found - must have already been deleted");
1124 }
1125 } else {
1126 $updraftplus->log("No nonce record found in the backup set, so cannot delete any remaining log file");
1127 }
1128 return false;
1129 } else {
1130 $updraftplus->log("This backup set remains non-empty (f=$contains_files/d=$contains_db); will retain in history");
1131 return $backup_to_examine;
1132 }
1133
1134 }
1135
1136 /**
1137 * Prune files from remote and local storage
1138 *
1139 * @param String $service Service to prune (one only)
1140 * @param Array|String $dofiles An array of files (or a single string for one file)
1141 * @param Array $method_object specific method object
1142 * @param Array $object_passback specific passback object
1143 * @param Array $file_sizes size of files
1144 */
1145 private function prune_file($service, $dofiles, $method_object = null, $object_passback = null, $file_sizes = array()) {
1146 global $updraftplus;
1147 if (!is_array($dofiles)) $dofiles = array($dofiles);
1148
1149 if (!apply_filters('updraftplus_prune_file', true, $dofiles, $service, $method_object, $object_passback, $file_sizes)) {
1150 $updraftplus->log("Prune: service=$service: skipped via filter");
1151 return;
1152 }
1153
1154 foreach ($dofiles as $dofile) {
1155 if (empty($dofile)) continue;
1156 $updraftplus->log("Delete file: $dofile, service=$service");
1157 $fullpath = $this->updraft_dir.'/'.$dofile;
1158 // delete it if it's locally available
1159 if (file_exists($fullpath)) {
1160 $updraftplus->log("Deleting local copy ($dofile)");
1161 unlink($fullpath);
1162 if (file_exists($fullpath.'.list.tmp')) {
1163 $updraftplus->log("Deleting zip manifest ({$dofile}.list.tmp)");
1164 unlink($fullpath.'.list.tmp');
1165 }
1166 }
1167 }
1168 // Despatch to the particular method's deletion routine
1169 if (!is_null($method_object)) $method_object->delete($dofiles, $object_passback, $file_sizes);
1170 }
1171
1172 /**
1173 * 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
1174 *
1175 * @param Array $a_arr First array to be compared
1176 * @param Array $b_arr Second array to be compared
1177 * @return Integer - according to the rules of usort()
1178 */
1179 private function backup_db_sorttables($a_arr, $b_arr) {
1180
1181 $a = $a_arr['name'];
1182 $a_table_type = $a_arr['type'];
1183 $b = $b_arr['name'];
1184 $b_table_type = $b_arr['type'];
1185
1186 // Views must always go after tables (since they can depend upon them)
1187 if ('VIEW' == $a_table_type && 'VIEW' != $b_table_type) return 1;
1188 if ('VIEW' == $b_table_type && 'VIEW' != $a_table_type) return -1;
1189
1190 if ('wp' != $this->whichdb) return strcmp($a, $b);
1191
1192 global $updraftplus;
1193 if ($a == $b) return 0;
1194 $our_table_prefix = $this->table_prefix_raw;
1195 if ($a == $our_table_prefix.'options') return -1;
1196 if ($b == $our_table_prefix.'options') return 1;
1197 if ($a == $our_table_prefix.'site') return -1;
1198 if ($b == $our_table_prefix.'site') return 1;
1199 if ($a == $our_table_prefix.'blogs') return -1;
1200 if ($b == $our_table_prefix.'blogs') return 1;
1201 if ($a == $our_table_prefix.'users') return -1;
1202 if ($b == $our_table_prefix.'users') return 1;
1203 if ($a == $our_table_prefix.'usermeta') return -1;
1204 if ($b == $our_table_prefix.'usermeta') return 1;
1205
1206 if (empty($our_table_prefix)) return strcmp($a, $b);
1207
1208 try {
1209 $core_tables = array_merge($this->wpdb_obj->tables, $this->wpdb_obj->global_tables, $this->wpdb_obj->ms_global_tables);
1210 } catch (Exception $e) {
1211 $updraftplus->log($e->getMessage());
1212 }
1213
1214 if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'termmeta', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions', 'blogmeta');
1215
1216 global $updraftplus;
1217 $na = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $a);
1218 $nb = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $b);
1219 if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
1220 if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
1221 return strcmp($a, $b);
1222 }
1223
1224 /**
1225 * Log the amount account space free/used, if possible
1226 */
1227 private function log_account_space() {
1228 global $updraftplus;
1229 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
1230 if (is_array($hosting_bytes_free)) {
1231 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
1232 $updraftplus->log(sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %"));
1233 }
1234 }
1235
1236 /**
1237 * Returns the basename up to and including the nonce (but not the entity)
1238 *
1239 * @param Integer $use_time epoch time to use
1240 * @return String
1241 */
1242 private function get_backup_file_basename_from_time($use_time) {
1243 global $updraftplus;
1244 return apply_filters('updraftplus_get_backup_file_basename_from_time', 'backup_'.get_date_from_gmt(gmdate('Y-m-d H:i:s', $use_time), 'Y-m-d-Hi').'_'.$this->site_name.'_'.$updraftplus->file_nonce, $use_time, $this->site_name);
1245 }
1246
1247 /**
1248 * Find the zip files in a given directory for a given nonce
1249 *
1250 * @param String $dir - directory to look in
1251 * @param Strign $match_nonce - backup ID to match
1252 *
1253 * @return Array
1254 */
1255 private function find_existing_zips($dir, $match_nonce) {
1256 $zips = array();
1257 if (!$handle = opendir($dir)) return $zips;
1258 while (false !== ($entry = readdir($handle))) {
1259 if ('.' == $entry || '..' == $entry) continue;
1260 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)) {
1261 if ($matches[6] !== $match_nonce) continue;
1262 $timestamp = mktime($matches[4], $matches[5], 0, $matches[2], $matches[3], $matches[1]);
1263 $entity = $matches[7];
1264 $index = empty($matches[8]) ? '0' : $matches[8];
1265 $zips[$entity][$index] = array($timestamp, $entry);
1266 }
1267 }
1268 return $zips;
1269 }
1270
1271 /**
1272 * Get information on whether a particular file exists in a set
1273 *
1274 * @param Array $files should be an array as returned by find_existing_zips()]
1275 * @param String $entity entty of the file (e.g. 'plugins')
1276 * @param Integer $index Index within the files array
1277 * @return String|Boolean - false if the file does not exist; otherwise, the basename
1278 */
1279 private function file_exists($files, $entity, $index = 0) {
1280 if (isset($files[$entity]) && isset($files[$entity][$index])) {
1281 $file = $files[$entity][$index];
1282 // Return the filename
1283 return $file[1];
1284 } else {
1285 return false;
1286 }
1287 }
1288
1289 /**
1290 * This function is resumable
1291 *
1292 * @param String $job_status Current status
1293 *
1294 * @return Array - array of backed-up files
1295 */
1296 private function backup_dirs($job_status) {
1297
1298 global $updraftplus;
1299
1300 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1301
1302 $use_time = $updraftplus->backup_time;
1303 $backup_file_basename = $this->get_backup_file_basename_from_time($use_time);
1304
1305 $backup_array = array();
1306
1307 // Was there a check-in last time? If not, then reduce the amount of data attempted
1308 if ('finished' != $job_status && $updraftplus->current_resumption >= 2) {
1309
1310 // 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)
1311
1312 // 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.
1313 // 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)
1314 if ($updraftplus->no_checkin_last_time || !$updraftplus->newresumption_scheduled || $updraftplus->resumption_scheduled_for_cleanup) {
1315 // 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.
1316 if ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2 || !$updraftplus->newresumption_scheduled) {
1317 $this->try_split = true;
1318 } elseif ($updraftplus->current_resumption <= 10) {
1319 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
1320 if ((int) $maxzipbatch < 1) $maxzipbatch = 26214400;
1321 $new_maxzipbatch = max(floor($maxzipbatch * 0.75), 20971520);
1322 if ($new_maxzipbatch < $maxzipbatch) {
1323 $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)");
1324 $updraftplus->jobdata_set('maxzipbatch', $new_maxzipbatch);
1325 $updraftplus->jobdata_set('maxzipbatch_ceiling', $new_maxzipbatch);
1326 }
1327 }
1328 }
1329 }
1330
1331 if ('finished' != $job_status && !UpdraftPlus_Filesystem_Functions::really_is_writable($this->updraft_dir)) {
1332 $updraftplus->log("Backup directory (".$this->updraft_dir.") is not writable, or does not exist");
1333 $updraftplus->log(sprintf(__("Backup directory (%s) is not writable, or does not exist.", 'updraftplus'), $this->updraft_dir), 'error');
1334 return array();
1335 }
1336
1337 $this->job_file_entities = $updraftplus->jobdata_get('job_file_entities');
1338
1339 // This is just used for the visual feedback (via the 'substatus' key)
1340 $which_entity = 0;
1341 // e.g. plugins, themes, uploads, others
1342 // $whichdir might be an array (if $youwhat is 'more')
1343
1344 // Returns an array (keyed off the entity) of ($timestamp, $filename) arrays
1345 $existing_zips = $this->find_existing_zips($this->updraft_dir, $updraftplus->file_nonce);
1346
1347 $possible_backups = $updraftplus->get_backupable_file_entities(true);
1348
1349 foreach ($possible_backups as $youwhat => $whichdir) {
1350
1351 if (!isset($this->job_file_entities[$youwhat])) {
1352 $updraftplus->log("No backup of $youwhat: excluded by user's options");
1353 continue;
1354 }
1355
1356 $index = (int) $this->job_file_entities[$youwhat]['index'];
1357 if (empty($index)) $index=0;
1358 $indextext = (0 == $index) ? '' : (1+$index);
1359
1360 $zip_file = $this->updraft_dir.'/'.$backup_file_basename.'-'.$youwhat.$indextext.'.zip';
1361
1362 // Split needed?
1363 $split_every = max((int) $updraftplus->jobdata_get('split_every'), 250);
1364
1365 if (false != ($existing_file = $this->file_exists($existing_zips, $youwhat, $index)) && filesize($this->updraft_dir.'/'.$existing_file) > $split_every*1048576) {
1366 $index++;
1367 $this->job_file_entities[$youwhat]['index'] = $index;
1368 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1369 }
1370
1371 // Populate prior parts of $backup_array, if we're on a subsequent zip file
1372 if ($index > 0) {
1373 for ($i=0; $i<$index; $i++) {
1374 $itext = (0 == $i) ? '' : ($i+1);
1375 // Get the previously-stored filename if possible (which should be always); failing that, base it on the current run
1376
1377 $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';
1378
1379 $backup_array[$youwhat][$i] = $zip_file;
1380 $z = $this->updraft_dir.'/'.$zip_file;
1381 $itext = (0 == $i) ? '' : $i;
1382
1383 $fs_key = $youwhat.$itext.'-size';
1384 if (file_exists($z)) {
1385 $backup_array[$fs_key] = filesize($z);
1386 } elseif (isset($this->backup_files_array[$fs_key])) {
1387 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1388 }
1389 }
1390 }
1391
1392 // I am not certain that all the conditions in here are possible. But there's no harm.
1393 if ('finished' == $job_status) {
1394 // Add the final part of the array
1395 if ($index > 0) {
1396 $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';
1397 $z = $this->updraft_dir.'/'.$zip_file;
1398 $fs_key = $youwhat.$index.'-size';
1399 $backup_array[$youwhat][$index] = $zip_file;
1400 if (file_exists($z)) {
1401 $backup_array[$fs_key] = filesize($z);
1402 } elseif (isset($this->backup_files_array[$fs_key])) {
1403 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1404 }
1405 } else {
1406 $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';
1407
1408 $backup_array[$youwhat] = $zip_file;
1409 $fs_key=$youwhat.'-size';
1410
1411 if (file_exists($zip_file)) {
1412 $backup_array[$fs_key] = filesize($zip_file);
1413 } elseif (isset($this->backup_files_array[$fs_key])) {
1414 $backup_array[$fs_key] = $this->backup_files_array[$fs_key];
1415 }
1416 }
1417 } else {
1418
1419 $which_entity++;
1420 $updraftplus->jobdata_set('filecreating_substatus', array('e' => $youwhat, 'i' => $which_entity, 't' => count($this->job_file_entities)));
1421
1422 if ('others' == $youwhat) $updraftplus->log("Beginning backup of other directories found in the content directory (index: $index)");
1423
1424 // Apply a filter to allow add-ons to provide their own method for creating a zip of the entity
1425 $created = apply_filters('updraftplus_backup_makezip_'.$youwhat, $whichdir, $backup_file_basename, $index);
1426
1427 // If the filter did not lead to something being created, then use the default method
1428 if ($created === $whichdir) {
1429
1430 // http://www.phpconcept.net/pclzip/user-guide/53
1431 /* First parameter to create is:
1432 An array of filenames or dirnames,
1433 or
1434 A string containing the filename or a dirname,
1435 or
1436 A string containing a list of filename or dirname separated by a comma.
1437 */
1438
1439 if ('others' == $youwhat) {
1440 $dirlist = $updraftplus->backup_others_dirlist(true);
1441 } elseif ('uploads' == $youwhat) {
1442 $dirlist = $updraftplus->backup_uploads_dirlist(true);
1443 } else {
1444 $dirlist = $whichdir;
1445 if (is_array($dirlist)) $dirlist = array_shift($dirlist);
1446 }
1447
1448 if (!empty($dirlist)) {
1449 $created = $this->create_zip($dirlist, $youwhat, $backup_file_basename, $index);
1450 // Now, store the results
1451 if (!is_string($created) && !is_array($created)) $updraftplus->log("$youwhat: create_zip returned an error");
1452 } else {
1453 $updraftplus->log("No backup of $youwhat: there was nothing found to backup");
1454 }
1455 }
1456
1457 if ($created != $whichdir && (is_string($created) || is_array($created))) {
1458 if (is_string($created)) $created =array($created);
1459 foreach ($created as $fname) {
1460 $backup_array[$youwhat][$index] = $fname;
1461 $itext = (0 == $index) ? '' : $index;
1462 $index++;
1463 // File may have already been uploaded so don't try to get the size
1464 if (file_exists($this->updraft_dir.'/'.$fname)) $backup_array[$youwhat.$itext.'-size'] = filesize($this->updraft_dir.'/'.$fname);
1465 }
1466 }
1467
1468 $this->job_file_entities[$youwhat]['index'] = $this->index;
1469 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
1470
1471 }
1472 }
1473
1474 return $backup_array;
1475 }
1476
1477 /**
1478 * 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.
1479 *
1480 * @param Integer $resumption_no Check for first run
1481 *
1482 * @return Array
1483 */
1484 public function resumable_backup_of_files($resumption_no) {
1485 global $updraftplus;
1486 // Backup directories and return a numerically indexed array of file paths to the backup files
1487 $bfiles_status = $updraftplus->jobdata_get('backup_files');
1488 $this->backup_files_array = $updraftplus->jobdata_get('backup_files_array');
1489
1490 if (!is_array($this->backup_files_array)) $this->backup_files_array = array();
1491 if ('finished' == $bfiles_status) {
1492 $updraftplus->log("Creation of backups of directories: already finished");
1493 // Check for recent activity
1494 foreach ($this->backup_files_array as $files) {
1495 if (!is_array($files)) $files =array($files);
1496 foreach ($files as $file) $updraftplus->check_recent_modification($this->updraft_dir.'/'.$file);
1497 }
1498 } elseif ('begun' == $bfiles_status) {
1499 $this->first_run = apply_filters('updraftplus_filerun_firstrun', 0);
1500 if ($resumption_no > $this->first_run) {
1501 $updraftplus->log("Creation of backups of directories: had begun; will resume");
1502 } else {
1503 $updraftplus->log("Creation of backups of directories: beginning");
1504 }
1505 $updraftplus->jobdata_set('jobstatus', 'filescreating');
1506 $this->backup_files_array = $this->backup_dirs($bfiles_status);
1507 $updraftplus->jobdata_set('backup_files_array', $this->backup_files_array);
1508 $updraftplus->jobdata_set('backup_files', 'finished');
1509 $updraftplus->jobdata_set('jobstatus', 'filescreated');
1510 } else {
1511 // This is not necessarily a backup run which is meant to contain files at all
1512 $updraftplus->log('This backup run is not intended for files - skipping');
1513 return array();
1514 }
1515
1516 /*
1517 // DOES NOT WORK: there is no crash-safe way to do this here - have to be renamed at cloud-upload time instead
1518 $new_backup_array = array();
1519 foreach ($backup_array as $entity => $files) {
1520 if (!is_array($files)) $files=array($files);
1521 $outof = count($files);
1522 foreach ($files as $ind => $file) {
1523 $nval = $file;
1524 if (preg_match('/^(backup_[\-0-9]{15}_.*_[0-9a-f]{12}-[\-a-z]+)([0-9]+)?\.zip$/i', $file, $matches)) {
1525 $num = max((int)$matches[2],1);
1526 $new = $matches[1].$num.'of'.$outof.'.zip';
1527 if (file_exists($this->updraft_dir.'/'.$file)) {
1528 if (@rename($this->updraft_dir.'/'.$file, $this->updraft_dir.'/'.$new)) {
1529 $updraftplus->log(sprintf("Renaming: %s to %s", $file, $new));
1530 $nval = $new;
1531 }
1532 } elseif (file_exists($this->updraft_dir.'/'.$new)) {
1533 $nval = $new;
1534 }
1535 }
1536 $new_backup_array[$entity][$ind] = $nval;
1537 }
1538 }
1539 */
1540 return $this->backup_files_array;
1541 }
1542
1543 /**
1544 * This function is resumable, using the following method:
1545 * Each table is written out to ($final_filename).table.tmp
1546 * When the writing finishes, it is renamed to ($final_filename).table
1547 * When all tables are finished, they are concatenated into the final file
1548 *
1549 * @param String $already_done Status of backup
1550 * @param String $whichdb Indicated which database is being backed up
1551 * @param Array $dbinfo is only used when whichdb != 'wp'; and the keys should be: user, pass, name, host, prefix
1552 *
1553 * @return Boolean|String - the basename of the database backup, or false for failure
1554 */
1555 public function backup_db($already_done = 'begun', $whichdb = 'wp', $dbinfo = array()) {
1556
1557 global $updraftplus, $wpdb;
1558
1559 $this->whichdb = $whichdb;
1560 $this->whichdb_suffix = ('wp' == $whichdb) ? '' : $whichdb;
1561
1562 if (!$updraftplus->backup_time) $updraftplus->backup_time_nonce();
1563 if (!$updraftplus->opened_log_time) $updraftplus->logfile_open($updraftplus->nonce);
1564
1565 if ('wp' == $this->whichdb) {
1566 $this->wpdb_obj = $wpdb;
1567 // The table prefix after being filtered - i.e. what filters what we'll actually backup
1568 $this->table_prefix = $updraftplus->get_table_prefix(true);
1569 // The unfiltered table prefix - i.e. the real prefix that things are relative to
1570 $this->table_prefix_raw = $updraftplus->get_table_prefix(false);
1571 $dbinfo['host'] = DB_HOST;
1572 $dbinfo['name'] = DB_NAME;
1573 $dbinfo['user'] = DB_USER;
1574 $dbinfo['pass'] = DB_PASSWORD;
1575 } else {
1576 if (!is_array($dbinfo) || empty($dbinfo['host'])) return false;
1577 // The methods that we may use: check_connection (WP>=3.9), get_results, get_row, query
1578 $this->wpdb_obj = new UpdraftPlus_WPDB_OtherDB($dbinfo['user'], $dbinfo['pass'], $dbinfo['name'], $dbinfo['host']);
1579 if (!empty($this->wpdb_obj->error)) {
1580 $updraftplus->log($dbinfo['user'].'@'.$dbinfo['host'].'/'.$dbinfo['name'].' : database connection attempt failed');
1581 $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');
1582 return $updraftplus->log_wp_error($this->wpdb_obj->error);
1583 }
1584 $this->table_prefix = $dbinfo['prefix'];
1585 $this->table_prefix_raw = $dbinfo['prefix'];
1586 }
1587
1588 $this->dbinfo = $dbinfo;
1589
1590 do_action('updraftplus_backup_db_begin', $whichdb, $dbinfo, $already_done, $this);
1591
1592 UpdraftPlus_Database_Utility::set_sql_mode(array(), array('ANSI_QUOTES'), $this->wpdb_obj);
1593
1594 $errors = 0;
1595
1596 $use_time = apply_filters('updraftplus_base_backup_timestamp', $updraftplus->backup_time);
1597 $file_base = $this->get_backup_file_basename_from_time($use_time);
1598 $backup_file_base = $this->updraft_dir.'/'.$file_base;
1599
1600 if ('finished' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz';
1601 if ('encrypted' == $already_done) return basename($backup_file_base).'-db'.(('wp' == $whichdb) ? '' : $whichdb).'.gz.crypt';
1602
1603 $updraftplus->jobdata_set('jobstatus', 'dbcreating'.$this->whichdb_suffix);
1604
1605 $binsqldump = $updraftplus->find_working_sqldump();
1606
1607 $total_tables = 0;
1608
1609 // 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
1610 if ('wp' == $whichdb && method_exists($this->wpdb_obj, 'check_connection') && (!defined('UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS') || !UPDRAFTPLUS_SUPPRESS_CONNECTION_CHECKS)) {
1611 if (!$this->wpdb_obj->check_connection(false)) {
1612 UpdraftPlus_Job_Scheduler::reschedule(60);
1613 $updraftplus->log("It seems the database went away; scheduling a resumption and terminating for now");
1614 UpdraftPlus_Job_Scheduler::record_still_alive();
1615 die;
1616 }
1617 }
1618
1619 // SHOW FULL - so that we get to know whether it's a BASE TABLE or a VIEW
1620 $all_tables = $this->wpdb_obj->get_results("SHOW FULL TABLES", ARRAY_N);
1621
1622 if (empty($all_tables) && !empty($this->wpdb_obj->last_error)) {
1623 $all_tables = $this->wpdb_obj->get_results("SHOW TABLES", ARRAY_N);
1624 $all_tables = array_map(array($this, 'cb_get_name_base_type'), $all_tables);
1625 } else {
1626 $all_tables = array_map(array($this, 'cb_get_name_type'), $all_tables);
1627 }
1628
1629 // If this is not the WP database, then we do not consider it a fatal error if there are no tables
1630 if ('wp' == $whichdb && 0 == count($all_tables)) {
1631 $extra = ($updraftplus->newresumption_scheduled) ? ' - '.__('please wait for the rescheduled attempt', 'updraftplus') : '';
1632 $updraftplus->log("Error: No WordPress database tables found (SHOW TABLES returned nothing)".$extra);
1633 $updraftplus->log(__("No database tables found", 'updraftplus').$extra, 'error');
1634 die;
1635 }
1636
1637 // Put the options table first
1638 usort($all_tables, array($this, 'backup_db_sorttables'));
1639
1640 $all_table_names = array_map(array($this, 'cb_get_name'), $all_tables);
1641
1642 if (!UpdraftPlus_Filesystem_Functions::really_is_writable($this->updraft_dir)) {
1643 $updraftplus->log("The backup directory (".$this->updraft_dir.") could not be written to (could be account/disk space full, or wrong permissions).");
1644 $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');
1645 // Why not just fail now? We saw a bizarre case when the results of really_is_writable() changed during the run.
1646 }
1647
1648 // 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
1649 $this->duplicate_tables_exist = false;
1650 foreach ($all_table_names as $table) {
1651 if (strtolower($table) != $table && in_array(strtolower($table), $all_table_names)) {
1652 $this->duplicate_tables_exist = true;
1653 $updraftplus->log("Tables with names differing only based on case-sensitivity exist in the MySQL database: $table / ".strtolower($table));
1654 }
1655 }
1656 $how_many_tables = count($all_tables);
1657
1658 $stitch_files = array();
1659 $found_options_table = false;
1660 $is_multisite = is_multisite();
1661
1662 $anonymisation_options = $updraftplus->jobdata_get('anonymisation_options', array());
1663
1664 if (!empty($anonymisation_options)) {
1665 $updraftplus->log("Anonymisation options have been set, so mysqldump (which does not support them) will be disabled.");
1666 }
1667
1668 // Gather the list of files that look like partial table files once only
1669 $potential_stitch_files = array();
1670 $table_file_prefix_base= $file_base.'-db'.$this->whichdb_suffix.'-table-';
1671 if (false !== ($dir_handle = opendir($this->updraft_dir))) {
1672 while (false !== ($e = readdir($dir_handle))) {
1673 // The 'r' in 'tmpr' indicates that the new scheme is being used. N.B. That does *not* imply that the table has a usable primary key.
1674 if (!is_file($this->updraft_dir.'/'.$e)) continue;
1675 if (preg_match('#'.$table_file_prefix_base.'.*\.table\.tmpr?(\d+)\.gz$#', $e, $matches)) {
1676 // We need to stich them in order
1677 $potential_stitch_files[] = $e;
1678 }
1679 }
1680 } else {
1681 $updraftplus->log("Error: Failed to open directory for reading");
1682 $updraftplus->log(__("Failed to open directory for reading:", 'updraftplus').' '.$this->updraft_dir, 'error');
1683 }
1684
1685 $errors_at_all_tables_start = $updraftplus->error_count();
1686
1687 foreach ($all_tables as $ti) {
1688
1689 $table = $ti['name'];
1690 $stitch_files[$table] = array();
1691 $table_type = $ti['type'];
1692 $errors_at_table_start = $updraftplus->error_count();
1693
1694 $this->many_rows_warning = false;
1695 $total_tables++;
1696
1697 // Increase script execution time-limit to 15 min for every table.
1698 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1699 // The table file may already exist if we have produced it on a previous run
1700 $table_file_prefix = $file_base.'-db'.$this->whichdb_suffix.'-table-'.$table.'.table';
1701
1702 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;
1703
1704 // Already finished?
1705 if (file_exists($this->updraft_dir.'/'.$table_file_prefix.'.gz')) {
1706 $stitched = count($stitch_files, COUNT_RECURSIVE);
1707 $skip_dblog = (($stitched > 10 && 0 != $stitched % 20) || ($stitched > 100 && 0 != $stitched % 100));
1708 $updraftplus->log("Table $table: corresponding file already exists; moving on", 'notice', false, $skip_dblog);
1709
1710 $max_record = false;
1711 foreach ($potential_stitch_files as $e) {
1712 // The 'r' in 'tmpr' indicates that the new scheme is being used. N.B. That does *not* imply that the table has a usable primary key.
1713 if (preg_match('#'.$table_file_prefix.'\.tmpr?(\d+)\.gz$#', $e, $matches)) {
1714 // We need to stich them in order
1715 $stitch_files[$table][$matches[1]] = $e;
1716 if (false === $max_record || $matches[1] > $max_record) $max_record = $matches[1];
1717 }
1718 }
1719 $stitch_files[$table][$max_record+1] = $table_file_prefix.'.gz';
1720
1721 // Move on to the next table
1722 continue;
1723 }
1724
1725 // === is needed with strpos/stripos, otherwise 'false' matches (i.e. prefix does not match)
1726 if (empty($this->table_prefix) || (!$this->duplicate_tables_exist && 0 === stripos($table, $this->table_prefix)) || ($this->duplicate_tables_exist && 0 === strpos($table, $this->table_prefix))) {
1727
1728 // Skip table due to filter?
1729 if (!apply_filters('updraftplus_backup_table', true, $table, $this->table_prefix, $whichdb, $dbinfo)) {
1730 $updraftplus->log("Skipping table (filtered): $table");
1731 if (empty($this->skipped_tables)) $this->skipped_tables = array();
1732
1733 // whichdb could be an int in which case to get the name of the database and the array key use the name from dbinfo
1734 $key = ('wp' === $whichdb) ? 'wp' : $dbinfo['name'];
1735
1736 if (empty($this->skipped_tables[$key])) $this->skipped_tables[$key] = array();
1737 $this->skipped_tables[$key][] = $table;
1738
1739 $total_tables--;
1740 continue;
1741 }
1742
1743 add_filter('updraftplus_backup_table_sql_where', array($this, 'backup_exclude_jobdata'), 3, 10);
1744
1745 $updraftplus->jobdata_set('dbcreating_substatus', array('t' => $table, 'i' => $total_tables, 'a' => $how_many_tables));
1746
1747 // .tmp.gz is the current temporary file. When the row limit has been reached, it is moved to .tmp1.gz, .tmp2.gz, etc. (depending on which already exist). When we're all done, then they all get stitched in.
1748
1749 $db_temp_file = $this->updraft_dir.'/'.$table_file_prefix.'.tmp.gz';
1750 $updraftplus->check_recent_modification($db_temp_file);
1751
1752 // Open file, store the handle
1753 if (false === $this->backup_db_open($db_temp_file, true)) return false;
1754
1755 $table_status = $this->wpdb_obj->get_row("SHOW TABLE STATUS WHERE Name='$table'");
1756
1757 // Create the preceding SQL statements for the table
1758 $this->stow("# " . sprintf('Table: %s', UpdraftPlus_Manipulation_Functions::backquote($table)) . "\n");
1759
1760 // Meaning: false = don't yet know; true = know and have logged it; integer = the expected number
1761 $this->expected_rows = false;
1762
1763 if (isset($table_status->Rows)) {
1764 $this->expected_rows = $table_status->Rows;
1765 }
1766
1767 // 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??)
1768
1769 // 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
1770 $bindump_threshold = (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && (2 == $updraftplus->current_resumption - $updraftplus->last_successful_resumption)) ? 1000 : 8000;
1771
1772 if (isset($table_status->Rows) && ($table_status->Rows > $bindump_threshold || (defined('UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP') && UPDRAFTPLUS_ALWAYS_TRY_MYSQLDUMP)) && is_string($binsqldump) && empty($anonymisation_options)) {
1773 if (!is_bool($this->expected_rows)) {
1774 $this->log_expected_rows($table, $this->expected_rows);
1775 $this->expected_rows = true;
1776 }
1777 $bindump = $this->backup_table_bindump($binsqldump, $table);
1778 } else {
1779 $bindump = false;
1780 }
1781
1782 // Means "start of table". N.B. The meaning of an integer depends upon whether the table has a usable primary key or not.
1783 $start_record = true;
1784 $can_use_primary_key = apply_filters('updraftplus_can_use_primary_key_default', true, $table);
1785 foreach ($potential_stitch_files as $e) {
1786 // The 'r' in 'tmpr' indicates that the new scheme is being used. N.B. That does *not* imply that the table has a usable primary key.
1787 if (preg_match('#'.$table_file_prefix.'\.tmp(r)?(\d+)\.gz$#', $e, $matches)) {
1788 $stitch_files[$table][$matches[2]] = $e;
1789 if (true === $start_record || $matches[2] > $start_record) $start_record = $matches[2];
1790 // Legacy scheme. The purpose of this is to prevent backups failing if one is in progress during an upgrade to a new version that implements the new scheme
1791 if ('r' !== $matches[1]) $can_use_primary_key = false;
1792 }
1793 }
1794
1795 // Legacy file-naming scheme in use
1796 if (false === $can_use_primary_key && true !== $start_record) {
1797 $start_record = ($start_record + 100) * 1000;
1798 }
1799
1800 if (true !== $bindump) {
1801
1802 while (!is_array($start_record) && !is_wp_error($start_record)) {
1803 $start_record = $this->backup_table($table, $table_type, $start_record, $can_use_primary_key);
1804 if (is_integer($start_record) || is_array($start_record)) {
1805
1806 $this->backup_db_close();
1807
1808 // Add one here in case no records were returned - don't want to over-write the previous file
1809 $use_record = is_array($start_record) ? (isset($start_record['next_record']) ? $start_record['next_record']+1 : false) : $start_record;
1810 if (!$can_use_primary_key) $use_record = (ceil($use_record/100000)-1) * 100;
1811
1812 if (false !== $use_record) {
1813 // N.B. Renaming using the *next* record is intentional - it allows UD to know where to resume from.
1814 $rename_base = $table_file_prefix.'.tmp'.($can_use_primary_key ? 'r' : '').$use_record.'.gz';
1815
1816 rename($db_temp_file, $this->updraft_dir.'/'.$rename_base);
1817 $stitch_files[$table][$use_record] = $rename_base;
1818 } elseif (is_array($start_record) && 'view' == strtolower($table_type)) {
1819 $rename_base = $table_file_prefix.'-view.tmp.gz';
1820 rename($db_temp_file, $this->updraft_dir.'/'.$rename_base);
1821 $stitch_files[$table][] = $rename_base;
1822 }
1823
1824 UpdraftPlus_Job_Scheduler::something_useful_happened();
1825
1826 if (false === $this->backup_db_open($db_temp_file, true)) return false;
1827
1828 } elseif (is_wp_error($start_record)) {
1829 $message = "Error (table=$table, type=$table_type) (".$start_record->get_error_code()."): ".$start_record->get_error_message();
1830 $updraftplus->log($message);
1831 // If it's a view, then the problem isn't recoverable; but views don't contain actual data except in the definition, which is likely in code, so we should not consider this a fatal error
1832 $level = 'error';
1833 if ('view' == strtolower($table_type) && 'table_details_error' == $start_record->get_error_code()) {
1834 $level = 'warning';
1835 $this->stow("# $message\n");
1836 }
1837 $updraftplus->log(__("Failed to backup database table:", 'updraftplus').' '.$start_record->get_error_message().' ('.$start_record->get_error_code().')', $level);
1838 }
1839
1840 }
1841 }
1842
1843 // If we got this far, then there were enough resources; the warning can be removed
1844 if (!empty($this->many_rows_warning)) $updraftplus->log_remove_warning('manyrows_'.$this->whichdb_suffix.$table);
1845
1846 $this->backup_db_close();
1847
1848 if ($updraftplus->error_count() > $errors_at_table_start) {
1849
1850 $updraftplus->log('Errors occurred during backing up the table; therefore the open file will be removed');
1851 @unlink($db_temp_file); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1852
1853 } else {
1854
1855 // Renaming the file indicates that writing to it finished
1856 rename($db_temp_file, $this->updraft_dir.'/'.$table_file_prefix.'.gz');
1857 UpdraftPlus_Job_Scheduler::something_useful_happened();
1858
1859 $final_stitch_value = empty($stitch_files[$table]) ? 1 : max(array_keys($stitch_files[$table])) + 1;
1860
1861 $stitch_files[$table][$final_stitch_value] = $table_file_prefix.'.gz';
1862
1863 $total_db_size = 0;
1864 // This is more verbose than it would be if we weren't supporting PHP 5.2
1865 foreach ($stitch_files[$table] as $basename) {
1866 $total_db_size += filesize($this->updraft_dir.'/'.$basename);
1867 }
1868
1869 $updraftplus->log("Table $table: finishing file(s) (".count($stitch_files[$table]).', '.round($total_db_size/1024, 1).' KB)', 'notice', false, false);
1870 }
1871
1872 } else {
1873 $total_tables--;
1874 $updraftplus->log("Skipping table (lacks our prefix (".$this->table_prefix.")): $table");
1875 if (empty($this->skipped_tables)) $this->skipped_tables = array();
1876 // whichdb could be an int in which case to get the name of the database and the array key use the name from dbinfo
1877 $key = ('wp' === $whichdb) ? 'wp' : $dbinfo['name'];
1878 if (empty($this->skipped_tables[$key])) $this->skipped_tables[$key] = array();
1879 $this->skipped_tables[$key][] = $table;
1880 }
1881 }
1882
1883 if ('wp' == $whichdb) {
1884 if (!$found_options_table) {
1885 if ($is_multisite) {
1886 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('no options or sitemeta table was found', 'updraftplus'), 'warning', 'optstablenotfound');
1887 } else {
1888 $updraftplus->log(__('The database backup appears to have failed', 'updraftplus').' - '.__('the options table was not found', 'updraftplus'), 'warning', 'optstablenotfound');
1889 }
1890 $time_this_run = time()-$updraftplus->opened_log_time;
1891 if ($time_this_run > 2000) {
1892 // 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.
1893 // If we have been running that long, then the resumption may be far off; bring it closer
1894 UpdraftPlus_Job_Scheduler::reschedule(60);
1895 $updraftplus->log("Have been running very long, and it seems the database went away; scheduling a resumption and terminating for now");
1896 UpdraftPlus_Job_Scheduler::record_still_alive();
1897 die;
1898 }
1899 } else {
1900 $updraftplus->log_remove_warning('optstablenotfound');
1901 }
1902 }
1903
1904 if ($updraftplus->error_count() > $errors_at_all_tables_start) {
1905 $updraftplus->log('Errors occurred whilst backing up the tables; will cease and wait for resumption');
1906 die;
1907 }
1908
1909 // Race detection - with zip files now being resumable, these can more easily occur, with two running side-by-side
1910 $backup_final_file_name = $backup_file_base.'-db'.$this->whichdb_suffix.'.gz';
1911 $time_now = time();
1912 $time_mod = (int) @filemtime($backup_final_file_name);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1913 if (file_exists($backup_final_file_name) && $time_mod>100 && ($time_now-$time_mod)<30) {
1914 UpdraftPlus_Job_Scheduler::terminate_due_to_activity($backup_final_file_name, $time_now, $time_mod);
1915 }
1916
1917 if (file_exists($backup_final_file_name)) {
1918 $updraftplus->log("The final database file ($backup_final_file_name) exists, but was 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.");
1919 }
1920
1921 // Finally, stitch the files together
1922 if (!function_exists('gzopen')) {
1923 $updraftplus->log("PHP function is disabled; abort expected: gzopen()");
1924 }
1925
1926 $decompress_mode = (defined('UPDRAFTPLUS_DB_STICH_DECOMPRESS') && UPDRAFTPLUS_DB_STICH_DECOMPRESS);
1927
1928 if (false === $this->backup_db_open($backup_final_file_name, true)) return false;
1929
1930 $this->backup_db_header();
1931
1932 // Re-open in plain binary append mode
1933 if (!$decompress_mode) {
1934 $this->backup_db_close();
1935 if (false === $this->backup_db_open($backup_final_file_name, false, true)) return false;
1936 }
1937
1938 // 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 would lead to files missing from the db dump
1939 $unlink_files = array();
1940
1941 $sind = 1;
1942
1943 // Happily they have the same syntax (as far as we need it)
1944 // Concatenating gz files produces a valid gz file. So, decompressing is not necessary. We retain the possibility for debugging and the possibility of broken implementations.
1945 $open_function = $decompress_mode ? 'gzopen' : 'fopen';
1946 $fgets_function = $decompress_mode ? 'gzgets' : 'fgets';
1947 $close_function = $decompress_mode ? 'gzclose' : 'fclose';
1948
1949 foreach ($stitch_files as $table => $table_stitch_files) {
1950 ksort($table_stitch_files);
1951 foreach ($table_stitch_files as $table_file) {
1952 $updraftplus->log("{$table_file} ($sind/$how_many_tables/$open_function): adding to final database dump");
1953
1954 if (filesize($this->updraft_dir.'/'.$table_file) < 27 && '.gz' == substr($table_file, -3, 3)) {
1955 // It's a null gzip file. Don't waste time on gzopen/gzgets/gzclose. This micro-optimisation was added after seeing a site with >3000 files that was running out of time (it could apparently process 30 files/second)
1956 $unlink_files[] = $this->updraft_dir.'/'.$table_file;
1957 } elseif (!$handle = call_user_func($open_function, $this->updraft_dir.'/'.$table_file, 'r')) {
1958 $updraftplus->log("Error: Failed to open database file for reading: ${table_file}");
1959 $updraftplus->log(__("Failed to open database file for reading:", 'updraftplus').' '.$table_file, 'error');
1960 $errors++;
1961 } else {
1962 while ($line = call_user_func($fgets_function, $handle, 65536)) {
1963 $this->stow($line);
1964 }
1965 call_user_func($close_function, $handle);
1966 $unlink_files[] = $this->updraft_dir.'/'.$table_file;
1967 }
1968 $sind++;
1969 // 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
1970 if (0 == $sind % 100) UpdraftPlus_Job_Scheduler::something_useful_happened();
1971 }
1972 }
1973
1974 // Re-open in gz append mode
1975 if (!$decompress_mode) {
1976 $this->backup_db_close();
1977 if (false === $this->backup_db_open($backup_final_file_name, true, true)) return false;
1978 }
1979
1980 // DB triggers
1981 if ($this->wpdb_obj->get_results("SHOW TRIGGERS")) {
1982 // N.B. DELIMITER is not a valid SQL command; you cannot pass it to the server. It has to be interpreted by the interpreter - e.g. /usr/bin/mysql, or UpdraftPlus, and used to interpret what follows. The effect of this is that using it means that some SQL clients will stumble; but, on the other hand, failure to use it means that others that don't have special support for CREATE TRIGGER may stumble, because they may feed incomplete statements to the SQL server. Since /usr/bin/mysql uses it, we choose to support it too (both reading and writing).
1983 // Whatever the delimiter is set to needs to be used in the DROP TRIGGER and CREATE TRIGGER commands in this section further down.
1984 $this->stow("DELIMITER ;;\n\n");
1985 foreach ($all_tables as $ti) {
1986 $table = $ti['name'];
1987 if (!empty($this->skipped_tables)) {
1988 if ('wp' == $this->whichdb) {
1989 if (in_array($table, $this->skipped_tables['wp'])) continue;
1990 } elseif (isset($this->skipped_tables[$this->dbinfo['name']])) {
1991 if (in_array($table, $this->skipped_tables[$this->dbinfo['name']])) continue;
1992 }
1993 }
1994 $table_triggers = $this->wpdb_obj->get_results($wpdb->prepare("SHOW TRIGGERS LIKE %s", $table), ARRAY_A);
1995 if ($table_triggers) {
1996 $this->stow("\n\n# Triggers of ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
1997 foreach ($table_triggers as $trigger) {
1998 $trigger_name = $trigger['Trigger'];
1999 $trigger_time = $trigger['Timing'];
2000 $trigger_event = $trigger['Event'];
2001 $trigger_statement = $trigger['Statement'];
2002 // Since trigger name can include backquotes and trigger name is typically enclosed with backquotes as well, the backquote escaping for the trigger name can be done by adding a leading backquote
2003 $quoted_escaped_trigger_name = UpdraftPlus_Manipulation_Functions::backquote(str_replace('`', '``', $trigger_name));
2004 $this->stow("DROP TRIGGER IF EXISTS $quoted_escaped_trigger_name;;\n");
2005 $trigger_query = "CREATE TRIGGER $quoted_escaped_trigger_name $trigger_time $trigger_event ON ".UpdraftPlus_Manipulation_Functions::backquote($table)." FOR EACH ROW $trigger_statement;;";
2006 $this->stow("$trigger_query\n\n");
2007 }
2008 }
2009 }
2010 $this->stow("DELIMITER ;\n\n");
2011 }
2012
2013 // DB Stored Routines
2014 $stored_routines = UpdraftPlus_Database_Utility::get_stored_routines();
2015 if (is_array($stored_routines) && !empty($stored_routines)) {
2016 $stored_routines_log = $inaccessible_routines = '';
2017 $stored_routines_delimiter_printed = false;
2018 $updraftplus->log("Dumping routines for database {$this->dbinfo['name']}");
2019 foreach ($stored_routines as $routine) {
2020 if (empty($routine['Create '.ucfirst(strtolower($routine['Type']))])) { // The value displayed for the Create Procedure or Create Function field is NULL if the WP database user has only CREATE ROUTINE, ALTER ROUTINE, and/or EXECUTE privileges. @see https://dev.mysql.com/doc/refman/8.0/en/show-create-procedure.html
2021 $inaccessible_routines = $inaccessible_routines ? $inaccessible_routines.', '.$routine['Name'] : $routine['Name'];
2022 continue;
2023 }
2024 $stored_routines_log = "Dumping routine: {$routine['Name']}";
2025 if (!$stored_routines_delimiter_printed) {
2026 $this->stow("\n\n# Dumping routines for database ".UpdraftPlus_Manipulation_Functions::backquote($this->dbinfo['name'])."\n\n");
2027 $this->stow("DELIMITER ;;\n\n");
2028 $stored_routines_delimiter_printed = true;
2029 }
2030 $routine_name = $routine['Name'];
2031 // Since routine name can include backquotes and routine name is typically enclosed with backquotes as well, the backquote escaping for the routine name can be done by adding a leading backquote
2032 $quoted_escaped_routine_name = UpdraftPlus_Manipulation_Functions::backquote(str_replace('`', '``', $routine_name));
2033 $this->stow("DROP {$routine['Type']} IF EXISTS $quoted_escaped_routine_name;;\n\n");
2034 $this->stow($routine['Create '.ucfirst(strtolower($routine['Type']))]."\n\n;;\n\n");
2035 $updraftplus->log($stored_routines_log." - (Successful)");
2036 }
2037 if ($inaccessible_routines) {
2038 $updraftplus->log(__('Dumping routines: ', 'updraftplus') . "({$inaccessible_routines})" . " - (". __('Failed', 'updraftplus') . " - " . __("Your WordPress database user doesn't have sufficient privileges to read these stored routines.", 'updraftplus') . " " .__('To be able to backup the routines, you must be the user named as the routine DEFINER(s), have the SHOW_ROUTINE privilege (for MySQL 8.0.20+ users), have the SELECT privilege at the global level, or have the CREATE ROUTINE, ALTER ROUTINE, or EXECUTE privilege granted at a scope that includes the routines.', 'updraftplus'). ")", 'warning');
2039 }
2040 if ($stored_routines_delimiter_printed) $this->stow("DELIMITER ;\n\n");
2041 } elseif (is_wp_error($stored_routines)) {
2042 $updraftplus->log($stored_routines->get_error_message());
2043 }
2044
2045 $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");
2046
2047 $updraftplus->log($file_base.'-db'.$this->whichdb_suffix.'.gz: finished writing out complete database file ('.round(filesize($backup_final_file_name)/1024, 1).' KB)');
2048 if (!$this->backup_db_close()) {
2049 $updraftplus->log('An error occurred whilst closing the final database file');
2050 $updraftplus->log(__('An error occurred whilst closing the final database file', 'updraftplus'), 'error');
2051 $errors++;
2052 }
2053
2054 foreach ($unlink_files as $unlink_file) {
2055 @unlink($unlink_file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2056 }
2057
2058 if ($errors > 0) return false;
2059
2060 // We no longer encrypt here - because the operation can take long, we made it resumable and moved it to the upload loop
2061 $updraftplus->jobdata_set('jobstatus', 'dbcreated'.$this->whichdb_suffix);
2062
2063 $checksums = $updraftplus->which_checksums();
2064
2065 $checksum_description = '';
2066
2067 foreach ($checksums as $checksum) {
2068
2069 $cksum = hash_file($checksum, $backup_final_file_name);
2070 $updraftplus->jobdata_set($checksum.'-db'.(('wp' == $whichdb) ? '0' : $whichdb.'0'), $cksum);
2071 if ($checksum_description) $checksum_description .= ', ';
2072 $checksum_description .= "$checksum: $cksum";
2073
2074 }
2075
2076 $updraftplus->log("Total database tables backed up: $total_tables (".basename($backup_final_file_name).", size: ".filesize($backup_final_file_name).", $checksum_description)");
2077
2078 return basename($backup_final_file_name);
2079
2080 }
2081
2082 /**
2083 * Log the number of expected rows (both to the backup log, and database backup file)
2084 *
2085 * @param String $table - table name
2086 * @param Integer $expected_rows - number of rows
2087 * @param Boolean $via_count - if the expected number comes via a SELECT COUNT() call
2088 * @param Boolean $log_in_backup - whether to log in the backup file (not just the backup log file)
2089 */
2090 private function log_expected_rows($table, $expected_rows, $via_count = false, $log_in_backup = true) {
2091 global $updraftplus;
2092 $description = $via_count ? 'via COUNT' : 'approximate';
2093 $updraftplus->log("Table $table: Total expected rows ($description): ".$expected_rows);
2094 if ($log_in_backup) $this->stow("# Approximate rows expected in table: $expected_rows\n");
2095 if ($expected_rows > UPDRAFTPLUS_WARN_DB_ROWS) {
2096 $this->many_rows_warning = true;
2097 $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, $expected_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);
2098 }
2099 }
2100
2101 /**
2102 * This function will return a SQL WHERE clause to exclude updraft jobdata
2103 *
2104 * @param array $where - an array of where clauses to add to
2105 * @param string $table - the table we want to add a where clause for
2106 *
2107 * @return array - returns an array of where clauses for the table
2108 */
2109 public function backup_exclude_jobdata($where, $table) {
2110 // Don't include the job data for any backups - so that when the database is restored, it doesn't continue an apparently incomplete backup
2111 global $updraftplus;
2112 $table_prefix = $updraftplus->get_table_prefix(false); // or we can just use $this->table_prefix_raw ??
2113 if ('wp' == $this->whichdb && (!empty($table_prefix) && strtolower($table_prefix.'sitemeta') == strtolower($table))) {
2114 $where[] = 'meta_key NOT LIKE "updraft_jobdata_%"';
2115 } elseif ('wp' == $this->whichdb && (!empty($table_prefix) && strtolower($table_prefix.'options') == strtolower($table))) {
2116 // These might look similar, but the quotes are different
2117 if ('win' == strtolower(substr(PHP_OS, 0, 3))) {
2118 $updraft_jobdata = "'updraft_jobdata_%'";
2119 $site_transient_update = "'_site_transient_update_%'";
2120 } else {
2121 $updraft_jobdata = '"updraft_jobdata_%"';
2122 $site_transient_update = '"_site_transient_update_%"';
2123 }
2124
2125 $where[] = 'option_name NOT LIKE '.$updraft_jobdata.' AND option_name NOT LIKE '.$site_transient_update.'';
2126 }
2127
2128 return $where;
2129 }
2130
2131 /**
2132 * Produce a dump of the table using a mysqldump binary
2133 *
2134 * @param String $potsql - the path to the mysqldump binary
2135 * @param String $table_name - the name of the table being dumped
2136 *
2137 * @return Boolean - success status
2138 */
2139 private function backup_table_bindump($potsql, $table_name) {
2140
2141 $microtime = microtime(true);
2142
2143 global $updraftplus, $wpdb;
2144
2145 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
2146 // Can't get binary mysqldump to make this transformation
2147 // $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;
2148
2149 $pfile = md5(time().rand()).'.tmp';
2150 file_put_contents($this->updraft_dir.'/'.$pfile, "[mysqldump]\npassword=\"".addslashes($this->dbinfo['pass'])."\"\n");
2151
2152 $where_array = apply_filters('updraftplus_backup_table_sql_where', array(), $table_name, $this);
2153 if ('win' === strtolower(substr(PHP_OS, 0, 3))) {
2154 // On Windows, the PHP escapeshellarg() replaces % char with white space, so we change the % char to [percent_sign] but change it back later after escapeshellarg finish processsing it
2155 $where_array = str_replace('%', '[percent_sign]', $where_array);
2156 }
2157 $where = '';
2158
2159 if (!empty($where_array) && is_array($where_array)) {
2160 // N.B. Don't add a WHERE prefix here; most versions of mysqldump silently strip it out, but one was encountered that didn't.
2161 $first_loop = true;
2162 foreach ($where_array as $condition) {
2163 if (!$first_loop) $where .= " AND ";
2164 $where .= $condition;
2165 $first_loop = false;
2166 }
2167 }
2168
2169 // Note: escapeshellarg() adds quotes around the string
2170 if ($where) $where = "--where=".escapeshellarg($where);
2171 if ('' !== $where && 'win' === strtolower(substr(PHP_OS, 0, 3))) {
2172 // change the [percent_sign] back to % char
2173 $where = str_replace('[percent_sign]', '%', $where);
2174 }
2175
2176 if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
2177 $exec = "cd ".escapeshellarg(str_replace('/', '\\', $this->updraft_dir))." & ";
2178 } else {
2179 $exec = "cd ".escapeshellarg($this->updraft_dir)."; ";
2180 }
2181
2182 // Allow --max_allowed_packet to be configured via constant. Experience has shown some customers with complex CMS or pagebuilder setups can have very large postmeta entries.
2183 $msqld_max_allowed_packet = (defined('UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET') && (is_int(UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET) || is_string(UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET))) ? UPDRAFTPLUS_MYSQLDUMP_MAX_ALLOWED_PACKET : '12M';
2184
2185 $exec .= "$potsql --defaults-file=$pfile $where --max-allowed-packet=$msqld_max_allowed_packet --quote-names --add-drop-table";
2186
2187 static $mysql_version = null;
2188 if (null === $mysql_version) {
2189 $mysql_version = $wpdb->get_var('SELECT VERSION()');
2190 if ('' == $mysql_version) $mysql_version = $wpdb->db_version();
2191 }
2192 if ($mysql_version && version_compare($mysql_version, '5.1', '>=')) {
2193 $exec .= " --no-tablespaces";
2194 }
2195
2196 $exec .= " --skip-comments --skip-set-charset --allow-keywords --dump-date --extended-insert --user=".escapeshellarg($this->dbinfo['user'])." ";
2197
2198 $host = $this->dbinfo['host'];
2199
2200 if (preg_match('#^(.*):(\d+)$#', $host, $matches)) {
2201 // The escapeshellarg() on $matches[2] is only to avoid tripping static analysis tools
2202 $exec .= "--host=".escapeshellarg($matches[1])." --port=".escapeshellarg($matches[2])." ";
2203 } elseif (preg_match('#^(.*):(.*)$#', $host, $matches) && file_exists($matches[2])) {
2204 $exec .= "--host=".escapeshellarg($matches[1])." --socket=".escapeshellarg($matches[2])." ";
2205 } else {
2206 $exec .= "--host=".escapeshellarg($host)." ";
2207 }
2208
2209 $exec .= $this->dbinfo['name']." ".escapeshellarg($table_name);
2210
2211 $ret = false;
2212 $any_output = false;
2213 $writes = 0;
2214 $write_bytes = 0;
2215 $handle = function_exists('popen') ? popen($exec, 'r') : false;
2216 if ($handle) {
2217 while (!feof($handle)) {
2218 $w = fgets($handle, 1048576);
2219 if (is_string($w) && $w) {
2220 $this->stow($w);
2221 $writes++;
2222 $write_bytes += strlen($w);
2223 $any_output = true;
2224 }
2225 }
2226 $ret = pclose($handle);
2227 // The manual page for pclose() claims that only -1 indicates an error, but this is untrue
2228 if (0 != $ret) {
2229 $updraftplus->log("Binary mysqldump: error (code: $ret)");
2230 // Keep counter of failures? Change value of binsqldump?
2231 $ret = false;
2232 } else {
2233 if ($any_output) {
2234 $updraftplus->log("Table $table_name: binary mysqldump finished (writes: $writes, bytes $write_bytes, return code $ret) in ".sprintf("%.02f", max(microtime(true)-$microtime, 0.00001))." seconds");
2235 $ret = true;
2236 }
2237 }
2238 } else {
2239 $updraftplus->log("Binary mysqldump error: bindump popen failed");
2240 }
2241
2242 // Clean temporary files
2243 @unlink($this->updraft_dir.'/'.$pfile);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2244
2245 return $ret;
2246
2247 }
2248
2249 /**
2250 * Write out the initial backup information for a table to the currently open file
2251 *
2252 * @param String $table - Full name of database table to backup
2253 * @param String $dump_as_table - Table name to use when writing out
2254 * @param String $table_type - Table type - 'VIEW' is supported; otherwise it is treated as an ordinary table
2255 * @param Array $table_structure - Table structure as returned by a DESCRIBE command
2256 */
2257 private function write_table_backup_beginning($table, $dump_as_table, $table_type, $table_structure) {
2258
2259 $this->stow("\n# Delete any existing table ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\nDROP TABLE IF EXISTS " . UpdraftPlus_Manipulation_Functions::backquote($dump_as_table).";\n");
2260
2261 if ('VIEW' == $table_type) {
2262 $this->stow("DROP VIEW IF EXISTS " . UpdraftPlus_Manipulation_Functions::backquote($dump_as_table) . ";\n");
2263 }
2264
2265 $description = ('VIEW' == $table_type) ? 'view' : 'table';
2266
2267 $this->stow("\n# Table structure of $description ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
2268
2269 $create_table = $this->wpdb_obj->get_results("SHOW CREATE TABLE ".UpdraftPlus_Manipulation_Functions::backquote($table), ARRAY_N);
2270 if (false === $create_table) {
2271 $this->stow("#\n# Error with SHOW CREATE TABLE for $table\n#\n");
2272 }
2273 $create_line = UpdraftPlus_Manipulation_Functions::str_lreplace('TYPE=', 'ENGINE=', $create_table[0][1]);
2274
2275 // Remove PAGE_CHECKSUM parameter from MyISAM - was internal, undocumented, later removed (so causes errors on import)
2276 if (preg_match('/ENGINE=([^\s;]+)/', $create_line, $eng_match)) {
2277 $engine = $eng_match[1];
2278 if ('myisam' == strtolower($engine)) {
2279 $create_line = preg_replace('/PAGE_CHECKSUM=\d\s?/', '', $create_line, 1);
2280 }
2281 }
2282
2283 if ($dump_as_table !== $table) $create_line = UpdraftPlus_Manipulation_Functions::str_replace_once($table, $dump_as_table, $create_line);
2284
2285 $this->stow($create_line.' ;');
2286
2287 if (false === $table_structure) {
2288 $this->stow("#\n# Error getting $description structure of $table\n#\n");
2289 }
2290
2291 // Add a comment preceding the beginning of the data
2292 $this->stow("\n\n# ".sprintf("Data contents of $description %s", UpdraftPlus_Manipulation_Functions::backquote($table))."\n\n");
2293
2294 }
2295
2296 /**
2297 * Suggest a beginning value for how many rows to fetch in each SELECT statement (before taking into account resumptions)
2298 *
2299 * @param String $table - the full table name
2300 *
2301 * @return Integer
2302 */
2303 private function get_rows_on_first_fetch($table) {
2304
2305 // In future, we could run over the table definition; if it is all non-massive defined lengths, we could base a calculation on that.
2306
2307 if ($this->table_prefix_raw.'term_relationships' == $table) {
2308 // This table is known to have very small data lengths
2309 $rows = 100000;
2310 } elseif (preg_match('/meta$/i', $table)) {
2311 // Meta-data rows tend to be short *on average*. 10MB / 4000 rows = 2.6KB/row, so this is still quite conservative.
2312 $rows = 4000;
2313 } else {
2314 // The very conservative default
2315 $rows = 1000;
2316 }
2317
2318 return $rows;
2319
2320 }
2321
2322 /**
2323 * Suggest how many rows to fetch in each SELECT statement
2324 *
2325 * @param String $table - the table being fetched
2326 * @param Boolean $allow_further_reductions - whether to enable a second level of reductions (i.e. even less rows)
2327 * @param Boolean $is_first_fetch_for_table - whether this is the first fetch on this table
2328 * @param Integer|Boolean $expected_rows - if an integer, an estimate of the number of rows
2329 * @param Boolean $expected_via_count - if $expected_rows is an integer, then this indicates whether the estimate was made via a SELECT COUNT() statement
2330 *
2331 * @return Integer
2332 */
2333 private function number_of_rows_to_fetch($table, $allow_further_reductions, $is_first_fetch_for_table, $expected_rows = false, $expected_via_count = false) {
2334
2335 global $updraftplus;
2336
2337 // 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. Now that we have implemented resumptions, the risk of timeouts is much lower (we just need to process enough rows).
2338 // October 2020: added further reductions
2339 // Listed in increasing order due to the handling below. At the end it gets quite drastic. Note, though, that currently we don't store this in the job-data.
2340 // A future improvement could, when things get drastic, grab and log data on the size of what is required, so that we can respond more dynamically. The strategy currently here will run out of road if memory falls short multiple times. See: https://stackoverflow.com/questions/4524019/how-to-get-the-byte-size-of-resultset-in-an-sql-query
2341 $fetch_rows_reductions = array(500, 250, 200, 100);
2342
2343 $default_on_first_fetch = $this->get_rows_on_first_fetch($table);
2344
2345 $known_bigger_than_table = (!is_bool($expected_rows) && $expected_rows && $expected_via_count && $default_on_first_fetch > 2 * $expected_rows);
2346
2347 if ($known_bigger_than_table) $allow_further_reductions = true;
2348
2349 if ($allow_further_reductions) {
2350 // If we're relying on LIMIT with offsets, then we have to be mindful of how that performs
2351 $fetch_rows_reductions = array_merge($fetch_rows_reductions, array(50, 20, 5));
2352 }
2353
2354 // Remove any that are far out of range
2355 if ($known_bigger_than_table) {
2356 foreach ($fetch_rows_reductions as $k => $reduce_to) {
2357 if ($reduce_to > $expected_rows * 2 && count($fetch_rows_reductions) > 2) {
2358 unset($fetch_rows_reductions[$k]);
2359 }
2360 }
2361 }
2362
2363 // If this is not the first fetch on a table, then get what was stored last time we set it (if we ever did). On the first fetch, reset back to the starting value (we presume problems are table-specific).
2364 // This means that the same value will persist whilst the table is being backed up, both during the current resumption, and subsequent ones
2365 $fetch_rows = $is_first_fetch_for_table ? $default_on_first_fetch : $updraftplus->jobdata_get('fetch_rows', $default_on_first_fetch);
2366
2367 $fetch_rows_at_start = $fetch_rows;
2368
2369 $resumptions_since_last_successful = $updraftplus->current_resumption - $updraftplus->last_successful_resumption;
2370
2371 // Do we need to reduce the number of rows we attempt to fetch?
2372 // If something useful has happened on this run, then we don't try any reductions (we save them for a resumption after one on which nothing useful happened)
2373 if ($known_bigger_than_table || (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && $resumptions_since_last_successful > 1)) {
2374
2375 $break_after = $is_first_fetch_for_table ? max($resumptions_since_last_successful - 1, 1) : 1;
2376
2377 foreach ($fetch_rows_reductions as $reduce_to) {
2378 if ($fetch_rows > $reduce_to) {
2379 // Go down one level
2380 $fetch_rows = $reduce_to;
2381 $break_after--;
2382 if ($break_after < 1) break;
2383 }
2384 }
2385
2386 $log_start = $updraftplus->current_resumption ? "Last successful resumption was $resumptions_since_last_successful runs ago" : "Table is relatively small";
2387 $updraftplus->log("$log_start; fetch_rows will thus be: $fetch_rows (allow_further_reductions=$allow_further_reductions, is_first_fetch=$is_first_fetch_for_table, known_bigger_than_table=$known_bigger_than_table)");
2388 }
2389
2390 // If it has changed, then preserve it in the job for the next resumption (of this table)
2391 if ($fetch_rows_at_start !== $fetch_rows || $is_first_fetch_for_table) $updraftplus->jobdata_set('fetch_rows', $fetch_rows);
2392
2393 return $fetch_rows;
2394
2395 }
2396
2397 /**
2398 * Return a list of primary keys (N.B. the method should not be called unless the caller already knows that the table has a single/simple primary key) for rows that have "over-sized" data.
2399 * Currently this only examines the "posts" table and any other table with a longtext type, which are the primary causes of problems. If others are revealed in future, it can be generalised (e.g. examine the whole definition/all cells).
2400 *
2401 * @param String $table - the full table name
2402 * @param Array $structure - the table structure, as from WPDB::get_results("DESCRIBE ...");
2403 * @param String $primary_key - the primary key to use; required if $structure is set (slightly redundant, since it can be derived from structure)
2404 *
2405 * @return Array - list of IDs
2406 */
2407 private function get_oversized_rows($table, $structure = array(), $primary_key = '') {
2408
2409 if ($this->table_prefix_raw.'posts' != $table) {
2410 if (empty($structure) || '' === $primary_key) return array();
2411 foreach ($structure as $item) {
2412 if ('' !== $item->Field && 'longtext' === $item->Type) {
2413 $use_field = $item->Field;
2414 }
2415 }
2416 } else {
2417 $primary_key = 'id';
2418 $use_field = 'post_content';
2419 }
2420
2421 if (!isset($use_field)) return array();
2422
2423 global $updraftplus;
2424
2425 // Look for the jobdata_delete() call elsewhere in this class - the key name needs to match
2426 $jobdata_key = 'oversized_rows_'.$table;
2427
2428 $oversized_list = $updraftplus->jobdata_get($jobdata_key);
2429
2430 if (is_array($oversized_list)) return $oversized_list;
2431
2432 $oversized_list = array();
2433
2434 // Allow over-ride via a constant
2435 $oversized_row_size = defined('UPDRAFTPLUS_OVERSIZED_ROW_SIZE') ? UPDRAFTPLUS_OVERSIZED_ROW_SIZE : 2048576;
2436
2437 $sql = $this->wpdb_obj->prepare("SELECT ".UpdraftPlus_Manipulation_Functions::backquote($primary_key)." FROM ".UpdraftPlus_Manipulation_Functions::backquote($table)." WHERE LENGTH(".UpdraftPlus_Manipulation_Functions::backquote($use_field).") > %d ORDER BY ".UpdraftPlus_Manipulation_Functions::backquote($primary_key)." ASC", $oversized_row_size);
2438
2439 $oversized_rows = $this->wpdb_obj->get_col($sql);
2440
2441 // Upon an error, just return an empty list
2442 if (!is_array($oversized_rows)) return array();
2443
2444 $updraftplus->jobdata_set($jobdata_key, $oversized_rows);
2445
2446 return $oversized_rows;
2447
2448 }
2449
2450 /**
2451 * Original version taken partially from phpMyAdmin and partially from Alain Wolf, Zurich - Switzerland to use the WordPress $wpdb object
2452 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
2453 * Modified by Scott Merrill (http://www.skippy.net/)
2454 * Subsequently heavily improved and modified
2455 *
2456 * This method should be called in a loop for a complete table backup (see the information for the returned parameter). The method may implement whatever strategy it likes for deciding when to return (the assumption is that when it does return with some results, the caller should register that something useful happened).
2457 *
2458 * @param String $table - Full name of database table to backup
2459 * @param String $table_type - Table type - 'VIEW' is supported; otherwise it is treated as an ordinary table
2460 * @param Integer|Boolean $start_record - Specify the starting record, or true to start at the beginning. Our internal page size is fixed at 1000 (though within that we might actually query in smaller batches).
2461 * @param Boolean $can_use_primary_key - Whether it is allowed to perform quicker SELECTS based on the primary key. The intended use case for false is to support backups running during a version upgrade. N.B. This "can" is not absolute; there may be other constraints dealt with within this method.
2462 *
2463 * @return Integer|Array|WP_Error - a WP_Error to indicate an error; an array indicates that it finished (if it includes 'next_record' that means it finished via producing something); an integer to indicate the next page the case that there are more to do.
2464 */
2465 private function backup_table($table, $table_type = 'BASE TABLE', $start_record = true, $can_use_primary_key = true) {
2466 $process_pages = 100;
2467
2468 // Preserve the passed-in value
2469 $original_start_record = $start_record;
2470
2471 global $updraftplus;
2472
2473 $microtime = microtime(true);
2474 $total_rows = 0;
2475
2476 // Deal with Windows/old MySQL setups with erroneous table prefixes differing in case
2477 $dump_as_table = (false == $this->duplicate_tables_exist && 0 === stripos($table, $this->table_prefix) && 0 !== strpos($table, $this->table_prefix)) ? $this->table_prefix.substr($table, strlen($this->table_prefix)) : $table;
2478
2479 $table_structure = $this->wpdb_obj->get_results("DESCRIBE ".UpdraftPlus_Manipulation_Functions::backquote($table));
2480 if (!$table_structure) {
2481 // $updraftplus->log(__('Error getting table details', 'updraftplus') . ": $table", 'error');
2482 $error_message = '';
2483 if ($this->wpdb_obj->last_error) $error_message .= ' ('.$this->wpdb_obj->last_error.')';
2484 return new WP_Error('table_details_error', $error_message);
2485 }
2486
2487 // If at the beginning of the dump for a table, then add the DROP and CREATE statements
2488 if (true === $start_record) {
2489 $this->write_table_backup_beginning($table, $dump_as_table, $table_type, $table_structure);
2490 }
2491
2492 // Some tables have optional data, and should be skipped if they do not work
2493 $table_sans_prefix = substr($table, strlen($this->table_prefix_raw));
2494 $data_optional_tables = ('wp' == $this->whichdb) ? apply_filters('updraftplus_data_optional_tables', explode(',', UPDRAFTPLUS_DATA_OPTIONAL_TABLES)) : array();
2495 if (in_array($table_sans_prefix, $data_optional_tables)) {
2496 if (!$updraftplus->something_useful_happened && !empty($updraftplus->current_resumption) && ($updraftplus->current_resumption - $updraftplus->last_successful_resumption > 2)) {
2497 $updraftplus->log("Table $table: Data skipped (previous attempts failed, and table is marked as non-essential)");
2498 return array();
2499 }
2500 }
2501
2502 $table_data = array();
2503 if ('VIEW' != $table_type) {
2504 $fields = array();
2505 $defs = array();
2506 $integer_fields = array();
2507 $binary_fields = array();
2508 $bit_fields = array();
2509 $bit_field_exists = false;
2510
2511 // false means "not yet set"; a string means what it was set to; null means that there are multiple (and so not useful to us). If it is not a string, then $primary_key_type is invalid and should not be used.
2512 $primary_key = false;
2513 $primary_key_type = false;
2514
2515 // $table_structure was from "DESCRIBE $table"
2516 foreach ($table_structure as $struct) {
2517
2518 if (isset($struct->Key) && 'PRI' == $struct->Key && '' != $struct->Field) {
2519 $primary_key = (false === $primary_key) ? $struct->Field : null;
2520 $primary_key_type = $struct->Type;
2521 }
2522
2523 if ((0 === strpos($struct->Type, 'tinyint')) || (0 === strpos(strtolower($struct->Type), 'smallint'))
2524 || (0 === strpos(strtolower($struct->Type), 'mediumint')) || (0 === strpos(strtolower($struct->Type), 'int')) || (0 === strpos(strtolower($struct->Type), 'bigint'))
2525 ) {
2526 $defs[strtolower($struct->Field)] = (null === $struct->Default) ? 'NULL' : $struct->Default;
2527 $integer_fields[strtolower($struct->Field)] = true;
2528 }
2529
2530 if ((0 === strpos(strtolower($struct->Type), 'binary')) || (0 === strpos(strtolower($struct->Type), 'varbinary')) || (0 === strpos(strtolower($struct->Type), 'tinyblob')) || (0 === strpos(strtolower($struct->Type), 'mediumblob')) || (0 === strpos(strtolower($struct->Type), 'blob')) || (0 === strpos(strtolower($struct->Type), 'longblob'))) {
2531 $binary_fields[strtolower($struct->Field)] = true;
2532 }
2533
2534 if (preg_match('/^bit(?:\(([0-9]+)\))?$/i', trim($struct->Type), $matches)) {
2535 if (!$bit_field_exists) $bit_field_exists = true;
2536 $bit_fields[strtolower($struct->Field)] = !empty($matches[1]) ? max(1, (int) $matches[1]) : 1;
2537 // the reason why if bit fields are found then the fields need to be cast into binary type is that if mysqli_query function is being used, mysql will convert the bit field value to a decimal number and represent it in a string format whereas, if mysql_query function is being used, mysql will not convert it to a decimal number but instead will keep it retained as it is
2538 $struct->Field = "CAST(".UpdraftPlus_Manipulation_Functions::backquote(str_replace('`', '``', $struct->Field))." AS BINARY) AS ".UpdraftPlus_Manipulation_Functions::backquote(str_replace('`', '``', $struct->Field));
2539 $fields[] = $struct->Field;
2540 } else {
2541 $fields[] = UpdraftPlus_Manipulation_Functions::backquote(str_replace('`', '``', $struct->Field));
2542 }
2543 }
2544
2545 $expected_via_count = false;
2546
2547 // N.B. At this stage this is for optimisation, mainly targets what is used on the core WP tables (bigint(20)); a value can be relied upon, but false is not definitive. N.B. https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/numeric-type-syntax.html (retrieved Aug 2021): "As of MySQL 8.0.17, the display width attribute is deprecated for integer data types; you should expect support for it to be removed in a future version of MySQL." MySQL 8.0.20 is not returning it.
2548 $use_primary_key = false;
2549 if ($can_use_primary_key && is_string($primary_key) && preg_match('#^(small|medium|big)?int(\(| |$)#i', $primary_key_type)) {
2550 $use_primary_key = true;
2551
2552 // We don't bother re-counting if it's likely to be so large that we're not going to do anything with the result
2553 if (is_bool($this->expected_rows) || $this->expected_rows < 1000) {
2554 $expected_rows = $this->wpdb_obj->get_var('SELECT COUNT('.UpdraftPlus_Manipulation_Functions::backquote($primary_key).') FROM '.UpdraftPlus_Manipulation_Functions::backquote($table));
2555 if (!is_bool($expected_rows)) {
2556 $this->expected_rows = $expected_rows;
2557 $expected_via_count = true;
2558 }
2559 }
2560
2561 $oversized_rows = $this->get_oversized_rows($table, $table_structure, $primary_key);
2562
2563 if (preg_match('# unsigned$#i', $primary_key_type)) {
2564 if (true === $start_record) $start_record = -1;
2565 } else {
2566 if (true === $start_record) {
2567 $min_value = $this->wpdb_obj->get_var('SELECT MIN('.UpdraftPlus_Manipulation_Functions::backquote($primary_key).') FROM '.UpdraftPlus_Manipulation_Functions::backquote($table));
2568 $start_record = (is_numeric($min_value) && $min_value) ? (int) $min_value - 1 : -1;
2569 }
2570 }
2571 }
2572
2573 if (!is_bool($this->expected_rows)) {
2574 $log_expected_records = (true === $original_start_record);
2575 $this->log_expected_rows($table, $this->expected_rows, $expected_via_count, $log_expected_records);
2576 }
2577
2578 $search = array("\x00", "\x0a", "\x0d", "\x1a");
2579 $replace = array('\0', '\n', '\r', '\Z');
2580
2581 $where_array = apply_filters('updraftplus_backup_table_sql_where', array(), $table, $this);
2582 $where = '';
2583 if (!empty($where_array) && is_array($where_array)) {
2584 $where = 'WHERE '.implode(' AND ', $where_array);
2585 }
2586
2587 // 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%)
2588
2589 $fetch_rows = $this->number_of_rows_to_fetch($table, $use_primary_key || $start_record < 500000, true === $original_start_record, $this->expected_rows, $expected_via_count);
2590
2591 if (!is_bool($this->expected_rows)) $this->expected_rows = true;
2592
2593 $original_fetch_rows = $fetch_rows;
2594
2595 $select = $bit_field_exists ? implode(', ', $fields) : '*';
2596
2597 $enough_for_now = false;
2598
2599 $began_writing_at = time();
2600
2601 $enough_data_after = 104857600;
2602 $enough_time_after = ($fetch_rows > 250) ? 15 : 9;
2603
2604 // Loop which retrieves data
2605 do {
2606
2607 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2608
2609 // Reset back to that which has constructed before the loop began
2610 $final_where = $where;
2611
2612 if ($use_primary_key) {
2613
2614 // The point of this is to leverage the indexing on the private key to make the SELECT much faster than index-less paging
2615 $final_where = $where . ($where ? ' AND ' : 'WHERE ');
2616
2617 // If it's -1, then we avoid mentioning a negative value, as the value may be unsigned
2618 $final_where .= UpdraftPlus_Manipulation_Functions::backquote($primary_key).((-1 === $start_record) ? ' >= 0' : " > $start_record");
2619
2620 $oversized_last_row_id = false;
2621 // Remove ones we've gone past
2622 foreach ($oversized_rows as $k => $row_id) {
2623 if ($start_record >= $row_id) {
2624 unset($oversized_rows[$k]);
2625 } else {
2626 $oversized_last_row_id = $row_id;
2627 // At this point we are only willing to fetch a single over-sized row. If this ever changes, we'll need to also keep track of their length.
2628 break;
2629 }
2630 }
2631 // Number the keys again from zero
2632 $oversized_rows = array_values($oversized_rows);
2633
2634 if ($oversized_last_row_id) {
2635 $final_where .= " AND ". UpdraftPlus_Manipulation_Functions::backquote($primary_key)." <= $oversized_last_row_id";
2636 }
2637
2638 $limit_statement = sprintf('LIMIT %d', $fetch_rows);
2639
2640 $order_by = 'ORDER BY '.UpdraftPlus_Manipulation_Functions::backquote($primary_key).' ASC';
2641
2642 } else {
2643 $order_by = '';
2644 if (true === $start_record) $start_record = 0;
2645 $limit_statement = sprintf('LIMIT %d, %d', $start_record, $fetch_rows);
2646 }
2647
2648 // $this->wpdb_obj->prepare() not needed (will throw a notice) as there are no parameters (all parts are already sanitised or cast to known-safe types if not sanitised here)
2649 $select_sql = "SELECT $select FROM ".UpdraftPlus_Manipulation_Functions::backquote($table)." $final_where $order_by $limit_statement";
2650
2651 if (defined('UPDRAFTPLUS_LOG_BACKUP_SELECTS') && UPDRAFTPLUS_LOG_BACKUP_SELECTS) $updraftplus->log($select_sql);
2652
2653 // Allow the data to be filtered (e.g. anonymisation)
2654 $table_data = apply_filters('updraftplus_backup_table_results', $this->wpdb_obj->get_results($select_sql, ARRAY_A), $table, $this->table_prefix, $this->whichdb);
2655
2656 if (null === $table_data) {
2657 $updraftplus->log("Database fetch error (null returned) when running: $select_sql");
2658 }
2659
2660 $oversized_changes = false;
2661
2662 if (!$table_data) {
2663 // Nothing was found - not even the expected over-sized row; this means it was deleted - so don't try to use a limitation based on it again, or we may get an infinite loop.
2664 if (isset($oversized_last_row_id) && false !== $oversized_last_row_id) {
2665 if (false !== ($key = array_search($oversized_last_row_id, $oversized_rows))) {
2666 unset($oversized_rows[$key]);
2667 $oversized_changes = true;
2668 }
2669 }
2670 if ($oversized_changes) $updraftplus->jobdata_set('oversized_rows_'.$table, $oversized_rows);
2671 continue;
2672 }
2673 $entries = 'INSERT INTO '.UpdraftPlus_Manipulation_Functions::backquote($dump_as_table).' VALUES ';
2674
2675 // \x08\\x09, not required
2676
2677 $this_entry = '';
2678 foreach ($table_data as $row) {
2679 $total_rows++;
2680 if ($this_entry) $this_entry .= ",\n ";
2681 $this_entry .= '(';
2682 $key_count = 0;
2683 foreach ($row as $key => $value) {
2684
2685 if ($key_count) $this_entry .= ', ';
2686 $key_count++;
2687
2688 if ($use_primary_key && strtolower($primary_key) == strtolower($key) && $value > $start_record) {
2689 $start_record = $value;
2690 foreach ($oversized_rows as $k => $row_id) {
2691 if ($start_record >= $row_id) {
2692 unset($oversized_rows[$k]);
2693 } else {
2694 break;
2695 }
2696 }
2697 }
2698
2699 if (isset($integer_fields[strtolower($key)])) {
2700 // make sure there are no blank spots in the insert syntax,
2701 // yet try to avoid quotation marks around integers
2702 $value = (null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
2703 $value = ('' === $value) ? "''" : $value;
2704 $this_entry .= $value;
2705 } elseif (isset($binary_fields[strtolower($key)])) {
2706 if (null === $value) {
2707 $this_entry .= 'NULL';
2708 } elseif ('' === $value) {
2709 $this_entry .= "''";
2710 } else {
2711 $this_entry .= "0x" . bin2hex(str_repeat("0", floor(strspn($value, "0") / 4)).$value);
2712 }
2713 } elseif (isset($bit_fields[$key])) {
2714 mbstring_binary_safe_encoding();
2715 $val_len = strlen($value);
2716 reset_mbstring_encoding();
2717 $hex = '';
2718 for ($i=0; $i<$val_len; $i++) {
2719 $hex .= sprintf('%02X', ord($value[$i]));
2720 }
2721 $this_entry .= "b'".str_pad($this->hex2bin($hex), $bit_fields[$key], '0', STR_PAD_LEFT)."'";
2722 } else {
2723 $this_entry .= (null === $value) ? 'NULL' : "'" . str_replace($search, $replace, str_replace('\'', '\\\'', str_replace('\\', '\\\\', $value))) . "'";
2724 }
2725 }
2726 $this_entry .= ')';
2727
2728 // Flush every 512KB
2729 if (strlen($this_entry) > 524288) {
2730 $this_entry .= ';';
2731 if (strlen($this_entry) > 10485760) {
2732 // This is an attempt to prevent avoidable duplication of long strings in-memory, at the cost of one extra write
2733 $this->stow(" \n".$entries);
2734 $this->stow($this_entry);
2735 } else {
2736 $this->stow(" \n".$entries.$this_entry);
2737 }
2738 $this_entry = '';
2739 // Potentially indicate that enough has been done to loop
2740 if ($this->db_current_raw_bytes > $enough_data_after || time() - $began_writing_at > $enough_time_after) {
2741 $enough_for_now = true;
2742 }
2743 }
2744
2745 }
2746
2747 if ($this_entry) {
2748 $this_entry .= ';';
2749 if (strlen($this_entry) > 10485760) {
2750 // This is an attempt to prevent avoidable duplication of long strings in-memory, at the cost of one extra write
2751 $this->stow(" \n".$entries);
2752 $this->stow($this_entry);
2753 } else {
2754 $this->stow(" \n".$entries.$this_entry);
2755 }
2756 }
2757
2758 // Increment this before any potential changes to $fetch_rows
2759 if (!$use_primary_key) {
2760 $start_record += $fetch_rows;
2761 }
2762
2763 // Potentially fetch more rows at once, if performance has been good on a sufficient number of rows
2764 // However - testing indicates that this makes very little difference to overall performance; MySQL's performance scales linearly with the number of rows requested. So optimisations here are unlikely to be worthwhile. (Probably better to remove LIMIT and ORDER BY entirely on tables that look small enough to fit into memory in one go)
2765 if (!$enough_for_now && $total_rows > 0 && $fetch_rows >= $original_fetch_rows && $fetch_rows < $original_fetch_rows * 8 && $this->db_current_raw_bytes > 10000 && $total_rows > 5000) {
2766 $bytes_per_row = $this->db_current_raw_bytes / $total_rows;
2767 // Increase the numbers of rows fetched if we still expect it to be less than 5MB, and the rate is acceptable
2768 if (2 * $fetch_rows * $bytes_per_row < 5242880) {
2769 // N.B. This does not persist across resumptions
2770 $fetch_rows = $fetch_rows * 2;
2771 $process_pages = $process_pages / 2;
2772 }
2773 }
2774
2775 if ($process_pages > 0) $process_pages--;
2776
2777 // The condition involving count($oversized_rows) is for when rows that were in oversized_rows got deleted before being fetched; the "ID < (row)" condition could result in no data being returned, even though the table isn't finished
2778 } while (!$enough_for_now && (count($table_data) > 0 || (isset($oversized_rows) && count($oversized_rows) > 0)) && (-1 == $process_pages || $process_pages > 0));
2779 }
2780
2781 $fetch_time = max(microtime(true)-$microtime, 0.00001);
2782
2783 $updraftplus->log("Table $table: Rows added in this batch (next record: $start_record): $total_rows (uncompressed bytes in this segment=".$this->db_current_raw_bytes.") in ".sprintf('%.02f', $fetch_time).' seconds');
2784
2785 // If all data has been fetched, then write out the closing comment
2786 if (-1 == $process_pages || 0 == count($table_data)) {
2787 $this->stow("\n# End of data contents of table ".UpdraftPlus_Manipulation_Functions::backquote($table)."\n\n");
2788 // Keep the keyname here in sync with what is in self::get_oversized_rows()
2789 $updraftplus->jobdata_delete('oversized_rows_'.$table);
2790 return is_numeric($start_record) ? array('next_record' => (int) $start_record) : array();
2791 }
2792
2793 return is_numeric($start_record) ? (int) $start_record : $start_record;
2794
2795 }
2796
2797 /**
2798 * Convert hexadecimal (base16) number into binary (base2) and no need to worry about the platform-dependent of 32bit/64bit size limitation
2799 *
2800 * @param String $hex Hexadecimal number
2801 * @return String a base2 format of the given hexadecimal number
2802 */
2803 public function hex2bin($hex) {
2804 $table = array(
2805 '0' => '0000',
2806 '1' => '0001',
2807 '2' => '0010',
2808 '3' => '0011',
2809 '4' => '0100',
2810 '5' => '0101',
2811 '6' => '0110',
2812 '7' => '0111',
2813 '8' => '1000',
2814 '9' => '1001',
2815 'a' => '1010',
2816 'b' => '1011',
2817 'c' => '1100',
2818 'd' => '1101',
2819 'e' => '1110',
2820 'f' => '1111'
2821 );
2822 $bin = '';
2823
2824 if (!preg_match('/^[0-9a-f]+$/i', $hex)) return '';
2825
2826 for ($i = 0; $i < strlen($hex); $i++) {
2827 $bin .= $table[strtolower(substr($hex, $i, 1))];
2828 }
2829
2830 return $bin;
2831 }
2832
2833 /**
2834 * Encrypts the file if the option is set; returns the basename of the file (according to whether it was encrypted or nto)
2835 *
2836 * @param String $file - file to encrypt
2837 *
2838 * @return array
2839 */
2840 public function encrypt_file($file) {
2841 global $updraftplus;
2842 $encryption = $updraftplus->get_job_option('updraft_encryptionphrase');
2843 if (strlen($encryption) > 0) {
2844 $updraftplus->log("Attempting to encrypt backup file");
2845 try {
2846 $result = apply_filters('updraft_encrypt_file', null, $file, $encryption, $this->whichdb, $this->whichdb_suffix);
2847 } catch (Exception $e) {
2848 $log_message = 'Exception ('.get_class($e).') occurred during encryption: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2849 error_log($log_message);
2850 // @codingStandardsIgnoreLine
2851 $log_message .= ' Backtrace: '.str_replace(array(ABSPATH, "\n"), array('', ', '), $e->getTraceAsString());
2852 $updraftplus->log($log_message);
2853 $updraftplus->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2854 die();
2855 // @codingStandardsIgnoreLine
2856 } catch (Error $e) {
2857 $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during encryption. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')';
2858 error_log($log_message);
2859 // @codingStandardsIgnoreLine
2860 $log_message .= ' Backtrace: '.str_replace(array(ABSPATH, "\n"), array('', ', '), $e->getTraceAsString());
2861 $updraftplus->log($log_message);
2862 $updraftplus->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error');
2863 die();
2864 }
2865 if (null === $result) return basename($file);
2866 return $result;
2867 } else {
2868 return basename($file);
2869 }
2870 }
2871
2872 /**
2873 * Close the database file currently being written
2874 *
2875 * @return Boolean
2876 */
2877 private function backup_db_close() {
2878 return $this->dbhandle_isgz ? gzclose($this->dbhandle) : fclose($this->dbhandle);
2879 }
2880
2881 /**
2882 * Open a file, store its file handle, and reset the class variable db_current_raw_bytes back to zero.
2883 *
2884 * @param String $file Full path to the file to open
2885 * @param Boolean $allow_gz Use gzopen() if available, instead of fopen()
2886 * @param Boolean $append Use append mode for writing
2887 *
2888 * @return Resource|Boolean - the opened file handle, or false for an error
2889 */
2890 public function backup_db_open($file, $allow_gz = true, $append = false) {
2891 $mode = $append ? 'ab' : 'w';
2892 if ($allow_gz && function_exists('gzopen')) {
2893 $this->dbhandle = gzopen($file, $mode);
2894 $this->dbhandle_isgz = true;
2895 } else {
2896 $this->dbhandle = fopen($file, $mode);
2897 $this->dbhandle_isgz = false;
2898 }
2899 if (false === $this->dbhandle) {
2900 global $updraftplus;
2901 $updraftplus->log("ERROR: $file: Could not open the backup file for writing (mode: $mode)");
2902 $updraftplus->log($file.": ".__("Could not open the backup file for writing", 'updraftplus'), 'error');
2903 }
2904 $this->db_current_raw_bytes = 0;
2905 return $this->dbhandle;
2906 }
2907
2908 /**
2909 * Adds a line to the database backup
2910 *
2911 * @param String $write_line - the line to write
2912 *
2913 * @return Integer|Boolean - the number of octets written, or false for a failure (as returned by gzwrite() / fwrite)
2914 */
2915 private function stow($write_line) {
2916
2917 if ('' === $write_line) return 0;
2918
2919 $write_function = $this->dbhandle_isgz ? 'gzwrite' : 'fwrite';
2920
2921 if (false == ($ret = call_user_func($write_function, $this->dbhandle, $write_line))) {
2922 $this->log_with_db_occasionally("There was an error writing a line to the backup file: $write_line");
2923 }
2924
2925 $this->db_current_raw_bytes += strlen($write_line);
2926
2927 return $ret;
2928 }
2929
2930 /**
2931 * Stow the database backup header
2932 */
2933 private function backup_db_header() {
2934
2935 global $updraftplus;
2936 $wp_version = $updraftplus->get_wordpress_version();
2937 $mysql_version = $this->wpdb_obj->get_var('SELECT VERSION()');
2938 if ('' == $mysql_version) $mysql_version = $this->wpdb_obj->db_version();
2939
2940 if ('wp' == $this->whichdb) {
2941 $wp_upload_dir = wp_upload_dir();
2942 $this->stow("# WordPress MySQL database backup\n");
2943 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
2944 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
2945 $this->stow("# Backup of: ".untrailingslashit(site_url())."\n");
2946 $this->stow("# Home URL: ".untrailingslashit(home_url())."\n");
2947 $this->stow("# Content URL: ".untrailingslashit(content_url())."\n");
2948 $this->stow("# Uploads URL: ".untrailingslashit($wp_upload_dir['baseurl'])."\n");
2949 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
2950 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
2951 $this->stow("# ABSPATH: ".trailingslashit(ABSPATH)."\n");
2952 $this->stow("# Site info: multisite=".(is_multisite() ? '1' : '0')."\n");
2953 $this->stow("# Site info: sql_mode=".$this->wpdb_obj->get_var('SELECT @@SESSION.sql_mode')."\n");
2954 $this->stow("# Site info: end\n");
2955 } else {
2956 $this->stow("# MySQL database backup (supplementary database ".$this->whichdb.")\n");
2957 $this->stow("# Created by UpdraftPlus version ".$updraftplus->version." (https://updraftplus.com)\n");
2958 $this->stow("# WordPress Version: $wp_version, running on PHP ".phpversion()." (".$_SERVER["SERVER_SOFTWARE"]."), MySQL $mysql_version\n");
2959 $this->stow("# ".sprintf('External database: (%s)', $this->dbinfo['user'].'@'.$this->dbinfo['host'].'/'.$this->dbinfo['name'])."\n");
2960 $this->stow("# Backup created by: ".untrailingslashit(site_url())."\n");
2961 $this->stow("# Table prefix: ".$this->table_prefix_raw."\n");
2962 $this->stow("# Filtered table prefix: ".$this->table_prefix."\n");
2963 }
2964
2965 $label = $updraftplus->jobdata_get('label');
2966 if (!empty($label)) $this->stow("# Label: $label\n");
2967
2968 $this->stow("\n# Generated: ".date("l j. F Y H:i T")."\n");
2969 $this->stow("# Hostname: ".$this->dbinfo['host']."\n");
2970 $this->stow("# Database: ".UpdraftPlus_Manipulation_Functions::backquote($this->dbinfo['name'])."\n");
2971
2972 if (!empty($this->skipped_tables[$this->whichdb])) {
2973 if ('wp' == $this->whichdb) {
2974 $this->stow("# Skipped tables: " . implode(', ', $this->skipped_tables['wp'])."\n");
2975 } elseif (isset($this->skipped_tables[$this->dbinfo['name']])) {
2976 $this->stow("# Skipped tables: " . implode(', ', $this->skipped_tables[$this->dbinfo['name']])."\n");
2977 }
2978 }
2979
2980 $this->stow("# --------------------------------------------------------\n");
2981
2982 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
2983 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
2984 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
2985 $this->stow("/*!40101 SET NAMES ".$updraftplus->get_connection_charset($this->wpdb_obj)." */;\n");
2986 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n\n");
2987
2988 }
2989
2990 /**
2991 * This function recursively packs the zip, dereferencing symlinks but packing into a single-parent tree for universal unpacking
2992 *
2993 * @param String $fullpath Full path
2994 * @param String $use_path_when_storing Controls the path to use when storing in the zip file
2995 * @param String $original_fullpath Original path
2996 * @param Integer $startlevels How deep within the directory structure the recursive operation has gone
2997 * @param Array $exclude passed by reference so that we can remove elements as they are matched - saves time checking against already-dealt-with objects]
2998 * @return Boolean
2999 */
3000 private function makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels, &$exclude) {
3001
3002 // $zipfile = $this->zip_basename.(($this->index == 0) ? '' : ($this->index+1)).'.zip.tmp';
3003
3004 global $updraftplus;
3005
3006 // 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.
3007 if (is_link($fullpath) && is_dir($fullpath) && 'more' == $this->whichone) {
3008 $updraftplus->log("Directory symlink encounted in more files backup: $use_path_when_storing -> ".readlink($fullpath).": skipping");
3009 return true;
3010 }
3011
3012 static $updraft_dir_realpath;
3013
3014 $updraft_dir_realpath = realpath($this->updraft_dir);
3015
3016 // 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
3017 $fullpath = realpath($fullpath);
3018 $original_fullpath = realpath($original_fullpath);
3019
3020 // Is the place we've ended up above the original base? That leads to infinite recursion
3021 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)))) {
3022 $updraftplus->log("Infinite recursion: symlink led us to $fullpath, which is within $original_fullpath");
3023 $updraftplus->log(__("Infinite recursion: consult your log for more information", 'updraftplus'), 'error');
3024 return false;
3025 }
3026
3027 // This is sufficient for the ones we have exclude options for - uploads, others, wpcore
3028 $stripped_storage_path = (1 == $startlevels) ? $use_path_when_storing : substr($use_path_when_storing, strpos($use_path_when_storing, '/') + 1);
3029 if (false !== ($fkey = array_search($stripped_storage_path, $exclude))) {
3030 $updraftplus->log("Entity excluded by configuration option: $stripped_storage_path");
3031 unset($exclude[$fkey]);
3032 return true;
3033 }
3034
3035 $if_altered_since = $this->makezip_if_altered_since;
3036
3037 if (is_file($fullpath)) {
3038 if (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($fullpath)) {
3039 $updraftplus->log("Entity excluded by configuration option (extension): ".basename($fullpath));
3040 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($fullpath)) {
3041 $updraftplus->log("Entity excluded by configuration option (prefix): ".basename($fullpath));
3042 } elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards(basename($fullpath))) {
3043 $updraftplus->log("Entity excluded by configuration option (wildcards): ".basename($fullpath));
3044 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath)) {
3045 $updraftplus->log("Entity excluded by filter: ".basename($fullpath));
3046 } elseif (is_readable($fullpath)) {
3047 $mtime = filemtime($fullpath);
3048 $key = ($fullpath == $original_fullpath) ? ((2 == $startlevels) ? $use_path_when_storing : $this->basename($fullpath)) : $use_path_when_storing.'/'.$this->basename($fullpath);
3049 if ($mtime > 0 && $mtime > $if_altered_since) {
3050 $this->zipfiles_batched[$fullpath] = $key;
3051 $this->makezip_recursive_batchedbytes += @filesize($fullpath);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3052 // @touch($zipfile);
3053 } else {
3054 $this->zipfiles_skipped_notaltered[$fullpath] = $key;
3055 }
3056 } else {
3057 $updraftplus->log("$fullpath: unreadable file");
3058 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up (check the file permissions and ownership)", 'updraftplus'), $fullpath), 'warning');
3059 }
3060 } elseif (is_dir($fullpath)) {
3061 if ($fullpath == $updraft_dir_realpath) {
3062 $updraftplus->log("Skip directory (UpdraftPlus backup directory): $use_path_when_storing");
3063 return true;
3064 }
3065
3066 if (apply_filters('updraftplus_exclude_directory', false, $fullpath, $use_path_when_storing)) {
3067 $updraftplus->log("Skip filtered directory: $use_path_when_storing");
3068 return true;
3069 }
3070
3071 if (file_exists($fullpath.'/.donotbackup')) {
3072 $updraftplus->log("Skip directory (.donotbackup file found): $use_path_when_storing");
3073 return true;
3074 }
3075
3076 if (!isset($this->existing_files[$use_path_when_storing])) $this->zipfiles_dirbatched[] = $use_path_when_storing;
3077
3078 if (!$dir_handle = @opendir($fullpath)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3079 $updraftplus->log("Failed to open directory: $fullpath");
3080 $updraftplus->log(sprintf(__("Failed to open directory (check the file permissions and ownership): %s", 'updraftplus'), $fullpath), 'error');
3081 return false;
3082 }
3083
3084 while (false !== ($e = readdir($dir_handle))) {
3085 if ('.' == $e || '..' == $e) continue;
3086
3087 if (is_link($fullpath.'/'.$e)) {
3088 $deref = realpath($fullpath.'/'.$e);
3089 if (is_file($deref)) {
3090 $use_stripped = $stripped_storage_path.'/'.$e;
3091 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
3092 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
3093 unset($exclude[$fkey]);
3094 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
3095 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
3096 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
3097 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
3098 } elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
3099 $updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
3100 } elseif (apply_filters('updraftplus_exclude_file', false, $deref, $use_stripped)) {
3101 $updraftplus->log("Entity excluded by filter: $use_stripped");
3102 } elseif (is_readable($deref)) {
3103 $mtime = filemtime($deref);
3104 if ($mtime > 0 && $mtime > $if_altered_since) {
3105 $this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
3106 $this->makezip_recursive_batchedbytes += @filesize($deref);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3107 // @touch($zipfile);
3108 } else {
3109 $this->zipfiles_skipped_notaltered[$deref] = $use_path_when_storing.'/'.$e;
3110 }
3111 } else {
3112 $updraftplus->log("$deref: unreadable file (de-referenced from the link $e in $fullpath)");
3113 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
3114 }
3115 } elseif (is_dir($deref)) {
3116 $this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
3117 }
3118 } elseif (is_file($fullpath.'/'.$e)) {
3119 $use_stripped = $stripped_storage_path.'/'.$e;
3120 if (false !== ($fkey = array_search($use_stripped, $exclude))) {
3121 $updraftplus->log("Entity excluded by configuration option: $use_stripped");
3122 unset($exclude[$fkey]);
3123 } elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
3124 $updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
3125 } elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
3126 $updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
3127 } elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
3128 $updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
3129 } elseif (apply_filters('updraftplus_exclude_file', false, $fullpath.'/'.$e)) {
3130 $updraftplus->log("Entity excluded by filter: $use_stripped");
3131 } elseif (is_readable($fullpath.'/'.$e)) {
3132 $mtime = filemtime($fullpath.'/'.$e);
3133 if ($mtime > 0 && $mtime > $if_altered_since) {
3134 $this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
3135 $this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3136 } else {
3137 $this->zipfiles_skipped_notaltered[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
3138 }
3139 } else {
3140 $updraftplus->log("$fullpath/$e: unreadable file");
3141 $updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up", 'updraftplus'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
3142 }
3143 } elseif (is_dir($fullpath.'/'.$e)) {
3144 $use_stripped = $stripped_storage_path.'/'.$e;
3145 if ('wpcore' == $this->whichone && 'updraft' == $e && basename($use_path_when_storing) == 'wp-content' && (!defined('UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS') || !UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS)) {
3146 // This test, of course, won't catch everything - it just aims to make things better by default
3147 $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);
3148 } elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
3149 $updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
3150 } else {
3151 // no need to add_empty_dir here, as it gets done when we recurse
3152 $this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
3153 }
3154 }
3155 }
3156 closedir($dir_handle);
3157 } else {
3158 $updraftplus->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
3159 }
3160
3161 return true;
3162
3163 }
3164
3165 /**
3166 * Get a list of excluded extensions
3167 *
3168 * @param Array $exclude - settings passed in
3169 *
3170 * @return Array
3171 */
3172 private function get_excluded_extensions($exclude) {
3173 if (!is_array($exclude)) $exclude = array();
3174 $exclude_extensions = array();
3175 foreach ($exclude as $ex) {
3176 if (preg_match('/^ext:(.+)$/i', $ex, $matches)) {
3177 $exclude_extensions[] = strtolower($matches[1]);
3178 }
3179 }
3180
3181 if (defined('UPDRAFTPLUS_EXCLUDE_EXTENSIONS')) {
3182 $exclude_from_define = explode(',', UPDRAFTPLUS_EXCLUDE_EXTENSIONS);
3183 foreach ($exclude_from_define as $ex) {
3184 $exclude_extensions[] = strtolower(trim($ex));
3185 }
3186 }
3187
3188 return $exclude_extensions;
3189 }
3190
3191 /**
3192 * Get a list of excluded prefixes
3193 *
3194 * @param Array $exclude - settings passed in
3195 *
3196 * @return Array - each is listed in lower case
3197 */
3198 private function get_excluded_prefixes($exclude) {
3199 if (!is_array($exclude)) $exclude = array();
3200 $exclude_prefixes = array();
3201 foreach ($exclude as $pref) {
3202 if (preg_match('/^prefix:(.+)$/i', $pref, $matches)) {
3203 $exclude_prefixes[] = strtolower($matches[1]);
3204 }
3205 }
3206 return $exclude_prefixes;
3207 }
3208
3209 /**
3210 * List all the wildcard patterns from the given excluded items
3211 *
3212 * @param Array $exclude the list of excluded items which may contain not just wildcard patterns but also specific file/directory names as well
3213 *
3214 * $exclude argument may contains data in an array format like below:
3215 * [
3216 * "snapshots" // definitely not a wildcard parttern, this could be directories/files named `snapshots` which are located in the root/parent directory
3217 * "2021/03/image.jpg", // not a wildcard parttern, this could be files/directories named `image.jpg` which are located in the 2021/03/ directory
3218 * "ext:zip", // not a wildcard pattern, this is to exclude all files that end with `zip` extension
3219 * "prefix:file-", // not a wildcard pattern, this is to exclude all files that begin with `file-` prefix
3220 * "2021/04", // not a wildcard pattern, this is to exclude all files/directories which are located in the 2021/04 directory
3221 * "backup*", // wildcard pattern that excludes all files/directories beginning with `backup` in the root/parent directory
3222 * "2021/*optimise*", // wildcard pattern that excludes all files/directories that have `optimise` anywhere in their names in the `2021` directory
3223 * "2021/04/*.tmp" // wildcard pattern that excludes all files/directories ending with `optimise` anywhere in their names in the `2021/04` directory
3224 * ]
3225 *
3226 * @return Array an array of wilcard patterns
3227 *
3228 * After the $exclude has gone through the regex parsing step, only excluded items containing valid wildcard patterns got captured and will return them in an array in a format like below:
3229 *
3230 * [
3231 * [
3232 * "directory_path" => "",
3233 * "pattern" => "backup*"
3234 * ],
3235 * [
3236 * "directory_path" => "2021\",
3237 * "pattern" => "*optimise*"
3238 * ],
3239 * [
3240 * "directory_path" => "2021\04\",
3241 * "pattern" => "*.tmp"
3242 * ]
3243 * ]
3244 */
3245 private function get_excluded_wildcards($exclude) {
3246 if (!is_array($exclude)) $exclude = array();
3247 $excluded_wildcards = array();
3248 foreach ($exclude as $wch) {
3249 // https://regex101.com/r/dMFI0P/1/
3250 if (preg_match('#(.*(?<!\\\)/)?(.*?(?<!\\\)\*.*)#i', $wch, $matches)) {
3251 // the regex will make sure only excluded items containing valid wildcard patterns get captured, it will lookup for asterisk char(s) at the very end of the string right after the last path separator (if any). e.g. foo/bar/b*a*z
3252 $excluded_wildcards[] = array(
3253 // in case the excluded item has doubled separators (e.g. dir1//dir2//file) or if the user added a directory separator at the beginning then trim and/or replace them
3254 'directory_path' => preg_replace(array('/^[\/\s]*/', '/\/\/*/', '/[\/\s]*$/'), array('', '/', ''), $matches[1]),
3255 'pattern' => $matches[2]
3256 );
3257 }
3258 }
3259 return $excluded_wildcards;
3260 }
3261
3262 /**
3263 * Check whether or not the given entity(file/directory) is excluded from the backup by matching it against a set of wildcard patterns
3264 *
3265 * @param String $entity the file/directory's stripped path
3266 * @return Boolean true if the entity is excluded, false otherwise
3267 */
3268 private function is_entity_excluded_by_wildcards($entity) {
3269 $entity_basename = untrailingslashit($entity);
3270 $entity_basename = substr_replace($entity_basename, '', 0, (false === strrpos($entity_basename, '/') ? 0 : strrpos($entity_basename, '/') + 1));
3271 foreach ($this->excluded_wildcards as $wch) {
3272 if (!is_array($wch) || empty($wch)) continue;
3273 if (substr_replace($entity, '', (int) strrpos($entity, '/'), strlen($entity) - (int) strrpos($entity, '/')) !== $wch['directory_path']) continue;
3274 if ('*' == substr($wch['pattern'], -1, 1) && '*' == substr($wch['pattern'], 0, 1) && strlen($wch['pattern']) > 2) {
3275 $wch['pattern'] = substr($wch['pattern'], 1, strlen($wch['pattern'])-2);
3276 $wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
3277 if (strpos($entity_basename, $wch['pattern']) !== false) return true;
3278 } elseif ('*' == substr($wch['pattern'], -1, 1) && strlen($wch['pattern']) > 1) {
3279 $wch['pattern'] = substr($wch['pattern'], 0, strlen($wch['pattern'])-1);
3280 $wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
3281 if (substr($entity_basename, 0, strlen($wch['pattern'])) == $wch['pattern']) return true;
3282 } elseif ('*' == substr($wch['pattern'], 0, 1) && strlen($wch['pattern']) > 1) {
3283 $wch['pattern'] = substr($wch['pattern'], 1);
3284 $wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
3285 if (strlen($entity_basename) >= strlen($wch['pattern']) && substr($entity_basename, strlen($wch['pattern'])*-1) == $wch['pattern']) return true;
3286 }
3287 }
3288 return false;
3289 }
3290
3291 private function is_entity_excluded_by_extension($entity) {
3292 foreach ($this->excluded_extensions as $ext) {
3293 if (!$ext) continue;
3294 $eln = strlen($ext);
3295 if (strtolower(substr($entity, -$eln, $eln)) == $ext) return true;
3296 }
3297 return false;
3298 }
3299
3300 private function is_entity_excluded_by_prefix($entity) {
3301 $entity = basename($entity);
3302 foreach ($this->excluded_prefixes as $pref) {
3303 if (!$pref) continue;
3304 $eln = strlen($pref);
3305 if (strtolower(substr($entity, 0, $eln)) == $pref) return true;
3306 }
3307 return false;
3308 }
3309
3310 private function unserialize_gz_cache_file($file) {
3311 if (!$whandle = gzopen($file, 'r')) return false;
3312 global $updraftplus;
3313 $emptimes = 0;
3314 $var = '';
3315 while (!gzeof($whandle)) {
3316 $bytes = @gzread($whandle, 1048576);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3317 if (empty($bytes)) {
3318 $emptimes++;
3319 $updraftplus->log("Got empty gzread ($emptimes times)");
3320 if ($emptimes>2) return false;
3321 } else {
3322 $var .= $bytes;
3323 }
3324 }
3325 gzclose($whandle);
3326 return unserialize($var);
3327 }
3328
3329 /**
3330 * Make Zip File.
3331 *
3332 * @param Array|String $source Caution: $source is allowed to be an array, not just a filename
3333 * @param String $backup_file_basename Name of backup file
3334 * @param String $whichone Backup entity type (e.g. 'plugins')
3335 * @param Boolean $retry_on_error Set to retry upon error
3336 * @return Boolean
3337 */
3338 private function make_zipfile($source, $backup_file_basename, $whichone, $retry_on_error = true) {
3339
3340 global $updraftplus;
3341
3342 $original_index = $this->index;
3343
3344 $itext = (empty($this->index)) ? '' : ($this->index+1);
3345 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
3346 // $destination is the temporary file (ending in .tmp)
3347 $destination = $this->updraft_dir.'/'.$destination_base;
3348
3349 // When to prefer PCL:
3350 // - We were asked to
3351 // - No zip extension present and no relevant method present
3352 // The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
3353
3354 // We need meta-info about $whichone
3355 $backupable_entities = $updraftplus->get_backupable_file_entities(true, false);
3356 // This is only used by one corner-case in BinZip
3357 // $this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
3358 $this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
3359
3360 $this->existing_files = array();
3361 // Used for tracking compression ratios
3362 $this->existing_files_rawsize = 0;
3363 $this->existing_zipfiles_size = 0;
3364
3365 // Enumerate existing files
3366 // Usually first_linked_index is zero; the exception being with more files, where previous zips' contents are irrelevant
3367 for ($j = $this->first_linked_index; $j <= $this->index; $j++) {
3368 $jtext = (0 == $j) ? '' : $j+1;
3369 // This is, in a non-obvious way, compatible with filenames which indicate increments
3370 // $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.
3371 $examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
3372
3373 // 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.
3374 // 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.
3375 // Other examples of this appear to be in HS#1001 and #1047
3376 if ($j != $this->index && !file_exists($examine_zip)) {
3377 $alt_examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index - 1) ? '.tmp' : '');
3378 if ($alt_examine_zip != $examine_zip && file_exists($alt_examine_zip) && is_readable($alt_examine_zip) && filesize($alt_examine_zip)>0) {
3379 $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)");
3380 if (rename($alt_examine_zip, $examine_zip)) {
3381 clearstatcache();
3382 } else {
3383 $updraftplus->log("Rename failed - backup zips likely to not have sequential numbers (does not affect backup integrity, but can cause user confusion)");
3384 }
3385 }
3386 }
3387
3388 // If the file exists, then we should grab its index of files inside, and sizes
3389 // Then, when we come to write a file, we should check if it's already there, and only add if it is not
3390 if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip) > 0) {
3391
3392 // Do not use (which also means do not create) a manifest if the file is still a .tmp file, since this may not be complete. If we are in this place in the code from a resumption, creating a manifest here will mean the manifest becomes out-of-date if further files are added.
3393 $this->populate_existing_files_list($examine_zip, substr($examine_zip, -4, 4) === '.zip');
3394
3395 // try_split is true if there have been no check-ins recently - or if it needs to be split anyway
3396 if ($j == $this->index) {
3397 if ($this->try_split) {
3398 if (filesize($examine_zip) > 50*1048576) {
3399 // We could, as a future enhancement, save this back to the job data, if we see a case that needs it
3400 $this->zip_split_every = max(
3401 (int) $this->zip_split_every/2,
3402 UPDRAFTPLUS_SPLIT_MIN*1048576,
3403 min(filesize($examine_zip)-1048576, $this->zip_split_every)
3404 );
3405 $updraftplus->jobdata_set('split_every', (int) ($this->zip_split_every/1048576));
3406 $updraftplus->log("No check-in on last two runs; bumping index and reducing zip split to: ".round($this->zip_split_every/1048576, 1)." MB");
3407 $do_bump_index = true;
3408 }
3409 unset($this->try_split);
3410 } elseif (filesize($examine_zip) > $this->zip_split_every) {
3411 $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));
3412 $do_bump_index = true;
3413 }
3414 }
3415
3416 } elseif (file_exists($examine_zip)) {
3417 $updraftplus->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
3418 @unlink($examine_zip);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3419 }
3420 }
3421
3422 $this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
3423
3424 $this->zipfiles_added = 0;
3425 $this->zipfiles_added_thisrun = 0;
3426 $this->zipfiles_dirbatched = array();
3427 $this->zipfiles_batched = array();
3428 $this->zipfiles_skipped_notaltered = array();
3429 $this->zipfiles_lastwritetime = time();
3430 $this->zip_basename = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone;
3431
3432 if (!empty($do_bump_index)) $this->bump_index();
3433
3434 $error_occurred = false;
3435
3436 // Store this in its original form
3437 // $this->source = $source;
3438
3439 // Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
3440 $this->makezip_recursive_batchedbytes = 0;
3441 if (!is_array($source)) $source = array($source);
3442
3443 $exclude = $updraftplus->get_exclude($whichone);
3444
3445 $files_enumerated_at = $updraftplus->jobdata_get('files_enumerated_at');
3446 if (!is_array($files_enumerated_at)) $files_enumerated_at = array();
3447 $files_enumerated_at[$whichone] = time();
3448 $updraftplus->jobdata_set('files_enumerated_at', $files_enumerated_at);
3449
3450 $this->makezip_if_altered_since = is_array($this->altered_since) ? (isset($this->altered_since[$whichone]) ? $this->altered_since[$whichone] : -1) : -1;
3451
3452 // Reset
3453 $got_uploads_from_cache = false;
3454
3455 // Uploads: can/should we get it back from the cache?
3456 // || 'others' == $whichone
3457 if (('uploads' == $whichone || 'others' == $whichone) && function_exists('gzopen') && function_exists('gzread')) {
3458 $use_cache_files = false;
3459 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
3460 // Cache file suffixes: -zfd.gz.tmp, -zfb.gz.tmp, -info.tmp, (possible)-zfs.gz.tmp
3461 if (file_exists($cache_file_base.'-zfd.gz.tmp') && file_exists($cache_file_base.'-zfb.gz.tmp') && file_exists($cache_file_base.'-info.tmp')) {
3462 // Cache files exist; shall we use them?
3463 $mtime = filemtime($cache_file_base.'-zfd.gz.tmp');
3464 // Require < 30 minutes old
3465 if (time() - $mtime < 1800) {
3466 $use_cache_files = true;
3467 }
3468 $any_failures = false;
3469 if ($use_cache_files) {
3470 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfd.gz.tmp');
3471 if (is_array($var)) {
3472 $this->zipfiles_dirbatched = $var;
3473 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfb.gz.tmp');
3474 if (is_array($var)) {
3475 $this->zipfiles_batched = $var;
3476 if (file_exists($cache_file_base.'-info.tmp')) {
3477 $var = maybe_unserialize(file_get_contents($cache_file_base.'-info.tmp'));
3478 if (is_array($var) && isset($var['makezip_recursive_batchedbytes'])) {
3479 $this->makezip_recursive_batchedbytes = $var['makezip_recursive_batchedbytes'];
3480 if (file_exists($cache_file_base.'-zfs.gz.tmp')) {
3481 $var = $this->unserialize_gz_cache_file($cache_file_base.'-zfs.gz.tmp');
3482 if (is_array($var)) {
3483 $this->zipfiles_skipped_notaltered = $var;
3484 } else {
3485 $any_failures = true;
3486 }
3487 } else {
3488 $this->zipfiles_skipped_notaltered = array();
3489 }
3490 } else {
3491 $any_failures = true;
3492 }
3493 }
3494 } else {
3495 $any_failures = true;
3496 }
3497 } else {
3498 $any_failures = true;
3499 }
3500 if ($any_failures) {
3501 $updraftplus->log("Failed to recover file lists from existing cache files");
3502 // Reset it all
3503 $this->zipfiles_skipped_notaltered = array();
3504 $this->makezip_recursive_batchedbytes = 0;
3505 $this->zipfiles_batched = array();
3506 $this->zipfiles_dirbatched = array();
3507 } else {
3508 $updraftplus->log("File lists recovered from cache files; sizes: ".count($this->zipfiles_batched).", ".count($this->zipfiles_batched).", ".count($this->zipfiles_skipped_notaltered).")");
3509 $got_uploads_from_cache = true;
3510 }
3511 }
3512 }
3513 }
3514
3515 $time_counting_began = time();
3516
3517 $this->excluded_extensions = $this->get_excluded_extensions($exclude);
3518 $this->excluded_prefixes = $this->get_excluded_prefixes($exclude);
3519 $this->excluded_wildcards = $this->get_excluded_wildcards($exclude);
3520
3521 foreach ($source as $element) {
3522 // makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
3523 if ('uploads' == $whichone) {
3524 if (empty($got_uploads_from_cache)) {
3525 $dirname = dirname($element);
3526 $basename = $this->basename($element);
3527 $add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.$basename, $element, 2, $exclude);
3528 } else {
3529 $add_them = true;
3530 }
3531 } else {
3532 if (empty($got_uploads_from_cache)) {
3533 $add_them = $this->makezip_recursive_add($element, $this->basename($element), $element, 1, $exclude);
3534 } else {
3535 $add_them = true;
3536 }
3537 }
3538 if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
3539 }
3540
3541 $time_counting_ended = time();
3542
3543 // Cache the file scan, if it looks like it'll be useful
3544 // We use gzip to reduce the size as on hosts which limit disk I/O, the cacheing may make things worse
3545 // || 'others' == $whichone
3546 if (('uploads' == $whichone || 'others' == $whichone) && !$error_occurred && function_exists('gzopen') && function_exists('gzwrite')) {
3547 $cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
3548
3549 // 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.
3550 $memory_needed_estimate = 0;
3551 foreach ($this->zipfiles_batched as $k => $v) {
3552 $memory_needed_estimate += strlen($k)+strlen($v)+12;
3553 }
3554
3555 // 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
3556 // Let us suppose we need 15% overhead for gzipping
3557
3558 $memory_limit = ini_get('memory_limit');
3559 $memory_usage = round(@memory_get_usage(false)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3560 $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3561
3562 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')) {
3563 $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)");
3564
3565 $buf = 'a:'.count($this->zipfiles_batched).':{';
3566 foreach ($this->zipfiles_batched as $file => $add_as) {
3567 $k = addslashes($file);
3568 $v = addslashes($add_as);
3569 $buf .= 's:'.strlen($k).':"'.$k.'";s:'.strlen($v).':"'.$v.'";';
3570 if (strlen($buf) > 1048576) {
3571 gzwrite($whandle, $buf, strlen($buf));
3572 $buf = '';
3573 }
3574 }
3575 $buf .= '}';
3576 $final = gzwrite($whandle, $buf);
3577 unset($buf);
3578
3579 if (!$final) {
3580 @unlink($cache_file_base.'-zfb.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3581 @gzclose($whandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3582 } else {
3583 gzclose($whandle);
3584 if (!empty($this->zipfiles_skipped_notaltered)) {
3585 if ($shandle = gzopen($cache_file_base.'-zfs.gz.tmp', 'w')) {
3586 if (!gzwrite($shandle, serialize($this->zipfiles_skipped_notaltered))) {
3587 $aborted_on_skipped = true;
3588 }
3589 gzclose($shandle);
3590 } else {
3591 $aborted_on_skipped = true;
3592 }
3593 }
3594 if (!empty($aborted_on_skipped)) {
3595 @unlink($cache_file_base.'-zfs.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3596 @unlink($cache_file_base.'-zfb.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3597 } else {
3598 $info_array = array('makezip_recursive_batchedbytes' => $this->makezip_recursive_batchedbytes);
3599 if (!file_put_contents($cache_file_base.'-info.tmp', serialize($info_array))) {
3600 @unlink($cache_file_base.'-zfs.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3601 @unlink($cache_file_base.'-zfb.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3602 }
3603 if ($dhandle = gzopen($cache_file_base.'-zfd.gz.tmp', 'w')) {
3604 if (!gzwrite($dhandle, serialize($this->zipfiles_dirbatched))) {
3605 $aborted_on_dirbatched = true;
3606 }
3607 gzclose($dhandle);
3608 } else {
3609 $aborted_on_dirbatched = true;
3610 }
3611 if (!empty($aborted_on_dirbatched)) {
3612 @unlink($cache_file_base.'-zfs.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3613 @unlink($cache_file_base.'-zfd.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3614 @unlink($cache_file_base.'-zfb.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3615 @unlink($cache_file_base.'-info.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3616 // @codingStandardsIgnoreLine
3617 } else {
3618 // Success.
3619 }
3620 }
3621 }
3622 }
3623
3624 /*
3625 Class variables that get altered:
3626 zipfiles_batched
3627 makezip_recursive_batchedbytes
3628 zipfiles_skipped_notaltered
3629 zipfiles_dirbatched
3630 Class variables that the result depends upon (other than the state of the filesystem):
3631 makezip_if_altered_since
3632 existing_files
3633 */
3634
3635 }
3636
3637 // 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.
3638 // 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.
3639 // $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'
3640 if ($retry_on_error) $updraftplus->check_recent_modification($destination);
3641 // 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')
3642 if (empty($do_bump_index)) touch($destination);
3643
3644 if (count($this->zipfiles_dirbatched) > 0 || count($this->zipfiles_batched) > 0) {
3645
3646 $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)));
3647
3648 // 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).
3649 $warn_on_failures = ($retry_on_error) ? false : true;
3650 $add_them = $this->makezip_addfiles($warn_on_failures);
3651
3652 if (is_wp_error($add_them)) {
3653 foreach ($add_them->get_error_messages() as $msg) {
3654 $updraftplus->log("Error returned from makezip_addfiles: ".$msg);
3655 }
3656 $error_occurred = true;
3657 } elseif (false === $add_them) {
3658 $updraftplus->log("Error: makezip_addfiles returned false");
3659 $error_occurred = true;
3660 }
3661
3662 }
3663
3664 // Reset these variables because the index may have changed since we began
3665
3666 $itext = empty($this->index) ? '' : $this->index+1;
3667 $destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
3668 $destination = $this->updraft_dir.'/'.$destination_base;
3669
3670 // ZipArchive::addFile sometimes fails - there's nothing when we expected something.
3671 // Did not used to have || $error_occured here. But it is better to retry, than to simply warn the user to check his logs.
3672 if (((file_exists($destination) || $this->index == $original_index) && @filesize($destination) < 90 && 'UpdraftPlus_ZipArchive' == $this->use_zip_object) || ($error_occurred && $retry_on_error)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3673 // 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.
3674 $updraftplus->log("makezip_addfiles(".$this->use_zip_object.") apparently failed (file=".basename($destination).", type=$whichone, size=".filesize($destination).") - retrying with PclZip");
3675 $saved_zip_object = $this->use_zip_object;
3676 $this->use_zip_object = 'UpdraftPlus_PclZip';
3677 $ret = $this->make_zipfile($source, $backup_file_basename, $whichone, false);
3678 $this->use_zip_object = $saved_zip_object;
3679 return $ret;
3680 }
3681
3682 // zipfiles_added > 0 means that $zip->close() has been called. i.e. An attempt was made to add something: something _should_ be there.
3683 // 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.
3684 // (file_exists($destination) || $this->index == $original_index) might be an alternative to $this->zipfiles_added > 0 - ? But, don't change what's not broken.
3685 if (false == $error_occurred || $this->zipfiles_added > 0) {
3686 return true;
3687 } else {
3688 $updraftplus->log("makezip failure: zipfiles_added=".$this->zipfiles_added.", error_occurred=".$error_occurred." (method=".$this->use_zip_object.")");
3689 return false;
3690 }
3691
3692 }
3693
3694 /**
3695 * 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.
3696 *
3697 * @param String $element
3698 *
3699 * @return String
3700 */
3701 private function basename($element) {
3702 $dirname = dirname($element);
3703 $basename_manual = preg_replace('#^[\\/]+#', '', substr($element, strlen($dirname)));
3704 $basename = basename($element);
3705 if ($basename_manual != $basename) {
3706 $locale = setlocale(LC_CTYPE, "0");
3707 if ('C' == $locale) {
3708 setlocale(LC_CTYPE, 'en_US.UTF8');
3709 $basename_new = basename($element);
3710 if ($basename_new == $basename_manual) $basename = $basename_new;
3711 setlocale(LC_CTYPE, $locale);
3712 }
3713 }
3714 return $basename;
3715 }
3716
3717 /**
3718 * Determine if a file should be stored without compression
3719 *
3720 * @param String $file - the filename
3721 *
3722 * @return Boolean
3723 */
3724 private function file_should_be_stored_without_compression($file) {
3725 if (!is_array($this->extensions_to_not_compress)) return false;
3726 foreach ($this->extensions_to_not_compress as $ext) {
3727 $ext_len = strlen($ext);
3728 if (strtolower(substr($file, -$ext_len, $ext_len)) == $ext) return true;
3729 }
3730 return false;
3731 }
3732
3733 /**
3734 * This method will add a manifest file to the backup zip
3735 *
3736 * @param String $whichone - the type of backup (e.g. 'plugins', 'themes')
3737 *
3738 * @return Boolean - success/failure status
3739 */
3740 private function updraftplus_include_manifest($whichone) {
3741 global $updraftplus;
3742
3743 $manifest_name = "updraftplus-manifest.json";
3744 $manifest = trailingslashit($this->updraft_dir).$manifest_name;
3745
3746 $updraftplus->log(sprintf("Creating file manifest ($manifest_name) for incremental backup (included: %d, skipped: %d)", count($this->zipfiles_batched), count($this->zipfiles_skipped_notaltered)));
3747
3748 if (false === ($handle = fopen($manifest, 'w+'))) return $updraftplus->log("Failed to open manifest file ($manifest_name)");
3749
3750 $this->manifest_path = $manifest;
3751
3752 $version = 1;
3753
3754 $go_to_levels = array(
3755 'plugins' => 2,
3756 'themes' => 2,
3757 'uploads' => 3,
3758 'others' => 3
3759 );
3760
3761 $go_to_levels = apply_filters('updraftplus_manifest_go_to_level', $go_to_levels, $whichone);
3762
3763 $go_to_level = isset($go_to_levels[$whichone]) ? $go_to_levels[$whichone] : 'all';
3764
3765 $directory = '';
3766
3767 if ('more' == $whichone) {
3768 foreach ($this->zipfiles_batched as $index => $dir) {
3769 $directory = '"directory":"' . dirname($index) . '",';
3770 }
3771 }
3772
3773 if (false === fwrite($handle, '{"version":'.$version.',"type":"'.$whichone.'",'.$directory.'"listed_levels":"'.$go_to_level.'","contents":{"directories":[')) $updraftplus->log("First write to manifest file failed ($manifest_name)");
3774
3775 // First loop: find out which is the last entry, so that we don't write the comma after it
3776 $last_dir_index = false;
3777 foreach ($this->zipfiles_dirbatched as $index => $dir) {
3778 if ('all' !== $go_to_level && substr_count($dir, '/') > $go_to_level - 1) continue;
3779 $last_dir_index = $index;
3780 }
3781
3782 // Second loop: write out the entry
3783 foreach ($this->zipfiles_dirbatched as $index => $dir) {
3784 if ('all' !== $go_to_level && substr_count($dir, '/') > $go_to_level - 1) continue;
3785 fwrite($handle, json_encode($dir).(($index != $last_dir_index) ? ',' : ''));
3786 }
3787
3788 // Now do the same for files
3789 fwrite($handle, '],"files":[');
3790
3791 $last_file_index = false;
3792 foreach ($this->zipfiles_batched as $store_as) {
3793 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3794 $last_file_index = $store_as;
3795 }
3796 foreach ($this->zipfiles_skipped_notaltered as $store_as) {
3797 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3798 $last_file_index = $store_as;
3799 }
3800
3801 foreach ($this->zipfiles_batched as $store_as) {
3802 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3803 fwrite($handle, json_encode($store_as).(($store_as != $last_file_index) ? ',' : ''));
3804 }
3805
3806 foreach ($this->zipfiles_skipped_notaltered as $store_as) {
3807 if ('all' !== $go_to_level && substr_count($store_as, '/') > $go_to_level - 1) continue;
3808 fwrite($handle, json_encode($store_as).(($store_as != $last_file_index) ? ',' : ''));
3809 }
3810
3811 fwrite($handle, ']}}');
3812 fclose($handle);
3813
3814 $this->zipfiles_batched[$manifest] = $manifest_name;
3815
3816 $updraftplus->log("Successfully created file manifest (size: ".filesize($manifest).")");
3817
3818 return true;
3819 }
3820
3821 // Q. Why don't we only open and close the zip file just once?
3822 // 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)
3823
3824 /**
3825 * We batch up the files, rather than do them one at a time. So we are more efficient than open,one-write,close.
3826 * 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.
3827 *
3828 * @param Boolean $warn_on_failures See if it warns on faliures or not
3829 *
3830 * @return Boolean|WP_Error
3831 */
3832 private function makezip_addfiles($warn_on_failures) {
3833
3834 global $updraftplus;
3835
3836 // Used to detect requests to bump the size
3837 $bump_index = false;
3838 $ret = true;
3839
3840 $zipfile = $this->zip_basename.((0 == $this->index) ? '' : ($this->index+1)).'.zip.tmp';
3841
3842 $maxzipbatch = $updraftplus->jobdata_get('maxzipbatch', 26214400);
3843 if ((int) $maxzipbatch < 1024) $maxzipbatch = 26214400;
3844
3845 // Short-circuit the null case, because we want to detect later if something useful happenned
3846 if (count($this->zipfiles_dirbatched) == 0 && count($this->zipfiles_batched) == 0) return true;
3847
3848 // 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)
3849 // This assumes that makezip_addfiles() is only called once so that we know about all needed files (the new style)
3850 // This is rather conservative - because it assumes zero compression. But we can't know that in advance.
3851 $force_allinone = false;
3852 if (0 == $this->index && $this->makezip_recursive_batchedbytes < $this->zip_split_every) {
3853 // So far, we only have a processor for this for PclZip; but that check can be removed - need to address the below items
3854 // 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.
3855 // TODO: Test this new method for PclZip - are we still getting the performance gains? Test for ZipArchive too.
3856 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))) {
3857 $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)");
3858 // $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)");
3859 $force_allinone = true;
3860 // if(!class_exists('PclZip')) require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php');
3861 // $zip = new PclZip($zipfile);
3862 // $remove_path = ($this->whichone == 'wpcore') ? untrailingslashit(ABSPATH) : WP_CONTENT_DIR;
3863 // $add_path = false;
3864 // Remove prefixes
3865 // $backupable_entities = $updraftplus->get_backupable_file_entities(true);
3866 // if (isset($backupable_entities[$this->whichone])) {
3867 // if ('plugins' == $this->whichone || 'themes' == $this->whichone || 'uploads' == $this->whichone) {
3868 // $remove_path = dirname($backupable_entities[$this->whichone]);
3869 // 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.
3870 // #$add_path = $this->whichone;
3871 // } else {
3872 // $remove_path = $backupable_entities[$this->whichone];
3873 // }
3874 // }
3875 // if ($add_path) {
3876 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path, PCLZIP_OPT_ADD_PATH, $add_path);
3877 // } else {
3878 // $zipcode = $zip->create($this->source, PCLZIP_OPT_REMOVE_PATH, $remove_path);
3879 // }
3880 // if ($zipcode == 0) {
3881 // $updraftplus->log("PclZip Error: ".$zip->errorInfo(true), 'warning');
3882 // return $zip->errorCode();
3883 // } else {
3884 // UpdraftPlus_Job_Scheduler::something_useful_happened();
3885 // return true;
3886 // }
3887 }
3888 }
3889
3890 // 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!
3891
3892 $data_added_since_reopen = 0;
3893 // static $data_added_this_resumption = 0;
3894 // $max_data_added_any_resumption = $updraftplus->jobdata_get('max_data_added_any_resumption', 0);
3895
3896 // 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)
3897 $files_zipadded_since_open = array();
3898
3899 $zip = new $this->use_zip_object;
3900 if (file_exists($zipfile)) {
3901 $original_size = filesize($zipfile);
3902 // PHP 8.1 throws a deprecation notice if opening a zero-size file with ZipArchive, so in that situation, we remove and re-create it
3903 if ($original_size > 0) {
3904 $opencode = $zip->open($zipfile);
3905 clearstatcache();
3906 } elseif (0 === $original_size) {
3907 unlink($zipfile);
3908 } else {
3909 $opencode = false;
3910 }
3911 } else {
3912 $original_size = 0;
3913 }
3914
3915 if (0 === $original_size) {
3916 $create_code = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CREATE')) ? ZIPARCHIVE::CREATE : 1;
3917 $opencode = $zip->open($zipfile, $create_code);
3918 }
3919
3920 if (true !== $opencode) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'), $zipfile, $zip->last_error));
3921
3922 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
3923 $this->updraftplus_include_manifest($this->whichone);
3924 }
3925
3926 // Make sure all directories are created before we start creating files
3927 while ($dir = array_pop($this->zipfiles_dirbatched)) {
3928 $zip->addEmptyDir($dir);
3929 }
3930 $zipfiles_added_thisbatch = 0;
3931
3932 // Go through all those batched files
3933 foreach ($this->zipfiles_batched as $file => $add_as) {
3934
3935 if (!file_exists($file)) {
3936 $updraftplus->log("File has vanished from underneath us; dropping: $add_as");
3937 continue;
3938 }
3939
3940 $fsize = filesize($file);
3941
3942 $large_file_warning_key = 'vlargefile_'.md5($this->whichone.'#'.$add_as);
3943
3944 if (defined('UPDRAFTPLUS_SKIP_FILE_OVER_SIZE') && UPDRAFTPLUS_SKIP_FILE_OVER_SIZE && $fsize > UPDRAFTPLUS_SKIP_FILE_OVER_SIZE) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3945 $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);
3946 continue;
3947 } elseif ($fsize > UPDRAFTPLUS_WARN_FILE_SIZE) {
3948
3949 $log_msg = __('A very large file was encountered: %s (size: %s Mb)', 'updraftplus');
3950
3951 // Was this warned about on a previous run?
3952 if ($updraftplus->warning_exists($large_file_warning_key)) {
3953 $updraftplus->log_remove_warning($large_file_warning_key);
3954 $large_file_warning_key .= '-2';
3955 $log_msg .= ' - '.__('a second attempt is being made (upon further failure it will be skipped)', 'updraftplus');
3956 } elseif ($updraftplus->warning_exists($large_file_warning_key.'-2') || $updraftplus->warning_exists($large_file_warning_key.'-final')) {
3957 $updraftplus->log_remove_warning($large_file_warning_key.'-2');
3958 $large_file_warning_key .= '-final';
3959 $log_msg .= ' - '.__('two unsuccessful attempts were made to include it, and it will now be omitted from the backup', 'updraftplus');
3960 }
3961
3962 $updraftplus->log(sprintf($log_msg, $add_as, round($fsize/1048576, 1)), 'warning', $large_file_warning_key);
3963
3964 if ('-final' == substr($large_file_warning_key, -6, 6)) {
3965 continue;
3966 }
3967 }
3968
3969 // Skips files that are already added
3970 if (!isset($this->existing_files[$add_as]) || $this->existing_files[$add_as] != $fsize) {
3971
3972 @touch($zipfile);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
3973 $zip->addFile($file, $add_as);
3974 $zipfiles_added_thisbatch++;
3975
3976 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($add_as) && false == $zip->setCompressionName($add_as, ZipArchive::CM_STORE)) {
3977 $updraftplus->log("Zip: setCompressionName failed on: $add_as");
3978 }
3979
3980 // 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).
3981 $this->zipfiles_added_thisrun++;
3982 $files_zipadded_since_open[] = array('file' => $file, 'addas' => $add_as);
3983
3984 $data_added_since_reopen += $fsize;
3985 // $data_added_this_resumption += $fsize;
3986 /* Conditions for forcing a write-out and re-open:
3987 - more than $maxzipbatch bytes have been batched
3988 - more than 2.0 seconds have passed since the last time we wrote
3989 - 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)
3990 - more than 500 files batched (should perhaps intelligently lower this as the zip file gets bigger - not yet needed)
3991 */
3992
3993 // 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)
3994 // 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
3995 $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;
3996
3997 if (!$force_allinone && ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES || $reaching_split_limit || $data_added_since_reopen > $maxzipbatch || (time() - $this->zipfiles_lastwritetime) > 2)) {
3998
3999 // We are coming towards a limit and about to close the zip, check if this is a more file backup and the manifest file has made it into this zip if not add it
4000 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
4001
4002 $manifest = false;
4003
4004 foreach ($files_zipadded_since_open as $info) {
4005 if ('updraftplus-manifest.json' == $info['file']) $manifest = true;
4006 }
4007
4008 if (!$manifest) {
4009 @touch($zipfile);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4010 $path = array_search('updraftplus-manifest.json', $this->zipfiles_batched);
4011 $zip->addFile($path, 'updraftplus-manifest.json');
4012 $zipfiles_added_thisbatch++;
4013
4014 if (method_exists($zip, 'setCompressionName') && $this->file_should_be_stored_without_compression($this->zipfiles_batched[$path])) {
4015 if (false == $zip->setCompressionName($this->zipfiles_batched[$path], ZipArchive::CM_STORE)) {
4016 $updraftplus->log("Zip: setCompressionName failed on: $this->zipfiles_batched[$path]");
4017 }
4018 }
4019
4020 // 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).
4021 $this->zipfiles_added_thisrun++;
4022 $files_zipadded_since_open[] = array('file' => $path, 'addas' => 'updraftplus-manifest.json');
4023 $data_added_since_reopen += filesize($path);
4024 // $data_added_this_resumption += filesize($path);
4025 }
4026 }
4027
4028 if (function_exists('set_time_limit')) @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4029 $something_useful_sizetest = false;
4030
4031 if ($data_added_since_reopen > $maxzipbatch) {
4032 $something_useful_sizetest = true;
4033 $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)');
4034 } elseif ($zipfiles_added_thisbatch > UPDRAFTPLUS_MAXBATCHFILES) {
4035 $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)');
4036 } elseif (!$reaching_split_limit) {
4037 $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)');
4038 } else {
4039 $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)');
4040 }
4041
4042 if (!$zip->close()) {
4043 // 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.
4044 $ret = false;
4045 $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
4046 }
4047
4048 // if ($data_added_this_resumption > $max_data_added_any_resumption) {
4049 // $max_data_added_any_resumption = $data_added_this_resumption;
4050 // $updraftplus->jobdata_set('max_data_added_any_resumption', $max_data_added_any_resumption);
4051 // }
4052
4053 $zipfiles_added_thisbatch = 0;
4054
4055 // This triggers a re-open, later
4056 unset($zip);
4057 $files_zipadded_since_open = array();
4058 // Call here, in case we've got so many big files that we don't complete the whole routine
4059 if (filesize($zipfile) > $original_size) {
4060
4061 // 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
4062 $this->zip_last_ratio = ($data_added_since_reopen > 0) ? min((filesize($zipfile) - $original_size)/$data_added_since_reopen, 1) : 1;
4063
4064 // We need a rolling update of this
4065 $original_size = filesize($zipfile);
4066
4067 // Move on to next zip?
4068 if ($reaching_split_limit || filesize($zipfile) > $this->zip_split_every) {
4069 $bump_index = true;
4070 // Take the filesize now because later we wanted to know we did clearstatcache()
4071 $bumped_at = round(filesize($zipfile)/1048576, 1);
4072 }
4073
4074 // Need to make sure that something_useful_happened() is always called
4075
4076 // 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.
4077 if (!$something_useful_sizetest) {
4078 UpdraftPlus_Job_Scheduler::something_useful_happened();
4079 } else {
4080
4081 // Do this as early as possible
4082 UpdraftPlus_Job_Scheduler::something_useful_happened();
4083
4084 $time_since_began = max(microtime(true)- $this->zipfiles_lastwritetime, 0.000001);
4085 $normalised_time_since_began = $time_since_began*($maxzipbatch/$data_added_since_reopen);
4086
4087 // Don't measure speed until after ZipArchive::close()
4088 $rate = round($data_added_since_reopen/$time_since_began, 1);
4089
4090 $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)));
4091
4092 // 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.
4093
4094 /* "Could have done more" - detect as:
4095 - A batch operation would still leave a "good chunk" of time in a run
4096 - "Good chunk" means that the time we took to add the batch is less than 50% of a run time
4097 - We can do that on any run after the first (when at least one ceiling on the maximum time is known)
4098 - 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.
4099 */
4100
4101 // 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
4102
4103 // Gather the data. We try not to do this unless necessary (may be time-sensitive)
4104 if ($updraftplus->current_resumption >= 1) {
4105 $time_passed = $updraftplus->jobdata_get('run_times');
4106 if (!is_array($time_passed)) $time_passed = array();
4107 list($max_time, $timings_string, $run_times_known) = UpdraftPlus_Manipulation_Functions::max_time_passed($time_passed, $updraftplus->current_resumption-1, $this->first_run);// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
4108 } else {
4109 // $run_times_known = 0;
4110 // $max_time = -1;
4111 $run_times_known = 1;
4112 $max_time = microtime(true)-$updraftplus->opened_log_time;
4113 }
4114
4115 if ($normalised_time_since_began < 6 || ($updraftplus->current_resumption >= 1 && $run_times_known >= 1 && $time_since_began < 0.6*$max_time) || (0 == $updraftplus->current_resumption && $max_time > 240)) {
4116
4117 // How much can we increase it by?
4118 if ($normalised_time_since_began < 6 || 0 == $updraftplus->current_resumption) {
4119 if ($run_times_known > 0 && $max_time > 0) {
4120 $new_maxzipbatch = min(floor(max($maxzipbatch*6/$normalised_time_since_began, $maxzipbatch*((0.6*$max_time)/$normalised_time_since_began))), $this->zip_batch_ceiling);
4121 } else {
4122 // Maximum of 200MB in a batch
4123 $new_maxzipbatch = min(floor($maxzipbatch*6/$normalised_time_since_began), $this->zip_batch_ceiling);
4124 }
4125 } else {
4126 // Use up to 60% of available time
4127 $new_maxzipbatch = min(floor($maxzipbatch*((0.6*$max_time)/$normalised_time_since_began)), $this->zip_batch_ceiling);
4128 }
4129
4130 // Throttle increases - don't increase by more than 2x in one go - ???
4131 // $new_maxzipbatch = floor(min(2*$maxzipbatch, $new_maxzipbatch));
4132 // 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
4133 // $new_maxzipbatch = floor(min(18*$rate ,$new_maxzipbatch));
4134
4135 // Don't go above the split amount (though we expect that to be higher anyway, unless sending via email)
4136 $new_maxzipbatch = min($new_maxzipbatch, $this->zip_split_every);
4137
4138 // Don't raise it above a level that failed on a previous run
4139 $maxzipbatch_ceiling = $updraftplus->jobdata_get('maxzipbatch_ceiling');
4140 if (is_numeric($maxzipbatch_ceiling) && $maxzipbatch_ceiling > 20*1048576 && $new_maxzipbatch > $maxzipbatch_ceiling) {
4141 $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");
4142 $new_maxzipbatch = $maxzipbatch_ceiling;
4143 }
4144
4145 // Final sanity check
4146 if ($new_maxzipbatch > 1048576) $updraftplus->jobdata_set('maxzipbatch', $new_maxzipbatch);
4147
4148 if ($new_maxzipbatch <= 1048576) {
4149 $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)");
4150 } elseif ($new_maxzipbatch > $maxzipbatch) {
4151 $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)");
4152 } elseif ($new_maxzipbatch < $maxzipbatch) {
4153 // Ironically, we thought we were speedy...
4154 $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)");
4155 } else {
4156 $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)");
4157 }
4158
4159 if ($new_maxzipbatch > 1048576) $maxzipbatch = $new_maxzipbatch;
4160 }
4161
4162 // Detect excessive slowness
4163 // 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)
4164
4165 // 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).
4166
4167 if (!$updraftplus->something_useful_happened && $updraftplus->current_resumption >= 7) {
4168
4169 UpdraftPlus_Job_Scheduler::something_useful_happened();
4170
4171 if ($run_times_known >= 5 && ($time_since_began > 0.8 * $max_time || $time_since_began + 7 > $max_time)) {
4172
4173 $new_maxzipbatch = max(floor($maxzipbatch*0.8), 20971520);
4174 if ($new_maxzipbatch < $maxzipbatch) {
4175 $maxzipbatch = $new_maxzipbatch;
4176 $updraftplus->jobdata_set("maxzipbatch", $new_maxzipbatch);
4177 $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)");
4178 } else {
4179 $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)");
4180 }
4181 }
4182
4183 } else {
4184 UpdraftPlus_Job_Scheduler::something_useful_happened();
4185 }
4186 }
4187 $data_added_since_reopen = 0;
4188 } else {
4189 // ZipArchive::close() can take a very long time, which we want to know about
4190 UpdraftPlus_Job_Scheduler::record_still_alive();
4191 }
4192
4193 clearstatcache();
4194 $this->zipfiles_lastwritetime = time();
4195 }
4196 } elseif (0 == $this->zipfiles_added_thisrun) {
4197 // 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.
4198 // Testing shows that calling time() 1000 times takes negligible time
4199 $this->zipfiles_lastwritetime = time();
4200 }
4201
4202 $this->zipfiles_added++;
4203
4204 // Don't call something_useful_happened() here - nothing necessarily happens until close() is called
4205 if (0 == $this->zipfiles_added % 100) {
4206 $skip_dblog = ($this->zipfiles_added_thisrun > 0 || 0 == $this->zipfiles_added % 1000) ? false : true;
4207 $updraftplus->log("Zip: ".basename($zipfile).": ".$this->zipfiles_added." files added (on-disk size: ".round(@filesize($zipfile)/1024, 1)." KB)", 'notice', false, $skip_dblog);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4208 }
4209
4210 if ($bump_index) {
4211 $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));
4212 $bump_index = false;
4213 $this->bump_index();
4214 $zipfile = $this->zip_basename.($this->index+1).'.zip.tmp';
4215 }
4216
4217 if (empty($zip)) {
4218 $zip = new $this->use_zip_object;
4219
4220 if (file_exists($zipfile)) {
4221 $opencode = $zip->open($zipfile);
4222 $original_size = filesize($zipfile);
4223 clearstatcache();
4224 } else {
4225 $create_code = defined('ZIPARCHIVE::CREATE') ? ZIPARCHIVE::CREATE : 1;
4226 $opencode = $zip->open($zipfile, $create_code);
4227 $original_size = 0;
4228 }
4229
4230 if (true !== $opencode) return new WP_Error('no_open', sprintf(__('Failed to open the zip file (%s) - %s', 'updraftplus'), $zipfile, $zip->last_error));
4231 }
4232
4233 }
4234
4235 // Reset array
4236 $this->zipfiles_batched = array();
4237 $this->zipfiles_skipped_notaltered = array();
4238
4239 if (false == ($nret = $zip->close())) $this->record_zip_error($files_zipadded_since_open, $zip->last_error, $warn_on_failures);
4240
4241 if (apply_filters('updraftplus_include_manifest', false, $this->whichone, $this)) {
4242 if (!empty($this->manifest_path) && file_exists($this->manifest_path)) {
4243 $updraftplus->log('Removing manifest file: '.basename($this->manifest_path).': '.(@unlink($this->manifest_path) ? 'OK' : 'failed'));// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4244 }
4245 }
4246
4247 $this->zipfiles_lastwritetime = time();
4248 // May not exist if the last thing we did was bump
4249 if (file_exists($zipfile) && filesize($zipfile) > $original_size) UpdraftPlus_Job_Scheduler::something_useful_happened();
4250
4251 // Move on to next archive?
4252 if (file_exists($zipfile) && filesize($zipfile) > $this->zip_split_every) {
4253 $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));
4254 $this->bump_index();
4255 }
4256
4257 $manifest = preg_replace('/\.tmp$/', '.list.tmp', $zipfile);
4258 if (!file_exists($manifest)) $this->write_zip_manifest_from_zip($zipfile);
4259
4260 clearstatcache();
4261
4262 return (false == $ret) ? false : $nret;
4263 }
4264
4265 private function record_zip_error($files_zipadded_since_open, $msg, $warn = true) {
4266 global $updraftplus;
4267
4268 if (!empty($updraftplus->cpanel_quota_readable)) {
4269 $hosting_bytes_free = $updraftplus->get_hosting_disk_quota_free();
4270 if (is_array($hosting_bytes_free)) {
4271 $perc = round(100*$hosting_bytes_free[1]/(max($hosting_bytes_free[2], 1)), 1);
4272 $quota_free_msg = sprintf('Free disk space in account: %s (%s used)', round($hosting_bytes_free[3]/1048576, 1)." MB", "$perc %");
4273 $updraftplus->log($quota_free_msg);
4274 if ($hosting_bytes_free[3] < 1048576*50) {
4275 $quota_low = true;
4276 $quota_free_mb = round($hosting_bytes_free[3]/1048576, 1);
4277 $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);
4278 }
4279 }
4280 }
4281
4282 // Always warn of this
4283 if (strpos($msg, 'File Size Limit Exceeded') !== false && 'UpdraftPlus_BinZip' == $this->use_zip_object) {
4284 $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');
4285 } elseif ($warn) {
4286 $warn_msg = __('A zip error occurred', 'updraftplus').' - ';
4287 if (!empty($quota_low)) {
4288 $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/');
4289 } else {
4290 $warn_msg .= __('check your log for more details.', 'updraftplus');
4291 }
4292 $updraftplus->log($warn_msg, 'warning', 'zipcloseerror-'.$this->whichone);
4293 }
4294
4295 $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).");
4296
4297 foreach ($files_zipadded_since_open as $ffile) {
4298 $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);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4299 }
4300 }
4301
4302 /**
4303 * Bump the zip index number. No parameters or returned value, since it is dealing with class variables.
4304 */
4305 private function bump_index() {
4306 global $updraftplus;
4307 $youwhat = $this->whichone;
4308
4309 $timetaken = max(microtime(true)-$this->zip_microtime_start, 0.000001);
4310
4311 $itext = (0 == $this->index) ? '' : ($this->index+1);
4312 $full_path = $this->zip_basename.$itext.'.zip';
4313
4314 $checksums = $updraftplus->which_checksums();
4315
4316 $checksum_description = '';
4317
4318 foreach ($checksums as $checksum) {
4319
4320 $cksum = hash_file($checksum, $full_path.'.tmp');
4321 $updraftplus->jobdata_set($checksum.'-'.$youwhat.$this->index, $cksum);
4322 if ($checksum_description) $checksum_description .= ', ';
4323 $checksum_description .= "$checksum: $cksum";
4324
4325 }
4326
4327 $next_full_path = $this->zip_basename.($this->index+2).'.zip';
4328 // We touch the next zip before renaming the temporary file; this indicates that the backup for the entity is not *necessarily* finished
4329 touch($next_full_path.'.tmp');
4330
4331 if (file_exists($full_path.'.tmp') && filesize($full_path.'.tmp') > 0) {
4332 if (!rename($full_path.'.tmp', $full_path)) {
4333 $updraftplus->log("Rename failed for $full_path.tmp");
4334 } else {
4335 $manifest = $full_path.'.list.tmp';
4336 if (!file_exists($manifest)) $this->write_zip_manifest_from_zip($full_path);
4337 UpdraftPlus_Job_Scheduler::something_useful_happened();
4338 }
4339 }
4340
4341 $kbsize = filesize($full_path)/1024;
4342 $rate = round($kbsize/$timetaken, 1);
4343 $updraftplus->log("Created ".$this->whichone." zip (".$this->index.") - ".round($kbsize, 1)." KB in ".round($timetaken, 1)." s ($rate KB/s) (checksums: $checksum_description)");
4344 $this->zip_microtime_start = microtime(true);
4345
4346 // No need to add $itext here - we can just delete any temporary files for this zip
4347 UpdraftPlus_Filesystem_Functions::clean_temporary_files('_'.$updraftplus->file_nonce."-".$youwhat, 600);
4348
4349 $this->index++;
4350 $this->job_file_entities[$youwhat]['index'] = $this->index;
4351 $updraftplus->jobdata_set('job_file_entities', $this->job_file_entities);
4352 $this->maybe_cloud_backup(basename($full_path), $this->whichone, $this->index);
4353 }
4354
4355 /**
4356 * This function will populate $this->existing_files with a list of files found inside the passed in zip
4357 *
4358 * @param string $zip_path - the zip file name we want to list files for; must end in .tmp
4359 * @param boolean $read_from_manifest - a boolean to indicate if we should try to read from the manifest or not
4360 *
4361 * @return void
4362 */
4363 private function populate_existing_files_list($zip_path, $read_from_manifest) {
4364 global $updraftplus;
4365
4366 // Get the name of the final manifest file
4367 if (preg_match('/\.tmp$/', $zip_path)) {
4368 $manifest = preg_replace('/\.tmp$/', '.list.tmp', $zip_path);
4369 } else {
4370 $manifest = $zip_path.'.list.tmp';
4371 }
4372
4373 if ($read_from_manifest && file_exists($manifest)) {
4374 $manifest_contents = json_decode(file_get_contents($manifest), true);
4375
4376 if (empty($manifest_contents)) {
4377 $updraftplus->log("Zip manifest file found, but reading failed: ".basename($manifest));
4378 } elseif (!empty($manifest_contents['files'])) {
4379 $this->existing_files = array_merge($this->existing_files, $manifest_contents['files'][0]);
4380 $updraftplus->log("Successfully read zip manifest file contents");
4381 return;
4382 } else {
4383 $updraftplus->log("Zip manifest file found, but no files contents were found: ".basename($manifest));
4384 }
4385 } elseif ($read_from_manifest) {
4386 $updraftplus->log("No zip manifest file found; will create one");
4387 }
4388
4389 $zip = new $this->use_zip_object;
4390 if (true !== $zip->open($zip_path)) {
4391 $updraftplus->log("Could not open zip file to examine (".$zip->last_error."); will remove: ".basename($zip_path));
4392 @unlink($zip_path);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4393 } else {
4394
4395 $this->existing_zipfiles_size += filesize($zip_path);
4396
4397 // Don't put this in the for loop, or the magic __get() method which accessing the property invokes gets repeatedly called every time the loop goes round
4398 $numfiles = $zip->numFiles;
4399
4400 for ($i=0; $i < $numfiles; $i++) {
4401 $si = $zip->statIndex($i);
4402 $name = $si['name'];
4403 // Exclude folders
4404 if ('/' == substr($name, -1)) continue;
4405 if (!isset($this->existing_files[$name])) {
4406 $this->existing_files[$name] = $si['size'];
4407 $this->existing_files_rawsize += $si['size'];
4408 }
4409 }
4410
4411 @$zip->close();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4412
4413 if (preg_match('/\.tmp$/', $zip_path)) {
4414 $manifest = preg_replace('/\.tmp$/', '.list-temp.tmp', $zip_path);
4415 } else {
4416 $manifest = $zip_path.'.list-temp.tmp';
4417 }
4418
4419 $this->write_zip_manifest_from_list($manifest, $this->existing_files);
4420
4421 $updraftplus->log(basename($zip_path).": Zip file already exists, with ".count($this->existing_files)." files");
4422 }
4423 }
4424
4425 /**
4426 * This function will get a list of files found inside the passed in zip and call the function to create the zip manifest, returns true on success and false on failure
4427 *
4428 * @uses self::write_zip_manifest_from_list()
4429 * @param string $zip_path - the zip file path; must end in .tmp
4430 *
4431 * @return boolean - returns true on success and false on failure
4432 */
4433 private function write_zip_manifest_from_zip($zip_path) {
4434 global $updraftplus;
4435
4436 $zip_files = array();
4437
4438 $zip = new $this->use_zip_object;
4439 if (true !== $zip->open($zip_path)) {
4440 $updraftplus->log("Could not open zip file to examine (".$zip->last_error."); file: ".basename($zip_path));
4441 return false;
4442 } else {
4443 // Don't put this in the for loop, or the magic __get() method gets repeatedly called every time the loop goes round
4444 $numfiles = $zip->numFiles;
4445
4446 for ($i=0; $i < $numfiles; $i++) {
4447 $si = $zip->statIndex($i);
4448 $name = $si['name'];
4449 // Exclude folders
4450 if ('/' == substr($name, -1)) continue;
4451 $zip_files[$name] = $si['size'];
4452 }
4453
4454 @$zip->close();// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
4455 }
4456
4457 if (empty($zip_files)) {
4458 $updraftplus->log("Did not find any files in the zip: ".basename($zip_path));
4459 return false;
4460 }
4461
4462 if (preg_match('/\.tmp$/', $zip_path)) {
4463 $manifest = preg_replace('/\.tmp$/', '.list-temp.tmp', $zip_path);
4464 } else {
4465 $manifest = $zip_path.'.list-temp.tmp';
4466 }
4467
4468 $this->write_zip_manifest_from_list($manifest, $zip_files);
4469
4470 return true;
4471 }
4472
4473 /**
4474 * This function will create and write the contents of the zip manifest
4475 *
4476 * @param string $manifest - path of the manifest file
4477 * @param array $zip_files - an array of files and their sizes
4478 *
4479 * @return boolean - returns false on failure to write
4480 */
4481 private function write_zip_manifest_from_list($manifest, $zip_files) {
4482 global $updraftplus;
4483
4484 $updraftplus->log('Creating zip file manifest ('.basename($manifest).')');
4485
4486 if (false === ($handle = fopen($manifest, 'w+'))) {
4487 $updraftplus->log('Failed to open zip manifest file ('.basename($manifest).')');
4488 return false;
4489 }
4490
4491 $version = 1;
4492
4493 if (false === fwrite($handle, '{"version":'.$version.', "files":[{')) {
4494 $updraftplus->log('First write to manifest file failed ('.basename($manifest).')');
4495 return false;
4496 }
4497
4498 $last_dir_index = key(array_slice($zip_files, -1, 1, true));
4499
4500 foreach ($zip_files as $name => $size) {
4501 fwrite($handle, json_encode($name).' : '.$size.(($name != $last_dir_index) ? ',' : ''));
4502 }
4503
4504 fwrite($handle, '}]}');
4505 fclose($handle);
4506
4507 $updraftplus->log("Successfully created zip file manifest (size: ".filesize($manifest).")");
4508
4509 $final_manifest = preg_replace('/\.list-temp.tmp$/', '.list.tmp', $manifest);
4510 rename($manifest, $final_manifest);
4511 }
4512
4513 /**
4514 * Returns the member of the array with key (int)0, as a new array. This function is used as a callback for array_map().
4515 *
4516 * @param Array $a - the array
4517 *
4518 * @return Array - with keys 'name' and 'type'
4519 */
4520 private function cb_get_name_base_type($a) {
4521 return array('name' => $a[0], 'type' => 'BASE TABLE');
4522 }
4523
4524 /**
4525 * Returns the members of the array with keys (int)0 and (int)1, as part of a new array.
4526 *
4527 * @param Array $a - the array
4528 *
4529 * @return Array - keys are 'name' and 'type'
4530 */
4531 private function cb_get_name_type($a) {
4532 return array('name' => $a[0], 'type' => $a[1]);
4533 }
4534
4535 /**
4536 * Returns the member of the array with key (string)'name'. This function is used as a callback for array_map().
4537 *
4538 * @param Array $a - the array
4539 *
4540 * @return Mixed - the value with key (string)'name'
4541 */
4542 private function cb_get_name($a) {
4543 return $a['name'];
4544 }
4545
4546 /**
4547 * Exclude files from backup
4548 *
4549 * @param Boolean $filter initial boolean value of whether the given file is excluded or not
4550 * @param String $file the full path of the filename to be checked
4551 * @return Boolean true if the specified file will be excluded, false otherwise
4552 */
4553 public function backup_exclude_file($filter, $file) {
4554 foreach ($this->backup_excluded_patterns as $pattern) {
4555 if (0 === stripos($file, $pattern['directory']) && preg_match($pattern['regex'], $file)) return true;
4556 }
4557 return $filter;
4558 }
4559 }
4560
4561 class UpdraftPlus_WPDB_OtherDB extends wpdb {
4562 /**
4563 * This adjusted bail() does two things: 1) Never dies and 2) logs in the UD log
4564 *
4565 * @param String $message Error text
4566 * @param String $error_code Error code
4567 *
4568 * @return Boolean
4569 */
4570 public function bail($message, $error_code = '500') {
4571 global $updraftplus;
4572 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.';
4573 $updraftplus->log("WPDB_OtherDB error: $message ($error_code)");
4574 // Now do the things that would have been done anyway
4575 if (class_exists('WP_Error')) {
4576 $this->error = new WP_Error($error_code, $message);
4577 } else {
4578 $this->error = $message;
4579 }
4580 return false;
4581 }
4582 }
4583