Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7cab03d
add preload patchs for client side media
adamsilverstein Feb 4, 2026
bf24f67
REST API: Add media processing settings to index endpoint
adamsilverstein Feb 4, 2026
f20f2f9
REST API: Add filename and filesize fields to attachments
adamsilverstein Feb 4, 2026
a979037
REST API: Add exif_orientation field to attachments
adamsilverstein Feb 4, 2026
d7d04fc
REST API: Add generate_sub_sizes and convert_format params
adamsilverstein Feb 4, 2026
2fc26fc
REST API: Add sideload endpoint for attachments
adamsilverstein Feb 4, 2026
f39001d
REST API: Improve missing_image_sizes for PDFs
adamsilverstein Feb 4, 2026
2f33085
Media: Add cross-origin isolation support
adamsilverstein Feb 4, 2026
2de4be6
Media: Add WASM MIME type to .htaccess rules
adamsilverstein Feb 4, 2026
07ca4ad
Media: Add crossorigin attributes to media templates
adamsilverstein Feb 4, 2026
4aef029
Tests: Update REST API tests for client-side media fields
adamsilverstein Feb 5, 2026
1271aa0
Docs: Update @since tags from 6.9.0 to 7.0.0
adamsilverstein Feb 5, 2026
a66bce9
Update src/wp-includes/media.php
adamsilverstein Feb 13, 2026
90af128
Update src/wp-includes/media.php
adamsilverstein Feb 13, 2026
1fb7a00
Update src/wp-includes/media.php
adamsilverstein Feb 13, 2026
2f569b5
Update src/wp-includes/rest-api/class-wp-rest-server.php
adamsilverstein Feb 13, 2026
b36bd28
Use WP_HTML_Tag_Processor in wp_override_media_templates
adamsilverstein Feb 13, 2026
82832f9
Fix EXIF orientation 0 treated as valid value
adamsilverstein Feb 13, 2026
7c995b0
Update REST API QUnit fixtures for Client Side Media changes
adamsilverstein Feb 14, 2026
0ddb051
Add wp_client_side_media_processing_enabled filter
adamsilverstein Feb 17, 2026
4134454
Apply suggestions from Weston
adamsilverstein Feb 19, 2026
5b26a13
Use static closure in sideload_item
adamsilverstein Feb 19, 2026
55c14ea
update gutenberg ref
adamsilverstein Feb 19, 2026
4a49d48
Fix fatal error from early function call in default-filters
adamsilverstein Feb 19, 2026
b0c156b
update GB ref, take 2
adamsilverstein Feb 19, 2026
5151c8b
update gb ref
adamsilverstein Feb 19, 2026
d034c01
Add client-side media processing JS flag to core
adamsilverstein Feb 19, 2026
375c82b
update gb ref
adamsilverstein Feb 19, 2026
4c81534
Fix method signature compatibility with parent WP_REST_Controller.
adamsilverstein Feb 19, 2026
6c50ba0
update gb ref
adamsilverstein Feb 19, 2026
3ae9f8e
Fix client-side media processing build issues in copy-gutenberg-build.js
adamsilverstein Feb 19, 2026
53ba07d
Update src/wp-includes/media.php
adamsilverstein Feb 20, 2026
7503a41
Update src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-c…
adamsilverstein Feb 20, 2026
caaefc2
Update src/wp-includes/media.php
adamsilverstein Feb 20, 2026
d57c5c1
Fix image_sizes format in REST API index for CSM
adamsilverstein Feb 20, 2026
f751318
Check IMG srcset for cross-origin URLs
adamsilverstein Feb 20, 2026
2a54622
Clean up CSM filters on all error paths
adamsilverstein Feb 20, 2026
629cb0d
Apply suggestions from code review from Weston and Mukesh
adamsilverstein Feb 20, 2026
5f1debd
Use shorter path in doc block
adamsilverstein Feb 20, 2026
7ac186e
Refactor cross-origin check to be data-driven
adamsilverstein Feb 20, 2026
c6c68dd
Add imagesrcset and poster to cross-origin checks
adamsilverstein Feb 20, 2026
894a9ff
Remove wp_filter_mod_rewrite_rules_for_wasm
adamsilverstein Feb 20, 2026
6b6d9e2
Add crossorigin check and docs to wp_override_media_templates
adamsilverstein Feb 20, 2026
dd58977
Remove unused params from filter_wp_unique_filename
adamsilverstein Feb 20, 2026
9203e5d
Merge branch 'trunk' into backport-preload-changes-for-csm-70
adamsilverstein Feb 20, 2026
1d4e8cd
Move crossorigin processing into wp_print_media_templates
adamsilverstein Feb 20, 2026
5a62665
Fix PHPCS array declaration spacing errors
adamsilverstein Feb 20, 2026
57c4f00
regenerate wp-api fixtures
adamsilverstein Feb 20, 2026
d56772b
Fix wp-api-generated.js fixture to match CI environment
adamsilverstein Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://develop.svn.wordpress.org/trunk"
},
"gutenberg": {
"ref": "7a11a53377a95cba4d3786d71cadd4c2f0c5ac52"
"ref": "b441348bb7e05af351c250b74283f253acaf9138"
},
"engines": {
"node": ">=20.10.0",
Expand Down
6 changes: 6 additions & 0 deletions src/wp-admin/edit-form-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions src/wp-admin/site-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,13 @@
add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 );
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' );
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 );
Expand Down
44 changes: 44 additions & 0 deletions src/wp-includes/media-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
__( '<a href="%1$s" %2$s>Learn how to describe the purpose of the image%3$s</a>. Leave empty if the image is purely decorative.' ),
Expand Down Expand Up @@ -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 <script type="text/html"> tags,
* whose content is treated as raw text by the HTML Tag Processor.
* Extract each script block's content, process it separately,
* then reassemble the full output.
*/
$script_processor = new WP_HTML_Tag_Processor( $html );
while ( $script_processor->next_tag( 'SCRIPT' ) ) {
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 )
&& ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
) {
$template_processor->set_attribute( 'crossorigin', 'anonymous' );
}
}
$script_processor->set_modifiable_text( $template_processor->get_updated_html() );
}

echo $script_processor->get_updated_html();
}
}
202 changes: 202 additions & 0 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -6359,3 +6359,205 @@ 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(): bool {
/**
* 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 (bool) 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(): void {
if ( ! wp_is_client_side_media_processing_enabled() ) {
return;
}

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( '@wordpress/vips/worker' )
);
}

/**
* Enables cross-origin isolation in the block editor.
*
* Required for enabling SharedArrayBuffer for WebAssembly-based
* media processing in the editor.
*
* @since 7.0.0
*
* @link https://web.dev/coop-coep/
*/
function wp_set_up_cross_origin_isolation(): void {
if ( ! wp_is_client_side_media_processing_enabled() ) {
return;
}

$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;
}

// Cross-origin isolation is not needed if users can't upload files anyway.
if ( ! current_user_can( '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 7.0.0
*
* @link https://web.dev/coop-coep/
*
* @global bool $is_safari
*/
function wp_start_cross_origin_isolation_output_buffer(): void {
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 7.0.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.
$cross_origin_tag_attributes = array(
'AUDIO' => array( 'src' => false ),
'IMG' => array(
'src' => false,
'srcset' => true,
),
'LINK' => array(
'href' => false,
'imagesrcset' => true,
),
'SCRIPT' => array( 'src' => false ),
'VIDEO' => array(
'src' => false,
'poster' => false,
),
'SOURCE' => array( 'src' => false ),
);

while ( $processor->next_tag() ) {
$tag = $processor->get_tag();

if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
continue;
}

if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
$processor->set_bookmark( 'audio-video-parent' );
}

$processor->set_bookmark( 'resume' );

$sought = false;

$crossorigin = $processor->get_attribute( 'crossorigin' );

$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;
}
}

if ( $is_cross_origin && ! 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();
}

28 changes: 28 additions & 0 deletions src/wp-includes/rest-api/class-wp-rest-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,34 @@ 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 ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) {
// Image sizes keyed by name for client-side media processing.
$available['image_sizes'] = array();
foreach ( wp_get_registered_image_subsizes() as $name => $size ) {
$available['image_sizes'][ $name ] = $size;
}

/** 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;
Comment on lines +1384 to +1389
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right. In looking at wp_get_image_editor_output_format(), it is passing in a mapping of source format to output format as an associative array. The default value it is filtering normally:

	$output_format = array(
		'image/heic'          => 'image/jpeg',
		'image/heif'          => 'image/jpeg',
		'image/heic-sequence' => 'image/jpeg',
		'image/heif-sequence' => 'image/jpeg',
	);

But here it is being passed an empty array, and filtering and re-filtering the same $output_formats variable. And there is no $filename param being supplied to the filter? Is this for the sake of plugins somehow? I'm struggling to understand what this does.

And is the (object) cast needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. I was trying to capture results for each possible mime type which in theory plugins could alter based on the input mime type; as you point out, however the filename parameter is not used at all. And typically plugins would get one mime type per upload file (the original type).

A better place to put this data would be in the response to the initial image upload - i'm already doing that with the exif rotation data to keep the rotation logic in one place (on the server).

If I don't get this fixed in the current PR, I will create a follow up issue to move the logic there. That way, we can also pass in the actual input filename and mime type and filter will operate just like it does on the server side.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to get this in as is then follow up to fix it during beta

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter - I created a trac ticket for this - https://core.trac.wordpress.org/ticket/64677 - I'll probably wind up applying a similar fix in GB since it will still use client side media for pre 7.0 WordPress


/** 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'] ?? '';
Expand Down
Loading
Loading