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