PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
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 / includes / class-filesystem-functions.php

class-filesystem-functions.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at includes/class-filesystem-functions.php

721 lines 29.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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