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

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

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