1<?php
2/**
3 * Filesystem API: Top-level functionality
4 *
5 * Functions for reading, writing, modifying, and deleting files on the file system.
6 * Includes functionality for theme-specific files as well as operations for uploading,
7 * archiving, and rendering output when necessary.
8 *
9 * @package WordPress
10 * @subpackage Filesystem
11 * @since 2.3.0
12 */
13
14/** The descriptions for theme files. */
15$wp_file_descriptions = array(
16 'functions.php' => __( 'Theme Functions' ),
17 'header.php' => __( 'Theme Header' ),
18 'footer.php' => __( 'Theme Footer' ),
19 'sidebar.php' => __( 'Sidebar' ),
20 'comments.php' => __( 'Comments' ),
21 'searchform.php' => __( 'Search Form' ),
22 '404.php' => __( '404 Template' ),
23 'link.php' => __( 'Links Template' ),
24 'theme.json' => __( 'Theme Styles & Block Settings' ),
25 // Archives.
26 'index.php' => __( 'Main Index Template' ),
27 'archive.php' => __( 'Archives' ),
28 'author.php' => __( 'Author Template' ),
29 'taxonomy.php' => __( 'Taxonomy Template' ),
30 'category.php' => __( 'Category Template' ),
31 'tag.php' => __( 'Tag Template' ),
32 'home.php' => __( 'Posts Page' ),
33 'search.php' => __( 'Search Results' ),
34 'date.php' => __( 'Date Template' ),
35 // Content.
36 'singular.php' => __( 'Singular Template' ),
37 'single.php' => __( 'Single Post' ),
38 'page.php' => __( 'Single Page' ),
39 'front-page.php' => __( 'Homepage' ),
40 'privacy-policy.php' => __( 'Privacy Policy Page' ),
41 // Attachments.
42 'attachment.php' => __( 'Attachment Template' ),
43 'image.php' => __( 'Image Attachment Template' ),
44 'video.php' => __( 'Video Attachment Template' ),
45 'audio.php' => __( 'Audio Attachment Template' ),
46 'application.php' => __( 'Application Attachment Template' ),
47 // Embeds.
48 'embed.php' => __( 'Embed Template' ),
49 'embed-404.php' => __( 'Embed 404 Template' ),
50 'embed-content.php' => __( 'Embed Content Template' ),
51 'header-embed.php' => __( 'Embed Header Template' ),
52 'footer-embed.php' => __( 'Embed Footer Template' ),
53 // Stylesheets.
54 'style.css' => __( 'Stylesheet' ),
55 'editor-style.css' => __( 'Visual Editor Stylesheet' ),
56 'editor-style-rtl.css' => __( 'Visual Editor RTL Stylesheet' ),
57 'rtl.css' => __( 'RTL Stylesheet' ),
58 // Other.
59 'my-hacks.php' => __( 'my-hacks.php (legacy hacks support)' ),
60 '.htaccess' => __( '.htaccess (for rewrite rules )' ),
61 // Deprecated files.
62 'wp-layout.css' => __( 'Stylesheet' ),
63 'wp-comments.php' => __( 'Comments Template' ),
64 'wp-comments-popup.php' => __( 'Popup Comments Template' ),
65 'comments-popup.php' => __( 'Popup Comments' ),
66);
67
68/**
69 * Gets the description for standard WordPress theme files.
70 *
71 * @since 1.5.0
72 *
73 * @global array $wp_file_descriptions Theme file descriptions.
74 * @global array $allowed_files List of allowed files.
75 *
76 * @param string $file Filesystem path or filename.
77 * @return string Description of file from $wp_file_descriptions or basename of $file if description doesn't exist.
78 * Appends 'Page Template' to basename of $file if the file is a page template.
79 */
80function get_file_description( $file ) {
81 global $wp_file_descriptions, $allowed_files;
82
83 $dirname = pathinfo( $file, PATHINFO_DIRNAME );
84 $file_path = $allowed_files[ $file ];
85
86 if ( isset( $wp_file_descriptions[ basename( $file ) ] ) && '.' === $dirname ) {
87 return $wp_file_descriptions[ basename( $file ) ];
88 } elseif ( file_exists( $file_path ) && is_file( $file_path ) ) {
89 $template_data = implode( '', file( $file_path ) );
90
91 if ( preg_match( '|Template Name:(.*)$|mi', $template_data, $name ) ) {
92 /* translators: %s: Template name. */
93 return sprintf( __( '%s Page Template' ), _cleanup_header_comment( $name[1] ) );
94 }
95 }
96
97 return trim( basename( $file ) );
98}
99
100/**
101 * Gets the absolute filesystem path to the root of the WordPress installation.
102 *
103 * @since 1.5.0
104 *
105 * @return string Full filesystem path to the root of the WordPress installation.
106 */
107function get_home_path() {
108 $home = set_url_scheme( get_option( 'home' ), 'http' );
109 $siteurl = set_url_scheme( get_option( 'siteurl' ), 'http' );
110
111 if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
112 $wp_path_rel_to_home = str_ireplace( $home, '', $siteurl ); /* $siteurl - $home */
113 $pos = strripos( str_replace( '\\', '/', $_SERVER['SCRIPT_FILENAME'] ), trailingslashit( $wp_path_rel_to_home ) );
114 $home_path = substr( $_SERVER['SCRIPT_FILENAME'], 0, $pos );
115 $home_path = trailingslashit( $home_path );
116 } else {
117 $home_path = ABSPATH;
118 }
119
120 return str_replace( '\\', '/', $home_path );
121}
122
123/**
124 * Returns a listing of all files in the specified folder and all subdirectories up to 100 levels deep.
125 *
126 * The depth of the recursiveness can be controlled by the $levels param.
127 *
128 * @since 2.6.0
129 * @since 4.9.0 Added the `$exclusions` parameter.
130 * @since 6.3.0 Added the `$include_hidden` parameter.
131 *
132 * @param string $folder Optional. Full path to folder. Default empty.
133 * @param int $levels Optional. Levels of folders to follow, Default 100 (PHP Loop limit).
134 * @param string[] $exclusions Optional. List of folders and files to skip.
135 * @param bool $include_hidden Optional. Whether to include details of hidden ("." prefixed) files.
136 * Default false.
137 * @return string[]|false Array of files on success, false on failure.
138 */
139function list_files( $folder = '', $levels = 100, $exclusions = array(), $include_hidden = false ) {
140 if ( empty( $folder ) ) {
141 return false;
142 }
143
144 $folder = trailingslashit( $folder );
145
146 if ( ! $levels ) {
147 return false;
148 }
149
150 $files = array();
151
152 $dir = @opendir( $folder );
153
154 if ( $dir ) {
155 while ( ( $file = readdir( $dir ) ) !== false ) {
156 // Skip current and parent folder links.
157 if ( in_array( $file, array( '.', '..' ), true ) ) {
158 continue;
159 }
160
161 // Skip hidden and excluded files.
162 if ( ( ! $include_hidden && '.' === $file[0] ) || in_array( $file, $exclusions, true ) ) {
163 continue;
164 }
165
166 if ( is_dir( $folder . $file ) ) {
167 $files2 = list_files( $folder . $file, $levels - 1, array(), $include_hidden );
168 if ( $files2 ) {
169 $files = array_merge( $files, $files2 );
170 } else {
171 $files[] = $folder . $file . '/';
172 }
173 } else {
174 $files[] = $folder . $file;
175 }
176 }
177
178 closedir( $dir );
179 }
180
181 return $files;
182}
183
184/**
185 * Gets the list of file extensions that are editable in plugins.
186 *
187 * @since 4.9.0
188 *
189 * @param string $plugin Path to the plugin file relative to the plugins directory.
190 * @return string[] Array of editable file extensions.
191 */
192function wp_get_plugin_file_editable_extensions( $plugin ) {
193
194 $default_types = array(
195 'bash',
196 'conf',
197 'css',
198 'diff',
199 'htm',
200 'html',
201 'http',
202 'inc',
203 'include',
204 'js',
205 'json',
206 'jsx',
207 'less',
208 'md',
209 'patch',
210 'php',
211 'php3',
212 'php4',
213 'php5',
214 'php7',
215 'phps',
216 'phtml',
217 'sass',
218 'scss',
219 'sh',
220 'sql',
221 'svg',
222 'text',
223 'txt',
224 'xml',
225 'yaml',
226 'yml',
227 );
228
229 /**
230 * Filters the list of file types allowed for editing in the plugin file editor.
231 *
232 * @since 2.8.0
233 * @since 4.9.0 Added the `$plugin` parameter.
234 *
235 * @param string[] $default_types An array of editable plugin file extensions.
236 * @param string $plugin Path to the plugin file relative to the plugins directory.
237 */
238 $file_types = (array) apply_filters( 'editable_extensions', $default_types, $plugin );
239
240 return $file_types;
241}
242
243/**
244 * Gets the list of file extensions that are editable for a given theme.
245 *
246 * @since 4.9.0
247 *
248 * @param WP_Theme $theme Theme object.
249 * @return string[] Array of editable file extensions.
250 */
251function wp_get_theme_file_editable_extensions( $theme ) {
252
253 $default_types = array(
254 'bash',
255 'conf',
256 'css',
257 'diff',
258 'htm',
259 'html',
260 'http',
261 'inc',
262 'include',
263 'js',
264 'json',
265 'jsx',
266 'less',
267 'md',
268 'patch',
269 'php',
270 'php3',
271 'php4',
272 'php5',
273 'php7',
274 'phps',
275 'phtml',
276 'sass',
277 'scss',
278 'sh',
279 'sql',
280 'svg',
281 'text',
282 'txt',
283 'xml',
284 'yaml',
285 'yml',
286 );
287
288 /**
289 * Filters the list of file types allowed for editing in the theme file editor.
290 *
291 * @since 4.4.0
292 *
293 * @param string[] $default_types An array of editable theme file extensions.
294 * @param WP_Theme $theme The active theme object.
295 */
296 $file_types = apply_filters( 'wp_theme_editor_filetypes', $default_types, $theme );
297
298 // Ensure that default types are still there.
299 return array_unique( array_merge( $file_types, $default_types ) );
300}
301
302/**
303 * Prints file editor templates (for plugins and themes).
304 *
305 * @since 4.9.0
306 */
307function wp_print_file_editor_templates() {
308 ?>
309 <script type="text/html" id="tmpl-wp-file-editor-notice">
310 <div class="notice inline notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}">
311 <# if ( 'php_error' === data.code ) { #>
312 <p>
313 <?php
314 printf(
315 /* translators: 1: Line number, 2: File path. */
316 __( 'Your PHP code changes were not applied due to an error on line %1$s of file %2$s. Please fix and try saving again.' ),
317 '{{ data.line }}',
318 '{{ data.file }}'
319 );
320 ?>
321 </p>
322 <pre>{{ data.message }}</pre>
323 <# } else if ( 'file_not_writable' === data.code ) { #>
324 <p>
325 <?php
326 printf(
327 /* translators: %s: Documentation URL. */
328 __( 'You need to make this file writable before you can save your changes. See <a href="%s">Changing File Permissions</a> for more information.' ),
329 __( 'https://developer.wordpress.org/advanced-administration/server/file-permissions/' )
330 );
331 ?>
332 </p>
333 <# } else { #>
334 <p>{{ data.message || data.code }}</p>
335
336 <# if ( 'lint_errors' === data.code ) { #>
337 <p>
338 <# var elementId = 'el-' + String( Math.random() ); #>
339 <input id="{{ elementId }}" type="checkbox">
340 <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
341 </p>
342 <# } #>
343 <# } #>
344 <# if ( data.dismissible ) { #>
345 <button type="button" class="notice-dismiss"><span class="screen-reader-text">
346 <?php
347 /* translators: Hidden accessibility text. */
348 _e( 'Dismiss' );
349 ?>
350 </span></button>
351 <# } #>
352 </div>
353 </script>
354 <?php
355}
356
357/**
358 * Attempts to edit a file for a theme or plugin.
359 *
360 * When editing a PHP file, loopback requests will be made to the admin and the homepage
361 * to attempt to see if there is a fatal error introduced. If so, the PHP change will be
362 * reverted.
363 *
364 * @since 4.9.0
365 *
366 * @param string[] $args {
367 * Args. Note that all of the arg values are already unslashed. They are, however,
368 * coming straight from `$_POST` and are not validated or sanitized in any way.
369 *
370 * @type string $file Relative path to file.
371 * @type string $plugin Path to the plugin file relative to the plugins directory.
372 * @type string $theme Theme being edited.
373 * @type string $newcontent New content for the file.
374 * @type string $nonce Nonce.
375 * }
376 * @return true|WP_Error True on success or `WP_Error` on failure.
377 */
378function wp_edit_theme_plugin_file( $args ) {
379 if ( empty( $args['file'] ) ) {
380 return new WP_Error( 'missing_file' );
381 }
382
383 if ( 0 !== validate_file( $args['file'] ) ) {
384 return new WP_Error( 'bad_file' );
385 }
386
387 if ( ! isset( $args['newcontent'] ) ) {
388 return new WP_Error( 'missing_content' );
389 }
390
391 if ( ! isset( $args['nonce'] ) ) {
392 return new WP_Error( 'missing_nonce' );
393 }
394
395 $file = $args['file'];
396 $content = $args['newcontent'];
397
398 $plugin = null;
399 $theme = null;
400 $real_file = null;
401
402 if ( ! empty( $args['plugin'] ) ) {
403 $plugin = $args['plugin'];
404
405 if ( ! current_user_can( 'edit_plugins' ) ) {
406 return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit plugins for this site.' ) );
407 }
408
409 if ( ! wp_verify_nonce( $args['nonce'], 'edit-plugin_' . $file ) ) {
410 return new WP_Error( 'nonce_failure' );
411 }
412
413 if ( ! array_key_exists( $plugin, get_plugins() ) ) {
414 return new WP_Error( 'invalid_plugin' );
415 }
416
417 if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
418 return new WP_Error( 'bad_plugin_file_path', __( 'Sorry, that file cannot be edited.' ) );
419 }
420
421 $editable_extensions = wp_get_plugin_file_editable_extensions( $plugin );
422
423 $real_file = WP_PLUGIN_DIR . '/' . $file;
424
425 $is_active = in_array(
426 $plugin,
427 (array) get_option( 'active_plugins', array() ),
428 true
429 );
430
431 } elseif ( ! empty( $args['theme'] ) ) {
432 $stylesheet = $args['theme'];
433
434 if ( 0 !== validate_file( $stylesheet ) ) {
435 return new WP_Error( 'bad_theme_path' );
436 }
437
438 if ( ! current_user_can( 'edit_themes' ) ) {
439 return new WP_Error( 'unauthorized', __( 'Sorry, you are not allowed to edit templates for this site.' ) );
440 }
441
442 $theme = wp_get_theme( $stylesheet );
443 if ( ! $theme->exists() ) {
444 return new WP_Error( 'non_existent_theme', __( 'The requested theme does not exist.' ) );
445 }
446
447 if ( ! wp_verify_nonce( $args['nonce'], 'edit-theme_' . $stylesheet . '_' . $file ) ) {
448 return new WP_Error( 'nonce_failure' );
449 }
450
451 if ( $theme->errors() && 'theme_no_stylesheet' === $theme->errors()->get_error_code() ) {
452 return new WP_Error(
453 'theme_no_stylesheet',
454 __( 'The requested theme does not exist.' ) . ' ' . $theme->errors()->get_error_message()
455 );
456 }
457
458 $editable_extensions = wp_get_theme_file_editable_extensions( $theme );
459
460 $allowed_files = array();
461 foreach ( $editable_extensions as $type ) {
462 switch ( $type ) {
463 case 'php':
464 $allowed_files = array_merge( $allowed_files, $theme->get_files( 'php', -1 ) );
465 break;
466 case 'css':
467 $style_files = $theme->get_files( 'css', -1 );
468 $allowed_files['style.css'] = $style_files['style.css'];
469 $allowed_files = array_merge( $allowed_files, $style_files );
470 break;
471 default:
472 $allowed_files = array_merge( $allowed_files, $theme->get_files( $type, -1 ) );
473 break;
474 }
475 }
476
477 // Compare based on relative paths.
478 if ( 0 !== validate_file( $file, array_keys( $allowed_files ) ) ) {
479 return new WP_Error( 'disallowed_theme_file', __( 'Sorry, that file cannot be edited.' ) );
480 }
481
482 $real_file = $theme->get_stylesheet_directory() . '/' . $file;
483
484 $is_active = ( get_stylesheet() === $stylesheet || get_template() === $stylesheet );
485
486 } else {
487 return new WP_Error( 'missing_theme_or_plugin' );
488 }
489
490 // Ensure file is real.
491 if ( ! is_file( $real_file ) ) {
492 return new WP_Error( 'file_does_not_exist', __( 'File does not exist! Please double check the name and try again.' ) );
493 }
494
495 // Ensure file extension is allowed.
496 $extension = null;
497 if ( preg_match( '/\.([^.]+)$/', $real_file, $matches ) ) {
498 $extension = strtolower( $matches[1] );
499 if ( ! in_array( $extension, $editable_extensions, true ) ) {
500 return new WP_Error( 'illegal_file_type', __( 'Files of this type are not editable.' ) );
501 }
502 }
503
504 $previous_content = file_get_contents( $real_file );
505
506 if ( ! is_writable( $real_file ) ) {
507 return new WP_Error( 'file_not_writable' );
508 }
509
510 $f = fopen( $real_file, 'w+' );
511
512 if ( false === $f ) {
513 return new WP_Error( 'file_not_writable' );
514 }
515
516 $written = fwrite( $f, $content );
517 fclose( $f );
518
519 if ( false === $written ) {
520 return new WP_Error( 'unable_to_write', __( 'Unable to write to file.' ) );
521 }
522
523 wp_opcache_invalidate( $real_file, true );
524
525 if ( $is_active && 'php' === $extension ) {
526
527 $scrape_key = md5( rand() );
528 $transient = 'scrape_key_' . $scrape_key;
529 $scrape_nonce = (string) rand();
530 // It shouldn't take more than 60 seconds to make the two loopback requests.
531 set_transient( $transient, $scrape_nonce, 60 );
532
533 $cookies = wp_unslash( $_COOKIE );
534 $scrape_params = array(
535 'wp_scrape_key' => $scrape_key,
536 'wp_scrape_nonce' => $scrape_nonce,
537 );
538 $headers = array(
539 'Cache-Control' => 'no-cache',
540 );
541
542 /** This filter is documented in wp-includes/class-wp-http-streams.php */
543 $sslverify = apply_filters( 'https_local_ssl_verify', false );
544
545 // Include Basic auth in loopback requests.
546 if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
547 $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
548 }
549
550 // Make sure PHP process doesn't die before loopback requests complete.
551 if ( function_exists( 'set_time_limit' ) ) {
552 set_time_limit( 5 * MINUTE_IN_SECONDS );
553 }
554
555 // Time to wait for loopback requests to finish.
556 $timeout = 100; // 100 seconds.
557
558 $needle_start = "###### wp_scraping_result_start:$scrape_key ######";
559 $needle_end = "###### wp_scraping_result_end:$scrape_key ######";
560
561 // Attempt loopback request to editor to see if user just whitescreened themselves.
562 if ( $plugin ) {
563 $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) );
564 } elseif ( isset( $stylesheet ) ) {
565 $url = add_query_arg(
566 array(
567 'theme' => $stylesheet,
568 'file' => $file,
569 ),
570 admin_url( 'theme-editor.php' )
571 );
572 } else {
573 $url = admin_url();
574 }
575
576 if ( function_exists( 'session_status' ) && PHP_SESSION_ACTIVE === session_status() ) {
577 /*
578 * Close any active session to prevent HTTP requests from timing out
579 * when attempting to connect back to the site.
580 */
581 session_write_close();
582 }
583
584 $url = add_query_arg( $scrape_params, $url );
585 $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
586 $body = wp_remote_retrieve_body( $r );
587 $scrape_result_position = strpos( $body, $needle_start );
588
589 $loopback_request_failure = array(
590 'code' => 'loopback_request_failed',
591 'message' => __( 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.' ),
592 );
593 $json_parse_failure = array(
594 'code' => 'json_parse_error',
595 );
596
597 $result = null;
598
599 if ( false === $scrape_result_position ) {
600 $result = $loopback_request_failure;
601 } else {
602 $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) );
603 $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) );
604 $result = json_decode( trim( $error_output ), true );
605 if ( empty( $result ) ) {
606 $result = $json_parse_failure;
607 }
608 }
609
610 // Try making request to homepage as well to see if visitors have been whitescreened.
611 if ( true === $result ) {
612 $url = home_url( '/' );
613 $url = add_query_arg( $scrape_params, $url );
614 $r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );
615 $body = wp_remote_retrieve_body( $r );
616 $scrape_result_position = strpos( $body, $needle_start );
617
618 if ( false === $scrape_result_position ) {
619 $result = $loopback_request_failure;
620 } else {
621 $error_output = substr( $body, $scrape_result_position + strlen( $needle_start ) );
622 $error_output = substr( $error_output, 0, strpos( $error_output, $needle_end ) );
623 $result = json_decode( trim( $error_output ), true );
624 if ( empty( $result ) ) {
625 $result = $json_parse_failure;
626 }
627 }
628 }
629
630 delete_transient( $transient );
631
632 if ( true !== $result ) {
633 // Roll-back file change.
634 file_put_contents( $real_file, $previous_content );
635 wp_opcache_invalidate( $real_file, true );
636
637 if ( ! isset( $result['message'] ) ) {
638 $message = __( 'An error occurred. Please try again later.' );
639 } else {
640 $message = $result['message'];
641 unset( $result['message'] );
642 }
643
644 return new WP_Error( 'php_error', $message, $result );
645 }
646 }
647
648 if ( $theme instanceof WP_Theme ) {
649 $theme->cache_delete();
650 }
651
652 return true;
653}
654
655
656/**
657 * Returns a filename of a temporary unique file.
658 *
659 * Please note that the calling function must delete or move the file.
660 *
661 * The filename is based off the passed parameter or defaults to the current unix timestamp,
662 * while the directory can either be passed as well, or by leaving it blank, default to a writable
663 * temporary directory.
664 *
665 * @since 2.6.0
666 *
667 * @param string $filename Optional. Filename to base the Unique file off. Default empty.
668 * @param string $dir Optional. Directory to store the file in. Default empty.
669 * @return string A writable filename.
670 */
671function wp_tempnam( $filename = '', $dir = '' ) {
672 if ( empty( $dir ) ) {
673 $dir = get_temp_dir();
674 }
675
676 if ( empty( $filename ) || in_array( $filename, array( '.', '/', '\\' ), true ) ) {
677 $filename = uniqid();
678 }
679
680 // Use the basename of the given file without the extension as the name for the temporary directory.
681 $temp_filename = basename( $filename );
682 $temp_filename = preg_replace( '|\.[^.]*$|', '', $temp_filename );
683
684 // If the folder is falsey, use its parent directory name instead.
685 if ( ! $temp_filename ) {
686 return wp_tempnam( dirname( $filename ), $dir );
687 }
688
689 // Suffix some random data to avoid filename conflicts.
690 $temp_filename .= '-' . wp_generate_password( 6, false );
691 $temp_filename .= '.tmp';
692 $temp_filename = wp_unique_filename( $dir, $temp_filename );
693
694 /*
695 * Filesystems typically have a limit of 255 characters for a filename.
696 *
697 * If the generated unique filename exceeds this, truncate the initial
698 * filename and try again.
699 *
700 * As it's possible that the truncated filename may exist, producing a
701 * suffix of "-1" or "-10" which could exceed the limit again, truncate
702 * it to 252 instead.
703 */
704 $characters_over_limit = strlen( $temp_filename ) - 252;
705 if ( $characters_over_limit > 0 ) {
706 $filename = substr( $filename, 0, -$characters_over_limit );
707 return wp_tempnam( $filename, $dir );
708 }
709
710 $temp_filename = $dir . $temp_filename;
711
712 $fp = @fopen( $temp_filename, 'x' );
713
714 if ( ! $fp && is_writable( $dir ) && file_exists( $temp_filename ) ) {
715 return wp_tempnam( $filename, $dir );
716 }
717
718 if ( $fp ) {
719 fclose( $fp );
720 }
721
722 return $temp_filename;
723}
724
725/**
726 * Makes sure that the file that was requested to be edited is allowed to be edited.
727 *
728 * Function will die if you are not allowed to edit the file.
729 *
730 * @since 1.5.0
731 *
732 * @param string $file File the user is attempting to edit.
733 * @param string[] $allowed_files Optional. Array of allowed files to edit.
734 * `$file` must match an entry exactly.
735 * @return string|void Returns the file name on success, dies on failure.
736 */
737function validate_file_to_edit( $file, $allowed_files = array() ) {
738 $code = validate_file( $file, $allowed_files );
739
740 if ( ! $code ) {
741 return $file;
742 }
743
744 switch ( $code ) {
745 case 1:
746 wp_die( __( 'Sorry, that file cannot be edited.' ) );
747
748 // case 2 :
749 // wp_die( __('Sorry, cannot call files with their real path.' ));
750
751 case 3:
752 wp_die( __( 'Sorry, that file cannot be edited.' ) );
753 }
754}
755
756/**
757 * Handles PHP uploads in WordPress.
758 *
759 * Sanitizes file names, checks extensions for mime type, and moves the file
760 * to the appropriate directory within the uploads directory.
761 *
762 * @access private
763 * @since 4.0.0
764 *
765 * @see wp_handle_upload_error
766 *
767 * @param array $file {
768 * Reference to a single element from `$_FILES`. Call the function once for each uploaded file.
769 *
770 * @type string $name The original name of the file on the client machine.
771 * @type string $type The mime type of the file, if the browser provided this information.
772 * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
773 * @type int $size The size, in bytes, of the uploaded file.
774 * @type int $error The error code associated with this file upload.
775 * }
776 * @param array|false $overrides {
777 * An array of override parameters for this file, or boolean false if none are provided.
778 *
779 * @type callable $upload_error_handler Function to call when there is an error during the upload process.
780 * See {@see wp_handle_upload_error()}.
781 * @type callable $unique_filename_callback Function to call when determining a unique file name for the file.
782 * See {@see wp_unique_filename()}.
783 * @type string[] $upload_error_strings The strings that describe the error indicated in
784 * `$_FILES[{form field}]['error']`.
785 * @type bool $test_form Whether to test that the `$_POST['action']` parameter is as expected.
786 * @type bool $test_size Whether to test that the file size is greater than zero bytes.
787 * @type bool $test_type Whether to test that the mime type of the file is as expected.
788 * @type string[] $mimes Array of allowed mime types keyed by their file extension regex.
789 * }
790 * @param string $time Time formatted in 'yyyy/mm'.
791 * @param string $action Expected value for `$_POST['action']`.
792 * @return array {
793 * On success, returns an associative array of file attributes.
794 * On failure, returns `$overrides['upload_error_handler']( &$file, $message )`
795 * or `array( 'error' => $message )`.
796 *
797 * @type string $file Filename of the newly-uploaded file.
798 * @type string $url URL of the newly-uploaded file.
799 * @type string $type Mime type of the newly-uploaded file.
800 * }
801 */
802function _wp_handle_upload( &$file, $overrides, $time, $action ) {
803 // The default error handler.
804 if ( ! function_exists( 'wp_handle_upload_error' ) ) {
805 function wp_handle_upload_error( &$file, $message ) {
806 return array( 'error' => $message );
807 }
808 }
809
810 /**
811 * Filters the data for a file before it is uploaded to WordPress.
812 *
813 * The dynamic portion of the hook name, `$action`, refers to the post action.
814 *
815 * Possible hook names include:
816 *
817 * - `wp_handle_sideload_prefilter`
818 * - `wp_handle_upload_prefilter`
819 *
820 * @since 2.9.0 as 'wp_handle_upload_prefilter'.
821 * @since 4.0.0 Converted to a dynamic hook with `$action`.
822 *
823 * @param array $file {
824 * Reference to a single element from `$_FILES`.
825 *
826 * @type string $name The original name of the file on the client machine.
827 * @type string $type The mime type of the file, if the browser provided this information.
828 * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
829 * @type int $size The size, in bytes, of the uploaded file.
830 * @type int $error The error code associated with this file upload.
831 * }
832 */
833 $file = apply_filters( "{$action}_prefilter", $file );
834
835 /**
836 * Filters the override parameters for a file before it is uploaded to WordPress.
837 *
838 * The dynamic portion of the hook name, `$action`, refers to the post action.
839 *
840 * Possible hook names include:
841 *
842 * - `wp_handle_sideload_overrides`
843 * - `wp_handle_upload_overrides`
844 *
845 * @since 5.7.0
846 *
847 * @param array|false $overrides An array of override parameters for this file. Boolean false if none are
848 * provided. See {@see _wp_handle_upload()}.
849 * @param array $file {
850 * Reference to a single element from `$_FILES`.
851 *
852 * @type string $name The original name of the file on the client machine.
853 * @type string $type The mime type of the file, if the browser provided this information.
854 * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
855 * @type int $size The size, in bytes, of the uploaded file.
856 * @type int $error The error code associated with this file upload.
857 * }
858 */
859 $overrides = apply_filters( "{$action}_overrides", $overrides, $file );
860
861 // You may define your own function and pass the name in $overrides['upload_error_handler'].
862 $upload_error_handler = 'wp_handle_upload_error';
863 if ( isset( $overrides['upload_error_handler'] ) ) {
864 $upload_error_handler = $overrides['upload_error_handler'];
865 }
866
867 // You may have had one or more 'wp_handle_upload_prefilter' functions error out the file. Handle that gracefully.
868 if ( isset( $file['error'] ) && ! is_numeric( $file['error'] ) && $file['error'] ) {
869 return call_user_func_array( $upload_error_handler, array( &$file, $file['error'] ) );
870 }
871
872 // Install user overrides. Did we mention that this voids your warranty?
873
874 // You may define your own function and pass the name in $overrides['unique_filename_callback'].
875 $unique_filename_callback = null;
876 if ( isset( $overrides['unique_filename_callback'] ) ) {
877 $unique_filename_callback = $overrides['unique_filename_callback'];
878 }
879
880 /*
881 * This may not have originally been intended to be overridable,
882 * but historically has been.
883 */
884 if ( isset( $overrides['upload_error_strings'] ) ) {
885 $upload_error_strings = $overrides['upload_error_strings'];
886 } else {
887 // Courtesy of php.net, the strings that describe the error indicated in $_FILES[{form field}]['error'].
888 $upload_error_strings = array(
889 false,
890 sprintf(
891 /* translators: 1: upload_max_filesize, 2: php.ini */
892 __( 'The uploaded file exceeds the %1$s directive in %2$s.' ),
893 'upload_max_filesize',
894 'php.ini'
895 ),
896 sprintf(
897 /* translators: %s: MAX_FILE_SIZE */
898 __( 'The uploaded file exceeds the %s directive that was specified in the HTML form.' ),
899 'MAX_FILE_SIZE'
900 ),
901 __( 'The uploaded file was only partially uploaded.' ),
902 __( 'No file was uploaded.' ),
903 '',
904 __( 'Missing a temporary folder.' ),
905 __( 'Failed to write file to disk.' ),
906 __( 'File upload stopped by extension.' ),
907 );
908 }
909
910 // All tests are on by default. Most can be turned off by $overrides[{test_name}] = false;
911 $test_form = isset( $overrides['test_form'] ) ? $overrides['test_form'] : true;
912 $test_size = isset( $overrides['test_size'] ) ? $overrides['test_size'] : true;
913
914 // If you override this, you must provide $ext and $type!!
915 $test_type = isset( $overrides['test_type'] ) ? $overrides['test_type'] : true;
916 $mimes = isset( $overrides['mimes'] ) ? $overrides['mimes'] : null;
917
918 // A correct form post will pass this test.
919 if ( $test_form && ( ! isset( $_POST['action'] ) || $_POST['action'] !== $action ) ) {
920 return call_user_func_array( $upload_error_handler, array( &$file, __( 'Invalid form submission.' ) ) );
921 }
922
923 // A successful upload will pass this test. It makes no sense to override this one.
924 if ( isset( $file['error'] ) && $file['error'] > 0 ) {
925 return call_user_func_array( $upload_error_handler, array( &$file, $upload_error_strings[ $file['error'] ] ) );
926 }
927
928 // A properly uploaded file will pass this test. There should be no reason to override this one.
929 $test_uploaded_file = 'wp_handle_upload' === $action ? is_uploaded_file( $file['tmp_name'] ) : @is_readable( $file['tmp_name'] );
930 if ( ! $test_uploaded_file ) {
931 return call_user_func_array( $upload_error_handler, array( &$file, __( 'Specified file failed upload test.' ) ) );
932 }
933
934 $test_file_size = 'wp_handle_upload' === $action ? $file['size'] : filesize( $file['tmp_name'] );
935 // A non-empty file will pass this test.
936 if ( $test_size && ! ( $test_file_size > 0 ) ) {
937 if ( is_multisite() ) {
938 $error_msg = __( 'File is empty. Please upload something more substantial.' );
939 } else {
940 $error_msg = sprintf(
941 /* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */
942 __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.' ),
943 'php.ini',
944 'post_max_size',
945 'upload_max_filesize'
946 );
947 }
948
949 return call_user_func_array( $upload_error_handler, array( &$file, $error_msg ) );
950 }
951
952 // A correct MIME type will pass this test. Override $mimes or use the upload_mimes filter.
953 if ( $test_type ) {
954 $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'], $mimes );
955 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
956 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
957 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
958
959 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
960 if ( $proper_filename ) {
961 $file['name'] = $proper_filename;
962 }
963
964 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
965 return call_user_func_array( $upload_error_handler, array( &$file, __( 'Sorry, you are not allowed to upload this file type.' ) ) );
966 }
967
968 if ( ! $type ) {
969 $type = $file['type'];
970 }
971 } else {
972 $type = '';
973 }
974
975 /*
976 * A writable uploads dir will pass this test. Again, there's no point
977 * overriding this one.
978 */
979 $uploads = wp_upload_dir( $time );
980 if ( ! ( $uploads && false === $uploads['error'] ) ) {
981 return call_user_func_array( $upload_error_handler, array( &$file, $uploads['error'] ) );
982 }
983
984 $filename = wp_unique_filename( $uploads['path'], $file['name'], $unique_filename_callback );
985
986 // Move the file to the uploads dir.
987 $new_file = $uploads['path'] . "/$filename";
988
989 /**
990 * Filters whether to short-circuit moving the uploaded file after passing all checks.
991 *
992 * If a non-null value is returned from the filter, moving the file and any related
993 * error reporting will be completely skipped.
994 *
995 * @since 4.9.0
996 *
997 * @param mixed $move_new_file If null (default) move the file after the upload.
998 * @param array $file {
999 * Reference to a single element from `$_FILES`.
1000 *
1001 * @type string $name The original name of the file on the client machine.
1002 * @type string $type The mime type of the file, if the browser provided this information.
1003 * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
1004 * @type int $size The size, in bytes, of the uploaded file.
1005 * @type int $error The error code associated with this file upload.
1006 * }
1007 * @param string $new_file Filename of the newly-uploaded file.
1008 * @param string $type Mime type of the newly-uploaded file.
1009 */
1010 $move_new_file = apply_filters( 'pre_move_uploaded_file', null, $file, $new_file, $type );
1011
1012 if ( null === $move_new_file ) {
1013 if ( 'wp_handle_upload' === $action ) {
1014 $move_new_file = @move_uploaded_file( $file['tmp_name'], $new_file );
1015 } else {
1016 // Use copy and unlink because rename breaks streams.
1017 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1018 $move_new_file = @copy( $file['tmp_name'], $new_file );
1019 unlink( $file['tmp_name'] );
1020 }
1021
1022 if ( false === $move_new_file ) {
1023 if ( str_starts_with( $uploads['basedir'], ABSPATH ) ) {
1024 $error_path = str_replace( ABSPATH, '', $uploads['basedir'] ) . $uploads['subdir'];
1025 } else {
1026 $error_path = basename( $uploads['basedir'] ) . $uploads['subdir'];
1027 }
1028
1029 return $upload_error_handler(
1030 $file,
1031 sprintf(
1032 /* translators: %s: Destination file path. */
1033 __( 'The uploaded file could not be moved to %s.' ),
1034 $error_path
1035 )
1036 );
1037 }
1038 }
1039
1040 // Set correct file permissions.
1041 $stat = stat( dirname( $new_file ) );
1042 $perms = $stat['mode'] & 0000666;
1043 chmod( $new_file, $perms );
1044
1045 // Compute the URL.
1046 $url = $uploads['url'] . "/$filename";
1047
1048 if ( is_multisite() ) {
1049 clean_dirsize_cache( $new_file );
1050 }
1051
1052 /**
1053 * Filters the data array for the uploaded file.
1054 *
1055 * @since 2.1.0
1056 *
1057 * @param array $upload {
1058 * Array of upload data.
1059 *
1060 * @type string $file Filename of the newly-uploaded file.
1061 * @type string $url URL of the newly-uploaded file.
1062 * @type string $type Mime type of the newly-uploaded file.
1063 * }
1064 * @param string $context The type of upload action. Values include 'upload' or 'sideload'.
1065 */
1066 return apply_filters(
1067 'wp_handle_upload',
1068 array(
1069 'file' => $new_file,
1070 'url' => $url,
1071 'type' => $type,
1072 ),
1073 'wp_handle_sideload' === $action ? 'sideload' : 'upload'
1074 );
1075}
1076
1077/**
1078 * Wrapper for _wp_handle_upload().
1079 *
1080 * Passes the {@see 'wp_handle_upload'} action.
1081 *
1082 * @since 2.0.0
1083 *
1084 * @see _wp_handle_upload()
1085 *
1086 * @param array $file Reference to a single element of `$_FILES`.
1087 * Call the function once for each uploaded file.
1088 * See _wp_handle_upload() for accepted values.
1089 * @param array|false $overrides Optional. An associative array of names => values
1090 * to override default variables. Default false.
1091 * See _wp_handle_upload() for accepted values.
1092 * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null.
1093 * @return array See _wp_handle_upload() for return value.
1094 */
1095function wp_handle_upload( &$file, $overrides = false, $time = null ) {
1096 /*
1097 * $_POST['action'] must be set and its value must equal $overrides['action']
1098 * or this:
1099 */
1100 $action = 'wp_handle_upload';
1101 if ( isset( $overrides['action'] ) ) {
1102 $action = $overrides['action'];
1103 }
1104
1105 return _wp_handle_upload( $file, $overrides, $time, $action );
1106}
1107
1108/**
1109 * Wrapper for _wp_handle_upload().
1110 *
1111 * Passes the {@see 'wp_handle_sideload'} action.
1112 *
1113 * @since 2.6.0
1114 *
1115 * @see _wp_handle_upload()
1116 *
1117 * @param array $file Reference to a single element of `$_FILES`.
1118 * Call the function once for each uploaded file.
1119 * See _wp_handle_upload() for accepted values.
1120 * @param array|false $overrides Optional. An associative array of names => values
1121 * to override default variables. Default false.
1122 * See _wp_handle_upload() for accepted values.
1123 * @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null.
1124 * @return array See _wp_handle_upload() for return value.
1125 */
1126function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
1127 /*
1128 * $_POST['action'] must be set and its value must equal $overrides['action']
1129 * or this:
1130 */
1131 $action = 'wp_handle_sideload';
1132 if ( isset( $overrides['action'] ) ) {
1133 $action = $overrides['action'];
1134 }
1135
1136 return _wp_handle_upload( $file, $overrides, $time, $action );
1137}
1138
1139/**
1140 * Downloads a URL to a local temporary file using the WordPress HTTP API.
1141 *
1142 * Please note that the calling function must delete or move the file.
1143 *
1144 * @since 2.5.0
1145 * @since 5.2.0 Signature Verification with SoftFail was added.
1146 * @since 5.9.0 Support for Content-Disposition filename was added.
1147 *
1148 * @param string $url The URL of the file to download.
1149 * @param int $timeout The timeout for the request to download the file.
1150 * Default 300 seconds.
1151 * @param bool $signature_verification Whether to perform Signature Verification.
1152 * Default false.
1153 * @return string|WP_Error Filename on success, WP_Error on failure.
1154 */
1155function download_url( $url, $timeout = 300, $signature_verification = false ) {
1156 // WARNING: The file is not automatically deleted, the script must delete or move the file.
1157 if ( ! $url ) {
1158 return new WP_Error( 'http_no_url', __( 'No URL Provided.' ) );
1159 }
1160
1161 $url_path = parse_url( $url, PHP_URL_PATH );
1162 $url_filename = '';
1163 if ( is_string( $url_path ) && '' !== $url_path ) {
1164 $url_filename = basename( $url_path );
1165 }
1166
1167 $tmpfname = wp_tempnam( $url_filename );
1168 if ( ! $tmpfname ) {
1169 return new WP_Error( 'http_no_file', __( 'Could not create temporary file.' ) );
1170 }
1171
1172 $response = wp_safe_remote_get(
1173 $url,
1174 array(
1175 'timeout' => $timeout,
1176 'stream' => true,
1177 'filename' => $tmpfname,
1178 )
1179 );
1180
1181 if ( is_wp_error( $response ) ) {
1182 unlink( $tmpfname );
1183 return $response;
1184 }
1185
1186 $response_code = wp_remote_retrieve_response_code( $response );
1187
1188 if ( 200 !== $response_code ) {
1189 $data = array(
1190 'code' => $response_code,
1191 );
1192
1193 // Retrieve a sample of the response body for debugging purposes.
1194 $tmpf = fopen( $tmpfname, 'rb' );
1195
1196 if ( $tmpf ) {
1197 /**
1198 * Filters the maximum error response body size in `download_url()`.
1199 *
1200 * @since 5.1.0
1201 *
1202 * @see download_url()
1203 *
1204 * @param int $size The maximum error response body size. Default 1 KB.
1205 */
1206 $response_size = apply_filters( 'download_url_error_max_body_size', KB_IN_BYTES );
1207
1208 $data['body'] = fread( $tmpf, $response_size );
1209 fclose( $tmpf );
1210 }
1211
1212 unlink( $tmpfname );
1213
1214 return new WP_Error( 'http_404', trim( wp_remote_retrieve_response_message( $response ) ), $data );
1215 }
1216
1217 $content_disposition = wp_remote_retrieve_header( $response, 'Content-Disposition' );
1218
1219 if ( $content_disposition ) {
1220 $content_disposition = strtolower( $content_disposition );
1221
1222 if ( str_starts_with( $content_disposition, 'attachment; filename=' ) ) {
1223 $tmpfname_disposition = sanitize_file_name( substr( $content_disposition, 21 ) );
1224 } else {
1225 $tmpfname_disposition = '';
1226 }
1227
1228 // Potential file name must be valid string.
1229 if ( $tmpfname_disposition && is_string( $tmpfname_disposition )
1230 && ( 0 === validate_file( $tmpfname_disposition ) )
1231 ) {
1232 $tmpfname_disposition = dirname( $tmpfname ) . '/' . $tmpfname_disposition;
1233
1234 if ( rename( $tmpfname, $tmpfname_disposition ) ) {
1235 $tmpfname = $tmpfname_disposition;
1236 }
1237
1238 if ( ( $tmpfname !== $tmpfname_disposition ) && file_exists( $tmpfname_disposition ) ) {
1239 unlink( $tmpfname_disposition );
1240 }
1241 }
1242 }
1243
1244 $mime_type = wp_remote_retrieve_header( $response, 'content-type' );
1245 if ( $mime_type && 'tmp' === pathinfo( $tmpfname, PATHINFO_EXTENSION ) ) {
1246 $valid_mime_types = array_flip( get_allowed_mime_types() );
1247 if ( ! empty( $valid_mime_types[ $mime_type ] ) ) {
1248 $extensions = explode( '|', $valid_mime_types[ $mime_type ] );
1249 $new_image_name = substr( $tmpfname, 0, -4 ) . ".{$extensions[0]}";
1250 if ( 0 === validate_file( $new_image_name ) ) {
1251 if ( rename( $tmpfname, $new_image_name ) ) {
1252 $tmpfname = $new_image_name;
1253 }
1254
1255 if ( ( $tmpfname !== $new_image_name ) && file_exists( $new_image_name ) ) {
1256 unlink( $new_image_name );
1257 }
1258 }
1259 }
1260 }
1261
1262 $content_md5 = wp_remote_retrieve_header( $response, 'Content-MD5' );
1263
1264 if ( $content_md5 ) {
1265 $md5_check = verify_file_md5( $tmpfname, $content_md5 );
1266
1267 if ( is_wp_error( $md5_check ) ) {
1268 unlink( $tmpfname );
1269 return $md5_check;
1270 }
1271 }
1272
1273 // If the caller expects signature verification to occur, check to see if this URL supports it.
1274 if ( $signature_verification ) {
1275 /**
1276 * Filters the list of hosts which should have Signature Verification attempted on.
1277 *
1278 * @since 5.2.0
1279 *
1280 * @param string[] $hostnames List of hostnames.
1281 */
1282 $signed_hostnames = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) );
1283
1284 $signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true );
1285 }
1286
1287 // Perform signature validation if supported.
1288 if ( $signature_verification ) {
1289 $signature = wp_remote_retrieve_header( $response, 'X-Content-Signature' );
1290
1291 if ( ! $signature ) {
1292 /*
1293 * Retrieve signatures from a file if the header wasn't included.
1294 * WordPress.org stores signatures at $package_url.sig.
1295 */
1296
1297 $signature_url = false;
1298
1299 if ( is_string( $url_path ) && ( str_ends_with( $url_path, '.zip' ) || str_ends_with( $url_path, '.tar.gz' ) ) ) {
1300 $signature_url = str_replace( $url_path, $url_path . '.sig', $url );
1301 }
1302
1303 /**
1304 * Filters the URL where the signature for a file is located.
1305 *
1306 * @since 5.2.0
1307 *
1308 * @param false|string $signature_url The URL where signatures can be found for a file, or false if none are known.
1309 * @param string $url The URL being verified.
1310 */
1311 $signature_url = apply_filters( 'wp_signature_url', $signature_url, $url );
1312
1313 if ( $signature_url ) {
1314 $signature_request = wp_safe_remote_get(
1315 $signature_url,
1316 array(
1317 'limit_response_size' => 10 * KB_IN_BYTES, // 10KB should be large enough for quite a few signatures.
1318 )
1319 );
1320
1321 if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) {
1322 $signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) );
1323 }
1324 }
1325 }
1326
1327 // Perform the checks.
1328 $signature_verification = verify_file_signature( $tmpfname, $signature, $url_filename );
1329 }
1330
1331 if ( is_wp_error( $signature_verification ) ) {
1332 if (
1333 /**
1334 * Filters whether Signature Verification failures should be allowed to soft fail.
1335 *
1336 * WARNING: This may be removed from a future release.
1337 *
1338 * @since 5.2.0
1339 *
1340 * @param bool $signature_softfail If a softfail is allowed.
1341 * @param string $url The url being accessed.
1342 */
1343 apply_filters( 'wp_signature_softfail', true, $url )
1344 ) {
1345 $signature_verification->add_data( $tmpfname, 'softfail-filename' );
1346 } else {
1347 // Hard-fail.
1348 unlink( $tmpfname );
1349 }
1350
1351 return $signature_verification;
1352 }
1353
1354 return $tmpfname;
1355}
1356
1357/**
1358 * Calculates and compares the MD5 of a file to its expected value.
1359 *
1360 * @since 3.7.0
1361 *
1362 * @param string $filename The filename to check the MD5 of.
1363 * @param string $expected_md5 The expected MD5 of the file, either a base64-encoded raw md5,
1364 * or a hex-encoded md5.
1365 * @return bool|WP_Error True on success, false when the MD5 format is unknown/unexpected,
1366 * WP_Error on failure.
1367 */
1368function verify_file_md5( $filename, $expected_md5 ) {
1369 if ( 32 === strlen( $expected_md5 ) ) {
1370 $expected_raw_md5 = pack( 'H*', $expected_md5 );
1371 } elseif ( 24 === strlen( $expected_md5 ) ) {
1372 $expected_raw_md5 = base64_decode( $expected_md5 );
1373 } else {
1374 return false; // Unknown format.
1375 }
1376
1377 $file_md5 = md5_file( $filename, true );
1378
1379 if ( $file_md5 === $expected_raw_md5 ) {
1380 return true;
1381 }
1382
1383 return new WP_Error(
1384 'md5_mismatch',
1385 sprintf(
1386 /* translators: 1: File checksum, 2: Expected checksum value. */
1387 __( 'The checksum of the file (%1$s) does not match the expected checksum value (%2$s).' ),
1388 bin2hex( $file_md5 ),
1389 bin2hex( $expected_raw_md5 )
1390 )
1391 );
1392}
1393
1394/**
1395 * Verifies the contents of a file against its ED25519 signature.
1396 *
1397 * @since 5.2.0
1398 *
1399 * @param string $filename The file to validate.
1400 * @param string|array $signatures A Signature provided for the file.
1401 * @param string|false $filename_for_errors Optional. A friendly filename for errors.
1402 * @return bool|WP_Error True on success, false if verification not attempted,
1403 * or WP_Error describing an error condition.
1404 */
1405function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) {
1406 if ( ! $filename_for_errors ) {
1407 $filename_for_errors = wp_basename( $filename );
1408 }
1409
1410 // Check we can process signatures.
1411 if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ), true ) ) {
1412 return new WP_Error(
1413 'signature_verification_unsupported',
1414 sprintf(
1415 /* translators: %s: The filename of the package. */
1416 __( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
1417 '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
1418 ),
1419 ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' )
1420 );
1421 }
1422
1423 // Verify runtime speed of Sodium_Compat is acceptable.
1424 if ( ! extension_loaded( 'sodium' ) && ! ParagonIE_Sodium_Compat::polyfill_is_fast() ) {
1425 $sodium_compat_is_fast = false;
1426
1427 // Allow for an old version of Sodium_Compat being loaded before the bundled WordPress one.
1428 if ( method_exists( 'ParagonIE_Sodium_Compat', 'runtime_speed_test' ) ) {
1429 /*
1430 * Run `ParagonIE_Sodium_Compat::runtime_speed_test()` in optimized integer mode,
1431 * as that's what WordPress utilizes during signing verifications.
1432 */
1433 // phpcs:disable WordPress.NamingConventions.ValidVariableName
1434 $old_fastMult = ParagonIE_Sodium_Compat::$fastMult;
1435 ParagonIE_Sodium_Compat::$fastMult = true;
1436 $sodium_compat_is_fast = ParagonIE_Sodium_Compat::runtime_speed_test( 100, 10 );
1437 ParagonIE_Sodium_Compat::$fastMult = $old_fastMult;
1438 // phpcs:enable
1439 }
1440
1441 /*
1442 * This cannot be performed in a reasonable amount of time.
1443 * https://github.com/paragonie/sodium_compat#help-sodium_compat-is-slow-how-can-i-make-it-fast
1444 */
1445 if ( ! $sodium_compat_is_fast ) {
1446 return new WP_Error(
1447 'signature_verification_unsupported',
1448 sprintf(
1449 /* translators: %s: The filename of the package. */
1450 __( 'The authenticity of %s could not be verified as signature verification is unavailable on this system.' ),
1451 '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
1452 ),
1453 array(
1454 'php' => PHP_VERSION,
1455 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
1456 'polyfill_is_fast' => false,
1457 'max_execution_time' => ini_get( 'max_execution_time' ),
1458 )
1459 );
1460 }
1461 }
1462
1463 if ( ! $signatures ) {
1464 return new WP_Error(
1465 'signature_verification_no_signature',
1466 sprintf(
1467 /* translators: %s: The filename of the package. */
1468 __( 'The authenticity of %s could not be verified as no signature was found.' ),
1469 '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
1470 ),
1471 array(
1472 'filename' => $filename_for_errors,
1473 )
1474 );
1475 }
1476
1477 $trusted_keys = wp_trusted_keys();
1478 $file_hash = hash_file( 'sha384', $filename, true );
1479
1480 mbstring_binary_safe_encoding();
1481
1482 $skipped_key = 0;
1483 $skipped_signature = 0;
1484
1485 foreach ( (array) $signatures as $signature ) {
1486 $signature_raw = base64_decode( $signature );
1487
1488 // Ensure only valid-length signatures are considered.
1489 if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) {
1490 ++$skipped_signature;
1491 continue;
1492 }
1493
1494 foreach ( (array) $trusted_keys as $key ) {
1495 $key_raw = base64_decode( $key );
1496
1497 // Only pass valid public keys through.
1498 if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) {
1499 ++$skipped_key;
1500 continue;
1501 }
1502
1503 if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
1504 reset_mbstring_encoding();
1505 return true;
1506 }
1507 }
1508 }
1509
1510 reset_mbstring_encoding();
1511
1512 return new WP_Error(
1513 'signature_verification_failed',
1514 sprintf(
1515 /* translators: %s: The filename of the package. */
1516 __( 'The authenticity of %s could not be verified.' ),
1517 '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
1518 ),
1519 // Error data helpful for debugging:
1520 array(
1521 'filename' => $filename_for_errors,
1522 'keys' => $trusted_keys,
1523 'signatures' => $signatures,
1524 'hash' => bin2hex( $file_hash ),
1525 'skipped_key' => $skipped_key,
1526 'skipped_sig' => $skipped_signature,
1527 'php' => PHP_VERSION,
1528 'sodium' => defined( 'SODIUM_LIBRARY_VERSION' ) ? SODIUM_LIBRARY_VERSION : ( defined( 'ParagonIE_Sodium_Compat::VERSION_STRING' ) ? ParagonIE_Sodium_Compat::VERSION_STRING : false ),
1529 )
1530 );
1531}
1532
1533/**
1534 * Retrieves the list of signing keys trusted by WordPress.
1535 *
1536 * @since 5.2.0
1537 *
1538 * @return string[] Array of base64-encoded signing keys.
1539 */
1540function wp_trusted_keys() {
1541 $trusted_keys = array();
1542
1543 if ( time() < 1617235200 ) {
1544 // WordPress.org Key #1 - This key is only valid before April 1st, 2021.
1545 $trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
1546 }
1547
1548 // TODO: Add key #2 with longer expiration.
1549
1550 /**
1551 * Filters the valid signing keys used to verify the contents of files.
1552 *
1553 * @since 5.2.0
1554 *
1555 * @param string[] $trusted_keys The trusted keys that may sign packages.
1556 */
1557 return apply_filters( 'wp_trusted_keys', $trusted_keys );
1558}
1559
1560/**
1561 * Determines whether the given file is a valid ZIP file.
1562 *
1563 * This function does not test to ensure that a file exists. Non-existent files
1564 * are not valid ZIPs, so those will also return false.
1565 *
1566 * @since 6.4.4
1567 *
1568 * @param string $file Full path to the ZIP file.
1569 * @return bool Whether the file is a valid ZIP file.
1570 */
1571function wp_zip_file_is_valid( $file ) {
1572 /** This filter is documented in wp-admin/includes/file.php */
1573 if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
1574 $archive = new ZipArchive();
1575 $archive_is_valid = $archive->open( $file, ZipArchive::CHECKCONS );
1576 if ( true === $archive_is_valid ) {
1577 $archive->close();
1578 return true;
1579 }
1580 }
1581
1582 // Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
1583 require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
1584
1585 $archive = new PclZip( $file );
1586 $archive_is_valid = is_array( $archive->properties() );
1587
1588 return $archive_is_valid;
1589}
1590
1591/**
1592 * Unzips a specified ZIP file to a location on the filesystem via the WordPress
1593 * Filesystem Abstraction.
1594 *
1595 * Assumes that WP_Filesystem() has already been called and set up. Does not extract
1596 * a root-level __MACOSX directory, if present.
1597 *
1598 * Attempts to increase the PHP memory limit to 256M before uncompressing. However,
1599 * the most memory required shouldn't be much larger than the archive itself.
1600 *
1601 * @since 2.5.0
1602 *
1603 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
1604 *
1605 * @param string $file Full path and filename of ZIP archive.
1606 * @param string $to Full path on the filesystem to extract archive to.
1607 * @return true|WP_Error True on success, WP_Error on failure.
1608 */
1609function unzip_file( $file, $to ) {
1610 global $wp_filesystem;
1611
1612 if ( ! $wp_filesystem || ! is_object( $wp_filesystem ) ) {
1613 return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
1614 }
1615
1616 // Unzip can use a lot of memory, but not this much hopefully.
1617 wp_raise_memory_limit( 'admin' );
1618
1619 $needed_dirs = array();
1620 $to = trailingslashit( $to );
1621
1622 // Determine any parent directories needed (of the upgrade directory).
1623 if ( ! $wp_filesystem->is_dir( $to ) ) { // Only do parents if no children exist.
1624 $path = preg_split( '![/\\\]!', untrailingslashit( $to ) );
1625 for ( $i = count( $path ); $i >= 0; $i-- ) {
1626 if ( empty( $path[ $i ] ) ) {
1627 continue;
1628 }
1629
1630 $dir = implode( '/', array_slice( $path, 0, $i + 1 ) );
1631 if ( preg_match( '!^[a-z]:$!i', $dir ) ) { // Skip it if it looks like a Windows Drive letter.
1632 continue;
1633 }
1634
1635 if ( ! $wp_filesystem->is_dir( $dir ) ) {
1636 $needed_dirs[] = $dir;
1637 } else {
1638 break; // A folder exists, therefore we don't need to check the levels below this.
1639 }
1640 }
1641 }
1642
1643 /**
1644 * Filters whether to use ZipArchive to unzip archives.
1645 *
1646 * @since 3.0.0
1647 *
1648 * @param bool $ziparchive Whether to use ZipArchive. Default true.
1649 */
1650 if ( class_exists( 'ZipArchive', false ) && apply_filters( 'unzip_file_use_ziparchive', true ) ) {
1651 $result = _unzip_file_ziparchive( $file, $to, $needed_dirs );
1652 if ( true === $result ) {
1653 return $result;
1654 } elseif ( is_wp_error( $result ) ) {
1655 if ( 'incompatible_archive' !== $result->get_error_code() ) {
1656 return $result;
1657 }
1658 }
1659 }
1660 // Fall through to PclZip if ZipArchive is not available, or encountered an error opening the file.
1661 return _unzip_file_pclzip( $file, $to, $needed_dirs );
1662}
1663
1664/**
1665 * Attempts to unzip an archive using the ZipArchive class.
1666 *
1667 * This function should not be called directly, use `unzip_file()` instead.
1668 *
1669 * Assumes that WP_Filesystem() has already been called and set up.
1670 *
1671 * @since 3.0.0
1672 * @access private
1673 *
1674 * @see unzip_file()
1675 *
1676 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
1677 *
1678 * @param string $file Full path and filename of ZIP archive.
1679 * @param string $to Full path on the filesystem to extract archive to.
1680 * @param string[] $needed_dirs A partial list of required folders needed to be created.
1681 * @return true|WP_Error True on success, WP_Error on failure.
1682 */
1683function _unzip_file_ziparchive( $file, $to, $needed_dirs = array() ) {
1684 global $wp_filesystem;
1685
1686 $z = new ZipArchive();
1687
1688 $zopen = $z->open( $file, ZIPARCHIVE::CHECKCONS );
1689
1690 if ( true !== $zopen ) {
1691 return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), array( 'ziparchive_error' => $zopen ) );
1692 }
1693
1694 $uncompressed_size = 0;
1695
1696 for ( $i = 0; $i < $z->numFiles; $i++ ) {
1697 $info = $z->statIndex( $i );
1698
1699 if ( ! $info ) {
1700 $z->close();
1701 return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
1702 }
1703
1704 if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
1705 continue;
1706 }
1707
1708 // Don't extract invalid files:
1709 if ( 0 !== validate_file( $info['name'] ) ) {
1710 continue;
1711 }
1712
1713 $uncompressed_size += $info['size'];
1714
1715 $dirname = dirname( $info['name'] );
1716
1717 if ( str_ends_with( $info['name'], '/' ) ) {
1718 // Directory.
1719 $needed_dirs[] = $to . untrailingslashit( $info['name'] );
1720 } elseif ( '.' !== $dirname ) {
1721 // Path to a file.
1722 $needed_dirs[] = $to . untrailingslashit( $dirname );
1723 }
1724 }
1725
1726 // Enough space to unzip the file and copy its contents, with a 10% buffer.
1727 $required_space = $uncompressed_size * 2.1;
1728
1729 /*
1730 * disk_free_space() could return false. Assume that any falsey value is an error.
1731 * A disk that has zero free bytes has bigger problems.
1732 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
1733 */
1734 if ( wp_doing_cron() ) {
1735 $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;
1736
1737 if ( $available_space && ( $required_space > $available_space ) ) {
1738 $z->close();
1739 return new WP_Error(
1740 'disk_full_unzip_file',
1741 __( 'Could not copy files. You may have run out of disk space.' ),
1742 compact( 'uncompressed_size', 'available_space' )
1743 );
1744 }
1745 }
1746
1747 $needed_dirs = array_unique( $needed_dirs );
1748
1749 foreach ( $needed_dirs as $dir ) {
1750 // Check the parent folders of the folders all exist within the creation array.
1751 if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
1752 continue;
1753 }
1754
1755 if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
1756 continue;
1757 }
1758
1759 $parent_folder = dirname( $dir );
1760
1761 while ( ! empty( $parent_folder )
1762 && untrailingslashit( $to ) !== $parent_folder
1763 && ! in_array( $parent_folder, $needed_dirs, true )
1764 ) {
1765 $needed_dirs[] = $parent_folder;
1766 $parent_folder = dirname( $parent_folder );
1767 }
1768 }
1769
1770 asort( $needed_dirs );
1771
1772 // Create those directories if need be:
1773 foreach ( $needed_dirs as $_dir ) {
1774 // Only check to see if the Dir exists upon creation failure. Less I/O this way.
1775 if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
1776 $z->close();
1777 return new WP_Error( 'mkdir_failed_ziparchive', __( 'Could not create directory.' ), $_dir );
1778 }
1779 }
1780
1781 /**
1782 * Filters archive unzipping to override with a custom process.
1783 *
1784 * @since 6.4.0
1785 *
1786 * @param null|true|WP_Error $result The result of the override. True on success, otherwise WP Error. Default null.
1787 * @param string $file Full path and filename of ZIP archive.
1788 * @param string $to Full path on the filesystem to extract archive to.
1789 * @param string[] $needed_dirs A full list of required folders that need to be created.
1790 * @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
1791 */
1792 $pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );
1793
1794 if ( null !== $pre ) {
1795 // Ensure the ZIP file archive has been closed.
1796 $z->close();
1797
1798 return $pre;
1799 }
1800
1801 for ( $i = 0; $i < $z->numFiles; $i++ ) {
1802 $info = $z->statIndex( $i );
1803
1804 if ( ! $info ) {
1805 $z->close();
1806 return new WP_Error( 'stat_failed_ziparchive', __( 'Could not retrieve file from archive.' ) );
1807 }
1808
1809 if ( str_ends_with( $info['name'], '/' ) ) { // Directory.
1810 continue;
1811 }
1812
1813 if ( str_starts_with( $info['name'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
1814 continue;
1815 }
1816
1817 // Don't extract invalid files:
1818 if ( 0 !== validate_file( $info['name'] ) ) {
1819 continue;
1820 }
1821
1822 $contents = $z->getFromIndex( $i );
1823
1824 if ( false === $contents ) {
1825 $z->close();
1826 return new WP_Error( 'extract_failed_ziparchive', __( 'Could not extract file from archive.' ), $info['name'] );
1827 }
1828
1829 if ( ! $wp_filesystem->put_contents( $to . $info['name'], $contents, FS_CHMOD_FILE ) ) {
1830 $z->close();
1831 return new WP_Error( 'copy_failed_ziparchive', __( 'Could not copy file.' ), $info['name'] );
1832 }
1833 }
1834
1835 $z->close();
1836
1837 /**
1838 * Filters the result of unzipping an archive.
1839 *
1840 * @since 6.4.0
1841 *
1842 * @param true|WP_Error $result The result of unzipping the archive. True on success, otherwise WP_Error. Default true.
1843 * @param string $file Full path and filename of ZIP archive.
1844 * @param string $to Full path on the filesystem the archive was extracted to.
1845 * @param string[] $needed_dirs A full list of required folders that were created.
1846 * @param float $required_space The space required to unzip the file and copy its contents, with a 10% buffer.
1847 */
1848 $result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );
1849
1850 unset( $needed_dirs );
1851
1852 return $result;
1853}
1854
1855/**
1856 * Attempts to unzip an archive using the PclZip library.
1857 *
1858 * This function should not be called directly, use `unzip_file()` instead.
1859 *
1860 * Assumes that WP_Filesystem() has already been called and set up.
1861 *
1862 * @since 3.0.0
1863 * @access private
1864 *
1865 * @see unzip_file()
1866 *
1867 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
1868 *
1869 * @param string $file Full path and filename of ZIP archive.
1870 * @param string $to Full path on the filesystem to extract archive to.
1871 * @param string[] $needed_dirs A partial list of required folders needed to be created.
1872 * @return true|WP_Error True on success, WP_Error on failure.
1873 */
1874function _unzip_file_pclzip( $file, $to, $needed_dirs = array() ) {
1875 global $wp_filesystem;
1876
1877 mbstring_binary_safe_encoding();
1878
1879 require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
1880
1881 $archive = new PclZip( $file );
1882
1883 $archive_files = $archive->extract( PCLZIP_OPT_EXTRACT_AS_STRING );
1884
1885 reset_mbstring_encoding();
1886
1887 // Is the archive valid?
1888 if ( ! is_array( $archive_files ) ) {
1889 return new WP_Error( 'incompatible_archive', __( 'Incompatible Archive.' ), $archive->errorInfo( true ) );
1890 }
1891
1892 if ( 0 === count( $archive_files ) ) {
1893 return new WP_Error( 'empty_archive_pclzip', __( 'Empty archive.' ) );
1894 }
1895
1896 $uncompressed_size = 0;
1897
1898 // Determine any children directories needed (From within the archive).
1899 foreach ( $archive_files as $archive_file ) {
1900 if ( str_starts_with( $archive_file['filename'], '__MACOSX/' ) ) { // Skip the OS X-created __MACOSX directory.
1901 continue;
1902 }
1903
1904 $uncompressed_size += $archive_file['size'];
1905
1906 $needed_dirs[] = $to . untrailingslashit( $archive_file['folder'] ? $archive_file['filename'] : dirname( $archive_file['filename'] ) );
1907 }
1908
1909 // Enough space to unzip the file and copy its contents, with a 10% buffer.
1910 $required_space = $uncompressed_size * 2.1;
1911
1912 /*
1913 * disk_free_space() could return false. Assume that any falsey value is an error.
1914 * A disk that has zero free bytes has bigger problems.
1915 * Require we have enough space to unzip the file and copy its contents, with a 10% buffer.
1916 */
1917 if ( wp_doing_cron() ) {
1918 $available_space = function_exists( 'disk_free_space' ) ? @disk_free_space( WP_CONTENT_DIR ) : false;
1919
1920 if ( $available_space && ( $required_space > $available_space ) ) {
1921 return new WP_Error(
1922 'disk_full_unzip_file',
1923 __( 'Could not copy files. You may have run out of disk space.' ),
1924 compact( 'uncompressed_size', 'available_space' )
1925 );
1926 }
1927 }
1928
1929 $needed_dirs = array_unique( $needed_dirs );
1930
1931 foreach ( $needed_dirs as $dir ) {
1932 // Check the parent folders of the folders all exist within the creation array.
1933 if ( untrailingslashit( $to ) === $dir ) { // Skip over the working directory, we know this exists (or will exist).
1934 continue;
1935 }
1936
1937 if ( ! str_contains( $dir, $to ) ) { // If the directory is not within the working directory, skip it.
1938 continue;
1939 }
1940
1941 $parent_folder = dirname( $dir );
1942
1943 while ( ! empty( $parent_folder )
1944 && untrailingslashit( $to ) !== $parent_folder
1945 && ! in_array( $parent_folder, $needed_dirs, true )
1946 ) {
1947 $needed_dirs[] = $parent_folder;
1948 $parent_folder = dirname( $parent_folder );
1949 }
1950 }
1951
1952 asort( $needed_dirs );
1953
1954 // Create those directories if need be:
1955 foreach ( $needed_dirs as $_dir ) {
1956 // Only check to see if the dir exists upon creation failure. Less I/O this way.
1957 if ( ! $wp_filesystem->mkdir( $_dir, FS_CHMOD_DIR ) && ! $wp_filesystem->is_dir( $_dir ) ) {
1958 return new WP_Error( 'mkdir_failed_pclzip', __( 'Could not create directory.' ), $_dir );
1959 }
1960 }
1961
1962 /** This filter is documented in src/wp-admin/includes/file.php */
1963 $pre = apply_filters( 'pre_unzip_file', null, $file, $to, $needed_dirs, $required_space );
1964
1965 if ( null !== $pre ) {
1966 return $pre;
1967 }
1968
1969 // Extract the files from the zip.
1970 foreach ( $archive_files as $archive_file ) {
1971 if ( $archive_file['folder'] ) {
1972 continue;
1973 }
1974
1975 if ( str_starts_with( $archive_file['filename'], '__MACOSX/' ) ) { // Don't extract the OS X-created __MACOSX directory files.
1976 continue;
1977 }
1978
1979 // Don't extract invalid files:
1980 if ( 0 !== validate_file( $archive_file['filename'] ) ) {
1981 continue;
1982 }
1983
1984 if ( ! $wp_filesystem->put_contents( $to . $archive_file['filename'], $archive_file['content'], FS_CHMOD_FILE ) ) {
1985 return new WP_Error( 'copy_failed_pclzip', __( 'Could not copy file.' ), $archive_file['filename'] );
1986 }
1987 }
1988
1989 /** This action is documented in src/wp-admin/includes/file.php */
1990 $result = apply_filters( 'unzip_file', true, $file, $to, $needed_dirs, $required_space );
1991
1992 unset( $needed_dirs );
1993
1994 return $result;
1995}
1996
1997/**
1998 * Copies a directory from one location to another via the WordPress Filesystem
1999 * Abstraction.
2000 *
2001 * Assumes that WP_Filesystem() has already been called and setup.
2002 *
2003 * @since 2.5.0
2004 *
2005 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
2006 *
2007 * @param string $from Source directory.
2008 * @param string $to Destination directory.
2009 * @param string[] $skip_list An array of files/folders to skip copying.
2010 * @return true|WP_Error True on success, WP_Error on failure.
2011 */
2012function copy_dir( $from, $to, $skip_list = array() ) {
2013 global $wp_filesystem;
2014
2015 $dirlist = $wp_filesystem->dirlist( $from );
2016
2017 if ( false === $dirlist ) {
2018 return new WP_Error( 'dirlist_failed_copy_dir', __( 'Directory listing failed.' ), basename( $from ) );
2019 }
2020
2021 $from = trailingslashit( $from );
2022 $to = trailingslashit( $to );
2023
2024 if ( ! $wp_filesystem->exists( $to ) && ! $wp_filesystem->mkdir( $to ) ) {
2025 return new WP_Error(
2026 'mkdir_destination_failed_copy_dir',
2027 __( 'Could not create the destination directory.' ),
2028 basename( $to )
2029 );
2030 }
2031
2032 foreach ( (array) $dirlist as $filename => $fileinfo ) {
2033 if ( in_array( $filename, $skip_list, true ) ) {
2034 continue;
2035 }
2036
2037 if ( 'f' === $fileinfo['type'] ) {
2038 if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
2039 // If copy failed, chmod file to 0644 and try again.
2040 $wp_filesystem->chmod( $to . $filename, FS_CHMOD_FILE );
2041
2042 if ( ! $wp_filesystem->copy( $from . $filename, $to . $filename, true, FS_CHMOD_FILE ) ) {
2043 return new WP_Error( 'copy_failed_copy_dir', __( 'Could not copy file.' ), $to . $filename );
2044 }
2045 }
2046
2047 wp_opcache_invalidate( $to . $filename );
2048 } elseif ( 'd' === $fileinfo['type'] ) {
2049 if ( ! $wp_filesystem->is_dir( $to . $filename ) ) {
2050 if ( ! $wp_filesystem->mkdir( $to . $filename, FS_CHMOD_DIR ) ) {
2051 return new WP_Error( 'mkdir_failed_copy_dir', __( 'Could not create directory.' ), $to . $filename );
2052 }
2053 }
2054
2055 // Generate the $sub_skip_list for the subdirectory as a sub-set of the existing $skip_list.
2056 $sub_skip_list = array();
2057
2058 foreach ( $skip_list as $skip_item ) {
2059 if ( str_starts_with( $skip_item, $filename . '/' ) ) {
2060 $sub_skip_list[] = preg_replace( '!^' . preg_quote( $filename, '!' ) . '/!i', '', $skip_item );
2061 }
2062 }
2063
2064 $result = copy_dir( $from . $filename, $to . $filename, $sub_skip_list );
2065
2066 if ( is_wp_error( $result ) ) {
2067 return $result;
2068 }
2069 }
2070 }
2071
2072 return true;
2073}
2074
2075/**
2076 * Moves a directory from one location to another.
2077 *
2078 * Recursively invalidates OPcache on success.
2079 *
2080 * If the renaming failed, falls back to copy_dir().
2081 *
2082 * Assumes that WP_Filesystem() has already been called and setup.
2083 *
2084 * This function is not designed to merge directories, copy_dir() should be used instead.
2085 *
2086 * @since 6.2.0
2087 *
2088 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
2089 *
2090 * @param string $from Source directory.
2091 * @param string $to Destination directory.
2092 * @param bool $overwrite Optional. Whether to overwrite the destination directory if it exists.
2093 * Default false.
2094 * @return true|WP_Error True on success, WP_Error on failure.
2095 */
2096function move_dir( $from, $to, $overwrite = false ) {
2097 global $wp_filesystem;
2098
2099 if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) {
2100 return new WP_Error( 'source_destination_same_move_dir', __( 'The source and destination are the same.' ) );
2101 }
2102
2103 if ( $wp_filesystem->exists( $to ) ) {
2104 if ( ! $overwrite ) {
2105 return new WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to );
2106 } elseif ( ! $wp_filesystem->delete( $to, true ) ) {
2107 // Can't overwrite if the destination couldn't be deleted.
2108 return new WP_Error( 'destination_not_deleted_move_dir', __( 'The destination directory already exists and could not be removed.' ) );
2109 }
2110 }
2111
2112 if ( $wp_filesystem->move( $from, $to ) ) {
2113 /*
2114 * When using an environment with shared folders,
2115 * there is a delay in updating the filesystem's cache.
2116 *
2117 * This is a known issue in environments with a VirtualBox provider.
2118 *
2119 * A 200ms delay gives time for the filesystem to update its cache,
2120 * prevents "Operation not permitted", and "No such file or directory" warnings.
2121 *
2122 * This delay is used in other projects, including Composer.
2123 * @link https://github.com/composer/composer/blob/2.5.1/src/Composer/Util/Platform.php#L228-L233
2124 */
2125 usleep( 200000 );
2126 wp_opcache_invalidate_directory( $to );
2127
2128 return true;
2129 }
2130
2131 // Fall back to a recursive copy.
2132 if ( ! $wp_filesystem->is_dir( $to ) ) {
2133 if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) {
2134 return new WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to );
2135 }
2136 }
2137
2138 $result = copy_dir( $from, $to, array( basename( $to ) ) );
2139
2140 // Clear the source directory.
2141 if ( true === $result ) {
2142 $wp_filesystem->delete( $from, true );
2143 }
2144
2145 return $result;
2146}
2147
2148/**
2149 * Initializes and connects the WordPress Filesystem Abstraction classes.
2150 *
2151 * This function will include the chosen transport and attempt connecting.
2152 *
2153 * Plugins may add extra transports, And force WordPress to use them by returning
2154 * the filename via the {@see 'filesystem_method_file'} filter.
2155 *
2156 * @since 2.5.0
2157 *
2158 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
2159 *
2160 * @param array|false $args Optional. Connection args, These are passed
2161 * directly to the `WP_Filesystem_*()` classes.
2162 * Default false.
2163 * @param string|false $context Optional. Context for get_filesystem_method().
2164 * Default false.
2165 * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
2166 * Default false.
2167 * @return bool|null True on success, false on failure,
2168 * null if the filesystem method class file does not exist.
2169 */
2170function WP_Filesystem( $args = false, $context = false, $allow_relaxed_file_ownership = false ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
2171 global $wp_filesystem;
2172
2173 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
2174
2175 $method = get_filesystem_method( $args, $context, $allow_relaxed_file_ownership );
2176
2177 if ( ! $method ) {
2178 return false;
2179 }
2180
2181 if ( ! class_exists( "WP_Filesystem_$method" ) ) {
2182
2183 /**
2184 * Filters the path for a specific filesystem method class file.
2185 *
2186 * @since 2.6.0
2187 *
2188 * @see get_filesystem_method()
2189 *
2190 * @param string $path Path to the specific filesystem method class file.
2191 * @param string $method The filesystem method to use.
2192 */
2193 $abstraction_file = apply_filters( 'filesystem_method_file', ABSPATH . 'wp-admin/includes/class-wp-filesystem-' . $method . '.php', $method );
2194
2195 if ( ! file_exists( $abstraction_file ) ) {
2196 return;
2197 }
2198
2199 require_once $abstraction_file;
2200 }
2201 $method = "WP_Filesystem_$method";
2202
2203 $wp_filesystem = new $method( $args );
2204
2205 /*
2206 * Define the timeouts for the connections. Only available after the constructor is called
2207 * to allow for per-transport overriding of the default.
2208 */
2209 if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) {
2210 define( 'FS_CONNECT_TIMEOUT', 30 ); // 30 seconds.
2211 }
2212 if ( ! defined( 'FS_TIMEOUT' ) ) {
2213 define( 'FS_TIMEOUT', 30 ); // 30 seconds.
2214 }
2215
2216 if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
2217 return false;
2218 }
2219
2220 if ( ! $wp_filesystem->connect() ) {
2221 return false; // There was an error connecting to the server.
2222 }
2223
2224 // Set the permission constants if not already set.
2225 if ( ! defined( 'FS_CHMOD_DIR' ) ) {
2226 define( 'FS_CHMOD_DIR', ( fileperms( ABSPATH ) & 0777 | 0755 ) );
2227 }
2228 if ( ! defined( 'FS_CHMOD_FILE' ) ) {
2229 define( 'FS_CHMOD_FILE', ( fileperms( ABSPATH . 'index.php' ) & 0777 | 0644 ) );
2230 }
2231
2232 return true;
2233}
2234
2235/**
2236 * Determines which method to use for reading, writing, modifying, or deleting
2237 * files on the filesystem.
2238 *
2239 * The priority of the transports are: Direct, SSH2, FTP PHP Extension, FTP Sockets
2240 * (Via Sockets class, or `fsockopen()`). Valid values for these are: 'direct', 'ssh2',
2241 * 'ftpext' or 'ftpsockets'.
2242 *
2243 * The return value can be overridden by defining the `FS_METHOD` constant in `wp-config.php`,
2244 * or filtering via {@see 'filesystem_method'}.
2245 *
2246 * @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#wordpress-upgrade-constants
2247 *
2248 * Plugins may define a custom transport handler, See WP_Filesystem().
2249 *
2250 * @since 2.5.0
2251 *
2252 * @global callable $_wp_filesystem_direct_method
2253 *
2254 * @param array $args Optional. Connection details. Default empty array.
2255 * @param string $context Optional. Full path to the directory that is tested
2256 * for being writable. Default empty.
2257 * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
2258 * Default false.
2259 * @return string The transport to use, see description for valid return values.
2260 */
2261function get_filesystem_method( $args = array(), $context = '', $allow_relaxed_file_ownership = false ) {
2262 // Please ensure that this is either 'direct', 'ssh2', 'ftpext', or 'ftpsockets'.
2263 $method = defined( 'FS_METHOD' ) ? FS_METHOD : false;
2264
2265 if ( ! $context ) {
2266 $context = WP_CONTENT_DIR;
2267 }
2268
2269 // If the directory doesn't exist (wp-content/languages) then use the parent directory as we'll create it.
2270 if ( WP_LANG_DIR === $context && ! is_dir( $context ) ) {
2271 $context = dirname( $context );
2272 }
2273
2274 $context = trailingslashit( $context );
2275
2276 if ( ! $method ) {
2277
2278 $temp_file_name = $context . 'temp-write-test-' . str_replace( '.', '-', uniqid( '', true ) );
2279 $temp_handle = @fopen( $temp_file_name, 'w' );
2280 if ( $temp_handle ) {
2281
2282 // Attempt to determine the file owner of the WordPress files, and that of newly created files.
2283 $wp_file_owner = false;
2284 $temp_file_owner = false;
2285 if ( function_exists( 'fileowner' ) ) {
2286 $wp_file_owner = @fileowner( __FILE__ );
2287 $temp_file_owner = @fileowner( $temp_file_name );
2288 }
2289
2290 if ( false !== $wp_file_owner && $wp_file_owner === $temp_file_owner ) {
2291 /*
2292 * WordPress is creating files as the same owner as the WordPress files,
2293 * this means it's safe to modify & create new files via PHP.
2294 */
2295 $method = 'direct';
2296 $GLOBALS['_wp_filesystem_direct_method'] = 'file_owner';
2297 } elseif ( $allow_relaxed_file_ownership ) {
2298 /*
2299 * The $context directory is writable, and $allow_relaxed_file_ownership is set,
2300 * this means we can modify files safely in this directory.
2301 * This mode doesn't create new files, only alter existing ones.
2302 */
2303 $method = 'direct';
2304 $GLOBALS['_wp_filesystem_direct_method'] = 'relaxed_ownership';
2305 }
2306
2307 fclose( $temp_handle );
2308 @unlink( $temp_file_name );
2309 }
2310 }
2311
2312 if ( ! $method && isset( $args['connection_type'] ) && 'ssh' === $args['connection_type'] && extension_loaded( 'ssh2' ) ) {
2313 $method = 'ssh2';
2314 }
2315 if ( ! $method && extension_loaded( 'ftp' ) ) {
2316 $method = 'ftpext';
2317 }
2318 if ( ! $method && ( extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) ) {
2319 $method = 'ftpsockets'; // Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread.
2320 }
2321
2322 /**
2323 * Filters the filesystem method to use.
2324 *
2325 * @since 2.6.0
2326 *
2327 * @param string $method Filesystem method to return.
2328 * @param array $args An array of connection details for the method.
2329 * @param string $context Full path to the directory that is tested for being writable.
2330 * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable.
2331 */
2332 return apply_filters( 'filesystem_method', $method, $args, $context, $allow_relaxed_file_ownership );
2333}
2334
2335/**
2336 * Displays a form to the user to request for their FTP/SSH details in order
2337 * to connect to the filesystem.
2338 *
2339 * All chosen/entered details are saved, excluding the password.
2340 *
2341 * Hostnames may be in the form of hostname:portnumber (eg: wordpress.org:2467)
2342 * to specify an alternate FTP/SSH port.
2343 *
2344 * Plugins may override this form by returning true|false via the {@see 'request_filesystem_credentials'} filter.
2345 *
2346 * @since 2.5.0
2347 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
2348 *
2349 * @global string $pagenow The filename of the current screen.
2350 *
2351 * @param string $form_post The URL to post the form to.
2352 * @param string $type Optional. Chosen type of filesystem. Default empty.
2353 * @param bool|WP_Error $error Optional. Whether the current request has failed
2354 * to connect, or an error object. Default false.
2355 * @param string $context Optional. Full path to the directory that is tested
2356 * for being writable. Default empty.
2357 * @param array $extra_fields Optional. Extra `POST` fields to be checked
2358 * for inclusion in the post. Default null.
2359 * @param bool $allow_relaxed_file_ownership Optional. Whether to allow Group/World writable.
2360 * Default false.
2361 * @return bool|array True if no filesystem credentials are required,
2362 * false if they are required but have not been provided,
2363 * array of credentials if they are required and have been provided.
2364 */
2365function request_filesystem_credentials( $form_post, $type = '', $error = false, $context = '', $extra_fields = null, $allow_relaxed_file_ownership = false ) {
2366 global $pagenow;
2367
2368 /**
2369 * Filters the filesystem credentials.
2370 *
2371 * Returning anything other than an empty string will effectively short-circuit
2372 * output of the filesystem credentials form, returning that value instead.
2373 *
2374 * A filter should return true if no filesystem credentials are required, false if they are required but have not been
2375 * provided, or an array of credentials if they are required and have been provided.
2376 *
2377 * @since 2.5.0
2378 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
2379 *
2380 * @param mixed $credentials Credentials to return instead. Default empty string.
2381 * @param string $form_post The URL to post the form to.
2382 * @param string $type Chosen type of filesystem.
2383 * @param bool|WP_Error $error Whether the current request has failed to connect,
2384 * or an error object.
2385 * @param string $context Full path to the directory that is tested for
2386 * being writable.
2387 * @param array $extra_fields Extra POST fields.
2388 * @param bool $allow_relaxed_file_ownership Whether to allow Group/World writable.
2389 */
2390 $req_cred = apply_filters( 'request_filesystem_credentials', '', $form_post, $type, $error, $context, $extra_fields, $allow_relaxed_file_ownership );
2391
2392 if ( '' !== $req_cred ) {
2393 return $req_cred;
2394 }
2395
2396 if ( empty( $type ) ) {
2397 $type = get_filesystem_method( array(), $context, $allow_relaxed_file_ownership );
2398 }
2399
2400 if ( 'direct' === $type ) {
2401 return true;
2402 }
2403
2404 if ( is_null( $extra_fields ) ) {
2405 $extra_fields = array( 'version', 'locale' );
2406 }
2407
2408 $credentials = get_option(
2409 'ftp_credentials',
2410 array(
2411 'hostname' => '',
2412 'username' => '',
2413 )
2414 );
2415
2416 $submitted_form = wp_unslash( $_POST );
2417
2418 // Verify nonce, or unset submitted form field values on failure.
2419 if ( ! isset( $_POST['_fs_nonce'] ) || ! wp_verify_nonce( $_POST['_fs_nonce'], 'filesystem-credentials' ) ) {
2420 unset(
2421 $submitted_form['hostname'],
2422 $submitted_form['username'],
2423 $submitted_form['password'],
2424 $submitted_form['public_key'],
2425 $submitted_form['private_key'],
2426 $submitted_form['connection_type']
2427 );
2428 }
2429
2430 $ftp_constants = array(
2431 'hostname' => 'FTP_HOST',
2432 'username' => 'FTP_USER',
2433 'password' => 'FTP_PASS',
2434 'public_key' => 'FTP_PUBKEY',
2435 'private_key' => 'FTP_PRIKEY',
2436 );
2437
2438 /*
2439 * If defined, set it to that. Else, if POST'd, set it to that. If not, set it to an empty string.
2440 * Otherwise, keep it as it previously was (saved details in option).
2441 */
2442 foreach ( $ftp_constants as $key => $constant ) {
2443 if ( defined( $constant ) ) {
2444 $credentials[ $key ] = constant( $constant );
2445 } elseif ( ! empty( $submitted_form[ $key ] ) ) {
2446 $credentials[ $key ] = $submitted_form[ $key ];
2447 } elseif ( ! isset( $credentials[ $key ] ) ) {
2448 $credentials[ $key ] = '';
2449 }
2450 }
2451
2452 // Sanitize the hostname, some people might pass in odd data.
2453 $credentials['hostname'] = preg_replace( '|\w+://|', '', $credentials['hostname'] ); // Strip any schemes off.
2454
2455 if ( strpos( $credentials['hostname'], ':' ) ) {
2456 list( $credentials['hostname'], $credentials['port'] ) = explode( ':', $credentials['hostname'], 2 );
2457 if ( ! is_numeric( $credentials['port'] ) ) {
2458 unset( $credentials['port'] );
2459 }
2460 } else {
2461 unset( $credentials['port'] );
2462 }
2463
2464 if ( ( defined( 'FTP_SSH' ) && FTP_SSH ) || ( defined( 'FS_METHOD' ) && 'ssh2' === FS_METHOD ) ) {
2465 $credentials['connection_type'] = 'ssh';
2466 } elseif ( ( defined( 'FTP_SSL' ) && FTP_SSL ) && 'ftpext' === $type ) { // Only the FTP Extension understands SSL.
2467 $credentials['connection_type'] = 'ftps';
2468 } elseif ( ! empty( $submitted_form['connection_type'] ) ) {
2469 $credentials['connection_type'] = $submitted_form['connection_type'];
2470 } elseif ( ! isset( $credentials['connection_type'] ) ) { // All else fails (and it's not defaulted to something else saved), default to FTP.
2471 $credentials['connection_type'] = 'ftp';
2472 }
2473
2474 if ( ! $error
2475 && ( ! empty( $credentials['hostname'] ) && ! empty( $credentials['username'] ) && ! empty( $credentials['password'] )
2476 || 'ssh' === $credentials['connection_type'] && ! empty( $credentials['public_key'] ) && ! empty( $credentials['private_key'] )
2477 )
2478 ) {
2479 $stored_credentials = $credentials;
2480
2481 if ( ! empty( $stored_credentials['port'] ) ) { // Save port as part of hostname to simplify above code.
2482 $stored_credentials['hostname'] .= ':' . $stored_credentials['port'];
2483 }
2484
2485 unset(
2486 $stored_credentials['password'],
2487 $stored_credentials['port'],
2488 $stored_credentials['private_key'],
2489 $stored_credentials['public_key']
2490 );
2491
2492 if ( ! wp_installing() ) {
2493 update_option( 'ftp_credentials', $stored_credentials, false );
2494 }
2495
2496 return $credentials;
2497 }
2498
2499 $hostname = isset( $credentials['hostname'] ) ? $credentials['hostname'] : '';
2500 $username = isset( $credentials['username'] ) ? $credentials['username'] : '';
2501 $public_key = isset( $credentials['public_key'] ) ? $credentials['public_key'] : '';
2502 $private_key = isset( $credentials['private_key'] ) ? $credentials['private_key'] : '';
2503 $port = isset( $credentials['port'] ) ? $credentials['port'] : '';
2504 $connection_type = isset( $credentials['connection_type'] ) ? $credentials['connection_type'] : '';
2505
2506 if ( $error ) {
2507 $error_string = __( '<strong>Error:</strong> Could not connect to the server. Please verify the settings are correct.' );
2508 if ( is_wp_error( $error ) ) {
2509 $error_string = esc_html( $error->get_error_message() );
2510 }
2511 wp_admin_notice(
2512 $error_string,
2513 array(
2514 'id' => 'message',
2515 'additional_classes' => array( 'error' ),
2516 )
2517 );
2518 }
2519
2520 $types = array();
2521 if ( extension_loaded( 'ftp' ) || extension_loaded( 'sockets' ) || function_exists( 'fsockopen' ) ) {
2522 $types['ftp'] = __( 'FTP' );
2523 }
2524 if ( extension_loaded( 'ftp' ) ) { // Only this supports FTPS.
2525 $types['ftps'] = __( 'FTPS (SSL)' );
2526 }
2527 if ( extension_loaded( 'ssh2' ) ) {
2528 $types['ssh'] = __( 'SSH2' );
2529 }
2530
2531 /**
2532 * Filters the connection types to output to the filesystem credentials form.
2533 *
2534 * @since 2.9.0
2535 * @since 4.6.0 The `$context` parameter default changed from `false` to an empty string.
2536 *
2537 * @param string[] $types Types of connections.
2538 * @param array $credentials Credentials to connect with.
2539 * @param string $type Chosen filesystem method.
2540 * @param bool|WP_Error $error Whether the current request has failed to connect,
2541 * or an error object.
2542 * @param string $context Full path to the directory that is tested for being writable.
2543 */
2544 $types = apply_filters( 'fs_ftp_connection_types', $types, $credentials, $type, $error, $context );
2545 ?>
2546<form action="<?php echo esc_url( $form_post ); ?>" method="post">
2547<div id="request-filesystem-credentials-form" class="request-filesystem-credentials-form">
2548 <?php
2549 // Print a H1 heading in the FTP credentials modal dialog, default is a H2.
2550 $heading_tag = 'h2';
2551 if ( 'plugins.php' === $pagenow || 'plugin-install.php' === $pagenow ) {
2552 $heading_tag = 'h1';
2553 }
2554 echo "<$heading_tag id='request-filesystem-credentials-title'>" . __( 'Connection Information' ) . "</$heading_tag>";
2555 ?>
2556<p id="request-filesystem-credentials-desc">
2557 <?php
2558 $label_user = __( 'Username' );
2559 $label_pass = __( 'Password' );
2560 _e( 'To perform the requested action, WordPress needs to access your web server.' );
2561 echo ' ';
2562 if ( ( isset( $types['ftp'] ) || isset( $types['ftps'] ) ) ) {
2563 if ( isset( $types['ssh'] ) ) {
2564 _e( 'Please enter your FTP or SSH credentials to proceed.' );
2565 $label_user = __( 'FTP/SSH Username' );
2566 $label_pass = __( 'FTP/SSH Password' );
2567 } else {
2568 _e( 'Please enter your FTP credentials to proceed.' );
2569 $label_user = __( 'FTP Username' );
2570 $label_pass = __( 'FTP Password' );
2571 }
2572 echo ' ';
2573 }
2574 _e( 'If you do not remember your credentials, you should contact your web host.' );
2575
2576 $hostname_value = esc_attr( $hostname );
2577 if ( ! empty( $port ) ) {
2578 $hostname_value .= ":$port";
2579 }
2580
2581 $password_value = '';
2582 if ( defined( 'FTP_PASS' ) ) {
2583 $password_value = '*****';
2584 }
2585 ?>
2586</p>
2587<label for="hostname">
2588 <span class="field-title"><?php _e( 'Hostname' ); ?></span>
2589 <input name="hostname" type="text" id="hostname" aria-describedby="request-filesystem-credentials-desc" class="code" placeholder="<?php esc_attr_e( 'example: www.wordpress.org' ); ?>" value="<?php echo $hostname_value; ?>"<?php disabled( defined( 'FTP_HOST' ) ); ?> />
2590</label>
2591<div class="ftp-username">
2592 <label for="username">
2593 <span class="field-title"><?php echo $label_user; ?></span>
2594 <input name="username" type="text" id="username" value="<?php echo esc_attr( $username ); ?>"<?php disabled( defined( 'FTP_USER' ) ); ?> />
2595 </label>
2596</div>
2597<div class="ftp-password">
2598 <label for="password">
2599 <span class="field-title"><?php echo $label_pass; ?></span>
2600 <input name="password" type="password" id="password" value="<?php echo $password_value; ?>"<?php disabled( defined( 'FTP_PASS' ) ); ?> spellcheck="false" />
2601 <?php
2602 if ( ! defined( 'FTP_PASS' ) ) {
2603 _e( 'This password will not be stored on the server.' );
2604 }
2605 ?>
2606 </label>
2607</div>
2608<fieldset>
2609<legend><?php _e( 'Connection Type' ); ?></legend>
2610 <?php
2611 $disabled = disabled( ( defined( 'FTP_SSL' ) && FTP_SSL ) || ( defined( 'FTP_SSH' ) && FTP_SSH ), true, false );
2612 foreach ( $types as $name => $text ) :
2613 ?>
2614 <label for="<?php echo esc_attr( $name ); ?>">
2615 <input type="radio" name="connection_type" id="<?php echo esc_attr( $name ); ?>" value="<?php echo esc_attr( $name ); ?>" <?php checked( $name, $connection_type ); ?> <?php echo $disabled; ?> />
2616 <?php echo $text; ?>
2617 </label>
2618 <?php
2619 endforeach;
2620 ?>
2621</fieldset>
2622 <?php
2623 if ( isset( $types['ssh'] ) ) {
2624 $hidden_class = '';
2625 if ( 'ssh' !== $connection_type ) {
2626 $hidden_class = ' class="hidden"';
2627 }
2628 ?>
2629<fieldset id="ssh-keys"<?php echo $hidden_class; ?>>
2630<legend><?php _e( 'Authentication Keys' ); ?></legend>
2631<label for="public_key">
2632 <span class="field-title"><?php _e( 'Public Key:' ); ?></span>
2633 <input name="public_key" type="text" id="public_key" aria-describedby="auth-keys-desc" value="<?php echo esc_attr( $public_key ); ?>"<?php disabled( defined( 'FTP_PUBKEY' ) ); ?> />
2634</label>
2635<label for="private_key">
2636 <span class="field-title"><?php _e( 'Private Key:' ); ?></span>
2637 <input name="private_key" type="text" id="private_key" value="<?php echo esc_attr( $private_key ); ?>"<?php disabled( defined( 'FTP_PRIKEY' ) ); ?> />
2638</label>
2639<p id="auth-keys-desc"><?php _e( 'Enter the location on the server where the public and private keys are located. If a passphrase is needed, enter that in the password field above.' ); ?></p>
2640</fieldset>
2641 <?php
2642 }
2643
2644 foreach ( (array) $extra_fields as $field ) {
2645 if ( isset( $submitted_form[ $field ] ) ) {
2646 echo '<input type="hidden" name="' . esc_attr( $field ) . '" value="' . esc_attr( $submitted_form[ $field ] ) . '" />';
2647 }
2648 }
2649
2650 /*
2651 * Make sure the `submit_button()` function is available during the REST API call
2652 * from WP_Site_Health_Auto_Updates::test_check_wp_filesystem_method().
2653 */
2654 if ( ! function_exists( 'submit_button' ) ) {
2655 require_once ABSPATH . 'wp-admin/includes/template.php';
2656 }
2657 ?>
2658 <p class="request-filesystem-credentials-action-buttons">
2659 <?php wp_nonce_field( 'filesystem-credentials', '_fs_nonce', false, true ); ?>
2660 <button class="button cancel-button" data-js-action="close" type="button"><?php _e( 'Cancel' ); ?></button>
2661 <?php submit_button( __( 'Proceed' ), 'primary', 'upgrade', false ); ?>
2662 </p>
2663</div>
2664</form>
2665 <?php
2666 return false;
2667}
2668
2669/**
2670 * Prints the filesystem credentials modal when needed.
2671 *
2672 * @since 4.2.0
2673 */
2674function wp_print_request_filesystem_credentials_modal() {
2675 $filesystem_method = get_filesystem_method();
2676
2677 ob_start();
2678 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
2679 ob_end_clean();
2680
2681 $request_filesystem_credentials = ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored );
2682 if ( ! $request_filesystem_credentials ) {
2683 return;
2684 }
2685 ?>
2686 <div id="request-filesystem-credentials-dialog" class="notification-dialog-wrap request-filesystem-credentials-dialog">
2687 <div class="notification-dialog-background"></div>
2688 <div class="notification-dialog" role="dialog" aria-labelledby="request-filesystem-credentials-title" tabindex="0">
2689 <div class="request-filesystem-credentials-dialog-content">
2690 <?php request_filesystem_credentials( site_url() ); ?>
2691 </div>
2692 </div>
2693 </div>
2694 <?php
2695}
2696
2697/**
2698 * Attempts to clear the opcode cache for an individual PHP file.
2699 *
2700 * This function can be called safely without having to check the file extension
2701 * or availability of the OPcache extension.
2702 *
2703 * Whether or not invalidation is possible is cached to improve performance.
2704 *
2705 * @since 5.5.0
2706 *
2707 * @link https://www.php.net/manual/en/function.opcache-invalidate.php
2708 *
2709 * @param string $filepath Path to the file, including extension, for which the opcode cache is to be cleared.
2710 * @param bool $force Invalidate even if the modification time is not newer than the file in cache.
2711 * Default false.
2712 * @return bool True if opcache was invalidated for `$filepath`, or there was nothing to invalidate.
2713 * False if opcache invalidation is not available, or is disabled via filter.
2714 */
2715function wp_opcache_invalidate( $filepath, $force = false ) {
2716 static $can_invalidate = null;
2717
2718 /*
2719 * Check to see if WordPress is able to run `opcache_invalidate()` or not, and cache the value.
2720 *
2721 * First, check to see if the function is available to call, then if the host has restricted
2722 * the ability to run the function to avoid a PHP warning.
2723 *
2724 * `opcache.restrict_api` can specify the path for files allowed to call `opcache_invalidate()`.
2725 *
2726 * If the host has this set, check whether the path in `opcache.restrict_api` matches
2727 * the beginning of the path of the origin file.
2728 *
2729 * `$_SERVER['SCRIPT_FILENAME']` approximates the origin file's path, but `realpath()`
2730 * is necessary because `SCRIPT_FILENAME` can be a relative path when run from CLI.
2731 *
2732 * For more details, see:
2733 * - https://www.php.net/manual/en/opcache.configuration.php
2734 * - https://www.php.net/manual/en/reserved.variables.server.php
2735 * - https://core.trac.wordpress.org/ticket/36455
2736 */
2737 if ( null === $can_invalidate
2738 && function_exists( 'opcache_invalidate' )
2739 && ( ! ini_get( 'opcache.restrict_api' )
2740 || stripos( realpath( $_SERVER['SCRIPT_FILENAME'] ), ini_get( 'opcache.restrict_api' ) ) === 0 )
2741 ) {
2742 $can_invalidate = true;
2743 }
2744
2745 // If invalidation is not available, return early.
2746 if ( ! $can_invalidate ) {
2747 return false;
2748 }
2749
2750 // Verify that file to be invalidated has a PHP extension.
2751 if ( '.php' !== strtolower( substr( $filepath, -4 ) ) ) {
2752 return false;
2753 }
2754
2755 /**
2756 * Filters whether to invalidate a file from the opcode cache.
2757 *
2758 * @since 5.5.0
2759 *
2760 * @param bool $will_invalidate Whether WordPress will invalidate `$filepath`. Default true.
2761 * @param string $filepath The path to the PHP file to invalidate.
2762 */
2763 if ( apply_filters( 'wp_opcache_invalidate_file', true, $filepath ) ) {
2764 return opcache_invalidate( $filepath, $force );
2765 }
2766
2767 return false;
2768}
2769
2770/**
2771 * Attempts to clear the opcode cache for a directory of files.
2772 *
2773 * @since 6.2.0
2774 *
2775 * @see wp_opcache_invalidate()
2776 * @link https://www.php.net/manual/en/function.opcache-invalidate.php
2777 *
2778 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
2779 *
2780 * @param string $dir The path to the directory for which the opcode cache is to be cleared.
2781 */
2782function wp_opcache_invalidate_directory( $dir ) {
2783 global $wp_filesystem;
2784
2785 if ( ! is_string( $dir ) || '' === trim( $dir ) ) {
2786 if ( WP_DEBUG ) {
2787 $error_message = sprintf(
2788 /* translators: %s: The function name. */
2789 __( '%s expects a non-empty string.' ),
2790 '<code>wp_opcache_invalidate_directory()</code>'
2791 );
2792 wp_trigger_error( '', $error_message );
2793 }
2794 return;
2795 }
2796
2797 $dirlist = $wp_filesystem->dirlist( $dir, false, true );
2798
2799 if ( empty( $dirlist ) ) {
2800 return;
2801 }
2802
2803 /*
2804 * Recursively invalidate opcache of files in a directory.
2805 *
2806 * WP_Filesystem_*::dirlist() returns an array of file and directory information.
2807 *
2808 * This does not include a path to the file or directory.
2809 * To invalidate files within sub-directories, recursion is needed
2810 * to prepend an absolute path containing the sub-directory's name.
2811 *
2812 * @param array $dirlist Array of file/directory information from WP_Filesystem_Base::dirlist(),
2813 * with sub-directories represented as nested arrays.
2814 * @param string $path Absolute path to the directory.
2815 */
2816 $invalidate_directory = static function ( $dirlist, $path ) use ( &$invalidate_directory ) {
2817 $path = trailingslashit( $path );
2818
2819 foreach ( $dirlist as $name => $details ) {
2820 if ( 'f' === $details['type'] ) {
2821 wp_opcache_invalidate( $path . $name, true );
2822 } elseif ( is_array( $details['files'] ) && ! empty( $details['files'] ) ) {
2823 $invalidate_directory( $details['files'], $path . $name );
2824 }
2825 }
2826 };
2827
2828 $invalidate_directory( $dirlist, $dir );
2829}
2830