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

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