From 7cab03d82fd0859d92fd6191359800574e3c7a42 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 14:34:02 -0800 Subject: [PATCH 01/48] add preload patchs for client side media --- src/wp-admin/edit-form-blocks.php | 6 ++++++ src/wp-admin/site-editor.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index d0f2000fdce17..2237fc69ce293 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -92,6 +92,12 @@ static function ( $classes ) { 'description', 'gmt_offset', 'home', + 'image_sizes', + 'image_size_threshold', + 'image_output_formats', + 'jpeg_interlaced', + 'png_interlaced', + 'gif_interlaced', 'name', 'site_icon', 'site_icon_url', diff --git a/src/wp-admin/site-editor.php b/src/wp-admin/site-editor.php index 1c8e8b525459b..9a8268c3392d7 100644 --- a/src/wp-admin/site-editor.php +++ b/src/wp-admin/site-editor.php @@ -218,6 +218,12 @@ static function ( $classes ) { 'description', 'gmt_offset', 'home', + 'image_sizes', + 'image_size_threshold', + 'image_output_formats', + 'jpeg_interlaced', + 'png_interlaced', + 'gif_interlaced', 'name', 'site_icon', 'site_icon_url', From bf24f67303e7e8beec1cfe246c5713ebecc5a81d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 14:56:24 -0800 Subject: [PATCH 02/48] REST API: Add media processing settings to index endpoint --- .../rest-api/class-wp-rest-server.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index dbf605523d2dc..b7418c41ea811 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1368,6 +1368,38 @@ public function get_index( $request ) { 'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ), ); + // Add media processing settings for users who can upload files. + if ( current_user_can( 'upload_files' ) ) { + // Image sizes with normalized data. + $sizes = wp_get_registered_image_subsizes(); + foreach ( $sizes as $name => &$size ) { + $size['height'] = (int) $size['height']; + $size['width'] = (int) $size['width']; + $size['name'] = $name; + } + unset( $size ); + $available['image_sizes'] = $sizes; + + /** This filter is documented in wp-admin/includes/image.php */ + $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); + + // Image output formats. + $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' ); + $output_formats = array(); + foreach ( $input_formats as $mime_type ) { + /** This filter is documented in wp-includes/class-wp-image-editor.php */ + $output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type ); + } + $available['image_output_formats'] = (object) $output_formats; + + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $available['gif_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); + } + $response = new WP_REST_Response( $available ); $fields = $request['_fields'] ?? ''; From f20f2f96c58e729006c6a647514175a6449ada33 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:28:07 -0800 Subject: [PATCH 03/48] REST API: Add filename and filesize fields to attachments Add filename and filesize REST fields to the attachments endpoint for client-side media processing. The filename returns the original attachment file name, and filesize returns the file size in bytes. --- .../class-wp-rest-attachments-controller.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index f824b0c9e2cab..491c77cececd1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -990,6 +990,14 @@ public function prepare_item_for_response( $item, $request ) { $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) ); } + if ( in_array( 'filename', $fields, true ) ) { + $data['filename'] = $this->get_attachment_filename( $post->ID ); + } + + if ( in_array( 'filesize', $fields, true ) ) { + $data['filesize'] = $this->get_attachment_filesize( $post->ID ); + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->filter_response_by_context( $data, $context ); @@ -1159,6 +1167,20 @@ public function get_item_schema() { 'readonly' => true, ); + $schema['properties']['filename'] = array( + 'description' => __( 'Original attachment file name.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['filesize'] = array( + 'description' => __( 'Attachment file size in bytes.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ); + unset( $schema['properties']['password'] ); $this->schema = $schema; @@ -1724,4 +1746,53 @@ protected function get_edit_media_item_args() { return $args; } + + /** + * Gets the attachment's original file name. + * + * @since 6.9.0 + * + * @param int $attachment_id Attachment ID. + * @return string|null Attachment file name, or null if not found. + */ + protected function get_attachment_filename( $attachment_id ) { + $path = wp_get_original_image_path( $attachment_id ); + + if ( $path ) { + return wp_basename( $path ); + } + + $path = get_attached_file( $attachment_id ); + + if ( $path ) { + return wp_basename( $path ); + } + + return null; + } + + /** + * Gets the attachment's file size in bytes. + * + * @since 6.9.0 + * + * @param int $attachment_id Attachment ID. + * @return int|null Attachment file size in bytes, or null if not available. + */ + protected function get_attachment_filesize( $attachment_id ) { + $meta = wp_get_attachment_metadata( $attachment_id ); + + if ( isset( $meta['filesize'] ) ) { + return $meta['filesize']; + } + + $original_path = wp_get_original_image_path( $attachment_id ); + $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id ); + + if ( is_string( $attached_file ) && file_exists( $attached_file ) ) { + return wp_filesize( $attached_file ); + } + + return null; + } } From a9790378884ce3cf761ba2ef18050e67330acf61 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:28:33 -0800 Subject: [PATCH 04/48] REST API: Add exif_orientation field to attachments Add exif_orientation field to the attachments REST endpoint for client-side EXIF rotation handling. Values 1-8 follow the EXIF specification, where 1 means no rotation is needed. --- .../class-wp-rest-attachments-controller.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 491c77cececd1..376ca901b12e7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -998,6 +998,22 @@ public function prepare_item_for_response( $item, $request ) { $data['filesize'] = $this->get_attachment_filesize( $post->ID ); } + if ( in_array( 'exif_orientation', $fields, true ) && wp_attachment_is_image( $post ) ) { + $metadata = wp_get_attachment_metadata( $post->ID, true ); + + // Default to 1 (no rotation needed) if orientation not set. + $orientation = 1; + + if ( + is_array( $metadata ) && + isset( $metadata['image_meta']['orientation'] ) + ) { + $orientation = (int) $metadata['image_meta']['orientation']; + } + + $data['exif_orientation'] = $orientation; + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->filter_response_by_context( $data, $context ); @@ -1181,6 +1197,13 @@ public function get_item_schema() { 'readonly' => true, ); + $schema['properties']['exif_orientation'] = array( + 'description' => __( 'EXIF orientation value. Values 1-8 follow the EXIF specification, where 1 means no rotation needed.' ), + 'type' => 'integer', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + unset( $schema['properties']['password'] ); $this->schema = $schema; From d7d04fc75e687ca0a1b9f9fd3b03024d9d8d75fc Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:29:26 -0800 Subject: [PATCH 05/48] REST API: Add generate_sub_sizes and convert_format params Add generate_sub_sizes and convert_format parameters to the attachments POST endpoint for client-side media processing control. When generate_sub_sizes is false, server-side sub-size generation and EXIF rotation are disabled. When convert_format is false, automatic image format conversion is disabled. --- .../class-wp-rest-attachments-controller.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 376ca901b12e7..f65132e6756c1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -65,6 +65,36 @@ public function register_routes() { ); } + /** + * Retrieves the query params for the attachments collection. + * + * @since 6.9.0 + * + * @param string $method Optional. HTTP method of the request. + * The arguments for `CREATABLE` requests are + * checked for required values and may fall-back to a given default. + * Default WP_REST_Server::CREATABLE. + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + $args = parent::get_endpoint_args_for_item_schema( $method ); + + if ( WP_REST_Server::CREATABLE === $method ) { + $args['generate_sub_sizes'] = array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to generate image sub sizes.' ), + ); + $args['convert_format'] = array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to convert image formats.' ), + ); + } + + return $args; + } + /** * Determines the allowed query_vars for a get_items() response and * prepares for WP_Query. @@ -192,6 +222,7 @@ public function create_item_permissions_check( $request ) { * Creates a single attachment. * * @since 4.7.0 + * @since 6.9.0 Added `generate_sub_sizes` and `convert_format` parameters. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. @@ -205,9 +236,28 @@ public function create_item( $request ) { ); } + // Handle generate_sub_sizes parameter. + if ( isset( $request['generate_sub_sizes'] ) && ! $request['generate_sub_sizes'] ) { + add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + // Disable server-side EXIF rotation so the client can handle it. + // This preserves the original orientation value in the metadata. + add_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); + } + + // Handle convert_format parameter. + if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) { + add_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + } + $insert = $this->insert_attachment( $request ); if ( is_wp_error( $insert ) ) { + // Clean up filters on error. + remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); return $insert; } @@ -283,6 +333,12 @@ public function create_item( $request ) { */ wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); + // Clean up filters. + remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + $response = $this->prepare_item_for_response( $attachment, $request ); $response = rest_ensure_response( $response ); $response->set_status( 201 ); From 2fc26fcc911ccc68dbdd2514aeabdd3f82ae87d9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:30:29 -0800 Subject: [PATCH 06/48] REST API: Add sideload endpoint for attachments Add sideload endpoint at /wp/v2/media/{id}/sideload for uploading sub-sized images to an existing attachment. Used by client-side media processing to upload generated image sizes without creating new attachments. Supports both images and PDFs. --- .../class-wp-rest-attachments-controller.php | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index f65132e6756c1..931569bdaa499 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -63,6 +63,43 @@ public function register_routes() { 'args' => $this->get_edit_media_item_args(), ) ); + + $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() ); + // Special case to set 'original_image' in attachment metadata. + $valid_image_sizes[] = 'original'; + // Used for PDF thumbnails. + $valid_image_sizes[] = 'full'; + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)/sideload', + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'sideload_item' ), + 'permission_callback' => array( $this, 'sideload_item_permissions_check' ), + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the attachment.' ), + 'type' => 'integer', + ), + 'image_size' => array( + 'description' => __( 'Image size.' ), + 'type' => 'string', + 'enum' => $valid_image_sizes, + 'required' => true, + ), + 'convert_format' => array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to convert image formats.' ), + ), + ), + ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); } /** @@ -1874,4 +1911,189 @@ protected function get_attachment_filesize( $attachment_id ) { return null; } + + /** + * Checks if a given request has access to sideload a file. + * + * Sideloading a file for an existing attachment + * requires both update and create permissions. + * + * @since 6.9.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. + */ + public function sideload_item_permissions_check( $request ) { + return $this->edit_media_item_permissions_check( $request ); + } + + /** + * Side-loads a media file without creating a new attachment. + * + * @since 6.9.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. + */ + public function sideload_item( WP_REST_Request $request ) { + $attachment_id = $request['id']; + + $post = $this->get_post( $attachment_id ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( + ! wp_attachment_is_image( $post ) && + ! wp_attachment_is( 'pdf', $post ) + ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID. Only images and PDFs can be sideloaded.' ), + array( 'status' => 400 ) + ); + } + + if ( isset( $request['convert_format'] ) && ! $request['convert_format'] ) { + // Prevent image conversion as that is done client-side. + add_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + } + + // Get the file via $_FILES or raw data. + $files = $request->get_file_params(); + $headers = $request->get_headers(); + + /* + * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. + * See https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 + * With the following filter we can work around this safeguard. + */ + $attachment_filename = get_attached_file( $attachment_id, true ); + $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null; + + $filter_filename = function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { + return $this->filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ); + }; + + add_filter( 'wp_unique_filename', $filter_filename, 10, 6 ); + + $parent_post = get_post_parent( $attachment_id ); + + $time = null; + + // Matches logic in media_handle_upload(). + // The post date doesn't usually matter for pages, so don't backdate this upload. + if ( $parent_post && 'page' !== $parent_post->post_type && substr( $parent_post->post_date, 0, 4 ) > 0 ) { + $time = $parent_post->post_date; + } + + if ( ! empty( $files ) ) { + $file = $this->upload_from_file( $files, $headers, $time ); + } else { + $file = $this->upload_from_data( $request->get_body(), $headers, $time ); + } + + remove_filter( 'wp_unique_filename', $filter_filename ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + + if ( is_wp_error( $file ) ) { + return $file; + } + + $type = $file['type']; + $path = $file['file']; + + $image_size = $request['image_size']; + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + + if ( ! $metadata ) { + $metadata = array(); + } + + if ( 'original' === $image_size ) { + $metadata['original_image'] = wp_basename( $path ); + } else { + $metadata['sizes'] = $metadata['sizes'] ?? array(); + + $size = wp_getimagesize( $path ); + + $metadata['sizes'][ $image_size ] = array( + 'width' => $size ? $size[0] : 0, + 'height' => $size ? $size[1] : 0, + 'file' => wp_basename( $path ), + 'mime-type' => $type, + 'filesize' => wp_filesize( $path ), + ); + } + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + $response_request = new WP_REST_Request( + WP_REST_Server::READABLE, + rest_get_route_for_post( $attachment_id ) + ); + + $response_request['context'] = 'edit'; + + if ( isset( $request['_fields'] ) ) { + $response_request['_fields'] = $request['_fields']; + } + + $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request ); + + $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) ); + + return $response; + } + + /** + * Filters wp_unique_filename during sideloads. + * + * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. + * Adding this closure to the filter helps work around this safeguard. + * + * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg, + * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg + * However, here it is desired not to add the suffix in order to maintain the same + * naming convention as if the file was uploaded regularly. + * + * @since 6.9.0 + * + * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 + * + * @param string $filename Unique file name. + * @param string $ext File extension. Example: ".png". + * @param string $dir Directory path. + * @param callable|null $unique_filename_callback Callback function that generates the unique file name. + * @param string[] $alt_filenames Array of alternate file names that were checked for collisions. + * @param int|string $number The highest number that was used to make the file name unique + * or an empty string if unused. + * @param string|null $attachment_filename Original attachment file name. + * @return string Filtered file name. + */ + private function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) { + if ( empty( $number ) || ! $attachment_filename ) { + return $filename; + } + + $ext = pathinfo( $filename, PATHINFO_EXTENSION ); + $name = pathinfo( $filename, PATHINFO_FILENAME ); + $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME ); + + if ( ! $ext || ! $name ) { + return $filename; + } + + $matches = array(); + if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) { + $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; + if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { + return $filename_without_suffix; + } + } + + return $filename; + } } From f39001d9df37ffffd0ee632a3bb7bb89d09ff0b7 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:30:52 -0800 Subject: [PATCH 07/48] REST API: Improve missing_image_sizes for PDFs Add PDF-specific handling for the missing_image_sizes field in the attachments endpoint. PDFs use fallback_intermediate_image_sizes filter to determine which thumbnail sizes should be generated, unlike regular images which use wp_get_missing_image_subsizes(). --- .../class-wp-rest-attachments-controller.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 931569bdaa499..711096e955009 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1081,6 +1081,34 @@ public function prepare_item_for_response( $item, $request ) { if ( in_array( 'missing_image_sizes', $fields, true ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; $data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) ); + + // Handle PDFs which don't use wp_get_missing_image_subsizes(). + if ( empty( $data['missing_image_sizes'] ) && 'application/pdf' === get_post_mime_type( $post ) ) { + $metadata = wp_get_attachment_metadata( $post->ID, true ); + + if ( ! is_array( $metadata ) ) { + $metadata = array(); + } + + $metadata['sizes'] = $metadata['sizes'] ?? array(); + + $fallback_sizes = array( + 'thumbnail', + 'medium', + 'large', + ); + + // The filter might have been added by ::create_item(). + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + + /** This filter is documented in wp-admin/includes/image.php */ + $fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); + + $registered_sizes = wp_get_registered_image_subsizes(); + $merged_sizes = array_keys( array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ) ); + + $data['missing_image_sizes'] = array_values( array_diff( $merged_sizes, array_keys( $metadata['sizes'] ) ) ); + } } if ( in_array( 'filename', $fields, true ) ) { From 2f33085ddfa52d108bfd22c790c2e22c97dd7ed2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:31:31 -0800 Subject: [PATCH 08/48] Media: Add cross-origin isolation support Add COOP and COEP headers in the block editor to enable SharedArrayBuffer for WebAssembly-based client-side media processing. Includes output buffer to automatically add crossorigin="anonymous" attributes to external resources. --- src/wp-includes/default-filters.php | 6 ++ src/wp-includes/media.php | 126 ++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 301b846343ee2..072e934fd68d7 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -675,6 +675,12 @@ add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 ); add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' ); +// Cross-origin isolation for client-side media processing. +add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); + // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); add_filter( 'nav_menu_css_class', 'wp_nav_menu_remove_menu_item_has_children_class', 10, 4 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 10845a592be47..c50a0d9cee816 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6359,3 +6359,129 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { */ return apply_filters( 'image_editor_output_format', $output_format, $filename, $mime_type ); } + +/** + * Enables cross-origin isolation in the block editor. + * + * Required for enabling SharedArrayBuffer for WebAssembly-based + * media processing in the editor. + * + * @since 6.9.0 + * + * @link https://web.dev/coop-coep/ + */ +function wp_set_up_cross_origin_isolation() { + $screen = get_current_screen(); + + if ( ! $screen ) { + return; + } + + if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) { + return; + } + + $user_id = get_current_user_id(); + if ( ! $user_id ) { + return; + } + + // Cross-origin isolation is not needed if users can't upload files anyway. + if ( ! user_can( $user_id, 'upload_files' ) ) { + return; + } + + wp_start_cross_origin_isolation_output_buffer(); +} + +/** + * Starts an output buffer to send cross-origin isolation headers. + * + * Sends headers and uses an output buffer to add crossorigin="anonymous" + * attributes where needed. + * + * @since 6.9.0 + * + * @link https://web.dev/coop-coep/ + * + * @global bool $is_safari + */ +function wp_start_cross_origin_isolation_output_buffer() { + global $is_safari; + + $coep = $is_safari ? 'require-corp' : 'credentialless'; + + ob_start( + static function ( string $output ) use ( $coep ): string { + header( 'Cross-Origin-Opener-Policy: same-origin' ); + header( "Cross-Origin-Embedder-Policy: $coep" ); + + return wp_add_crossorigin_attributes( $output ); + } + ); +} + +/** + * Adds crossorigin="anonymous" to relevant tags in the given HTML string. + * + * @since 6.9.0 + * + * @param string $html HTML input. + * @return string Modified HTML. + */ +function wp_add_crossorigin_attributes( string $html ): string { + $site_url = site_url(); + + $processor = new WP_HTML_Tag_Processor( $html ); + + // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin. + $tags = array( + 'AUDIO' => 'src', + 'IMG' => 'src', + 'LINK' => 'href', + 'SCRIPT' => 'src', + 'VIDEO' => 'src', + 'SOURCE' => 'src', + ); + + $tag_names = array_keys( $tags ); + + while ( $processor->next_tag() ) { + $tag = $processor->get_tag(); + + if ( ! in_array( $tag, $tag_names, true ) ) { + continue; + } + + if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) { + $processor->set_bookmark( 'audio-video-parent' ); + } + + $processor->set_bookmark( 'resume' ); + + $sought = false; + + $crossorigin = $processor->get_attribute( 'crossorigin' ); + + $url = $processor->get_attribute( $tags[ $tag ] ); + + if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) { + if ( 'SOURCE' === $tag ) { + $sought = $processor->seek( 'audio-video-parent' ); + + if ( $sought ) { + $processor->set_attribute( 'crossorigin', 'anonymous' ); + } + } else { + $processor->set_attribute( 'crossorigin', 'anonymous' ); + } + + if ( $sought ) { + $processor->seek( 'resume' ); + $processor->release_bookmark( 'audio-video-parent' ); + } + } + } + + return $processor->get_updated_html(); +} From 2de4be6f33c7a125264f437c125d85eb12f7fdcc Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:31:53 -0800 Subject: [PATCH 09/48] Media: Add WASM MIME type to .htaccess rules Add AddType directive for WebAssembly files to the mod_rewrite_rules filter. This enables serving .wasm files for client-side media processing using libraries like wasm-vips. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 072e934fd68d7..3a72e4edc9cd9 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -680,6 +680,7 @@ add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); +add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index c50a0d9cee816..23b8bc8f962e8 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6485,3 +6485,21 @@ function wp_add_crossorigin_attributes( string $html ): string { return $processor->get_updated_html(); } + +/** + * Filters the list of rewrite rules formatted for output to an .htaccess file. + * + * Adds support for serving WebAssembly files used by client-side media processing. + * + * @since 6.9.0 + * + * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess. + * @return string Filtered rewrite rules. + */ +function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { + $rules .= "\n# BEGIN WordPress client-side media processing\n" . + "AddType application/wasm wasm\n" . + "# END WordPress client-side media processing\n"; + + return $rules; +} From 07ca4ad7f6217893bf4c7e160e170412f23b8d29 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 15:32:15 -0800 Subject: [PATCH 10/48] Media: Add crossorigin attributes to media templates Override wp_print_media_templates to add crossorigin="anonymous" attributes to audio, img, and video tags. Required for cross-origin isolation compliance in the media library modal. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 32 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 3a72e4edc9cd9..6a5e2c6efe258 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -681,6 +681,7 @@ add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); +add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 23b8bc8f962e8..40be4692a1576 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6503,3 +6503,35 @@ function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { return $rules; } + +/** + * Overrides templates from wp_print_media_templates with custom ones. + * + * Adds `crossorigin` attribute to all tags that could have assets + * loaded from a different domain for cross-origin isolation support. + * + * @since 6.9.0 + */ +function wp_override_media_templates() { + remove_action( 'admin_footer', 'wp_print_media_templates' ); + add_action( + 'admin_footer', + static function () { + ob_start(); + wp_print_media_templates(); + $html = (string) ob_get_clean(); + + $tags = array( + 'audio', + 'img', + 'video', + ); + + foreach ( $tags as $tag ) { + $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html ); + } + + echo $html; + } + ); +} From 4aef029563bb477ee5928c21f57daf19bcb6dfd2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 4 Feb 2026 22:10:18 -0800 Subject: [PATCH 11/48] Tests: Update REST API tests for client-side media fields Update test_get_item_schema to expect 32 properties instead of 29, adding assertions for the new filename, filesize, and exif_orientation fields. Add the sideload endpoint to expected routes in schema test. Co-Authored-By: Claude Opus 4.5 --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 5 ++++- tests/phpunit/tests/rest-api/rest-schema-setup.php | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 05029e0845d96..796e58c45d97d 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1939,9 +1939,12 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 29, $properties ); + $this->assertCount( 32, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'alt_text', $properties ); + $this->assertArrayHasKey( 'exif_orientation', $properties ); + $this->assertArrayHasKey( 'filename', $properties ); + $this->assertArrayHasKey( 'filesize', $properties ); $this->assertArrayHasKey( 'caption', $properties ); $this->assertArrayHasKey( 'raw', $properties['caption']['properties'] ); $this->assertArrayHasKey( 'rendered', $properties['caption']['properties'] ); diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 9c6c431e5ef35..0e0e00b934359 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -109,6 +109,7 @@ public function test_expected_routes_in_schema() { '/wp/v2/media/(?P[\\d]+)', '/wp/v2/media/(?P[\\d]+)/post-process', '/wp/v2/media/(?P[\\d]+)/edit', + '/wp/v2/media/(?P[\\d]+)/sideload', '/wp/v2/blocks', '/wp/v2/blocks/(?P[\d]+)', '/wp/v2/blocks/(?P[\d]+)/autosaves', From 1271aa0b761cccf8adc421d4f183d4429210f62f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 5 Feb 2026 08:51:13 -0800 Subject: [PATCH 12/48] Docs: Update @since tags from 6.9.0 to 7.0.0 Co-Authored-By: Claude Opus 4.5 --- src/wp-includes/media.php | 10 +++++----- .../class-wp-rest-attachments-controller.php | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 40be4692a1576..cddb9f6f839ee 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6366,7 +6366,7 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { * Required for enabling SharedArrayBuffer for WebAssembly-based * media processing in the editor. * - * @since 6.9.0 + * @since 7.0.0 * * @link https://web.dev/coop-coep/ */ @@ -6400,7 +6400,7 @@ function wp_set_up_cross_origin_isolation() { * Sends headers and uses an output buffer to add crossorigin="anonymous" * attributes where needed. * - * @since 6.9.0 + * @since 7.0.0 * * @link https://web.dev/coop-coep/ * @@ -6424,7 +6424,7 @@ static function ( string $output ) use ( $coep ): string { /** * Adds crossorigin="anonymous" to relevant tags in the given HTML string. * - * @since 6.9.0 + * @since 7.0.0 * * @param string $html HTML input. * @return string Modified HTML. @@ -6491,7 +6491,7 @@ function wp_add_crossorigin_attributes( string $html ): string { * * Adds support for serving WebAssembly files used by client-side media processing. * - * @since 6.9.0 + * @since 7.0.0 * * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess. * @return string Filtered rewrite rules. @@ -6510,7 +6510,7 @@ function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { * Adds `crossorigin` attribute to all tags that could have assets * loaded from a different domain for cross-origin isolation support. * - * @since 6.9.0 + * @since 7.0.0 */ function wp_override_media_templates() { remove_action( 'admin_footer', 'wp_print_media_templates' ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 711096e955009..69c93d301b357 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -105,7 +105,7 @@ public function register_routes() { /** * Retrieves the query params for the attachments collection. * - * @since 6.9.0 + * @since 7.0.0 * * @param string $method Optional. HTTP method of the request. * The arguments for `CREATABLE` requests are @@ -259,7 +259,7 @@ public function create_item_permissions_check( $request ) { * Creates a single attachment. * * @since 4.7.0 - * @since 6.9.0 Added `generate_sub_sizes` and `convert_format` parameters. + * @since 7.0.0 Added `generate_sub_sizes` and `convert_format` parameters. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. @@ -1894,7 +1894,7 @@ protected function get_edit_media_item_args() { /** * Gets the attachment's original file name. * - * @since 6.9.0 + * @since 7.0.0 * * @param int $attachment_id Attachment ID. * @return string|null Attachment file name, or null if not found. @@ -1918,7 +1918,7 @@ protected function get_attachment_filename( $attachment_id ) { /** * Gets the attachment's file size in bytes. * - * @since 6.9.0 + * @since 7.0.0 * * @param int $attachment_id Attachment ID. * @return int|null Attachment file size in bytes, or null if not available. @@ -1946,7 +1946,7 @@ protected function get_attachment_filesize( $attachment_id ) { * Sideloading a file for an existing attachment * requires both update and create permissions. * - * @since 6.9.0 + * @since 7.0.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. @@ -1958,7 +1958,7 @@ public function sideload_item_permissions_check( $request ) { /** * Side-loads a media file without creating a new attachment. * - * @since 6.9.0 + * @since 7.0.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. @@ -2087,7 +2087,7 @@ public function sideload_item( WP_REST_Request $request ) { * However, here it is desired not to add the suffix in order to maintain the same * naming convention as if the file was uploaded regularly. * - * @since 6.9.0 + * @since 7.0.0 * * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 * From a66bce98af5e15ac821486f4dcfb3dee1876e366 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 13 Feb 2026 22:10:18 +0700 Subject: [PATCH 13/48] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index cddb9f6f839ee..ce162d40789d8 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6444,8 +6444,6 @@ function wp_add_crossorigin_attributes( string $html ): string { 'SOURCE' => 'src', ); - $tag_names = array_keys( $tags ); - while ( $processor->next_tag() ) { $tag = $processor->get_tag(); From 90af12867f0c9f786a3d32a6b8f28f3e95aa2a12 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 13 Feb 2026 22:10:25 +0700 Subject: [PATCH 14/48] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index ce162d40789d8..d77a733e64f92 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6447,7 +6447,7 @@ function wp_add_crossorigin_attributes( string $html ): string { while ( $processor->next_tag() ) { $tag = $processor->get_tag(); - if ( ! in_array( $tag, $tag_names, true ) ) { + if ( ! isset( $tags[ $tag ] ) ) { continue; } From 1fb7a001ee6335ceff809dd8f2227c22bfe3c9b3 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 13 Feb 2026 22:10:44 +0700 Subject: [PATCH 15/48] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index d77a733e64f92..03a15670ef725 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6381,13 +6381,8 @@ function wp_set_up_cross_origin_isolation() { return; } - $user_id = get_current_user_id(); - if ( ! $user_id ) { - return; - } - // Cross-origin isolation is not needed if users can't upload files anyway. - if ( ! user_can( $user_id, 'upload_files' ) ) { + if ( ! current_user_can( 'upload_files' ) ) { return; } From 2f569b5b1ab92d0a7e667c81aad06bd45d89aa35 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 13 Feb 2026 22:15:29 +0700 Subject: [PATCH 16/48] Update src/wp-includes/rest-api/class-wp-rest-server.php Co-authored-by: Weston Ruter --- src/wp-includes/rest-api/class-wp-rest-server.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index b7418c41ea811..77f4e2d0b5810 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1371,14 +1371,14 @@ public function get_index( $request ) { // Add media processing settings for users who can upload files. if ( current_user_can( 'upload_files' ) ) { // Image sizes with normalized data. - $sizes = wp_get_registered_image_subsizes(); - foreach ( $sizes as $name => &$size ) { - $size['height'] = (int) $size['height']; - $size['width'] = (int) $size['width']; - $size['name'] = $name; + $available['image_sizes'] = array(); + foreach ( wp_get_registered_image_subsizes() as $name => $size ) { + $available['image_sizes'][] = array( + 'height' => (int) $size['height'], + 'width' => (int) $size['width'], + 'name' => $name, + ); } - unset( $size ); - $available['image_sizes'] = $sizes; /** This filter is documented in wp-admin/includes/image.php */ $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); From b36bd28ab572e6e03d6b2d610f706f12f22732ff Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 13 Feb 2026 22:22:12 +0700 Subject: [PATCH 17/48] Use WP_HTML_Tag_Processor in wp_override_media_templates Replace naive str_replace with WP_HTML_Tag_Processor for adding crossorigin attributes to media template tags. Since templates live inside )#s', + static function ( $matches ) { + $processor = new WP_HTML_Tag_Processor( $matches[2] ); + + while ( $processor->next_tag() ) { + if ( in_array( $processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true ) ) { + $processor->set_attribute( 'crossorigin', 'anonymous' ); + } + } - foreach ( $tags as $tag ) { - $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html ); - } + return $matches[1] . $processor->get_updated_html() . $matches[3]; + }, + $html + ); echo $html; } From 82832f91235ade70e7287cc349a731c22a516f90 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 13 Feb 2026 22:28:41 +0700 Subject: [PATCH 18/48] Fix EXIF orientation 0 treated as valid value EXIF orientation value 0 (undefined/no EXIF data) was being used as-is instead of falling through to the default of 1. Add a > 0 guard so orientation values of 0 or negative are treated as no rotation needed. --- .../endpoints/class-wp-rest-attachments-controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 69c93d301b357..4d811772c300e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1127,7 +1127,8 @@ public function prepare_item_for_response( $item, $request ) { if ( is_array( $metadata ) && - isset( $metadata['image_meta']['orientation'] ) + isset( $metadata['image_meta']['orientation'] ) && + (int) $metadata['image_meta']['orientation'] > 0 ) { $orientation = (int) $metadata['image_meta']['orientation']; } From 7c995b0f6be28219ef3f125591cdab845930c738 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 14 Feb 2026 07:04:24 +0700 Subject: [PATCH 19/48] Update REST API QUnit fixtures for Client Side Media changes Add new media endpoint args (generate_sub_sizes, convert_format), sideload route, image size settings, and filename/filesize fields. Co-Authored-By: Claude Opus 4.6 --- tests/qunit/fixtures/wp-api-generated.js | 96 +++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 046afe6730090..da31eaa402242 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3149,6 +3149,18 @@ mockedApiResponse.Schema = { "description": "The ID for the associated post of the attachment.", "type": "integer", "required": false + }, + "generate_sub_sizes": { + "type": "boolean", + "default": true, + "description": "Whether to generate image sub sizes.", + "required": false + }, + "convert_format": { + "type": "boolean", + "default": true, + "description": "Whether to convert image formats.", + "required": false } } } @@ -3665,6 +3677,47 @@ mockedApiResponse.Schema = { } ] }, + "/wp/v2/media/(?P[\\d]+)/sideload": { + "namespace": "wp/v2", + "methods": [ + "POST" + ], + "endpoints": [ + { + "methods": [ + "POST" + ], + "args": { + "id": { + "description": "Unique identifier for the attachment.", + "type": "integer", + "required": false + }, + "image_size": { + "description": "Image size.", + "type": "string", + "enum": [ + "thumbnail", + "medium", + "medium_large", + "large", + "1536x1536", + "2048x2048", + "original", + "full" + ], + "required": true + }, + "convert_format": { + "type": "boolean", + "default": true, + "description": "Whether to convert image formats.", + "required": false + } + } + } + ] + }, "/wp/v2/menu-items": { "namespace": "wp/v2", "methods": [ @@ -12806,6 +12859,43 @@ mockedApiResponse.Schema = { } } }, + "image_sizes": [ + { + "height": 150, + "width": 150, + "name": "thumbnail" + }, + { + "height": 300, + "width": 300, + "name": "medium" + }, + { + "height": 0, + "width": 768, + "name": "medium_large" + }, + { + "height": 1024, + "width": 1024, + "name": "large" + }, + { + "height": 1536, + "width": 1536, + "name": "1536x1536" + }, + { + "height": 2048, + "width": 2048, + "name": "2048x2048" + } + ], + "image_size_threshold": 2560, + "image_output_formats": {}, + "jpeg_interlaced": false, + "png_interlaced": false, + "gif_interlaced": false, "site_logo": 0, "site_icon": 0, "site_icon_url": "" @@ -13618,6 +13708,8 @@ mockedApiResponse.MediaCollection = [ "media_details": {}, "post": null, "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg", + "filename": "canola.jpg", + "filesize": null, "_links": { "self": [ { @@ -13695,7 +13787,9 @@ mockedApiResponse.MediaModel = { "mime_type": "image/jpeg", "media_details": {}, "post": null, - "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg" + "source_url": "http://example.org/wp-content/uploads//tmp/canola.jpg", + "filename": "canola.jpg", + "filesize": null }; mockedApiResponse.TypesCollection = { From 0ddb051cd8914201288604608b0d7fd744feb1d9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 17 Feb 2026 13:14:30 +0700 Subject: [PATCH 20/48] Add wp_client_side_media_processing_enabled filter Allows sites to disable client-side media processing by returning false from the filter. When disabled, cross-origin isolation hooks, WASM rewrite rules, REST API index media settings, the sideload endpoint, and generate_sub_sizes / convert_format params are all skipped. Backports gutenberg_client_side_media_processing_enabled from Gutenberg PR #75112. --- src/wp-includes/default-filters.php | 17 +++-- src/wp-includes/media.php | 21 ++++++ .../rest-api/class-wp-rest-server.php | 2 +- .../class-wp-rest-attachments-controller.php | 70 ++++++++++--------- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 6a5e2c6efe258..5d43e6ff1e4ad 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -675,13 +675,16 @@ add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 ); add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' ); -// Cross-origin isolation for client-side media processing. -add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); -add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); -add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); -add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); -add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); -add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); +// Client-side media processing. +if ( wp_is_client_side_media_processing_enabled() ) { + // Cross-origin isolation for client-side media processing. + add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); + add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); + add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); + add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); + add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); + add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); +} // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 47828230a4454..0f900988fb799 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6360,6 +6360,27 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { return apply_filters( 'image_editor_output_format', $output_format, $filename, $mime_type ); } +/** + * Checks whether client-side media processing is enabled. + * + * Client-side media processing uses the browser's capabilities to handle + * tasks like image resizing and compression before uploading to the server. + * + * @since 7.0.0 + * + * @return bool Whether client-side media processing is enabled. + */ +function wp_is_client_side_media_processing_enabled() { + /** + * Filters whether client-side media processing is enabled. + * + * @since 7.0.0 + * + * @param bool $enabled Whether client-side media processing is enabled. Default true. + */ + return apply_filters( 'wp_client_side_media_processing_enabled', true ); +} + /** * Enables cross-origin isolation in the block editor. * diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 77f4e2d0b5810..f0220c667eb4c 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1369,7 +1369,7 @@ public function get_index( $request ) { ); // Add media processing settings for users who can upload files. - if ( current_user_can( 'upload_files' ) ) { + if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) { // Image sizes with normalized data. $available['image_sizes'] = array(); foreach ( wp_get_registered_image_subsizes() as $name => $size ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 4d811772c300e..1ccf79f0e9dd9 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -64,42 +64,44 @@ public function register_routes() { ) ); - $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() ); - // Special case to set 'original_image' in attachment metadata. - $valid_image_sizes[] = 'original'; - // Used for PDF thumbnails. - $valid_image_sizes[] = 'full'; - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\d]+)/sideload', - array( + if ( wp_is_client_side_media_processing_enabled() ) { + $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() ); + // Special case to set 'original_image' in attachment metadata. + $valid_image_sizes[] = 'original'; + // Used for PDF thumbnails. + $valid_image_sizes[] = 'full'; + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)/sideload', array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'sideload_item' ), - 'permission_callback' => array( $this, 'sideload_item_permissions_check' ), - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the attachment.' ), - 'type' => 'integer', - ), - 'image_size' => array( - 'description' => __( 'Image size.' ), - 'type' => 'string', - 'enum' => $valid_image_sizes, - 'required' => true, - ), - 'convert_format' => array( - 'type' => 'boolean', - 'default' => true, - 'description' => __( 'Whether to convert image formats.' ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'sideload_item' ), + 'permission_callback' => array( $this, 'sideload_item_permissions_check' ), + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the attachment.' ), + 'type' => 'integer', + ), + 'image_size' => array( + 'description' => __( 'Image size.' ), + 'type' => 'string', + 'enum' => $valid_image_sizes, + 'required' => true, + ), + 'convert_format' => array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to convert image formats.' ), + ), ), ), - ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } } /** @@ -116,7 +118,7 @@ public function register_routes() { public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { $args = parent::get_endpoint_args_for_item_schema( $method ); - if ( WP_REST_Server::CREATABLE === $method ) { + if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) { $args['generate_sub_sizes'] = array( 'type' => 'boolean', 'default' => true, From 41344546c0d312aefc7f82a6479f81f4cc47e991 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Thu, 19 Feb 2026 16:41:54 +0700 Subject: [PATCH 21/48] Apply suggestions from Weston Co-authored-by: Weston Ruter --- .../endpoints/class-wp-rest-attachments-controller.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 1ccf79f0e9dd9..369c2a4fb1098 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -113,9 +113,9 @@ public function register_routes() { * The arguments for `CREATABLE` requests are * checked for required values and may fall-back to a given default. * Default WP_REST_Server::CREATABLE. - * @return array Endpoint arguments. + * @return array> Endpoint arguments. */ - public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + public function get_endpoint_args_for_item_schema( string $method = WP_REST_Server::CREATABLE ): array { $args = parent::get_endpoint_args_for_item_schema( $method ); if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) { @@ -1902,7 +1902,7 @@ protected function get_edit_media_item_args() { * @param int $attachment_id Attachment ID. * @return string|null Attachment file name, or null if not found. */ - protected function get_attachment_filename( $attachment_id ) { + protected function get_attachment_filename( $attachment_id ): ?string { $path = wp_get_original_image_path( $attachment_id ); if ( $path ) { @@ -1926,7 +1926,7 @@ protected function get_attachment_filename( $attachment_id ) { * @param int $attachment_id Attachment ID. * @return int|null Attachment file size in bytes, or null if not available. */ - protected function get_attachment_filesize( $attachment_id ) { + protected function get_attachment_filesize( $attachment_id ): ?int { $meta = wp_get_attachment_metadata( $attachment_id ); if ( isset( $meta['filesize'] ) ) { From 5b26a13ff1a82874a88156ab90f768a73a3353e6 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 16:44:54 +0700 Subject: [PATCH 22/48] Use static closure in sideload_item The closure and the method it calls don't use instance state, so making both static avoids binding $this unnecessarily. --- .../endpoints/class-wp-rest-attachments-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 369c2a4fb1098..13fc29c89fb24 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -2003,8 +2003,8 @@ public function sideload_item( WP_REST_Request $request ) { $attachment_filename = get_attached_file( $attachment_id, true ); $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null; - $filter_filename = function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { - return $this->filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ); + $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { + return self::filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ); }; add_filter( 'wp_unique_filename', $filter_filename, 10, 6 ); @@ -2104,7 +2104,7 @@ public function sideload_item( WP_REST_Request $request ) { * @param string|null $attachment_filename Original attachment file name. * @return string Filtered file name. */ - private function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) { + private static function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) { if ( empty( $number ) || ! $attachment_filename ) { return $filename; } From 55c14ea3aca30401ebca7c6f29d02bb6236f927d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 16:59:36 +0700 Subject: [PATCH 23/48] update gutenberg ref --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aa478d95a387..853a4df992330 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "7a11a53377a95cba4d3786d71cadd4c2f0c5ac52" + "ref": "fdd98c5d45449ed1f2223dfd46f199838fb98851" }, "engines": { "node": ">=20.10.0", From 4a49d485f0689ddd8e552cff0c4754a5572658f6 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 17:14:19 +0700 Subject: [PATCH 24/48] Fix fatal error from early function call in default-filters default-filters.php is loaded before media.php in wp-settings, so calling wp_is_client_side_media_processing_enabled() at file load time causes a fatal. Move the check into each callback where media.php is guaranteed to be loaded. --- src/wp-includes/default-filters.php | 16 +++++++--------- src/wp-includes/media.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 5d43e6ff1e4ad..a42f86af7e3b7 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -676,15 +676,13 @@ add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' ); // Client-side media processing. -if ( wp_is_client_side_media_processing_enabled() ) { - // Cross-origin isolation for client-side media processing. - add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); - add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); - add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); - add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); - add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); - add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); -} +// Cross-origin isolation for client-side media processing. +add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); +add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); +add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); +add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 0f900988fb799..350d6466ffda5 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6392,6 +6392,10 @@ function wp_is_client_side_media_processing_enabled() { * @link https://web.dev/coop-coep/ */ function wp_set_up_cross_origin_isolation() { + if ( ! wp_is_client_side_media_processing_enabled() ) { + return; + } + $screen = get_current_screen(); if ( ! $screen ) { @@ -6511,6 +6515,10 @@ function wp_add_crossorigin_attributes( string $html ): string { * @return string Filtered rewrite rules. */ function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { + if ( ! wp_is_client_side_media_processing_enabled() ) { + return $rules; + } + $rules .= "\n# BEGIN WordPress client-side media processing\n" . "AddType application/wasm wasm\n" . "# END WordPress client-side media processing\n"; @@ -6527,6 +6535,10 @@ function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { * @since 7.0.0 */ function wp_override_media_templates() { + if ( ! wp_is_client_side_media_processing_enabled() ) { + return; + } + remove_action( 'admin_footer', 'wp_print_media_templates' ); add_action( 'admin_footer', From b0c156b65101a6d6d0a01722d87cb2ee8bc3ff8c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 17:32:24 +0700 Subject: [PATCH 25/48] update GB ref, take 2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 853a4df992330..d7a87cc8f27dc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "fdd98c5d45449ed1f2223dfd46f199838fb98851" + "ref": "a1f0a3bff4395528f0478fddc487d1eb67b8ad28" }, "engines": { "node": ">=20.10.0", From 5151c8b73d8a41495d7e9dd2cfe69f4c961227ac Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 19:47:06 +0700 Subject: [PATCH 26/48] update gb ref --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7a87cc8f27dc..49e6f557d58ed 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "a1f0a3bff4395528f0478fddc487d1eb67b8ad28" + "ref": "6d6652f915362ec5001f1a80df865238189a1c3d" }, "engines": { "node": ">=20.10.0", From d034c0132b3d6ebfb43568d1fa873f1b1d4994d4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 20:09:44 +0700 Subject: [PATCH 27/48] Add client-side media processing JS flag to core The window.__clientSideMediaProcessing global was only set in the Gutenberg plugin, not in core. Without it, the block editor falls back to the legacy upload path which never sends generate_sub_sizes=false to the REST API. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index a42f86af7e3b7..34bb7ddc6cf1c 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -676,6 +676,7 @@ add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' ); // Client-side media processing. +add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' ); // Cross-origin isolation for client-side media processing. add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 350d6466ffda5..715409b02e294 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6381,6 +6381,19 @@ function wp_is_client_side_media_processing_enabled() { return apply_filters( 'wp_client_side_media_processing_enabled', true ); } +/** + * Sets a global JS variable to indicate that client-side media processing is enabled. + * + * @since 7.0.0 + */ +function wp_set_client_side_media_processing_flag() { + if ( ! wp_is_client_side_media_processing_enabled() ) { + return; + } + + wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' ); +} + /** * Enables cross-origin isolation in the block editor. * From 375c82bc7b709b92d833b460626adb05cda0da67 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 21:48:59 +0700 Subject: [PATCH 28/48] update gb ref --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49e6f557d58ed..123607f63abf8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "6d6652f915362ec5001f1a80df865238189a1c3d" + "ref": "62da54eeae90076d828299e62d386d3e082db9ed" }, "engines": { "node": ">=20.10.0", From 4c81534af21a2e68d3106b70c42c47df6c939cf2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 22:02:32 +0700 Subject: [PATCH 29/48] Fix method signature compatibility with parent WP_REST_Controller. Remove type declarations from `get_endpoint_args_for_item_schema()` override to match the parent class signature and avoid a PHP 8.0+ fatal error. Co-Authored-By: Claude Opus 4.6 --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 13fc29c89fb24..62c780f54dd3e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -115,7 +115,7 @@ public function register_routes() { * Default WP_REST_Server::CREATABLE. * @return array> Endpoint arguments. */ - public function get_endpoint_args_for_item_schema( string $method = WP_REST_Server::CREATABLE ): array { + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { $args = parent::get_endpoint_args_for_item_schema( $method ); if ( WP_REST_Server::CREATABLE === $method && wp_is_client_side_media_processing_enabled() ) { From 6c50ba078da76f21bc02f2d9d2fc28b4ec7e43a9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 19 Feb 2026 22:11:10 +0700 Subject: [PATCH 30/48] update gb ref --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 123607f63abf8..f4c110c5dacec 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "62da54eeae90076d828299e62d386d3e082db9ed" + "ref": "b441348bb7e05af351c250b74283f253acaf9138" }, "engines": { "node": ">=20.10.0", From 3ae9f8e2164f6376c40f844dbef76394d0505ce9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 02:43:12 +0700 Subject: [PATCH 31/48] Fix client-side media processing build issues in copy-gutenberg-build.js The Gutenberg build copy script had two bugs that broke client-side media processing (CSM) when backported to core: 1. `removeSourceMaps` regex matched `//# sourceMappingURL=` inside the `workerCode` string literal in vips/worker.js, stripping the closing `';` and producing a SyntaxError. Fixed by anchoring with `^\s*` so it only matches actual source map comments at the start of a line. 2. `module_dependencies` were deleted from classic script assets, preventing `@wordpress/vips/worker` from appearing in the browser import map. The dynamic `import()` in upload-media.js could not resolve the bare specifier. Fixed by preserving `module_dependencies` and reading them in `wp_default_packages_scripts()`. Also adds a runtime fallback in `wp_set_client_side_media_processing_flag()` to ensure the vips worker module dependency is registered for the import map. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/media.php | 16 ++++++++++++++++ src/wp-includes/script-loader.php | 4 ++++ tools/gutenberg/copy-gutenberg-build.js | 12 +++++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 715409b02e294..a4188539d5acc 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6392,6 +6392,22 @@ function wp_set_client_side_media_processing_flag() { } wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' ); + + /* + * Register the @wordpress/vips/worker script module as a dynamic dependency + * of the wp-upload-media classic script. This ensures it is included in the + * import map so that the dynamic import() in upload-media.js can resolve it. + */ + wp_scripts()->add_data( + 'wp-upload-media', + 'module_dependencies', + array( + array( + 'id' => '@wordpress/vips/worker', + 'import' => 'dynamic', + ), + ) + ); } /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 6d1b7dee5d0bf..f9ea36720baea 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -310,6 +310,10 @@ function wp_default_packages_scripts( $scripts ) { $scripts->add( $handle, $path, $dependencies, $package_data['version'], 1 ); + if ( ! empty( $package_data['module_dependencies'] ) ) { + $scripts->add_data( $handle, 'module_dependencies', $package_data['module_dependencies'] ); + } + if ( in_array( 'wp-i18n', $dependencies, true ) ) { $scripts->set_translations( $handle ); } diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 36b55bcae5d64..0d1c454ca8085 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -441,15 +441,11 @@ function generateScriptLoaderPackages() { // Parse PHP array to JavaScript object const assetData = parsePHPArray( match[ 1 ] ); - // For regular scripts, use dependencies as-is - // Keep dependencies array (don't use module_dependencies) + // For regular scripts, use dependencies as-is. if ( ! assetData.dependencies ) { assetData.dependencies = []; } - // Remove module_dependencies if present (not used for regular scripts) - delete assetData.module_dependencies; - // Create entries for both minified and non-minified versions const jsPathMin = `${ entry.name }.min.js`; const jsPathRegular = `${ entry.name }.js`; @@ -920,9 +916,11 @@ async function main() { const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source ); const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination ); - // Transform function to remove source map comments from all JS files + // Transform function to remove source map comments from all JS files. + // Only match actual source map comments at the start of a line (possibly + // with whitespace), not occurrences inside string literals. const removeSourceMaps = ( content ) => { - return content.replace( /\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd(); + return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd(); }; if ( fs.existsSync( scriptsSrc ) ) { From 53ba07d28d4d9502ca8fcb1d22b27655d1a869d0 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 20 Feb 2026 08:49:19 +0700 Subject: [PATCH 32/48] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index a4188539d5acc..a249dc2f53c29 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6401,12 +6401,7 @@ function wp_set_client_side_media_processing_flag() { wp_scripts()->add_data( 'wp-upload-media', 'module_dependencies', - array( - array( - 'id' => '@wordpress/vips/worker', - 'import' => 'dynamic', - ), - ) + array( '@wordpress/vips/worker' ) ); } From 7503a416fae378f3db28ac9ec61f1c38709ae747 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 20 Feb 2026 08:49:50 +0700 Subject: [PATCH 33/48] Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Co-authored-by: Weston Ruter --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 62c780f54dd3e..d8286298e79c5 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1936,7 +1936,7 @@ protected function get_attachment_filesize( $attachment_id ): ?int { $original_path = wp_get_original_image_path( $attachment_id ); $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id ); - if ( is_string( $attached_file ) && file_exists( $attached_file ) ) { + if ( is_string( $attached_file ) && is_readable( $attached_file ) ) { return wp_filesize( $attached_file ); } From caaefc2e0460cbae06e2bc2015a4eb4c4f0d7c79 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 20 Feb 2026 08:57:52 +0700 Subject: [PATCH 34/48] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index a249dc2f53c29..aa6236742aecd 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6577,23 +6577,21 @@ static function () { * Extract each script block's content, process it separately, * then reassemble the full output. */ - $html = (string) preg_replace_callback( - '#(]*>)(.*?)()#s', - static function ( $matches ) { - $processor = new WP_HTML_Tag_Processor( $matches[2] ); - - while ( $processor->next_tag() ) { - if ( in_array( $processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true ) ) { - $processor->set_attribute( 'crossorigin', 'anonymous' ); - } + $script_processor = new WP_HTML_Tag_Processor( $html ); + while ( $script_processor->next_tag( 'SCRIPT' ) ) { + if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) { + continue; + } + $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() ); + while ( $template_processor->next_tag() ) { + if ( in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true ) ) { + $template_processor->set_attribute( 'crossorigin', 'anonymous' ); } + } + $script_processor->set_modifiable_text( $template_processor->get_updated_html() ); + } - return $matches[1] . $processor->get_updated_html() . $matches[3]; - }, - $html - ); - - echo $html; + echo $script_processor->get_updated_html(); } ); } From d57c5c169039362b2d59fc378aa303d6d08d82b2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 12:31:33 +0700 Subject: [PATCH 35/48] Fix image_sizes format in REST API index for CSM The image_sizes field was returned as a flat array of objects, but the client-side media processing JS expects an object keyed by size name. This caused all sub-size lookups to fail with "Image size not found in configuration" warnings, preventing client-side thumbnail generation. Also adds the missing crop property needed by the resize operation. --- src/wp-includes/rest-api/class-wp-rest-server.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index f0220c667eb4c..0f00b7cf7adc9 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1370,15 +1370,16 @@ public function get_index( $request ) { // Add media processing settings for users who can upload files. if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) { - // Image sizes with normalized data. - $available['image_sizes'] = array(); + // Image sizes keyed by name for client-side media processing. + $image_sizes = array(); foreach ( wp_get_registered_image_subsizes() as $name => $size ) { - $available['image_sizes'][] = array( + $image_sizes[ $name ] = array( 'height' => (int) $size['height'], 'width' => (int) $size['width'], - 'name' => $name, + 'crop' => (bool) $size['crop'], ); } + $available['image_sizes'] = (object) $image_sizes; /** This filter is documented in wp-admin/includes/image.php */ $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); From f751318320280e377cc423c65323ad3f430da83b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 12:39:51 +0700 Subject: [PATCH 36/48] Check IMG srcset for cross-origin URLs An IMG element can validly have only a srcset attribute without src. This ensures crossorigin="anonymous" is added when any srcset candidate URL is cross-origin. --- src/wp-includes/media.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index aa6236742aecd..5b305e5dae62e 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6505,9 +6505,24 @@ function wp_add_crossorigin_attributes( string $html ): string { $crossorigin = $processor->get_attribute( 'crossorigin' ); - $url = $processor->get_attribute( $tags[ $tag ] ); + $url = $processor->get_attribute( $tags[ $tag ] ); + $is_cross_origin = is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ); + + // For IMG tags, also check srcset for cross-origin URLs. + if ( ! $is_cross_origin && 'IMG' === $tag ) { + $srcset = $processor->get_attribute( 'srcset' ); + if ( is_string( $srcset ) ) { + foreach ( explode( ',', $srcset ) as $candidate ) { + $candidate_url = strtok( trim( $candidate ), ' ' ); + if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) { + $is_cross_origin = true; + break; + } + } + } + } - if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) { + if ( $is_cross_origin && ! is_string( $crossorigin ) ) { if ( 'SOURCE' === $tag ) { $sought = $processor->seek( 'audio-video-parent' ); From 2a546224b7d4eaba2047c8859b6c386afcf259f4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 12:41:18 +0700 Subject: [PATCH 37/48] Clean up CSM filters on all error paths The filters added for client-side media processing were not removed on several early error returns in create_item(). Extracts cleanup into a reusable private method and calls it from every exit point. --- .../class-wp-rest-attachments-controller.php | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index d8286298e79c5..817b627e0b93d 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -292,11 +292,7 @@ public function create_item( $request ) { $insert = $this->insert_attachment( $request ); if ( is_wp_error( $insert ) ) { - // Clean up filters on error. - remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); - remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); - remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); - remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + $this->remove_client_side_media_processing_filters(); return $insert; } @@ -314,6 +310,7 @@ public function create_item( $request ) { $thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment_id ); if ( is_wp_error( $thumbnail_update ) ) { + $this->remove_client_side_media_processing_filters(); return $thumbnail_update; } } @@ -322,6 +319,7 @@ public function create_item( $request ) { $meta_update = $this->meta->update_value( $request['meta'], $attachment_id ); if ( is_wp_error( $meta_update ) ) { + $this->remove_client_side_media_processing_filters(); return $meta_update; } } @@ -330,12 +328,14 @@ public function create_item( $request ) { $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); if ( is_wp_error( $fields_update ) ) { + $this->remove_client_side_media_processing_filters(); return $fields_update; } $terms_update = $this->handle_terms( $attachment_id, $request ); if ( is_wp_error( $terms_update ) ) { + $this->remove_client_side_media_processing_filters(); return $terms_update; } @@ -372,11 +372,7 @@ public function create_item( $request ) { */ wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) ); - // Clean up filters. - remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); - remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); - remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); - remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + $this->remove_client_side_media_processing_filters(); $response = $this->prepare_item_for_response( $attachment, $request ); $response = rest_ensure_response( $response ); @@ -386,6 +382,18 @@ public function create_item( $request ) { return $response; } + /** + * Removes filters added for client-side media processing. + * + * @since 7.0.0 + */ + private function remove_client_side_media_processing_filters() { + remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + remove_filter( 'wp_image_maybe_exif_rotate', '__return_false', 100 ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + } + /** * Inserts the attachment post in the database. Does not update the attachment meta. * From 629cb0d970c22210aeac19a09bb42d318f99c70c Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 20 Feb 2026 16:01:18 +0700 Subject: [PATCH 38/48] Apply suggestions from code review from Weston and Mukesh Co-authored-by: Weston Ruter Co-authored-by: Mukesh Panchal --- src/wp-includes/media.php | 14 +++++++------- src/wp-includes/rest-api/class-wp-rest-server.php | 9 ++------- .../class-wp-rest-attachments-controller.php | 6 +++--- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 5b305e5dae62e..35fea48b8b317 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6370,7 +6370,7 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { * * @return bool Whether client-side media processing is enabled. */ -function wp_is_client_side_media_processing_enabled() { +function wp_is_client_side_media_processing_enabled(): bool { /** * Filters whether client-side media processing is enabled. * @@ -6378,7 +6378,7 @@ function wp_is_client_side_media_processing_enabled() { * * @param bool $enabled Whether client-side media processing is enabled. Default true. */ - return apply_filters( 'wp_client_side_media_processing_enabled', true ); + return (bool) apply_filters( 'wp_client_side_media_processing_enabled', true ); } /** @@ -6386,7 +6386,7 @@ function wp_is_client_side_media_processing_enabled() { * * @since 7.0.0 */ -function wp_set_client_side_media_processing_flag() { +function wp_set_client_side_media_processing_flag(): void { if ( ! wp_is_client_side_media_processing_enabled() ) { return; } @@ -6415,7 +6415,7 @@ function wp_set_client_side_media_processing_flag() { * * @link https://web.dev/coop-coep/ */ -function wp_set_up_cross_origin_isolation() { +function wp_set_up_cross_origin_isolation(): void { if ( ! wp_is_client_side_media_processing_enabled() ) { return; } @@ -6450,7 +6450,7 @@ function wp_set_up_cross_origin_isolation() { * * @global bool $is_safari */ -function wp_start_cross_origin_isolation_output_buffer() { +function wp_start_cross_origin_isolation_output_buffer(): void { global $is_safari; $coep = $is_safari ? 'require-corp' : 'credentialless'; @@ -6505,7 +6505,7 @@ function wp_add_crossorigin_attributes( string $html ): string { $crossorigin = $processor->get_attribute( 'crossorigin' ); - $url = $processor->get_attribute( $tags[ $tag ] ); + $url = $processor->get_attribute( $tags[ $tag ] ); $is_cross_origin = is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ); // For IMG tags, also check srcset for cross-origin URLs. @@ -6573,7 +6573,7 @@ function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { * * @since 7.0.0 */ -function wp_override_media_templates() { +function wp_override_media_templates(): void { if ( ! wp_is_client_side_media_processing_enabled() ) { return; } diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 0f00b7cf7adc9..2953d50505577 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1371,15 +1371,10 @@ public function get_index( $request ) { // Add media processing settings for users who can upload files. if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) { // Image sizes keyed by name for client-side media processing. - $image_sizes = array(); + $available['image_sizes'] = array(); foreach ( wp_get_registered_image_subsizes() as $name => $size ) { - $image_sizes[ $name ] = array( - 'height' => (int) $size['height'], - 'width' => (int) $size['width'], - 'crop' => (bool) $size['crop'], - ); + $available['image_sizes'][ $name ] = $size; } - $available['image_sizes'] = (object) $image_sizes; /** This filter is documented in wp-admin/includes/image.php */ $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 817b627e0b93d..fc2974cbaebe1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1910,7 +1910,7 @@ protected function get_edit_media_item_args() { * @param int $attachment_id Attachment ID. * @return string|null Attachment file name, or null if not found. */ - protected function get_attachment_filename( $attachment_id ): ?string { + protected function get_attachment_filename( int $attachment_id ): ?string { $path = wp_get_original_image_path( $attachment_id ); if ( $path ) { @@ -1934,7 +1934,7 @@ protected function get_attachment_filename( $attachment_id ): ?string { * @param int $attachment_id Attachment ID. * @return int|null Attachment file size in bytes, or null if not available. */ - protected function get_attachment_filesize( $attachment_id ): ?int { + protected function get_attachment_filesize( int $attachment_id ): ?int { $meta = wp_get_attachment_metadata( $attachment_id ); if ( isset( $meta['filesize'] ) ) { @@ -2023,7 +2023,7 @@ public function sideload_item( WP_REST_Request $request ) { // Matches logic in media_handle_upload(). // The post date doesn't usually matter for pages, so don't backdate this upload. - if ( $parent_post && 'page' !== $parent_post->post_type && substr( $parent_post->post_date, 0, 4 ) > 0 ) { + if ( $parent_post && 'page' !== $parent_post->post_type && ! str_starts_with( $parent_post->post_date, '0000-00-00' ) ) { $time = $parent_post->post_date; } From 5f1debd947dd93320fd48e947f39184e22faf4cf Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:03:35 +0700 Subject: [PATCH 39/48] Use shorter path in doc block --- .../rest-api/endpoints/class-wp-rest-attachments-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index fc2974cbaebe1..47fe8e830486c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -2005,7 +2005,7 @@ public function sideload_item( WP_REST_Request $request ) { /* * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. - * See https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 + * See /wp-includes/functions.php. * With the following filter we can work around this safeguard. */ $attachment_filename = get_attached_file( $attachment_id, true ); From 7ac186ec48bd71efafef65a357a47bb4681339e6 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:11:03 +0700 Subject: [PATCH 40/48] Refactor cross-origin check to be data-driven Rename $tags to $cross_origin_tag_attributes and restructure it as a mapping of tag name to attributes with boolean values indicating whether each is a srcset. This replaces the hardcoded IMG srcset special case with a generic loop, making it easier to extend (e.g. for imagesrcset on LINK). --- src/wp-includes/media.php | 49 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 35fea48b8b317..dcf711f6f7d39 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6479,19 +6479,19 @@ function wp_add_crossorigin_attributes( string $html ): string { $processor = new WP_HTML_Tag_Processor( $html ); // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin. - $tags = array( - 'AUDIO' => 'src', - 'IMG' => 'src', - 'LINK' => 'href', - 'SCRIPT' => 'src', - 'VIDEO' => 'src', - 'SOURCE' => 'src', + $cross_origin_tag_attributes = array( + 'AUDIO' => array( 'src' => false ), + 'IMG' => array( 'src' => false, 'srcset' => true ), + 'LINK' => array( 'href' => false ), + 'SCRIPT' => array( 'src' => false ), + 'VIDEO' => array( 'src' => false ), + 'SOURCE' => array( 'src' => false ), ); while ( $processor->next_tag() ) { $tag = $processor->get_tag(); - if ( ! isset( $tags[ $tag ] ) ) { + if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) { continue; } @@ -6505,20 +6505,29 @@ function wp_add_crossorigin_attributes( string $html ): string { $crossorigin = $processor->get_attribute( 'crossorigin' ); - $url = $processor->get_attribute( $tags[ $tag ] ); - $is_cross_origin = is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ); - - // For IMG tags, also check srcset for cross-origin URLs. - if ( ! $is_cross_origin && 'IMG' === $tag ) { - $srcset = $processor->get_attribute( 'srcset' ); - if ( is_string( $srcset ) ) { - foreach ( explode( ',', $srcset ) as $candidate ) { - $candidate_url = strtok( trim( $candidate ), ' ' ); - if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) { - $is_cross_origin = true; - break; + $is_cross_origin = false; + + foreach ( $cross_origin_tag_attributes[ $tag ] as $attr => $is_srcset ) { + if ( $is_srcset ) { + $srcset = $processor->get_attribute( $attr ); + if ( is_string( $srcset ) ) { + foreach ( explode( ',', $srcset ) as $candidate ) { + $candidate_url = strtok( trim( $candidate ), ' ' ); + if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) { + $is_cross_origin = true; + break; + } } } + } else { + $url = $processor->get_attribute( $attr ); + if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) { + $is_cross_origin = true; + } + } + + if ( $is_cross_origin ) { + break; } } From c6c68ddc7f4639b176ce3a74e2939d5a0722b476 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:13:18 +0700 Subject: [PATCH 41/48] Add imagesrcset and poster to cross-origin checks LINK elements can have imagesrcset for preloading responsive images, and VIDEO elements can have a poster image URL. Both need cross-origin handling. --- src/wp-includes/media.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index dcf711f6f7d39..09683046feaa3 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6482,9 +6482,9 @@ function wp_add_crossorigin_attributes( string $html ): string { $cross_origin_tag_attributes = array( 'AUDIO' => array( 'src' => false ), 'IMG' => array( 'src' => false, 'srcset' => true ), - 'LINK' => array( 'href' => false ), + 'LINK' => array( 'href' => false, 'imagesrcset' => true ), 'SCRIPT' => array( 'src' => false ), - 'VIDEO' => array( 'src' => false ), + 'VIDEO' => array( 'src' => false, 'poster' => false ), 'SOURCE' => array( 'src' => false ), ); From 894a9ff1f7eece41c8c1816e9111844755bc1a1c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:17:39 +0700 Subject: [PATCH 42/48] Remove wp_filter_mod_rewrite_rules_for_wasm WASM files are now inlined as base64 data URLs at build time, so the .htaccess AddType rule is no longer needed. --- src/wp-includes/default-filters.php | 1 - src/wp-includes/media.php | 22 ---------------------- 2 files changed, 23 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 34bb7ddc6cf1c..38ae6cbc9a11f 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -682,7 +682,6 @@ add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); -add_filter( 'mod_rewrite_rules', 'wp_filter_mod_rewrite_rules_for_wasm' ); add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); // Nav menu. diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 09683046feaa3..047db0d895888 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6552,28 +6552,6 @@ function wp_add_crossorigin_attributes( string $html ): string { return $processor->get_updated_html(); } -/** - * Filters the list of rewrite rules formatted for output to an .htaccess file. - * - * Adds support for serving WebAssembly files used by client-side media processing. - * - * @since 7.0.0 - * - * @param string $rules mod_rewrite Rewrite rules formatted for .htaccess. - * @return string Filtered rewrite rules. - */ -function wp_filter_mod_rewrite_rules_for_wasm( string $rules ): string { - if ( ! wp_is_client_side_media_processing_enabled() ) { - return $rules; - } - - $rules .= "\n# BEGIN WordPress client-side media processing\n" . - "AddType application/wasm wasm\n" . - "# END WordPress client-side media processing\n"; - - return $rules; -} - /** * Overrides templates from wp_print_media_templates with custom ones. * From 6b6d9e2491477ad23aca92db3f92a3698db1aaf4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:32:43 +0700 Subject: [PATCH 43/48] Add crossorigin check and docs to wp_override_media_templates Skip setting crossorigin when already present, and document why the simpler approach (vs wp_add_crossorigin_attributes) is needed for Underscore.js template placeholders. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/media.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 047db0d895888..b5d567c2129c4 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6584,9 +6584,21 @@ static function () { if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) { continue; } + /* + * Unlike wp_add_crossorigin_attributes(), this does not check whether + * URLs are actually cross-origin. Media templates use Underscore.js + * template expressions (e.g. {{ data.url }}) as placeholder URLs, + * so actual URLs are not available at parse time. + * The crossorigin attribute is added unconditionally to all relevant + * media tags to ensure cross-origin isolation works regardless of + * the final URL value at render time. + */ $template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() ); while ( $template_processor->next_tag() ) { - if ( in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true ) ) { + if ( + in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true ) + && ! is_string( $template_processor->get_attribute( 'crossorigin' ) ) + ) { $template_processor->set_attribute( 'crossorigin', 'anonymous' ); } } From dd589777049ca982300d133af062ba9225f173df Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:44:42 +0700 Subject: [PATCH 44/48] Remove unused params from filter_wp_unique_filename The $ext, $unique_filename_callback, and $alt_filenames parameters were not used in the method body. The closure still receives all 6 filter args positionally but now only passes the 4 that are needed. Co-Authored-By: Claude Opus 4.6 --- .../class-wp-rest-attachments-controller.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 47fe8e830486c..1a135ba546779 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -2012,7 +2012,7 @@ public function sideload_item( WP_REST_Request $request ) { $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null; $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { - return self::filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ); + return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ); }; add_filter( 'wp_unique_filename', $filter_filename, 10, 6 ); @@ -2102,17 +2102,14 @@ public function sideload_item( WP_REST_Request $request ) { * * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 * - * @param string $filename Unique file name. - * @param string $ext File extension. Example: ".png". - * @param string $dir Directory path. - * @param callable|null $unique_filename_callback Callback function that generates the unique file name. - * @param string[] $alt_filenames Array of alternate file names that were checked for collisions. - * @param int|string $number The highest number that was used to make the file name unique - * or an empty string if unused. - * @param string|null $attachment_filename Original attachment file name. + * @param string $filename Unique file name. + * @param string $dir Directory path. + * @param int|string $number The highest number that was used to make the file name unique + * or an empty string if unused. + * @param string|null $attachment_filename Original attachment file name. * @return string Filtered file name. */ - private static function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) { + private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) { if ( empty( $number ) || ! $attachment_filename ) { return $filename; } From 1d4e8cd8789e541a840283e08251adc5d6d2ff5b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Feb 2026 16:56:40 +0700 Subject: [PATCH 45/48] Move crossorigin processing into wp_print_media_templates Instead of using a separate wp_override_media_templates function that removes and re-adds the admin_footer action with an anonymous closure, add the crossorigin attribute processing directly within wp_print_media_templates itself. This simplifies the approach and eliminates the indirection of action hook manipulation. --- src/wp-includes/default-filters.php | 2 - src/wp-includes/media-template.php | 44 ++++++++++++++++++++++ src/wp-includes/media.php | 57 ----------------------------- 3 files changed, 44 insertions(+), 59 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 38ae6cbc9a11f..2291c92134641 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -682,8 +682,6 @@ add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' ); add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' ); -add_action( 'wp_enqueue_media', 'wp_override_media_templates' ); - // Nav menu. add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 ); add_filter( 'nav_menu_css_class', 'wp_nav_menu_remove_menu_item_has_children_class', 10, 4 ); diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 80399acd8ce0a..7a8ec1a07056a 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -156,6 +156,12 @@ class="wp-video-shortcode {{ classes.join( ' ' ) }}" function wp_print_media_templates() { $class = 'media-modal wp-core-ui'; + $is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled(); + + if ( $is_cross_origin_isolation_enabled ) { + ob_start(); + } + $alt_text_description = sprintf( /* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */ __( 'Learn how to describe the purpose of the image%3$s. Leave empty if the image is purely decorative.' ), @@ -1582,4 +1588,42 @@ function wp_print_media_templates() { * @since 3.5.0 */ do_action( 'print_media_templates' ); + + if ( $is_cross_origin_isolation_enabled ) { + $html = (string) ob_get_clean(); + + /* + * The media templates are inside