} elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
$updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
} elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
$updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
} elseif (apply_filters('updraftplus_exclude_file', false, $deref, $use_stripped)) {
$updraftplus->log("Entity excluded by filter: $use_stripped");
} elseif (is_readable($deref)) {
$mtime = filemtime($deref);
if ($mtime > 0 && $mtime > $if_altered_since) {
$this->zipfiles_batched[$deref] = $use_path_when_storing.'/'.$e;
$this->makezip_recursive_batchedbytes += @filesize($deref);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$this->zipfiles_skipped_notaltered[$deref] = $use_path_when_storing.'/'.$e;
$updraftplus->log("$deref: unreadable file (de-referenced from the link $e in $fullpath)");
$updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up"), $deref), 'warning');
} elseif (is_dir($deref)) {
$this->makezip_recursive_add($deref, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
} elseif (is_file($fullpath.'/'.$e)) {
$use_stripped = $stripped_storage_path.'/'.$e;
if (false !== ($fkey = array_search($use_stripped, $exclude))) {
$updraftplus->log("Entity excluded by configuration option: $use_stripped");
} elseif (!empty($this->excluded_extensions) && $this->is_entity_excluded_by_extension($e)) {
$updraftplus->log("Entity excluded by configuration option (extension): $use_stripped");
} elseif (!empty($this->excluded_prefixes) && $this->is_entity_excluded_by_prefix($e)) {
$updraftplus->log("Entity excluded by configuration option (prefix): $use_stripped");
} elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
$updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
} elseif (apply_filters('updraftplus_exclude_file', false, $fullpath.'/'.$e)) {
$updraftplus->log("Entity excluded by filter: $use_stripped");
} elseif (is_readable($fullpath.'/'.$e)) {
$mtime = filemtime($fullpath.'/'.$e);
if ($mtime > 0 && $mtime > $if_altered_since) {
$this->zipfiles_batched[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
$this->makezip_recursive_batchedbytes += @filesize($fullpath.'/'.$e);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$this->zipfiles_skipped_notaltered[$fullpath.'/'.$e] = $use_path_when_storing.'/'.$e;
$updraftplus->log("$fullpath/$e: unreadable file");
$updraftplus->log(sprintf(__("%s: unreadable file - could not be backed up", 'updraftplus'), $use_path_when_storing.'/'.$e), 'warning', "unrfile-$e");
} elseif (is_dir($fullpath.'/'.$e)) {
$use_stripped = $stripped_storage_path.'/'.$e;
if ('wpcore' == $this->whichone && 'updraft' == $e && basename($use_path_when_storing) == 'wp-content' && (!defined('UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS') || !UPDRAFTPLUS_WPCORE_INCLUDE_UPDRAFT_DIRS)) {
// This test, of course, won't catch everything - it just aims to make things better by default
$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);
} elseif (!empty($this->excluded_wildcards) && $this->is_entity_excluded_by_wildcards($use_stripped)) {
$updraftplus->log("Entity excluded by configuration option (wildcards): $use_stripped");
// no need to add_empty_dir here, as it gets done when we recurse
$this->makezip_recursive_add($fullpath.'/'.$e, $use_path_when_storing.'/'.$e, $original_fullpath, $startlevels, $exclude);
$updraftplus->log("Unexpected: path ($use_path_when_storing) fails both is_file() and is_dir()");
* Get a list of excluded extensions
* @param Array $exclude - settings passed in
private function get_excluded_extensions($exclude) {
if (!is_array($exclude)) $exclude = array();
$exclude_extensions = array();
foreach ($exclude as $ex) {
if (preg_match('/^ext:(.+)$/i', $ex, $matches)) {
$exclude_extensions[] = strtolower($matches[1]);
if (defined('UPDRAFTPLUS_EXCLUDE_EXTENSIONS')) {
$exclude_from_define = explode(',', UPDRAFTPLUS_EXCLUDE_EXTENSIONS);
foreach ($exclude_from_define as $ex) {
$exclude_extensions[] = strtolower(trim($ex));
return $exclude_extensions;
* Get a list of excluded prefixes
* @param Array $exclude - settings passed in
* @return Array - each is listed in lower case
private function get_excluded_prefixes($exclude) {
if (!is_array($exclude)) $exclude = array();
$exclude_prefixes = array();
foreach ($exclude as $pref) {
if (preg_match('/^prefix:(.+)$/i', $pref, $matches)) {
$exclude_prefixes[] = strtolower($matches[1]);
return $exclude_prefixes;
* List all the wildcard patterns from the given excluded items
* @param Array $exclude the list of excluded items which may contain not just wildcard patterns but also specific file/directory names as well
* $exclude argument may contains data in an array format like below:
* "snapshots" // definitely not a wildcard parttern, this could be directories/files named `snapshots` which are located in the root/parent directory
* "2021/03/image.jpg", // not a wildcard parttern, this could be files/directories named `image.jpg` which are located in the 2021/03/ directory
* "ext:zip", // not a wildcard pattern, this is to exclude all files that end with `zip` extension
* "prefix:file-", // not a wildcard pattern, this is to exclude all files that begin with `file-` prefix
* "2021/04", // not a wildcard pattern, this is to exclude all files/directories which are located in the 2021/04 directory
* "backup*", // wildcard pattern that excludes all files/directories beginning with `backup` in the root/parent directory
* "2021/*optimise*", // wildcard pattern that excludes all files/directories that have `optimise` anywhere in their names in the `2021` directory
* "2021/04/*.tmp" // wildcard pattern that excludes all files/directories ending with `optimise` anywhere in their names in the `2021/04` directory
* @return Array an array of wilcard patterns
* 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:
* "directory_path" => "",
* "directory_path" => "2021\",
* "pattern" => "*optimise*"
* "directory_path" => "2021\04\",
private function get_excluded_wildcards($exclude) {
if (!is_array($exclude)) $exclude = array();
$excluded_wildcards = array();
foreach ($exclude as $wch) {
// https://regex101.com/r/dMFI0P/1/
if (preg_match('#(.*(?<!\\\)/)?(.*?(?<!\\\)\*.*)#i', $wch, $matches)) {
// 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
$excluded_wildcards[] = array(
// 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
'directory_path' => preg_replace(array('/^[\/\s]*/', '/\/\/*/', '/[\/\s]*$/'), array('', '/', ''), $matches[1]),
return $excluded_wildcards;
* Check whether or not the given entity(file/directory) is excluded from the backup by matching it against a set of wildcard patterns
* @param String $entity the file/directory's stripped path
* @return Boolean true if the entity is excluded, false otherwise
private function is_entity_excluded_by_wildcards($entity) {
$entity_basename = untrailingslashit($entity);
$entity_basename = substr_replace($entity_basename, '', 0, (false === strrpos($entity_basename, '/') ? 0 : strrpos($entity_basename, '/') + 1));
foreach ($this->excluded_wildcards as $wch) {
if (!is_array($wch) || empty($wch)) continue;
if (substr_replace($entity, '', (int) strrpos($entity, '/'), strlen($entity) - (int) strrpos($entity, '/')) !== $wch['directory_path']) continue;
if ('*' == substr($wch['pattern'], -1, 1) && '*' == substr($wch['pattern'], 0, 1) && strlen($wch['pattern']) > 2) {
$wch['pattern'] = substr($wch['pattern'], 1, strlen($wch['pattern'])-2);
$wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
if (strpos($entity_basename, $wch['pattern']) !== false) return true;
} elseif ('*' == substr($wch['pattern'], -1, 1) && strlen($wch['pattern']) > 1) {
$wch['pattern'] = substr($wch['pattern'], 0, strlen($wch['pattern'])-1);
$wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
if (substr($entity_basename, 0, strlen($wch['pattern'])) == $wch['pattern']) return true;
} elseif ('*' == substr($wch['pattern'], 0, 1) && strlen($wch['pattern']) > 1) {
$wch['pattern'] = substr($wch['pattern'], 1);
$wch['pattern'] = str_replace('\*', '*', $wch['pattern']);
if (strlen($entity_basename) >= strlen($wch['pattern']) && substr($entity_basename, strlen($wch['pattern'])*-1) == $wch['pattern']) return true;
private function is_entity_excluded_by_extension($entity) {
foreach ($this->excluded_extensions as $ext) {
if (strtolower(substr($entity, -$eln, $eln)) == $ext) return true;
private function is_entity_excluded_by_prefix($entity) {
$entity = basename($entity);
foreach ($this->excluded_prefixes as $pref) {
if (strtolower(substr($entity, 0, $eln)) == $pref) return true;
private function unserialize_gz_cache_file($file) {
if (!$whandle = gzopen($file, 'r')) return false;
while (!gzeof($whandle)) {
$bytes = @gzread($whandle, 1048576);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$updraftplus->log("Got empty gzread ($emptimes times)");
if ($emptimes>2) return false;
return unserialize($var);
* @param Array|String $source Caution: $source is allowed to be an array, not just a filename
* @param String $backup_file_basename Name of backup file
* @param String $whichone Backup entity type (e.g. 'plugins')
* @param Boolean $retry_on_error Set to retry upon error
private function make_zipfile($source, $backup_file_basename, $whichone, $retry_on_error = true) {
$original_index = $this->index;
$itext = (empty($this->index)) ? '' : ($this->index+1);
$destination_base = $backup_file_basename.'-'.$whichone.$itext.'.zip.tmp';
// $destination is the temporary file (ending in .tmp)
$destination = $this->updraft_dir.'/'.$destination_base;
// - No zip extension present and no relevant method present
// The zip extension check is not redundant, because method_exists segfaults some PHP installs, leading to support requests
// We need meta-info about $whichone
$backupable_entities = $updraftplus->get_backupable_file_entities(true, false);
// This is only used by one corner-case in BinZip
// $this->make_zipfile_source = (isset($backupable_entities[$whichone])) ? $backupable_entities[$whichone] : $source;
$this->make_zipfile_source = (is_array($source) && isset($backupable_entities[$whichone])) ? (('uploads' == $whichone) ? dirname($backupable_entities[$whichone]) : $backupable_entities[$whichone]) : dirname($source);
$this->existing_files = array();
// Used for tracking compression ratios
$this->existing_files_rawsize = 0;
$this->existing_zipfiles_size = 0;
// Enumerate existing files
// Usually first_linked_index is zero; the exception being with more files, where previous zips' contents are irrelevant
for ($j = $this->first_linked_index; $j <= $this->index; $j++) {
$jtext = (0 == $j) ? '' : $j+1;
// This is, in a non-obvious way, compatible with filenames which indicate increments
// $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.
$examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index) ? '.tmp' : '');
// 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.
// 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.
// Other examples of this appear to be in HS#1001 and #1047
if ($j != $this->index && !file_exists($examine_zip)) {
$alt_examine_zip = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone.$jtext.'.zip'.(($j == $this->index - 1) ? '.tmp' : '');
if ($alt_examine_zip != $examine_zip && file_exists($alt_examine_zip) && is_readable($alt_examine_zip) && filesize($alt_examine_zip)>0) {
$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)");
if (rename($alt_examine_zip, $examine_zip)) {
$updraftplus->log("Rename failed - backup zips likely to not have sequential numbers (does not affect backup integrity, but can cause user confusion)");
// If the file exists, then we should grab its index of files inside, and sizes
// Then, when we come to write a file, we should check if it's already there, and only add if it is not
if (file_exists($examine_zip) && is_readable($examine_zip) && filesize($examine_zip) > 0) {
// 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.
$this->populate_existing_files_list($examine_zip, substr($examine_zip, -4, 4) === '.zip');
// try_split is true if there have been no check-ins recently - or if it needs to be split anyway
if ($j == $this->index) {
if (filesize($examine_zip) > 50*1048576) {
// We could, as a future enhancement, save this back to the job data, if we see a case that needs it
$this->zip_split_every = max(
(int) $this->zip_split_every/2,
UPDRAFTPLUS_SPLIT_MIN*1048576,
min(filesize($examine_zip)-1048576, $this->zip_split_every)
$updraftplus->jobdata_set('split_every', (int) ($this->zip_split_every/1048576));
$updraftplus->log("No check-in on last two runs; bumping index and reducing zip split to: ".round($this->zip_split_every/1048576, 1)." MB");
} elseif (filesize($examine_zip) > $this->zip_split_every) {
$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));
} elseif (file_exists($examine_zip)) {
$updraftplus->log("Zip file already exists, but is not readable or was zero-sized; will remove: ".basename($examine_zip));
@unlink($examine_zip);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$this->zip_last_ratio = ($this->existing_files_rawsize > 0) ? ($this->existing_zipfiles_size/$this->existing_files_rawsize) : 1;
$this->zipfiles_added = 0;
$this->zipfiles_added_thisrun = 0;
$this->zipfiles_dirbatched = array();
$this->zipfiles_batched = array();
$this->zipfiles_skipped_notaltered = array();
$this->zipfiles_lastwritetime = time();
$this->zip_basename = $this->updraft_dir.'/'.$backup_file_basename.'-'.$whichone;
if (!empty($do_bump_index)) $this->bump_index();
// Store this in its original form
// Reset. This counter is used only with PcLZip, to decide if it's better to do it all-in-one
$this->makezip_recursive_batchedbytes = 0;
if (!is_array($source)) $source = array($source);
$exclude = $updraftplus->get_exclude($whichone);
$files_enumerated_at = $updraftplus->jobdata_get('files_enumerated_at');
if (!is_array($files_enumerated_at)) $files_enumerated_at = array();
$files_enumerated_at[$whichone] = time();
$updraftplus->jobdata_set('files_enumerated_at', $files_enumerated_at);
$this->makezip_if_altered_since = is_array($this->altered_since) ? (isset($this->altered_since[$whichone]) ? $this->altered_since[$whichone] : -1) : -1;
$got_uploads_from_cache = false;
// Uploads: can/should we get it back from the cache?
// || 'others' == $whichone
if (('uploads' == $whichone || 'others' == $whichone) && function_exists('gzopen') && function_exists('gzread')) {
$use_cache_files = false;
$cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
// Cache file suffixes: -zfd.gz.tmp, -zfb.gz.tmp, -info.tmp, (possible)-zfs.gz.tmp
if (file_exists($cache_file_base.'-zfd.gz.tmp') && file_exists($cache_file_base.'-zfb.gz.tmp') && file_exists($cache_file_base.'-info.tmp')) {
// Cache files exist; shall we use them?
$mtime = filemtime($cache_file_base.'-zfd.gz.tmp');
// Require < 30 minutes old
if (time() - $mtime < 1800) {
$var = $this->unserialize_gz_cache_file($cache_file_base.'-zfd.gz.tmp');
$this->zipfiles_dirbatched = $var;
$var = $this->unserialize_gz_cache_file($cache_file_base.'-zfb.gz.tmp');
$this->zipfiles_batched = $var;
if (file_exists($cache_file_base.'-info.tmp')) {
$var = maybe_unserialize(file_get_contents($cache_file_base.'-info.tmp'));
if (is_array($var) && isset($var['makezip_recursive_batchedbytes'])) {
$this->makezip_recursive_batchedbytes = $var['makezip_recursive_batchedbytes'];
if (file_exists($cache_file_base.'-zfs.gz.tmp')) {
$var = $this->unserialize_gz_cache_file($cache_file_base.'-zfs.gz.tmp');
$this->zipfiles_skipped_notaltered = $var;
$this->zipfiles_skipped_notaltered = array();
$updraftplus->log("Failed to recover file lists from existing cache files");
$this->zipfiles_skipped_notaltered = array();
$this->makezip_recursive_batchedbytes = 0;
$this->zipfiles_batched = array();
$this->zipfiles_dirbatched = array();
$updraftplus->log("File lists recovered from cache files; sizes: ".count($this->zipfiles_batched).", ".count($this->zipfiles_batched).", ".count($this->zipfiles_skipped_notaltered).")");
$got_uploads_from_cache = true;
$time_counting_began = time();
$this->excluded_extensions = $this->get_excluded_extensions($exclude);
$this->excluded_prefixes = $this->get_excluded_prefixes($exclude);
$this->excluded_wildcards = $this->get_excluded_wildcards($exclude);
foreach ($source as $element) {
// makezip_recursive_add($fullpath, $use_path_when_storing, $original_fullpath, $startlevels = 1, $exclude_array)
if ('uploads' == $whichone) {
if (empty($got_uploads_from_cache)) {
$dirname = dirname($element);
$basename = $this->basename($element);
$add_them = $this->makezip_recursive_add($element, basename($dirname).'/'.$basename, $element, 2, $exclude);
if (empty($got_uploads_from_cache)) {
$add_them = $this->makezip_recursive_add($element, $this->basename($element), $element, 1, $exclude);
if (is_wp_error($add_them) || false === $add_them) $error_occurred = true;
$time_counting_ended = time();
// Cache the file scan, if it looks like it'll be useful
// We use gzip to reduce the size as on hosts which limit disk I/O, the cacheing may make things worse
// || 'others' == $whichone
if (('uploads' == $whichone || 'others' == $whichone) && !$error_occurred && function_exists('gzopen') && function_exists('gzwrite')) {
$cache_file_base = $this->zip_basename.'-cachelist-'.$this->makezip_if_altered_since;
// 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.
$memory_needed_estimate = 0;
foreach ($this->zipfiles_batched as $k => $v) {
$memory_needed_estimate += strlen($k)+strlen($v)+12;
// 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
// Let us suppose we need 15% overhead for gzipping
$memory_limit = ini_get('memory_limit');
$memory_usage = round(@memory_get_usage(false)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
$memory_usage2 = round(@memory_get_usage(true)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
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')) {
$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)");
$buf = 'a:'.count($this->zipfiles_batched).':{';
foreach ($this->zipfiles_batched as $file => $add_as) {
$v = addslashes($add_as);
$buf .= 's:'.strlen($k).':"'.$k.'";s:'.strlen($v).':"'.$v.'";';
if (strlen($buf) > 1048576) {
gzwrite($whandle, $buf, strlen($buf));
$final = gzwrite($whandle, $buf);
@unlink($cache_file_base.'-zfb.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
@gzclose($whandle);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if (!empty($this->zipfiles_skipped_notaltered)) {
if ($shandle = gzopen($cache_file_base.'-zfs.gz.tmp', 'w')) {
if (!gzwrite($shandle, serialize($this->zipfiles_skipped_notaltered))) {
$aborted_on_skipped = true;
$aborted_on_skipped = true;
if (!empty($aborted_on_skipped)) {
@unlink($cache_file_base.'-zfs.gz.tmp');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged