Changeset 62081
- Timestamp:
- 03/20/2026 05:09:14 PM (8 days ago)
- Location:
- trunk
- Files:
-
- 14 edited
-
Gruntfile.js (modified) (1 diff)
-
src/wp-includes/default-filters.php (modified) (1 diff)
-
src/wp-includes/media-template.php (modified) (2 diffs)
-
src/wp-includes/media.php (modified) (1 diff)
-
src/wp-includes/rest-api/class-wp-rest-server.php (modified) (1 diff)
-
src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php (modified) (11 diffs)
-
src/wp-includes/script-modules.php (modified) (1 diff)
-
tests/phpunit/tests/media/wpCrossOriginIsolation.php (modified) (1 diff)
-
tests/phpunit/tests/media/wpGetChromiumMajorVersion.php (modified) (1 diff)
-
tests/phpunit/tests/rest-api/rest-attachments-controller.php (modified) (3 diffs)
-
tests/phpunit/tests/rest-api/rest-schema-setup.php (modified) (2 diffs)
-
tests/phpunit/tests/script-modules/wpScriptModules.php (modified) (2 diffs)
-
tests/qunit/fixtures/wp-api-generated.js (modified) (3 diffs)
-
tools/gutenberg/copy.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/Gruntfile.js
r62079 r62081 660 660 '**/*', 661 661 '!**/*.map', 662 // Skip non-minified VIPS files — they are ~16MB of inlined WASM 663 // with no debugging value over the minified versions. 664 '!vips/!(*.min).js', 662 '!vips/**', 665 663 ], 666 664 dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/', -
trunk/src/wp-includes/default-filters.php
r62058 r62081 679 679 add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' ); 680 680 681 // Client-side media processing.682 add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' );683 // Cross-origin isolation for client-side media processing.684 add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' );685 add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' );686 add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' );687 add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' );688 681 // Nav menu. 689 682 add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); -
trunk/src/wp-includes/media-template.php
r61757 r62081 157 157 $class = 'media-modal wp-core-ui'; 158 158 159 $is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled();160 161 if ( $is_cross_origin_isolation_enabled ) {162 ob_start();163 }164 165 159 $alt_text_description = sprintf( 166 160 /* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */ … … 1589 1583 */ 1590 1584 do_action( 'print_media_templates' ); 1591 1592 if ( $is_cross_origin_isolation_enabled ) {1593 $html = (string) ob_get_clean();1594 1595 /*1596 * The media templates are inside <script type="text/html"> tags,1597 * whose content is treated as raw text by the HTML Tag Processor.1598 * Extract each script block's content, process it separately,1599 * then reassemble the full output.1600 */1601 $script_processor = new WP_HTML_Tag_Processor( $html );1602 while ( $script_processor->next_tag( 'SCRIPT' ) ) {1603 if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {1604 continue;1605 }1606 /*1607 * Unlike wp_add_crossorigin_attributes(), this does not check whether1608 * URLs are actually cross-origin. Media templates use Underscore.js1609 * template expressions (e.g. {{ data.url }}) as placeholder URLs,1610 * so actual URLs are not available at parse time.1611 * The crossorigin attribute is added unconditionally to all relevant1612 * media tags to ensure cross-origin isolation works regardless of1613 * the final URL value at render time.1614 */1615 $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );1616 while ( $template_processor->next_tag() ) {1617 if (1618 in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true )1619 && ! is_string( $template_processor->get_attribute( 'crossorigin' ) )1620 ) {1621 $template_processor->set_attribute( 'crossorigin', 'anonymous' );1622 }1623 }1624 $script_processor->set_modifiable_text( $template_processor->get_updated_html() );1625 }1626 1627 echo $script_processor->get_updated_html();1628 }1629 1585 } -
trunk/src/wp-includes/media.php
r62048 r62081 6401 6401 } 6402 6402 6403 /**6404 * Checks whether client-side media processing is enabled.6405 *6406 * Client-side media processing uses the browser's capabilities to handle6407 * tasks like image resizing and compression before uploading to the server.6408 *6409 * @since 7.0.06410 *6411 * @return bool Whether client-side media processing is enabled.6412 */6413 function wp_is_client_side_media_processing_enabled(): bool {6414 // This is due to SharedArrayBuffer requiring a secure context.6415 $host = strtolower( (string) strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) );6416 $enabled = ( is_ssl() || 'localhost' === $host || str_ends_with( $host, '.localhost' ) );6417 6418 /**6419 * Filters whether client-side media processing is enabled.6420 *6421 * @since 7.0.06422 *6423 * @param bool $enabled Whether client-side media processing is enabled. Default true if the page is served in a secure context.6424 */6425 return (bool) apply_filters( 'wp_client_side_media_processing_enabled', $enabled );6426 }6427 6428 /**6429 * Sets a global JS variable to indicate that client-side media processing is enabled.6430 *6431 * @since 7.0.06432 */6433 function wp_set_client_side_media_processing_flag(): void {6434 if ( ! wp_is_client_side_media_processing_enabled() ) {6435 return;6436 }6437 6438 wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true;', 'before' );6439 6440 $chromium_version = wp_get_chromium_major_version();6441 6442 if ( null !== $chromium_version && $chromium_version >= 137 ) {6443 wp_add_inline_script( 'wp-block-editor', 'window.__documentIsolationPolicy = true;', 'before' );6444 }6445 6446 /*6447 * Register the @wordpress/vips/worker script module as a dynamic dependency6448 * of the wp-upload-media classic script. This ensures it is included in the6449 * import map so that the dynamic import() in upload-media.js can resolve it.6450 */6451 wp_scripts()->add_data(6452 'wp-upload-media',6453 'module_dependencies',6454 array( '@wordpress/vips/worker' )6455 );6456 }6457 6458 /**6459 * Returns the major Chrome/Chromium version from the current request's User-Agent.6460 *6461 * Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).6462 *6463 * @since 7.0.06464 *6465 * @return int|null The major Chrome version, or null if not a Chromium browser.6466 */6467 function wp_get_chromium_major_version(): ?int {6468 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {6469 return null;6470 }6471 if ( preg_match( '#Chrome/(\d+)#', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {6472 return (int) $matches[1];6473 }6474 return null;6475 }6476 6477 /**6478 * Enables cross-origin isolation in the block editor.6479 *6480 * Required for enabling SharedArrayBuffer for WebAssembly-based6481 * media processing in the editor. Uses Document-Isolation-Policy6482 * on supported browsers (Chromium 137+).6483 *6484 * Skips setup when a third-party page builder overrides the block6485 * editor via a custom `action` query parameter, as DIP would block6486 * same-origin iframe access that these editors rely on.6487 *6488 * @since 7.0.06489 */6490 function wp_set_up_cross_origin_isolation(): void {6491 if ( ! wp_is_client_side_media_processing_enabled() ) {6492 return;6493 }6494 6495 $screen = get_current_screen();6496 6497 if ( ! $screen ) {6498 return;6499 }6500 6501 if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {6502 return;6503 }6504 6505 /*6506 * Skip when a third-party page builder overrides the block editor.6507 * DIP isolates the document into its own agent cluster,6508 * which blocks same-origin iframe access that these editors rely on.6509 */6510 if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {6511 return;6512 }6513 6514 // Cross-origin isolation is not needed if users can't upload files anyway.6515 if ( ! current_user_can( 'upload_files' ) ) {6516 return;6517 }6518 6519 wp_start_cross_origin_isolation_output_buffer();6520 }6521 6522 /**6523 * Sends the Document-Isolation-Policy header for cross-origin isolation.6524 *6525 * Uses an output buffer to add crossorigin="anonymous" where needed.6526 *6527 * @since 7.0.06528 */6529 function wp_start_cross_origin_isolation_output_buffer(): void {6530 $chromium_version = wp_get_chromium_major_version();6531 6532 if ( null === $chromium_version || $chromium_version < 137 ) {6533 return;6534 }6535 6536 ob_start(6537 static function ( string $output ): string {6538 header( 'Document-Isolation-Policy: isolate-and-credentialless' );6539 6540 return wp_add_crossorigin_attributes( $output );6541 }6542 );6543 }6544 6545 /**6546 * Adds crossorigin="anonymous" to relevant tags in the given HTML string.6547 *6548 * @since 7.0.06549 *6550 * @param string $html HTML input.6551 * @return string Modified HTML.6552 */6553 function wp_add_crossorigin_attributes( string $html ): string {6554 $site_url = site_url();6555 6556 $processor = new WP_HTML_Tag_Processor( $html );6557 6558 // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.6559 $cross_origin_tag_attributes = array(6560 'AUDIO' => array( 'src' => false ),6561 'LINK' => array( 'href' => false ),6562 'SCRIPT' => array( 'src' => false ),6563 'VIDEO' => array(6564 'src' => false,6565 'poster' => false,6566 ),6567 'SOURCE' => array( 'src' => false ),6568 );6569 6570 while ( $processor->next_tag() ) {6571 $tag = $processor->get_tag();6572 6573 if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {6574 continue;6575 }6576 6577 if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {6578 $processor->set_bookmark( 'audio-video-parent' );6579 }6580 6581 $processor->set_bookmark( 'resume' );6582 6583 $sought = false;6584 6585 $crossorigin = $processor->get_attribute( 'crossorigin' );6586 6587 $is_cross_origin = false;6588 6589 foreach ( $cross_origin_tag_attributes[ $tag ] as $attr => $is_srcset ) {6590 if ( $is_srcset ) {6591 $srcset = $processor->get_attribute( $attr );6592 if ( is_string( $srcset ) ) {6593 foreach ( explode( ',', $srcset ) as $candidate ) {6594 $candidate_url = strtok( trim( $candidate ), ' ' );6595 if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) {6596 $is_cross_origin = true;6597 break;6598 }6599 }6600 }6601 } else {6602 $url = $processor->get_attribute( $attr );6603 if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {6604 $is_cross_origin = true;6605 }6606 }6607 6608 if ( $is_cross_origin ) {6609 break;6610 }6611 }6612 6613 if ( $is_cross_origin && ! is_string( $crossorigin ) ) {6614 if ( 'SOURCE' === $tag ) {6615 $sought = $processor->seek( 'audio-video-parent' );6616 6617 if ( $sought ) {6618 $processor->set_attribute( 'crossorigin', 'anonymous' );6619 }6620 } else {6621 $processor->set_attribute( 'crossorigin', 'anonymous' );6622 }6623 6624 if ( $sought ) {6625 $processor->seek( 'resume' );6626 $processor->release_bookmark( 'audio-video-parent' );6627 }6628 }6629 }6630 6631 return $processor->get_updated_html();6632 }6633 -
trunk/src/wp-includes/rest-api/class-wp-rest-server.php
r61878 r62081 1369 1369 ); 1370 1370 1371 // Add media processing settings for users who can upload files.1372 if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) {1373 // Image sizes keyed by name for client-side media processing.1374 $available['image_sizes'] = array();1375 foreach ( wp_get_registered_image_subsizes() as $name => $size ) {1376 $available['image_sizes'][ $name ] = $size;1377 }1378 1379 /** This filter is documented in wp-admin/includes/image.php */1380 $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 );1381 1382 // Image output formats.1383 $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' );1384 $output_formats = array();1385 foreach ( $input_formats as $mime_type ) {1386 /** This filter is documented in wp-includes/media.php */1387 $output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type );1388 }1389 $available['image_output_formats'] = (object) $output_formats;1390 1391 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */1392 $available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' );1393 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */1394 $available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' );1395 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */1396 $available['gif_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' );1397 }1398 1399 1371 $response = new WP_REST_Response( $available ); 1400 1372 -
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
r61982 r62081 64 64 ) 65 65 ); 66 67 if ( wp_is_client_side_media_processing_enabled() ) {68 $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );69 // Special case to set 'original_image' in attachment metadata.70 $valid_image_sizes[] = 'original';71 // Used for PDF thumbnails.72 $valid_image_sizes[] = 'full';73 // Client-side big image threshold: sideload the scaled version.74 $valid_image_sizes[] = 'scaled';75 76 register_rest_route(77 $this->namespace,78 '/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',79 array(80 array(81 'methods' => WP_REST_Server::CREATABLE,82 'callback' => array( $this, 'sideload_item' ),83 'permission_callback' => array( $this, 'sideload_item_permissions_check' ),84 'args' => array(85 'id' => array(86 'description' => __( 'Unique identifier for the attachment.' ),87 'type' => 'integer',88 ),89 'image_size' => array(90 'description' => __( 'Image size.' ),91 'type' => 'string',92 'enum' => $valid_image_sizes,93 'required' => true,94 ),95 'convert_format' => array(96 'type' => 'boolean',97 'default' => true,98 'description' => __( 'Whether to convert image formats.' ),99 ),100 ),101 ),102 'allow_batch' => $this->allow_batch,103 'schema' => array( $this, 'get_public_item_schema' ),104 )105 );106 107 register_rest_route(108 $this->namespace,109 '/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',110 array(111 array(112 'methods' => WP_REST_Server::CREATABLE,113 'callback' => array( $this, 'finalize_item' ),114 'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),115 'args' => array(116 'id' => array(117 'description' => __( 'Unique identifier for the attachment.' ),118 'type' => 'integer',119 ),120 ),121 ),122 'allow_batch' => $this->allow_batch,123 'schema' => array( $this, 'get_public_item_schema' ),124 )125 );126 }127 }128 129 /**130 * Retrieves the query params for the attachments collection.131 *132 * @since 7.0.0133 *134 * @param string $method Optional. HTTP method of the request.135 * The arguments for `CREATABLE` requests are136 * checked for required values and may fall-back to a given default.137 * Default WP_REST_Server::CREATABLE.138 * @return array<string, array<string, mixed>> Endpoint arguments.139 */140 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {141 $args = parent::get_endpoint_args_for_item_schema( $method );142 143 if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) {144 $args['generate_sub_sizes'] = array(145 'type' => 'boolean',146 'default' => true,147 'description' => __( 'Whether to generate image sub sizes.' ),148 );149 $args['convert_format'] = array(150 'type' => 'boolean',151 'default' => true,152 'description' => __( 'Whether to convert image formats.' ),153 );154 }155 156 return $args;157 66 } 158 67 … … 253 162 $prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, $files['file']['type'] ?? null ); 254 163 255 // When the client handles image processing (generate_sub_sizes is false),256 // skip the server-side image editor support check.257 if ( false === $request['generate_sub_sizes'] ) {258 $prevent_unsupported_uploads = false;259 }260 261 164 // If the upload is an image, check if the server can handle the mime type. 262 165 if ( … … 290 193 * 291 194 * @since 4.7.0 292 * @since 7.0.0 Added `generate_sub_sizes` and `convert_format` parameters.293 195 * 294 196 * @param WP_REST_Request $request Full details about the request. … … 304 206 } 305 207 306 // Handle generate_sub_sizes parameter.307 if ( false === $request['generate_sub_sizes'] ) {308 add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );309 add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );310 // Disable server-side EXIF rotation so the client can handle it.311 // This preserves the original orientation value in the metadata.312 add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );313 }314 315 // Handle convert_format parameter.316 if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {317 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );318 }319 320 208 $insert = $this->insert_attachment( $request ); 321 209 322 210 if ( is_wp_error( $insert ) ) { 323 $this->remove_client_side_media_processing_filters();324 211 return $insert; 325 212 } … … 339 226 340 227 if ( is_wp_error( $thumbnail_update ) ) { 341 $this->remove_client_side_media_processing_filters();342 228 return $thumbnail_update; 343 229 } … … 348 234 349 235 if ( is_wp_error( $meta_update ) ) { 350 $this->remove_client_side_media_processing_filters();351 236 return $meta_update; 352 237 } … … 357 242 358 243 if ( is_wp_error( $fields_update ) ) { 359 $this->remove_client_side_media_processing_filters();360 244 return $fields_update; 361 245 } … … 364 248 365 249 if ( is_wp_error( $terms_update ) ) { 366 $this->remove_client_side_media_processing_filters();367 250 return $terms_update; 368 251 } … … 401 284 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); 402 285 403 $this->remove_client_side_media_processing_filters();404 405 286 $response = $this->prepare_item_for_response( $attachment, $request ); 406 287 $response = rest_ensure_response( $response ); … … 409 290 410 291 return $response; 411 }412 413 /**414 * Removes filters added for client-side media processing.415 *416 * @since 7.0.0417 */418 private function remove_client_side_media_processing_filters() {419 remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );420 remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );421 remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 );422 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );423 292 } 424 293 … … 1988 1857 return null; 1989 1858 } 1990 1991 /**1992 * Checks if a given request has access to sideload a file.1993 *1994 * Sideloading a file for an existing attachment1995 * requires both update and create permissions.1996 *1997 * @since 7.0.01998 *1999 * @param WP_REST_Request $request Full details about the request.2000 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.2001 */2002 public function sideload_item_permissions_check( $request ) {2003 return $this->edit_media_item_permissions_check( $request );2004 }2005 2006 /**2007 * Side-loads a media file without creating a new attachment.2008 *2009 * @since 7.0.02010 *2011 * @param WP_REST_Request $request Full details about the request.2012 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.2013 */2014 public function sideload_item( WP_REST_Request $request ) {2015 $attachment_id = $request['id'];2016 2017 $post = $this->get_post( $attachment_id );2018 2019 if ( is_wp_error( $post ) ) {2020 return $post;2021 }2022 2023 if (2024 ! wp_attachment_is_image( $post ) &&2025 ! wp_attachment_is( 'pdf', $post )2026 ) {2027 return new WP_Error(2028 'rest_post_invalid_id',2029 __( 'Invalid post ID. Only images and PDFs can be sideloaded.' ),2030 array( 'status' => 400 )2031 );2032 }2033 2034 if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) {2035 // Prevent image conversion as that is done client-side.2036 add_filter( 'image_editor_output_format', '__return_empty_array', 100 );2037 }2038 2039 // Get the file via $_FILES or raw data.2040 $files = $request->get_file_params();2041 $headers = $request->get_headers();2042 2043 /*2044 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.2045 * See /wp-includes/functions.php.2046 * With the following filter we can work around this safeguard.2047 */2048 $attachment_filename = get_attached_file( $attachment_id, true );2049 $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null;2050 2051 $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {2052 return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename );2053 };2054 2055 add_filter( 'wp_unique_filename', $filter_filename, 10, 6 );2056 2057 $parent_post = get_post_parent( $attachment_id );2058 2059 $time = null;2060 2061 // Matches logic in media_handle_upload().2062 // The post date doesn't usually matter for pages, so don't backdate this upload.2063 if ( $parent_post && 'page' !== $parent_post->post_type && ! str_starts_with( $parent_post->post_date, '0000-00-00' ) ) {2064 $time = $parent_post->post_date;2065 }2066 2067 if ( ! empty( $files ) ) {2068 $file = $this->upload_from_file( $files, $headers, $time );2069 } else {2070 $file = $this->upload_from_data( $request->get_body(), $headers, $time );2071 }2072 2073 remove_filter( 'wp_unique_filename', $filter_filename );2074 remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );2075 2076 if ( is_wp_error( $file ) ) {2077 return $file;2078 }2079 2080 $type = $file['type'];2081 $path = $file['file'];2082 2083 $image_size = $request['image_size'];2084 2085 $metadata = wp_get_attachment_metadata( $attachment_id, true );2086 2087 if ( ! $metadata ) {2088 $metadata = array();2089 }2090 2091 if ( 'original' === $image_size ) {2092 $metadata['original_image'] = wp_basename( $path );2093 } elseif ( 'scaled' === $image_size ) {2094 // The current attached file is the original; record it as original_image.2095 $current_file = get_attached_file( $attachment_id, true );2096 2097 if ( ! $current_file ) {2098 return new WP_Error(2099 'rest_sideload_no_attached_file',2100 __( 'Unable to retrieve the attached file for this attachment.' ),2101 array( 'status' => 404 )2102 );2103 }2104 2105 $metadata['original_image'] = wp_basename( $current_file );2106 2107 // Validate the scaled image before updating the attached file.2108 $size = wp_getimagesize( $path );2109 $filesize = wp_filesize( $path );2110 2111 if ( ! $size || ! $filesize ) {2112 return new WP_Error(2113 'rest_sideload_invalid_image',2114 __( 'Unable to read the scaled image file.' ),2115 array( 'status' => 500 )2116 );2117 }2118 2119 // Update the attached file to point to the scaled version.2120 if (2121 get_attached_file( $attachment_id, true ) !== $path &&2122 ! update_attached_file( $attachment_id, $path )2123 ) {2124 return new WP_Error(2125 'rest_sideload_update_attached_file_failed',2126 __( 'Unable to update the attached file for this attachment.' ),2127 array( 'status' => 500 )2128 );2129 }2130 2131 $metadata['width'] = $size[0];2132 $metadata['height'] = $size[1];2133 $metadata['filesize'] = $filesize;2134 $metadata['file'] = _wp_relative_upload_path( $path );2135 } else {2136 $metadata['sizes'] = $metadata['sizes'] ?? array();2137 2138 $size = wp_getimagesize( $path );2139 2140 $metadata['sizes'][ $image_size ] = array(2141 'width' => $size ? $size[0] : 0,2142 'height' => $size ? $size[1] : 0,2143 'file' => wp_basename( $path ),2144 'mime-type' => $type,2145 'filesize' => wp_filesize( $path ),2146 );2147 }2148 2149 wp_update_attachment_metadata( $attachment_id, $metadata );2150 2151 $response_request = new WP_REST_Request(2152 WP_REST_Server::READABLE,2153 rest_get_route_for_post( $attachment_id )2154 );2155 2156 $response_request['context'] = 'edit';2157 2158 if ( isset( $request['_fields'] ) ) {2159 $response_request['_fields'] = $request['_fields'];2160 }2161 2162 $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );2163 2164 $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) );2165 2166 return $response;2167 }2168 2169 /**2170 * Filters wp_unique_filename during sideloads.2171 *2172 * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts.2173 * Adding this closure to the filter helps work around this safeguard.2174 *2175 * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg,2176 * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg2177 * However, here it is desired not to add the suffix in order to maintain the same2178 * naming convention as if the file was uploaded regularly.2179 *2180 * @since 7.0.02181 *2182 * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L25822183 *2184 * @param string $filename Unique file name.2185 * @param string $dir Directory path.2186 * @param int|string $number The highest number that was used to make the file name unique2187 * or an empty string if unused.2188 * @param string|null $attachment_filename Original attachment file name.2189 * @return string Filtered file name.2190 */2191 private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) {2192 if ( ! is_int( $number ) || ! $attachment_filename ) {2193 return $filename;2194 }2195 2196 $ext = pathinfo( $filename, PATHINFO_EXTENSION );2197 $name = pathinfo( $filename, PATHINFO_FILENAME );2198 $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME );2199 2200 if ( ! $ext || ! $name ) {2201 return $filename;2202 }2203 2204 $matches = array();2205 if ( preg_match( '/(.*)-(\d+x\d+|scaled)-' . $number . '$/', $name, $matches ) ) {2206 $filename_without_suffix = $matches[1] . '-' . $matches[2] . ".$ext";2207 if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) {2208 return $filename_without_suffix;2209 }2210 }2211 2212 return $filename;2213 }2214 2215 /**2216 * Finalizes an attachment after client-side media processing.2217 *2218 * Triggers the 'wp_generate_attachment_metadata' filter so that2219 * server-side plugins can process the attachment after all client-side2220 * operations (upload, thumbnail generation, sideloads) are complete.2221 *2222 * @since 7.0.02223 *2224 * @param WP_REST_Request $request Full details about the request.2225 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.2226 */2227 public function finalize_item( WP_REST_Request $request ) {2228 $attachment_id = $request['id'];2229 2230 $post = $this->get_post( $attachment_id );2231 if ( is_wp_error( $post ) ) {2232 return $post;2233 }2234 2235 $metadata = wp_get_attachment_metadata( $attachment_id );2236 if ( ! is_array( $metadata ) ) {2237 $metadata = array();2238 }2239 2240 /** This filter is documented in wp-admin/includes/image.php */2241 $metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );2242 2243 wp_update_attachment_metadata( $attachment_id, $metadata );2244 2245 $response_request = new WP_REST_Request(2246 WP_REST_Server::READABLE,2247 rest_get_route_for_post( $attachment_id )2248 );2249 2250 $response_request['context'] = 'edit';2251 2252 if ( isset( $request['_fields'] ) ) {2253 $response_request['_fields'] = $request['_fields'];2254 }2255 2256 return $this->prepare_item_for_response( $post, $response_request );2257 }2258 1859 } -
trunk/src/wp-includes/script-modules.php
r62072 r62081 191 191 } 192 192 193 // VIPS files are always minified — the non-minified versions are not 194 // shipped because they are ~10MB of inlined WASM with no debugging value. 195 if ( str_starts_with( $file_name, 'vips/' ) ) { 196 $file_name = str_replace( '.js', '.min.js', $file_name ); 197 } elseif ( '' !== $suffix ) { 193 if ( '' !== $suffix ) { 198 194 $file_name = str_replace( '.js', $suffix . '.js', $file_name ); 199 195 } -
trunk/tests/phpunit/tests/media/wpCrossOriginIsolation.php
r62048 r62081 1 <?php2 3 /**4 * Tests for cross-origin isolation functions.5 *6 * @group media7 * @covers ::wp_set_up_cross_origin_isolation8 * @covers ::wp_start_cross_origin_isolation_output_buffer9 * @covers ::wp_is_client_side_media_processing_enabled10 */11 class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase {12 13 /**14 * Original HTTP_USER_AGENT value.15 */16 private ?string $original_user_agent;17 18 /**19 * Original HTTP_HOST value.20 */21 private ?string $original_http_host;22 23 /**24 * Original HTTPS value.25 */26 private ?string $original_https;27 28 /**29 * Original $_GET['action'] value.30 */31 private ?string $original_get_action;32 33 public function set_up() {34 parent::set_up();35 $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null;36 $this->original_http_host = $_SERVER['HTTP_HOST'] ?? null;37 $this->original_https = $_SERVER['HTTPS'] ?? null;38 $this->original_get_action = $_GET['action'] ?? null;39 }40 41 public function tear_down() {42 if ( null === $this->original_user_agent ) {43 unset( $_SERVER['HTTP_USER_AGENT'] );44 } else {45 $_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent;46 }47 48 if ( null === $this->original_http_host ) {49 unset( $_SERVER['HTTP_HOST'] );50 } else {51 $_SERVER['HTTP_HOST'] = $this->original_http_host;52 }53 54 if ( null === $this->original_https ) {55 unset( $_SERVER['HTTPS'] );56 } else {57 $_SERVER['HTTPS'] = $this->original_https;58 }59 60 if ( null === $this->original_get_action ) {61 unset( $_GET['action'] );62 } else {63 $_GET['action'] = $this->original_get_action;64 }65 66 // Clean up any output buffers started during tests.67 while ( ob_get_level() > 1 ) {68 ob_end_clean();69 }70 71 remove_all_filters( 'wp_client_side_media_processing_enabled' );72 parent::tear_down();73 }74 75 /**76 * @ticket 6476677 */78 public function test_returns_early_when_client_side_processing_disabled() {79 add_filter( 'wp_client_side_media_processing_enabled', '__return_false' );80 81 // Should not error or start an output buffer.82 $level_before = ob_get_level();83 wp_set_up_cross_origin_isolation();84 $level_after = ob_get_level();85 86 $this->assertSame( $level_before, $level_after );87 }88 89 /**90 * @ticket 6476691 */92 public function test_returns_early_when_no_screen() {93 // No screen is set, so it should return early.94 $level_before = ob_get_level();95 wp_set_up_cross_origin_isolation();96 $level_after = ob_get_level();97 98 $this->assertSame( $level_before, $level_after );99 }100 101 /**102 * This test must run in a separate process because the output buffer103 * callback sends HTTP headers via header(), which would fail in the104 * main PHPUnit process where output has already started.105 *106 * @ticket 64766107 *108 * @runInSeparateProcess109 * @preserveGlobalState disabled110 */111 public function test_starts_output_buffer_for_chrome_137() {112 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';113 114 $level_before = ob_get_level();115 wp_start_cross_origin_isolation_output_buffer();116 $level_after = ob_get_level();117 118 $this->assertSame( $level_before + 1, $level_after, 'Output buffer should be started for Chrome 137.' );119 120 ob_end_clean();121 }122 123 /**124 * @ticket 64766125 */126 public function test_does_not_start_output_buffer_for_chrome_136() {127 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';128 129 $level_before = ob_get_level();130 wp_start_cross_origin_isolation_output_buffer();131 $level_after = ob_get_level();132 133 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Chrome < 137.' );134 }135 136 /**137 * @ticket 64766138 */139 public function test_does_not_start_output_buffer_for_firefox() {140 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0';141 142 $level_before = ob_get_level();143 wp_start_cross_origin_isolation_output_buffer();144 $level_after = ob_get_level();145 146 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Firefox.' );147 }148 149 /**150 * @ticket 64766151 */152 public function test_does_not_start_output_buffer_for_safari() {153 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15';154 155 $level_before = ob_get_level();156 wp_start_cross_origin_isolation_output_buffer();157 $level_after = ob_get_level();158 159 $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' );160 }161 162 /**163 * @ticket 64803164 */165 public function test_client_side_processing_disabled_on_non_secure_origin() {166 $_SERVER['HTTP_HOST'] = 'example.com';167 $_SERVER['HTTPS'] = '';168 169 $this->assertFalse(170 wp_is_client_side_media_processing_enabled(),171 'Client-side media processing should be disabled on non-secure, non-localhost origins.'172 );173 }174 175 /**176 * @ticket 64803177 */178 public function test_client_side_processing_enabled_on_localhost() {179 $_SERVER['HTTP_HOST'] = 'localhost';180 $_SERVER['HTTPS'] = '';181 182 $this->assertTrue(183 wp_is_client_side_media_processing_enabled(),184 'Client-side media processing should be enabled on localhost.'185 );186 }187 188 /**189 * Verifies that cross-origin elements get crossorigin="anonymous" added.190 *191 * @ticket 64766192 *193 * @runInSeparateProcess194 * @preserveGlobalState disabled195 *196 * @dataProvider data_elements_that_should_get_crossorigin197 *198 * @param string $html HTML input to process.199 */200 public function test_output_buffer_adds_crossorigin( $html ) {201 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';202 203 ob_start();204 205 wp_start_cross_origin_isolation_output_buffer();206 echo $html;207 208 ob_end_flush();209 $output = ob_get_clean();210 211 $this->assertStringContainsString( 'crossorigin="anonymous"', $output );212 }213 214 /**215 * Data provider for elements that should receive crossorigin="anonymous".216 *217 * @return array[]218 */219 public function data_elements_that_should_get_crossorigin() {220 return array(221 'cross-origin script' => array(222 '<script src="https://external.example.com/script.js"></script>',223 ),224 'cross-origin audio' => array(225 '<audio src="https://external.example.com/audio.mp3"></audio>',226 ),227 'cross-origin video' => array(228 '<video src="https://external.example.com/video.mp4"></video>',229 ),230 'cross-origin link stylesheet' => array(231 '<link rel="stylesheet" href="https://external.example.com/style.css" />',232 ),233 'cross-origin source inside video' => array(234 '<video><source src="https://external.example.com/video.mp4" type="video/mp4" /></video>',235 ),236 );237 }238 239 /**240 * Verifies that certain elements do not get crossorigin="anonymous" added.241 *242 * Images are excluded because under Document-Isolation-Policy:243 * isolate-and-credentialless, the browser handles cross-origin images244 * in credentialless mode without needing explicit CORS headers.245 *246 * @ticket 64766247 *248 * @runInSeparateProcess249 * @preserveGlobalState disabled250 *251 * @dataProvider data_elements_that_should_not_get_crossorigin252 *253 * @param string $html HTML input to process.254 */255 public function test_output_buffer_does_not_add_crossorigin( $html ) {256 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';257 258 ob_start();259 260 wp_start_cross_origin_isolation_output_buffer();261 echo $html;262 263 ob_end_flush();264 $output = ob_get_clean();265 266 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );267 }268 269 /**270 * Data provider for elements that should not receive crossorigin="anonymous".271 *272 * @return array[]273 */274 public function data_elements_that_should_not_get_crossorigin() {275 return array(276 'cross-origin img' => array(277 '<img src="https://external.example.com/image.jpg" />',278 ),279 'cross-origin img with srcset' => array(280 '<img src="https://external.example.com/image.jpg" srcset="https://external.example.com/image-2x.jpg 2x" />',281 ),282 'link with cross-origin imagesrcset only' => array(283 '<link rel="preload" as="image" imagesrcset="https://external.example.com/image.jpg 1x" href="https://core.trac.wordpress.org/local-fallback.jpg" />',284 ),285 'relative URL script' => array(286 '<script src="https://core.trac.wordpress.org/wp-includes/js/wp-embed.min.js"></script>',287 ),288 );289 }290 291 /**292 * Same-origin URLs should not get crossorigin="anonymous".293 *294 * Uses site_url() at runtime since the test domain varies by CI config.295 *296 * @ticket 64766297 *298 * @runInSeparateProcess299 * @preserveGlobalState disabled300 */301 public function test_output_buffer_does_not_add_crossorigin_to_same_origin() {302 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';303 304 ob_start();305 306 wp_start_cross_origin_isolation_output_buffer();307 echo '<script src="' . site_url( '/wp-includes/js/wp-embed.min.js' ) . '"></script>';308 309 ob_end_flush();310 $output = ob_get_clean();311 312 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );313 }314 315 /**316 * Elements that already have a crossorigin attribute should not be modified.317 *318 * @ticket 64766319 *320 * @runInSeparateProcess321 * @preserveGlobalState disabled322 */323 public function test_output_buffer_does_not_override_existing_crossorigin() {324 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';325 326 ob_start();327 328 wp_start_cross_origin_isolation_output_buffer();329 echo '<script src="https://external.example.com/script.js" crossorigin="use-credentials"></script>';330 331 ob_end_flush();332 $output = ob_get_clean();333 334 $this->assertStringContainsString( 'crossorigin="use-credentials"', $output, 'Existing crossorigin attribute should not be overridden.' );335 $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );336 }337 338 /**339 * Multiple tags in the same output should each be handled correctly.340 *341 * @ticket 64766342 *343 * @runInSeparateProcess344 * @preserveGlobalState disabled345 */346 public function test_output_buffer_handles_mixed_tags() {347 $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';348 349 ob_start();350 351 wp_start_cross_origin_isolation_output_buffer();352 echo '<img src="https://external.example.com/image.jpg" />';353 echo '<script src="https://external.example.com/script.js"></script>';354 echo '<audio src="https://external.example.com/audio.mp3"></audio>';355 356 ob_end_flush();357 $output = ob_get_clean();358 359 // IMG should NOT have crossorigin.360 $this->assertStringContainsString( '<img src="https://external.example.com/image.jpg" />', $output, 'IMG should not be modified.' );361 362 // Script and audio should have crossorigin.363 $this->assertSame( 2, substr_count( $output, 'crossorigin="anonymous"' ), 'Script and audio should both get crossorigin, but not img.' );364 }365 } -
trunk/tests/phpunit/tests/media/wpGetChromiumMajorVersion.php
r61844 r62081 1 <?php2 3 /**4 * Tests for the `wp_get_chromium_major_version()` function.5 *6 * @group media7 * @covers ::wp_get_chromium_major_version8 */9 class Tests_Media_wpGetChromiumMajorVersion extends WP_UnitTestCase {10 11 /**12 * Original HTTP_USER_AGENT value.13 *14 * @var string|null15 */16 private $original_user_agent;17 18 public function set_up() {19 parent::set_up();20 $this->original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null;21 }22 23 public function tear_down() {24 if ( null === $this->original_user_agent ) {25 unset( $_SERVER['HTTP_USER_AGENT'] );26 } else {27 $_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent;28 }29 parent::tear_down();30 }31 32 /**33 * @ticket 6476634 */35 public function test_returns_null_when_no_user_agent() {36 unset( $_SERVER['HTTP_USER_AGENT'] );37 $this->assertNull( wp_get_chromium_major_version() );38 }39 40 /**41 * @ticket 6476642 *43 * @dataProvider data_user_agents44 *45 * @param string $user_agent The user agent string.46 * @param int|null $expected The expected Chromium major version, or null.47 */48 public function test_returns_expected_version( $user_agent, $expected ) {49 $_SERVER['HTTP_USER_AGENT'] = $user_agent;50 $this->assertSame( $expected, wp_get_chromium_major_version() );51 }52 53 /**54 * Data provider for test_returns_expected_version.55 *56 * @return array[]57 */58 public function data_user_agents() {59 return array(60 'empty user agent' => array( '', null ),61 'Firefox' => array( 'Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0', null ),62 'Safari' => array( 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15', null ),63 'Chrome 137' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', 137 ),64 'Edge 137' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0', 137 ),65 'Opera (Chrome 136)' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 OPR/122.0.0.0', 136 ),66 'Chrome 100' => array( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36', 100 ),67 );68 }69 } -
trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php
r61984 r62081 193 193 194 194 parent::tear_down(); 195 }196 197 /**198 * Enables client-side media processing and reinitializes the REST server199 * so that the sideload and finalize routes are registered.200 */201 private function enable_client_side_media_processing(): void {202 add_filter( 'wp_client_side_media_processing_enabled', '__return_true' );203 204 global $wp_rest_server;205 $wp_rest_server = new Spy_REST_Server();206 do_action( 'rest_api_init', $wp_rest_server );207 195 } 208 196 … … 2943 2931 2944 2932 /** 2945 * Test that unsupported image type check is skipped when not generating sub-sizes.2946 *2947 * When the client handles image processing (generate_sub_sizes is false),2948 * the server should not check image editor support.2949 *2950 * Tests the permissions check directly with file params set, since the core2951 * check uses get_file_params() which is only populated for multipart uploads.2952 *2953 * @ticket 648362954 */2955 public function test_upload_unsupported_image_type_skipped_when_not_generating_sub_sizes() {2956 wp_set_current_user( self::$author_id );2957 2958 add_filter( 'wp_image_editors', '__return_empty_array' );2959 2960 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );2961 $request->set_file_params(2962 array(2963 'file' => array(2964 'name' => 'avif-lossy.avif',2965 'type' => 'image/avif',2966 'tmp_name' => self::$test_avif_file,2967 'error' => 0,2968 'size' => filesize( self::$test_avif_file ),2969 ),2970 )2971 );2972 $request->set_param( 'generate_sub_sizes', false );2973 2974 $controller = new WP_REST_Attachments_Controller( 'attachment' );2975 $result = $controller->create_item_permissions_check( $request );2976 2977 // Should pass because generate_sub_sizes is false (client handles processing).2978 $this->assertTrue( $result );2979 }2980 2981 /**2982 2933 * Test that unsupported image type check is enforced when generating sub-sizes. 2983 2934 * … … 3241 3192 $this->assertIsArray( $captured_data, 'Data passed to wp_insert_attachment should be an array' ); 3242 3193 } 3243 3244 /**3245 * Tests sideloading a scaled image for an existing attachment.3246 *3247 * @ticket 647373248 * @requires function imagejpeg3249 */3250 public function test_sideload_scaled_image() {3251 $this->enable_client_side_media_processing();3252 3253 wp_set_current_user( self::$author_id );3254 3255 // First, create an attachment.3256 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3257 $request->set_header( 'Content-Type', 'image/jpeg' );3258 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3259 $request->set_body( file_get_contents( self::$test_file ) );3260 $response = rest_get_server()->dispatch( $request );3261 $data = $response->get_data();3262 $attachment_id = $data['id'];3263 3264 $this->assertSame( 201, $response->get_status() );3265 3266 $original_file = get_attached_file( $attachment_id, true );3267 3268 // Sideload a "scaled" version of the image.3269 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );3270 $request->set_header( 'Content-Type', 'image/jpeg' );3271 $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );3272 $request->set_param( 'image_size', 'scaled' );3273 $request->set_body( file_get_contents( self::$test_file ) );3274 $response = rest_get_server()->dispatch( $request );3275 3276 $this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' );3277 3278 $metadata = wp_get_attachment_metadata( $attachment_id );3279 3280 // The original file should now be recorded as original_image.3281 $this->assertArrayHasKey( 'original_image', $metadata, 'Metadata should contain original_image.' );3282 $this->assertSame( wp_basename( $original_file ), $metadata['original_image'], 'original_image should be the basename of the original attached file.' );3283 3284 // The attached file should now point to the scaled version.3285 $new_file = get_attached_file( $attachment_id, true );3286 $this->assertStringContainsString( 'scaled', wp_basename( $new_file ), 'Attached file should now be the scaled version.' );3287 3288 // Metadata should have width, height, filesize, and file updated.3289 $this->assertArrayHasKey( 'width', $metadata, 'Metadata should contain width.' );3290 $this->assertArrayHasKey( 'height', $metadata, 'Metadata should contain height.' );3291 $this->assertArrayHasKey( 'filesize', $metadata, 'Metadata should contain filesize.' );3292 $this->assertArrayHasKey( 'file', $metadata, 'Metadata should contain file.' );3293 $this->assertStringContainsString( 'scaled', $metadata['file'], 'Metadata file should reference the scaled version.' );3294 $this->assertGreaterThan( 0, $metadata['width'], 'Width should be positive.' );3295 $this->assertGreaterThan( 0, $metadata['height'], 'Height should be positive.' );3296 $this->assertGreaterThan( 0, $metadata['filesize'], 'Filesize should be positive.' );3297 }3298 3299 /**3300 * Tests that sideloading scaled image requires authentication.3301 *3302 * @ticket 647373303 * @requires function imagejpeg3304 */3305 public function test_sideload_scaled_image_requires_auth() {3306 $this->enable_client_side_media_processing();3307 3308 wp_set_current_user( self::$author_id );3309 3310 // Create an attachment.3311 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3312 $request->set_header( 'Content-Type', 'image/jpeg' );3313 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3314 $request->set_body( file_get_contents( self::$test_file ) );3315 $response = rest_get_server()->dispatch( $request );3316 $attachment_id = $response->get_data()['id'];3317 3318 // Try sideloading without authentication.3319 wp_set_current_user( 0 );3320 3321 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );3322 $request->set_header( 'Content-Type', 'image/jpeg' );3323 $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );3324 $request->set_param( 'image_size', 'scaled' );3325 $request->set_body( file_get_contents( self::$test_file ) );3326 $response = rest_get_server()->dispatch( $request );3327 3328 $this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 );3329 }3330 3331 /**3332 * Tests that the sideload endpoint includes 'scaled' in the image_size enum.3333 *3334 * @ticket 647373335 */3336 public function test_sideload_route_includes_scaled_enum() {3337 $this->enable_client_side_media_processing();3338 3339 $server = rest_get_server();3340 $routes = $server->get_routes();3341 3342 $endpoint = '/wp/v2/media/(?P<id>[\d]+)/sideload';3343 $this->assertArrayHasKey( $endpoint, $routes, 'Sideload route should exist.' );3344 3345 $route = $routes[ $endpoint ];3346 $endpoint = $route[0];3347 $args = $endpoint['args'];3348 3349 $param_name = 'image_size';3350 $this->assertArrayHasKey( $param_name, $args, 'Route should have image_size arg.' );3351 $this->assertContains( 'scaled', $args[ $param_name ]['enum'], 'image_size enum should include scaled.' );3352 }3353 3354 /**3355 * Tests the filter_wp_unique_filename method handles the -scaled suffix.3356 *3357 * @ticket 647373358 * @requires function imagejpeg3359 */3360 public function test_sideload_scaled_unique_filename() {3361 $this->enable_client_side_media_processing();3362 3363 wp_set_current_user( self::$author_id );3364 3365 // Create an attachment.3366 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3367 $request->set_header( 'Content-Type', 'image/jpeg' );3368 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3369 $request->set_body( file_get_contents( self::$test_file ) );3370 $response = rest_get_server()->dispatch( $request );3371 $attachment_id = $response->get_data()['id'];3372 3373 // Sideload with the -scaled suffix.3374 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" );3375 $request->set_header( 'Content-Type', 'image/jpeg' );3376 $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );3377 $request->set_param( 'image_size', 'scaled' );3378 $request->set_body( file_get_contents( self::$test_file ) );3379 $response = rest_get_server()->dispatch( $request );3380 3381 $this->assertSame( 200, $response->get_status(), 'Sideloading scaled image should succeed.' );3382 3383 // The filename should retain the -scaled suffix without numeric disambiguation.3384 $new_file = get_attached_file( $attachment_id, true );3385 $basename = wp_basename( $new_file );3386 $this->assertMatchesRegularExpression( '/canola-scaled\.jpg$/', $basename, 'Scaled filename should not have numeric suffix appended.' );3387 }3388 3389 /**3390 * Tests that sideloading a scaled image for a different attachment retains the numeric suffix3391 * when a file with the same name already exists on disk.3392 *3393 * @ticket 647373394 * @requires function imagejpeg3395 */3396 public function test_sideload_scaled_unique_filename_conflict() {3397 $this->enable_client_side_media_processing();3398 3399 wp_set_current_user( self::$author_id );3400 3401 // Create the first attachment.3402 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3403 $request->set_header( 'Content-Type', 'image/jpeg' );3404 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3405 $request->set_body( file_get_contents( self::$test_file ) );3406 $response = rest_get_server()->dispatch( $request );3407 $attachment_id_a = $response->get_data()['id'];3408 3409 // Sideload a scaled image for attachment A, creating canola-scaled.jpg on disk.3410 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_a}/sideload" );3411 $request->set_header( 'Content-Type', 'image/jpeg' );3412 $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );3413 $request->set_param( 'image_size', 'scaled' );3414 $request->set_body( file_get_contents( self::$test_file ) );3415 $response = rest_get_server()->dispatch( $request );3416 3417 $this->assertSame( 200, $response->get_status(), 'First sideload should succeed.' );3418 3419 // Create a second, different attachment.3420 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3421 $request->set_header( 'Content-Type', 'image/jpeg' );3422 $request->set_header( 'Content-Disposition', 'attachment; filename=other.jpg' );3423 $request->set_body( file_get_contents( self::$test_file ) );3424 $response = rest_get_server()->dispatch( $request );3425 $attachment_id_b = $response->get_data()['id'];3426 3427 // Sideload scaled for attachment B using the same filename that already exists on disk.3428 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id_b}/sideload" );3429 $request->set_header( 'Content-Type', 'image/jpeg' );3430 $request->set_header( 'Content-Disposition', 'attachment; filename=canola-scaled.jpg' );3431 $request->set_param( 'image_size', 'scaled' );3432 $request->set_body( file_get_contents( self::$test_file ) );3433 $response = rest_get_server()->dispatch( $request );3434 3435 $this->assertSame( 200, $response->get_status(), 'Second sideload should succeed.' );3436 3437 // The filename should have a numeric suffix since the base name does not match this attachment.3438 $new_file = get_attached_file( $attachment_id_b, true );3439 $basename = wp_basename( $new_file );3440 $this->assertMatchesRegularExpression( '/canola-scaled-\d+\.jpg$/', $basename, 'Scaled filename should have numeric suffix when file conflicts with a different attachment.' );3441 }3442 3443 /**3444 * Tests that the finalize endpoint triggers wp_generate_attachment_metadata.3445 *3446 * @ticket 622433447 * @covers WP_REST_Attachments_Controller::finalize_item3448 * @requires function imagejpeg3449 */3450 public function test_finalize_item(): void {3451 $this->enable_client_side_media_processing();3452 3453 wp_set_current_user( self::$author_id );3454 3455 // Create an attachment.3456 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3457 $request->set_header( 'Content-Type', 'image/jpeg' );3458 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3459 $request->set_body( (string) file_get_contents( self::$test_file ) );3460 $response = rest_get_server()->dispatch( $request );3461 $attachment_id = $response->get_data()['id'];3462 3463 $this->assertSame( 201, $response->get_status() );3464 3465 // Track whether wp_generate_attachment_metadata filter fires.3466 $filter_metadata = null;3467 $filter_id = null;3468 $filter_context = null;3469 add_filter(3470 'wp_generate_attachment_metadata',3471 function ( array $metadata, int $id, string $context ) use ( &$filter_metadata, &$filter_id, &$filter_context ) {3472 $filter_metadata = $metadata;3473 $filter_id = $id;3474 $filter_context = $context;3475 $metadata['foo'] = 'bar';3476 return $metadata;3477 },3478 10,3479 33480 );3481 3482 // Call the finalize endpoint.3483 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );3484 $response = rest_get_server()->dispatch( $request );3485 3486 $this->assertSame( 200, $response->get_status(), 'Finalize endpoint should return 200.' );3487 $this->assertIsArray( $filter_metadata );3488 $this->assertStringContainsString( 'canola', $filter_metadata['file'], 'Expected the canola image to have been had its metadata updated.' );3489 $this->assertSame( $attachment_id, $filter_id, 'Expected the post ID to be passed to the filter.' );3490 $this->assertSame( 'update', $filter_context, 'Filter context should be "update".' );3491 $resulting_metadata = wp_get_attachment_metadata( $attachment_id );3492 $this->assertIsArray( $resulting_metadata );3493 $this->assertArrayHasKey( 'foo', $resulting_metadata, 'Expected new metadata key to have been added.' );3494 $this->assertSame( 'bar', $resulting_metadata['foo'], 'Expected filtered metadata to be updated.' );3495 }3496 3497 /**3498 * Tests that the finalize endpoint requires authentication.3499 *3500 * @ticket 622433501 * @covers WP_REST_Attachments_Controller::finalize_item3502 * @requires function imagejpeg3503 */3504 public function test_finalize_item_requires_auth(): void {3505 $this->enable_client_side_media_processing();3506 3507 wp_set_current_user( self::$author_id );3508 3509 // Create an attachment.3510 $request = new WP_REST_Request( 'POST', '/wp/v2/media' );3511 $request->set_header( 'Content-Type', 'image/jpeg' );3512 $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' );3513 $request->set_body( (string) file_get_contents( self::$test_file ) );3514 $response = rest_get_server()->dispatch( $request );3515 $attachment_id = $response->get_data()['id'];3516 3517 // Try finalizing without authentication.3518 wp_set_current_user( 0 );3519 3520 $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/finalize" );3521 $response = rest_get_server()->dispatch( $request );3522 3523 $this->assertErrorResponse( 'rest_cannot_edit_image', $response, 401 );3524 }3525 3526 /**3527 * Tests that the finalize endpoint returns error for invalid attachment ID.3528 *3529 * @ticket 622433530 * @covers WP_REST_Attachments_Controller::finalize_item3531 */3532 public function test_finalize_item_invalid_id(): void {3533 $this->enable_client_side_media_processing();3534 3535 wp_set_current_user( self::$author_id );3536 3537 $invalid_id = PHP_INT_MAX;3538 $this->assertNull( get_post( $invalid_id ), 'Expected invalid ID to not exist for an existing post.' );3539 $request = new WP_REST_Request( 'POST', "/wp/v2/media/$invalid_id/finalize" );3540 $response = rest_get_server()->dispatch( $request );3541 3542 $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 );3543 }3544 3194 } -
trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php
r61982 r62081 16 16 public function set_up() { 17 17 parent::set_up(); 18 19 // Ensure client-side media processing is enabled so the sideload route is registered.20 add_filter( 'wp_client_side_media_processing_enabled', '__return_true' );21 18 22 19 /** @var WP_REST_Server $wp_rest_server */ … … 113 110 '/wp/v2/media/(?P<id>[\\d]+)/post-process', 114 111 '/wp/v2/media/(?P<id>[\\d]+)/edit', 115 '/wp/v2/media/(?P<id>[\\d]+)/sideload',116 '/wp/v2/media/(?P<id>[\\d]+)/finalize',117 112 '/wp/v2/blocks', 118 113 '/wp/v2/blocks/(?P<id>[\d]+)', -
trunk/tests/phpunit/tests/script-modules/wpScriptModules.php
r61794 r62081 1905 1905 1906 1906 /** 1907 * Tests that VIPS script modules always use minified file paths. 1908 * 1909 * Non-minified VIPS files are not shipped because they are ~10MB of 1910 * inlined WASM with no debugging value, so the registration should 1911 * always point to the .min.js variants. 1912 * 1913 * @ticket 64734 1907 * Tests that VIPS script modules are not registered in Core. 1908 * 1909 * The wasm-vips library is plugin-only and should not be included 1910 * in WordPress Core builds due to its large size (~16MB per file). 1911 * 1912 * @ticket 64906 1914 1913 * 1915 1914 * @covers ::wp_default_script_modules 1916 1915 */ 1917 public function test_vips_script_modules_ always_use_minified_paths() {1916 public function test_vips_script_modules_not_registered_in_core() { 1918 1917 wp_default_script_modules(); 1919 1918 wp_enqueue_script_module( '@wordpress/vips/loader' ); … … 1921 1920 $actual = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); 1922 1921 1923 $this->assertStringContainsString( 'vips/loader.min.js', $actual ); 1924 $this->assertStringNotContainsString( 'vips/loader.js"', $actual ); 1922 $this->assertStringNotContainsString( 'vips', $actual ); 1925 1923 } 1926 1924 -
trunk/tests/qunit/fixtures/wp-api-generated.js
r62058 r62081 3148 3148 "description": "The ID for the associated post of the attachment.", 3149 3149 "type": "integer", 3150 "required": false3151 },3152 "generate_sub_sizes": {3153 "type": "boolean",3154 "default": true,3155 "description": "Whether to generate image sub sizes.",3156 "required": false3157 },3158 "convert_format": {3159 "type": "boolean",3160 "default": true,3161 "description": "Whether to convert image formats.",3162 3150 "required": false 3163 3151 } … … 3677 3665 ] 3678 3666 }, 3679 "/wp/v2/media/(?P<id>[\\d]+)/sideload": {3680 "namespace": "wp/v2",3681 "methods": [3682 "POST"3683 ],3684 "endpoints": [3685 {3686 "methods": [3687 "POST"3688 ],3689 "args": {3690 "id": {3691 "description": "Unique identifier for the attachment.",3692 "type": "integer",3693 "required": false3694 },3695 "image_size": {3696 "description": "Image size.",3697 "type": "string",3698 "enum": [3699 "thumbnail",3700 "medium",3701 "medium_large",3702 "large",3703 "1536x1536",3704 "2048x2048",3705 "original",3706 "full",3707 "scaled"3708 ],3709 "required": true3710 },3711 "convert_format": {3712 "type": "boolean",3713 "default": true,3714 "description": "Whether to convert image formats.",3715 "required": false3716 }3717 }3718 }3719 ]3720 },3721 "/wp/v2/media/(?P<id>[\\d]+)/finalize": {3722 "namespace": "wp/v2",3723 "methods": [3724 "POST"3725 ],3726 "endpoints": [3727 {3728 "methods": [3729 "POST"3730 ],3731 "args": {3732 "id": {3733 "description": "Unique identifier for the attachment.",3734 "type": "integer",3735 "required": false3736 }3737 }3738 }3739 ]3740 },3741 3667 "/wp/v2/menu-items": { 3742 3668 "namespace": "wp/v2", … … 12775 12701 } 12776 12702 }, 12777 "image_sizes": {12778 "thumbnail": {12779 "width": 150,12780 "height": 150,12781 "crop": true12782 },12783 "medium": {12784 "width": 300,12785 "height": 300,12786 "crop": false12787 },12788 "medium_large": {12789 "width": 768,12790 "height": 0,12791 "crop": false12792 },12793 "large": {12794 "width": 1024,12795 "height": 1024,12796 "crop": false12797 },12798 "1536x1536": {12799 "width": 1536,12800 "height": 1536,12801 "crop": false12802 },12803 "2048x2048": {12804 "width": 2048,12805 "height": 2048,12806 "crop": false12807 }12808 },12809 "image_size_threshold": 2560,12810 "image_output_formats": {},12811 "jpeg_interlaced": false,12812 "png_interlaced": false,12813 "gif_interlaced": false,12814 12703 "site_logo": 0, 12815 12704 "site_icon": 0, -
trunk/tools/gutenberg/copy.js
r62073 r62081 260 260 261 261 if ( entry.isDirectory() ) { 262 // Skip plugin-only packages (e.g., vips/wasm) that should not be in Core. 263 if ( entry.name === 'vips' ) { 264 continue; 265 } 262 266 processDirectory( fullPath, baseDir ); 263 267 } else if ( entry.name.endsWith( '.min.asset.php' ) ) { … … 343 347 if ( ! assetData.dependencies ) { 344 348 assetData.dependencies = []; 349 } 350 351 // Strip plugin-only module dependencies (e.g., vips) that are not in Core. 352 if ( Array.isArray( assetData.module_dependencies ) ) { 353 assetData.module_dependencies = 354 assetData.module_dependencies.filter( 355 ( dep ) => 356 ! ( dep.id || dep ).startsWith( 357 '@wordpress/vips' 358 ) 359 ); 345 360 } 346 361
Note: See TracChangeset
for help on using the changeset viewer.