| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) die('No direct access.'); |
| 4 |
|
| 5 |
/** |
| 6 |
* Here live some stand-alone filesystem manipulation functions |
| 7 |
*/ |
| 8 |
class UpdraftPlus_Filesystem_Functions { |
| 9 |
|
| 10 |
/** |
| 11 |
* If $basedirs is passed as an array, then $directorieses must be too |
| 12 |
* Note: Reason $directorieses is being used because $directories is used within the foreach-within-a-foreach further down |
| 13 |
* |
| 14 |
* @param Array|String $directorieses List of of directories, or a single one |
| 15 |
* @param Array $exclude An exclusion array of directories |
| 16 |
* @param Array|String $basedirs A list of base directories, or a single one |
| 17 |
* @param String $format Return format - 'text' or 'numeric' |
| 18 |
* @return String|Integer |
| 19 |
*/ |
| 20 |
public static function recursive_directory_size($directorieses, $exclude = array(), $basedirs = '', $format = 'text') { |
| 21 |
|
| 22 |
$size = 0; |
| 23 |
|
| 24 |
if (is_string($directorieses)) { |
| 25 |
$basedirs = $directorieses; |
| 26 |
$directorieses = array($directorieses); |
| 27 |
} |
| 28 |
|
| 29 |
if (is_string($basedirs)) $basedirs = array($basedirs); |
| 30 |
|
| 31 |
foreach ($directorieses as $ind => $directories) { |
| 32 |
if (!is_array($directories)) $directories = array($directories); |
| 33 |
|
| 34 |
$basedir = empty($basedirs[$ind]) ? $basedirs[0] : $basedirs[$ind]; |
| 35 |
|
| 36 |
foreach ($directories as $dir) { |
| 37 |
if (is_file($dir)) { |
| 38 |
$size += @filesize($dir); |
| 39 |
} else { |
| 40 |
$suffix = ('' != $basedir) ? ((0 === strpos($dir, $basedir.'/')) ? substr($dir, 1+strlen($basedir)) : '') : ''; |
| 41 |
$size += self::recursive_directory_size_raw($basedir, $exclude, $suffix); |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
} |
| 46 |
|
| 47 |
if ('numeric' == $format) return $size; |
| 48 |
|
| 49 |
return UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size); |
| 50 |
|
| 51 |
} |
| 52 |
|
| 53 |
/** |
| 54 |
* Ensure that WP_Filesystem is instantiated and functional. Otherwise, outputs necessary HTML and dies. |
| 55 |
* Will also consult $_POST['updraft_restore'] and can set $_POST['updraft_restore_*'] and $_POST['updraft_restorer_*'] |
| 56 |
* |
| 57 |
* @param Array|Null $second_loop_entities - array of files grouped by type |
| 58 |
* @param Array $url_parameters - parameters and values to be added to the URL output |
| 59 |
*/ |
| 60 |
public static function ensure_wp_filesystem_set_up_for_restore($second_loop_entities, $url_parameters = array()) { |
| 61 |
|
| 62 |
global $wp_filesystem; |
| 63 |
|
| 64 |
// request_filesystem_credentials passes on fields just via hidden name/value pairs. |
| 65 |
// Build array of parameters to be passed via this |
| 66 |
$extra_fields = array(); |
| 67 |
if (isset($_POST['updraft_restore']) && is_array($_POST['updraft_restore'])) { |
| 68 |
foreach ($_POST['updraft_restore'] as $entity) { |
| 69 |
$_POST['updraft_restore_'.$entity] = 1; |
| 70 |
$extra_fields[] = 'updraft_restore_'.$entity; |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
foreach ($second_loop_entities as $type => $files) { |
| 75 |
$_POST['updraft_restore_'.$type] = 1; |
| 76 |
if (!in_array('updraft_restore_'.$type, $extra_fields)) $extra_fields[] = 'updraft_restore_'.$type; |
| 77 |
} |
| 78 |
|
| 79 |
// Now make sure that updraft_restorer_ option fields get passed along to request_filesystem_credentials |
| 80 |
foreach ($_POST as $key => $value) { |
| 81 |
if (0 === strpos($key, 'updraft_restorer_')) $extra_fields[] = $key; |
| 82 |
} |
| 83 |
|
| 84 |
$build_url = UpdraftPlus_Options::admin_page().'?page=updraftplus&action=updraft_restore'; |
| 85 |
|
| 86 |
foreach ($url_parameters as $k => $v) { |
| 87 |
$build_url .= '&'.$k.'='.$v; |
| 88 |
} |
| 89 |
|
| 90 |
$credentials = request_filesystem_credentials($build_url, '', false, false, $extra_fields); |
| 91 |
|
| 92 |
WP_Filesystem($credentials); |
| 93 |
|
| 94 |
if ($wp_filesystem->errors->get_error_code()) { |
| 95 |
echo '<p><em><a href="'.apply_filters('updraftplus_com_link', "https://updraftplus.com/faqs/asked-ftp-details-upon-restorationmigration-updates/").'" target="_blank">'.__('Why am I seeing this?', 'updraftplus').'</a></em></p>'; |
| 96 |
foreach ($wp_filesystem->errors->get_error_messages() as $message) show_message($message); |
| 97 |
exit; |
| 98 |
} |
| 99 |
|
| 100 |
} |
| 101 |
|
| 102 |
/** |
| 103 |
* Get the html of "Web-server disk space" line which resides above of the existing backup table |
| 104 |
* |
| 105 |
* @param Boolean $will_immediately_calculate_disk_space Whether disk space should be counted now or when user click Refresh link |
| 106 |
* |
| 107 |
* @return String Web server disk space html to render |
| 108 |
*/ |
| 109 |
public static function web_server_disk_space($will_immediately_calculate_disk_space = true) { |
| 110 |
if ($will_immediately_calculate_disk_space) { |
| 111 |
$disk_space_used = self::get_disk_space_used('updraft', 'numeric'); |
| 112 |
if ($disk_space_used > apply_filters('updraftplus_display_usage_line_threshold_size', 104857600)) { // 104857600 = 100 MB = (100 * 1024 * 1024) |
| 113 |
$disk_space_text = UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($disk_space_used); |
| 114 |
$refresh_link_text = __('refresh', 'updraftplus'); |
| 115 |
return self::web_server_disk_space_html($disk_space_text, $refresh_link_text); |
| 116 |
} else { |
| 117 |
return ''; |
| 118 |
} |
| 119 |
} else { |
| 120 |
$disk_space_text = ''; |
| 121 |
$refresh_link_text = __('calculate', 'updraftplus'); |
| 122 |
return self::web_server_disk_space_html($disk_space_text, $refresh_link_text); |
| 123 |
} |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Get the html of "Web-server disk space" line which resides above of the existing backup table |
| 128 |
* |
| 129 |
* @param String $disk_space_text The texts which represents disk space usage |
| 130 |
* @param String $refresh_link_text Refresh disk space link text |
| 131 |
* |
| 132 |
* @return String - Web server disk space HTML |
| 133 |
*/ |
| 134 |
public static function web_server_disk_space_html($disk_space_text, $refresh_link_text) { |
| 135 |
return '<li class="updraft-server-disk-space" title="'.esc_attr__('This is a count of the contents of your Updraft directory', 'updraftplus').'"><strong>'.__('Web-server disk space in use by UpdraftPlus', 'updraftplus').':</strong> <span class="updraft_diskspaceused"><em>'.$disk_space_text.'</em></span> <a class="updraft_diskspaceused_update" href="#">'.$refresh_link_text.'</a></li>'; |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Cleans up temporary files found in the updraft directory (and some in the site root - pclzip) |
| 140 |
* Always cleans up temporary files over 12 hours old. |
| 141 |
* With parameters, also cleans up those. |
| 142 |
* Also cleans out old job data older than 12 hours old (immutable value) |
| 143 |
* include_cachelist also looks to match any files of cached file analysis data |
| 144 |
* |
| 145 |
* @param String $match - if specified, then a prefix to require |
| 146 |
* @param Integer $older_than - in seconds |
| 147 |
* @param Boolean $include_cachelist - include cachelist files in what can be purged |
| 148 |
*/ |
| 149 |
public static function clean_temporary_files($match = '', $older_than = 43200, $include_cachelist = false) { |
| 150 |
|
| 151 |
global $updraftplus; |
| 152 |
|
| 153 |
// Clean out old job data |
| 154 |
if ($older_than > 10000) { |
| 155 |
|
| 156 |
global $wpdb; |
| 157 |
$table = is_multisite() ? $wpdb->sitemeta : $wpdb->options; |
| 158 |
$key_column = is_multisite() ? 'meta_key' : 'option_name'; |
| 159 |
$value_column = is_multisite() ? 'meta_value' : 'option_value'; |
| 160 |
|
| 161 |
// Limit the maximum number for performance (the rest will get done next time, if for some reason there was a back-log) |
| 162 |
$all_jobs = $wpdb->get_results("SELECT $key_column, $value_column FROM $table WHERE $key_column LIKE 'updraft_jobdata_%' LIMIT 100", ARRAY_A); |
| 163 |
|
| 164 |
foreach ($all_jobs as $job) { |
| 165 |
$val = maybe_unserialize($job[$value_column]); |
| 166 |
// TODO: Can simplify this after a while (now all jobs use job_time_ms) - 1 Jan 2014 |
| 167 |
$delete = false; |
| 168 |
if (!empty($val['next_increment_start_scheduled_for'])) { |
| 169 |
if (time() > $val['next_increment_start_scheduled_for'] + 86400) $delete = true; |
| 170 |
} elseif (!empty($val['backup_time_ms']) && time() > $val['backup_time_ms'] + 86400) { |
| 171 |
$delete = true; |
| 172 |
} elseif (!empty($val['job_time_ms']) && time() > $val['job_time_ms'] + 86400) { |
| 173 |
$delete = true; |
| 174 |
} elseif (!empty($val['job_type']) && 'backup' != $val['job_type'] && empty($val['backup_time_ms']) && empty($val['job_time_ms'])) { |
| 175 |
$delete = true; |
| 176 |
} |
| 177 |
if ($delete) delete_site_option($job[$key_column]); |
| 178 |
} |
| 179 |
} |
| 180 |
$updraft_dir = $updraftplus->backups_dir_location(); |
| 181 |
$now_time = time(); |
| 182 |
$files_deleted = 0; |
| 183 |
if ($handle = opendir($updraft_dir)) { |
| 184 |
while (false !== ($entry = readdir($handle))) { |
| 185 |
$manifest_match = preg_match("/updraftplus-manifest.json/", $entry); |
| 186 |
// This match is for files created internally by zipArchive::addFile |
| 187 |
$ziparchive_match = preg_match("/$match([0-9]+)?\.zip\.tmp\.([A-Za-z0-9]){6}?$/i", $entry); |
| 188 |
// zi followed by 6 characters is the pattern used by /usr/bin/zip on Linux systems. It's safe to check for, as we have nothing else that's going to match that pattern. |
| 189 |
$binzip_match = preg_match("/^zi([A-Za-z0-9]){6}$/", $entry); |
| 190 |
$cachelist_match = ($include_cachelist) ? preg_match("/$match-cachelist-.*.tmp$/i", $entry) : false; |
| 191 |
$browserlog_match = preg_match('/^log\.[0-9a-f]+-browser\.txt$/', $entry); |
| 192 |
// Temporary files from the database dump process - not needed, as is caught by the catch-all |
| 193 |
// $table_match = preg_match("/${match}-table-(.*)\.table(\.tmp)?\.gz$/i", $entry); |
| 194 |
// The gz goes in with the txt, because we *don't* want to reap the raw .txt files |
| 195 |
if ((preg_match("/$match\.(tmp|table|txt\.gz)(\.gz)?$/i", $entry) || $cachelist_match || $ziparchive_match || $binzip_match || $manifest_match || $browserlog_match) && is_file($updraft_dir.'/'.$entry)) { |
| 196 |
// We delete if a parameter was specified (and either it is a ZipArchive match or an order to delete of whatever age), or if over 12 hours old |
| 197 |
if (($match && ($ziparchive_match || $binzip_match || $cachelist_match || $manifest_match || 0 == $older_than) && $now_time-filemtime($updraft_dir.'/'.$entry) >= $older_than) || $now_time-filemtime($updraft_dir.'/'.$entry)>43200) { |
| 198 |
$skip_dblog = (0 == $files_deleted % 25) ? false : true; |
| 199 |
$updraftplus->log("Deleting old temporary file: $entry", 'notice', false, $skip_dblog); |
| 200 |
@unlink($updraft_dir.'/'.$entry); |
| 201 |
$files_deleted++; |
| 202 |
} |
| 203 |
} elseif (preg_match('/^log\.[0-9a-f]+\.txt$/', $entry) && $now_time-filemtime($updraft_dir.'/'.$entry)> apply_filters('updraftplus_log_delete_age', 86400 * 40, $entry)) { |
| 204 |
$skip_dblog = (0 == $files_deleted % 25) ? false : true; |
| 205 |
$updraftplus->log("Deleting old log file: $entry", 'notice', false, $skip_dblog); |
| 206 |
@unlink($updraft_dir.'/'.$entry); |
| 207 |
$files_deleted++; |
| 208 |
} |
| 209 |
} |
| 210 |
@closedir($handle); |
| 211 |
} |
| 212 |
|
| 213 |
// Depending on the PHP setup, the current working directory could be ABSPATH or wp-admin - scan both |
| 214 |
// Since 1.9.32, we set them to go into $updraft_dir, so now we must check there too. Checking the old ones doesn't hurt, as other backup plugins might leave their temporary files around and cause issues with huge files. |
| 215 |
foreach (array(ABSPATH, ABSPATH.'wp-admin/', $updraft_dir.'/') as $path) { |
| 216 |
if ($handle = opendir($path)) { |
| 217 |
while (false !== ($entry = readdir($handle))) { |
| 218 |
// With the old pclzip temporary files, there is no need to keep them around after they're not in use - so we don't use $older_than here - just go for 15 minutes |
| 219 |
if (preg_match("/^pclzip-[a-z0-9]+.tmp$/", $entry) && $now_time-filemtime($path.$entry) >= 900) { |
| 220 |
$updraftplus->log("Deleting old PclZip temporary file: $entry (from ".basename($path).")"); |
| 221 |
@unlink($path.$entry); |
| 222 |
} |
| 223 |
} |
| 224 |
@closedir($handle); |
| 225 |
} |
| 226 |
} |
| 227 |
} |
| 228 |
|
| 229 |
/** |
| 230 |
* Find out whether we really can write to a particular folder |
| 231 |
* |
| 232 |
* @param String $dir - the folder path |
| 233 |
* |
| 234 |
* @return Boolean - the result |
| 235 |
*/ |
| 236 |
public static function really_is_writable($dir) { |
| 237 |
// Suppress warnings, since if the user is dumping warnings to screen, then invalid JavaScript results and the screen breaks. |
| 238 |
if (!@is_writable($dir)) return false; |
| 239 |
// Found a case - GoDaddy server, Windows, PHP 5.2.17 - where is_writable returned true, but writing failed |
| 240 |
$rand_file = "$dir/test-".md5(rand().time()).".txt"; |
| 241 |
while (file_exists($rand_file)) { |
| 242 |
$rand_file = "$dir/test-".md5(rand().time()).".txt"; |
| 243 |
} |
| 244 |
$ret = @file_put_contents($rand_file, 'testing...'); |
| 245 |
@unlink($rand_file); |
| 246 |
return ($ret > 0); |
| 247 |
} |
| 248 |
|
| 249 |
/** |
| 250 |
* Remove a directory from the local filesystem |
| 251 |
* |
| 252 |
* @param String $dir - the directory |
| 253 |
* @param Boolean $contents_only - if set to true, then do not remove the directory, but only empty it of contents |
| 254 |
* |
| 255 |
* @return Boolean - success/failure |
| 256 |
*/ |
| 257 |
public static function remove_local_directory($dir, $contents_only = false) { |
| 258 |
// PHP 5.3+ only |
| 259 |
// foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) { |
| 260 |
// $path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname()); |
| 261 |
// } |
| 262 |
// return rmdir($dir); |
| 263 |
|
| 264 |
if ($handle = @opendir($dir)) { |
| 265 |
while (false !== ($entry = readdir($handle))) { |
| 266 |
if ('.' !== $entry && '..' !== $entry) { |
| 267 |
if (is_dir($dir.'/'.$entry)) { |
| 268 |
self::remove_local_directory($dir.'/'.$entry, false); |
| 269 |
} else { |
| 270 |
@unlink($dir.'/'.$entry); |
| 271 |
} |
| 272 |
} |
| 273 |
} |
| 274 |
@closedir($handle); |
| 275 |
} |
| 276 |
|
| 277 |
return $contents_only ? true : rmdir($dir); |
| 278 |
} |
| 279 |
|
| 280 |
/** |
| 281 |
* Perform gzopen(), but with various extra bits of help for potential problems |
| 282 |
* |
| 283 |
* @param String $file - the filesystem path |
| 284 |
* @param Array $warn - warnings |
| 285 |
* @param Array $err - errors |
| 286 |
* |
| 287 |
* @return Boolean|Resource - returns false upon failure, otherwise the handle as from gzopen() |
| 288 |
*/ |
| 289 |
public static function gzopen_for_read($file, &$warn, &$err) { |
| 290 |
if (!function_exists('gzopen') || !function_exists('gzread')) { |
| 291 |
$missing = ''; |
| 292 |
if (!function_exists('gzopen')) $missing .= 'gzopen'; |
| 293 |
if (!function_exists('gzread')) $missing .= ($missing) ? ', gzread' : 'gzread'; |
| 294 |
$err[] = sprintf(__("Your web server's PHP installation has these functions disabled: %s.", 'updraftplus'), $missing).' '.sprintf(__('Your hosting company must enable these functions before %s can work.', 'updraftplus'), __('restoration', 'updraftplus')); |
| 295 |
return false; |
| 296 |
} |
| 297 |
if (false === ($dbhandle = gzopen($file, 'r'))) return false; |
| 298 |
|
| 299 |
if (!function_exists('gzseek')) return $dbhandle; |
| 300 |
|
| 301 |
if (false === ($bytes = gzread($dbhandle, 3))) return false; |
| 302 |
// Double-gzipped? |
| 303 |
if ('H4sI' != base64_encode($bytes)) { |
| 304 |
if (0 === gzseek($dbhandle, 0)) { |
| 305 |
return $dbhandle; |
| 306 |
} else { |
| 307 |
@gzclose($dbhandle); |
| 308 |
return gzopen($file, 'r'); |
| 309 |
} |
| 310 |
} |
| 311 |
// Yes, it's double-gzipped |
| 312 |
|
| 313 |
$what_to_return = false; |
| 314 |
$mess = __('The database file appears to have been compressed twice - probably the website you downloaded it from had a mis-configured webserver.', 'updraftplus'); |
| 315 |
$messkey = 'doublecompress'; |
| 316 |
$err_msg = ''; |
| 317 |
|
| 318 |
if (false === ($fnew = fopen($file.".tmp", 'w')) || !is_resource($fnew)) { |
| 319 |
|
| 320 |
@gzclose($dbhandle); |
| 321 |
$err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus'); |
| 322 |
|
| 323 |
} else { |
| 324 |
|
| 325 |
@fwrite($fnew, $bytes); |
| 326 |
$emptimes = 0; |
| 327 |
while (!gzeof($dbhandle)) { |
| 328 |
$bytes = @gzread($dbhandle, 262144); |
| 329 |
if (empty($bytes)) { |
| 330 |
$emptimes++; |
| 331 |
global $updraftplus; |
| 332 |
$updraftplus->log("Got empty gzread ($emptimes times)"); |
| 333 |
if ($emptimes>2) break; |
| 334 |
} else { |
| 335 |
@fwrite($fnew, $bytes); |
| 336 |
} |
| 337 |
} |
| 338 |
|
| 339 |
gzclose($dbhandle); |
| 340 |
fclose($fnew); |
| 341 |
// On some systems (all Windows?) you can't rename a gz file whilst it's gzopened |
| 342 |
if (!rename($file.".tmp", $file)) { |
| 343 |
$err_msg = __('The attempt to undo the double-compression failed.', 'updraftplus'); |
| 344 |
} else { |
| 345 |
$mess .= ' '.__('The attempt to undo the double-compression succeeded.', 'updraftplus'); |
| 346 |
$messkey = 'doublecompressfixed'; |
| 347 |
$what_to_return = gzopen($file, 'r'); |
| 348 |
} |
| 349 |
|
| 350 |
} |
| 351 |
|
| 352 |
$warn[$messkey] = $mess; |
| 353 |
if (!empty($err_msg)) $err[] = $err_msg; |
| 354 |
return $what_to_return; |
| 355 |
} |
| 356 |
|
| 357 |
public static function recursive_directory_size_raw($prefix_directory, &$exclude = array(), $suffix_directory = '') { |
| 358 |
|
| 359 |
$directory = $prefix_directory.('' == $suffix_directory ? '' : '/'.$suffix_directory); |
| 360 |
$size = 0; |
| 361 |
if (substr($directory, -1) == '/') $directory = substr($directory, 0, -1); |
| 362 |
|
| 363 |
if (!file_exists($directory) || !is_dir($directory) || !is_readable($directory)) return -1; |
| 364 |
if (file_exists($directory.'/.donotbackup')) return 0; |
| 365 |
|
| 366 |
if ($handle = opendir($directory)) { |
| 367 |
while (($file = readdir($handle)) !== false) { |
| 368 |
if ('.' != $file && '..' != $file) { |
| 369 |
$spath = ('' == $suffix_directory) ? $file : $suffix_directory.'/'.$file; |
| 370 |
if (false !== ($fkey = array_search($spath, $exclude))) { |
| 371 |
unset($exclude[$fkey]); |
| 372 |
continue; |
| 373 |
} |
| 374 |
$path = $directory.'/'.$file; |
| 375 |
if (is_file($path)) { |
| 376 |
$size += filesize($path); |
| 377 |
} elseif (is_dir($path)) { |
| 378 |
$handlesize = self::recursive_directory_size_raw($prefix_directory, $exclude, $suffix_directory.('' == $suffix_directory ? '' : '/').$file); |
| 379 |
if ($handlesize >= 0) { |
| 380 |
$size += $handlesize; |
| 381 |
} |
| 382 |
} |
| 383 |
} |
| 384 |
} |
| 385 |
closedir($handle); |
| 386 |
} |
| 387 |
|
| 388 |
return $size; |
| 389 |
|
| 390 |
} |
| 391 |
|
| 392 |
/** |
| 393 |
* Get information on disk space used by an entity, or by UD's internal directory. Returns as a human-readable string. |
| 394 |
* |
| 395 |
* @param String $entity - the entity (e.g. 'plugins'; 'all' for all entities, or 'ud' for UD's internal directory) |
| 396 |
* @param String $format Return format - 'text' or 'numeric' |
| 397 |
* @return String|Integer If $format is text, It returns strings. Otherwise integer value. |
| 398 |
*/ |
| 399 |
public static function get_disk_space_used($entity, $format = 'text') { |
| 400 |
global $updraftplus; |
| 401 |
if ('updraft' == $entity) return self::recursive_directory_size($updraftplus->backups_dir_location(), array(), '', $format); |
| 402 |
|
| 403 |
$backupable_entities = $updraftplus->get_backupable_file_entities(true, false); |
| 404 |
|
| 405 |
if ('all' == $entity) { |
| 406 |
$total_size = 0; |
| 407 |
foreach ($backupable_entities as $entity => $data) { |
| 408 |
// Might be an array |
| 409 |
$basedir = $backupable_entities[$entity]; |
| 410 |
$dirs = apply_filters('updraftplus_dirlist_'.$entity, $basedir); |
| 411 |
$size = self::recursive_directory_size($dirs, $updraftplus->get_exclude($entity), $basedir, 'numeric'); |
| 412 |
if (is_numeric($size) && $size>0) $total_size += $size; |
| 413 |
} |
| 414 |
|
| 415 |
if ('numeric' == $format) { |
| 416 |
return $total_size; |
| 417 |
} else { |
| 418 |
return UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($total_size); |
| 419 |
} |
| 420 |
|
| 421 |
} elseif (!empty($backupable_entities[$entity])) { |
| 422 |
// Might be an array |
| 423 |
$basedir = $backupable_entities[$entity]; |
| 424 |
$dirs = apply_filters('updraftplus_dirlist_'.$entity, $basedir); |
| 425 |
return self::recursive_directory_size($dirs, $updraftplus->get_exclude($entity), $basedir, $format); |
| 426 |
} |
| 427 |
|
| 428 |
// Default fallback |
| 429 |
return apply_filters('updraftplus_get_disk_space_used_none', __('Error', 'updraftplus'), $entity, $backupable_entities); |
| 430 |
} |
| 431 |
|
| 432 |
/** |
| 433 |
* Unzips a specified ZIP file to a location on the filesystem via the WordPress |
| 434 |
* Filesystem Abstraction. Forked from WordPress core in version 5.1-alpha-44182. |
| 435 |
* Forked to allow us to modify the behaviour (eventually, to provide feedback on progress) |
| 436 |
* |
| 437 |
* Assumes that WP_Filesystem() has already been called and set up. Does not extract |
| 438 |
* a root-level __MACOSX directory, if present. |
| 439 |
* |
| 440 |
* Attempts to increase the PHP memory limit before uncompressing. However, |
| 441 |
* the most memory required shouldn't be much larger than the archive itself. |
| 442 |
* |
| 443 |
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. |
| 444 |
* |
| 445 |
* @param String $file - Full path and filename of ZIP archive. |
| 446 |
* @param String $to - Full path on the filesystem to extract archive to. |
| 447 |
* @param Integer $starting_index - index of entry to start unzipping from (allows resumption) |
| 448 |
* |
| 449 |
* @return Boolean|WP_Error True on success, WP_Error on failure. |
| 450 |
*/ |
| 451 |
public static function unzip_file($file, $to, $starting_index = 0) { |
| 452 |
global $wp_filesystem; |
| 453 |
|
| 454 |
if (!$wp_filesystem || !is_object($wp_filesystem)) { |
| 455 |
return new WP_Error('fs_unavailable', __('Could not access filesystem.')); |
| 456 |
} |
| 457 |
|
| 458 |
// Unzip can use a lot of memory, but not this much hopefully. |
| 459 |
if (function_exists('wp_raise_memory_limit')) wp_raise_memory_limit('admin'); |
| 460 |
|
| 461 |
$needed_dirs = array(); |
| 462 |
$to = trailingslashit($to); |
| 463 |
|
| 464 |
// Determine any parent dir's needed (of the upgrade directory) |
| 465 |
if (!$wp_filesystem->is_dir($to)) { // Only do parents if no children exist |
| 466 |
$path = preg_split('![/\\\]!', untrailingslashit($to)); |
| 467 |
for ($i = count($path); $i >= 0; $i--) { |
| 468 |
|
| 469 |
if (empty($path[$i])) continue; |
| 470 |
|
| 471 |
$dir = implode('/', array_slice($path, 0, $i + 1)); |
| 472 |
|
| 473 |
// Skip it if it looks like a Windows Drive letter. |
| 474 |
if (preg_match('!^[a-z]:$!i', $dir)) continue; |
| 475 |
|
| 476 |
// A folder exists; therefore, we don't need the check the levels below this |
| 477 |
if ($wp_filesystem->is_dir($dir)) break; |
| 478 |
|
| 479 |
$needed_dirs[] = $dir; |
| 480 |
|
| 481 |
} |
| 482 |
} |
| 483 |
|
| 484 |
static $added_unzip_action = false; |
| 485 |
if (!$added_unzip_action) { |
| 486 |
add_action('updraftplus_unzip_file_unzipped', array('UpdraftPlus_Filesystem_Functions', 'unzip_file_unzipped'), 10, 5); |
| 487 |
$added_unzip_action = true; |
| 488 |
} |
| 489 |
|
| 490 |
if (class_exists('ZipArchive', false) && apply_filters('unzip_file_use_ziparchive', true)) { |
| 491 |
$result = self::unzip_file_go($file, $to, $needed_dirs, 'ziparchive', $starting_index); |
| 492 |
if (true === $result || (is_wp_error($result) && 'incompatible_archive' != $result->get_error_code())) return $result; |
| 493 |
} |
| 494 |
|
| 495 |
// Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file. |
| 496 |
// The switch here is a sort-of emergency switch-off in case something in WP's version diverges or behaves differently |
| 497 |
if (!defined('UPDRAFTPLUS_USE_INTERNAL_PCLZIP') || UPDRAFTPLUS_USE_INTERNAL_PCLZIP) { |
| 498 |
return self::unzip_file_go($file, $to, $needed_dirs, 'pclzip', $starting_index); |
| 499 |
} else { |
| 500 |
return _unzip_file_pclzip($file, $to, $needed_dirs); |
| 501 |
} |
| 502 |
} |
| 503 |
|
| 504 |
/** |
| 505 |
* Called upon the WP action updraftplus_unzip_file_unzipped, to indicate that a file has been unzipped. |
| 506 |
* |
| 507 |
* @param String $file - the file being unzipped |
| 508 |
* @param Integer $i - the file index that was written (0, 1, ...) |
| 509 |
* @param Array $info - information about the file written, from the statIndex() method (see https://php.net/manual/en/ziparchive.statindex.php) |
| 510 |
* @param Integer $size_written - net total number of bytes thus far |
| 511 |
* @param Integer $num_files - the total number of files (i.e. one more than the the maximum value of $i) |
| 512 |
*/ |
| 513 |
public static function unzip_file_unzipped($file, $i, $info, $size_written, $num_files) { |
| 514 |
|
| 515 |
global $updraftplus; |
| 516 |
|
| 517 |
static $last_file_seen = null; |
| 518 |
|
| 519 |
static $last_logged_bytes; |
| 520 |
static $last_logged_index; |
| 521 |
static $last_logged_time; |
| 522 |
static $last_saved_time; |
| 523 |
|
| 524 |
$jobdata_key = self::get_jobdata_progress_key($file); |
| 525 |
|
| 526 |
// Detect a new zip file; reset state |
| 527 |
if ($file !== $last_file_seen) { |
| 528 |
$last_file_seen = $file; |
| 529 |
$last_logged_bytes = 0; |
| 530 |
$last_logged_index = 0; |
| 531 |
$last_logged_time = time(); |
| 532 |
$last_saved_time = time(); |
| 533 |
} |
| 534 |
|
| 535 |
// Useful for debugging |
| 536 |
$record_every_indexes = (defined('UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES') && UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES > 0) ? UPDRAFTPLUS_UNZIP_PROGRESS_RECORD_AFTER_INDEXES : 1000; |
| 537 |
|
| 538 |
// We always log the last one for clarity (the log/display looks odd if the last mention of something being unzipped isn't the last). Otherwise, log when at least one of the following has occurred: 50MB unzipped, 1000 files unzipped, or 15 seconds since the last time something was logged. |
| 539 |
if ($i >= $num_files -1 || $size_written > $last_logged_bytes + 100 * 1048576 || $i > $last_logged_index + $record_every_indexes || time() > $last_logged_time + 15) { |
| 540 |
|
| 541 |
$updraftplus->jobdata_set($jobdata_key, array('index' => $i, 'info' => $info, 'size_written' => $size_written)); |
| 542 |
|
| 543 |
$updraftplus->log(sprintf(__('Unzip progress: %d out of %d files', 'updraftplus').' (%s, %s)', $i+1, $num_files, UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size_written), $info['name']), 'notice-restore'); |
| 544 |
$updraftplus->log(sprintf('Unzip progress: %d out of %d files (%s, %s)', $i+1, $num_files, UpdraftPlus_Manipulation_Functions::convert_numeric_size_to_text($size_written), $info['name']), 'notice'); |
| 545 |
|
| 546 |
$last_logged_bytes = $size_written; |
| 547 |
$last_logged_index = $i; |
| 548 |
$last_logged_time = time(); |
| 549 |
$last_saved_time = time(); |
| 550 |
} |
| 551 |
|
| 552 |
// Because a lot can happen in 5 seconds, we update the job data more often |
| 553 |
if (time() > $last_saved_time + 5) { |
| 554 |
// N.B. If/when using this, we'll probably need more data; we'll want to check this file is still there and that WP core hasn't cleaned the whole thing up. |
| 555 |
$updraftplus->jobdata_set($jobdata_key, array('index' => $i, 'info' => $info, 'size_written' => $size_written)); |
| 556 |
$last_saved_time = time(); |
| 557 |
} |
| 558 |
} |
| 559 |
|
| 560 |
/** |
| 561 |
* This method abstracts the calculation for a consistent jobdata key name for the indicated name |
| 562 |
* |
| 563 |
* @param String $file - the filename; only the basename will be used |
| 564 |
* |
| 565 |
* @return String |
| 566 |
*/ |
| 567 |
public static function get_jobdata_progress_key($file) { |
| 568 |
return 'last_index_'.md5(basename($file)); |
| 569 |
} |
| 570 |
|
| 571 |
/** |
| 572 |
* Compatibility function (exists in WP 4.8+) |
| 573 |
*/ |
| 574 |
public static function wp_doing_cron() { |
| 575 |
if (function_exists('wp_doing_cron')) return wp_doing_cron(); |
| 576 |
return apply_filters('wp_doing_cron', defined('DOING_CRON') && DOING_CRON); |
| 577 |
} |
| 578 |
|
| 579 |
/** |
| 580 |
* Attempts to unzip an archive; forked from _unzip_file_ziparchive() in WordPress 5.1-alpha-44182, and modified to use the UD zip classes. |
| 581 |
* |
| 582 |
* Assumes that WP_Filesystem() has already been called and set up. |
| 583 |
* |
| 584 |
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. |
| 585 |
* |
| 586 |
* @param String $file - full path and filename of ZIP archive. |
| 587 |
* @param String $to - full path on the filesystem to extract archive to. |
| 588 |
* @param Array $needed_dirs - a partial list of required folders needed to be created. |
| 589 |
* @param String $method - either 'ziparchive' or 'pclzip'. |
| 590 |
* @param Integer $starting_index - index of entry to start unzipping from (allows resumption) |
| 591 |
* |
| 592 |
* @return Boolean|WP_Error True on success, WP_Error on failure. |
| 593 |
*/ |
| 594 |
private static function unzip_file_go($file, $to, $needed_dirs = array(), $method = 'ziparchive', $starting_index = 0) { |
| 595 |
global $wp_filesystem, $updraftplus; |
| 596 |
|
| 597 |
$class_to_use = ('ziparchive' == $method) ? 'UpdraftPlus_ZipArchive' : 'UpdraftPlus_PclZip'; |
| 598 |
|
| 599 |
if (!class_exists($class_to_use)) require_once(UPDRAFTPLUS_DIR.'/includes/class-zip.php'); |
| 600 |
|
| 601 |
$updraftplus->log('Unzipping '.basename($file).' to '.$to.' using '.$class_to_use.', starting index '.$starting_index); |
| 602 |
|
| 603 |
$z = new $class_to_use; |
| 604 |
|
| 605 |
$flags = (version_compare(PHP_VERSION, '5.2.12', '>') && defined('ZIPARCHIVE::CHECKCONS')) ? ZIPARCHIVE::CHECKCONS : 4; |
| 606 |
|
| 607 |
// This is just for crazy people with mbstring.func_overload enabled (deprecated from PHP 7.2) |
| 608 |
// This belongs somewhere else |
| 609 |
// if ('UpdraftPlus_PclZip' == $class_to_use) mbstring_binary_safe_encoding(); |
| 610 |
// if ('UpdraftPlus_PclZip' == $class_to_use) reset_mbstring_encoding(); |
| 611 |
|
| 612 |
$zopen = $z->open($file, $flags); |
| 613 |
|
| 614 |
if (true !== $zopen) { |
| 615 |
return new WP_Error('incompatible_archive', __('Incompatible Archive.'), array($method.'_error' => $z->last_error)); |
| 616 |
} |
| 617 |
|
| 618 |
$uncompressed_size = 0; |
| 619 |
|
| 620 |
$num_files = $z->numFiles; |
| 621 |
|
| 622 |
for ($i = $starting_index; $i < $num_files; $i++) { |
| 623 |
if (!$info = $z->statIndex($i)) { |
| 624 |
return new WP_Error('stat_failed_'.$method, __('Could not retrieve file from archive.').' ('.$z->last_error.')'); |
| 625 |
} |
| 626 |
|
| 627 |
// Skip the OS X-created __MACOSX directory |
| 628 |
if ('__MACOSX/' === substr($info['name'], 0, 9)) continue; |
| 629 |
|
| 630 |
// Don't extract invalid files: |
| 631 |
if (0 !== validate_file($info['name'])) continue; |
| 632 |
|
| 633 |
$uncompressed_size += $info['size']; |
| 634 |
|
| 635 |
if ('/' === substr($info['name'], -1)) { |
| 636 |
// Directory. |
| 637 |
$needed_dirs[] = $to . untrailingslashit($info['name']); |
| 638 |
} elseif ('.' !== ($dirname = dirname($info['name']))) { |
| 639 |
// Path to a file. |
| 640 |
$needed_dirs[] = $to . untrailingslashit($dirname); |
| 641 |
} |
| 642 |
} |
| 643 |
|
| 644 |
/* |
| 645 |
* disk_free_space() could return false. Assume that any falsey value is an error. |
| 646 |
* A disk that has zero free bytes has bigger problems. |
| 647 |
* Require we have enough space to unzip the file and copy its contents, with a 10% buffer. |
| 648 |
*/ |
| 649 |
if (self::wp_doing_cron()) { |
| 650 |
$available_space = @disk_free_space(WP_CONTENT_DIR); |
| 651 |
if ($available_space && ($uncompressed_size * 2.1) > $available_space) { |
| 652 |
return new WP_Error('disk_full_unzip_file', __('Could not copy files. You may have run out of disk space.'), compact('uncompressed_size', 'available_space')); |
| 653 |
} |
| 654 |
} |
| 655 |
|
| 656 |
$needed_dirs = array_unique($needed_dirs); |
| 657 |
foreach ($needed_dirs as $dir) { |
| 658 |
// Check the parent folders of the folders all exist within the creation array. |
| 659 |
if (untrailingslashit($to) == $dir) { // Skip over the working directory, We know this exists (or will exist) |
| 660 |
continue; |
| 661 |
} |
| 662 |
|
| 663 |
// If the directory is not within the working directory, Skip it |
| 664 |
if (false === strpos($dir, $to)) continue; |
| 665 |
|
| 666 |
$parent_folder = dirname($dir); |
| 667 |
while (!empty($parent_folder) && untrailingslashit($to) != $parent_folder && !in_array($parent_folder, $needed_dirs)) { |
| 668 |
$needed_dirs[] = $parent_folder; |
| 669 |
$parent_folder = dirname($parent_folder); |
| 670 |
} |
| 671 |
} |
| 672 |
asort($needed_dirs); |
| 673 |
|
| 674 |
// Create those directories if need be: |
| 675 |
foreach ($needed_dirs as $_dir) { |
| 676 |
// Only check to see if the Dir exists upon creation failure. Less I/O this way. |
| 677 |
if (!$wp_filesystem->mkdir($_dir, FS_CHMOD_DIR) && !$wp_filesystem->is_dir($_dir)) { |
| 678 |
return new WP_Error('mkdir_failed_'.$method, __('Could not create directory.'), substr($_dir, strlen($to))); |
| 679 |
} |
| 680 |
} |
| 681 |
unset($needed_dirs); |
| 682 |
|
| 683 |
$size_written = 0; |
| 684 |
|
| 685 |
for ($i = $starting_index; $i < $num_files; $i++) { |
| 686 |
if (!$info = $z->statIndex($i)) { |
| 687 |
return new WP_Error('stat_failed_'.$method, __('Could not retrieve file from archive.')); |
| 688 |
} |
| 689 |
|
| 690 |
// directory |
| 691 |
if ('/' == substr($info['name'], -1)) continue; |
| 692 |
|
| 693 |
// Don't extract the OS X-created __MACOSX |
| 694 |
if ('__MACOSX/' === substr($info['name'], 0, 9)) continue; |
| 695 |
|
| 696 |
// Don't extract invalid files: |
| 697 |
if (0 !== validate_file($info['name'])) continue; |
| 698 |
|
| 699 |
// PclZip will return (boolean)false for an empty file |
| 700 |
$contents = (isset($info['size']) && 0 == $info['size']) ? '' : $z->getFromIndex($i); |
| 701 |
|
| 702 |
if (false === $contents && ('pclzip' !== $method || 0 !== $info['size'])) { |
| 703 |
return new WP_Error('extract_failed_'.$method, __('Could not extract file from archive.').' '.$z->last_error, json_encode($info)); |
| 704 |
} |
| 705 |
|
| 706 |
if (!$wp_filesystem->put_contents($to . $info['name'], $contents, FS_CHMOD_FILE)) { |
| 707 |
return new WP_Error('copy_failed_'.$method, __('Could not copy file.'), $info['name']); |
| 708 |
} |
| 709 |
|
| 710 |
if (!empty($info['size'])) $size_written += $info['size']; |
| 711 |
|
| 712 |
do_action('updraftplus_unzip_file_unzipped', $file, $i, $info, $size_written, $num_files); |
| 713 |
|
| 714 |
} |
| 715 |
|
| 716 |
$z->close(); |
| 717 |
|
| 718 |
return true; |
| 719 |
} |
| 720 |
} |
| 721 |
|