Skip to content

Commit 0544d56

Browse files
Editor: backport client side media PHP changes to core.
Bring over the changes required to implement client side media in core. This feature recently graduated from experiments and is ready for testing in beta. Props adamsilverstein, westonruter, mamaduka, mukesh27, swissspidy, andrewserong, ellatrix, ramonjd. Fixes #62243. git-svn-id: https://develop.svn.wordpress.org/trunk@61703 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 702af38 commit 0544d56

File tree

13 files changed

+811
-10
lines changed

13 files changed

+811
-10
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://develop.svn.wordpress.org/trunk"
88
},
99
"gutenberg": {
10-
"ref": "7a11a53377a95cba4d3786d71cadd4c2f0c5ac52"
10+
"ref": "b441348bb7e05af351c250b74283f253acaf9138"
1111
},
1212
"engines": {
1313
"node": ">=20.10.0",

src/wp-admin/edit-form-blocks.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ static function ( $classes ) {
9292
'description',
9393
'gmt_offset',
9494
'home',
95+
'image_sizes',
96+
'image_size_threshold',
97+
'image_output_formats',
98+
'jpeg_interlaced',
99+
'png_interlaced',
100+
'gif_interlaced',
95101
'name',
96102
'site_icon',
97103
'site_icon_url',

src/wp-admin/site-editor.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ static function ( $classes ) {
218218
'description',
219219
'gmt_offset',
220220
'home',
221+
'image_sizes',
222+
'image_size_threshold',
223+
'image_output_formats',
224+
'jpeg_interlaced',
225+
'png_interlaced',
226+
'gif_interlaced',
221227
'name',
222228
'site_icon',
223229
'site_icon_url',

src/wp-includes/default-filters.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,13 @@
675675
add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 );
676676
add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' );
677677

678+
// Client-side media processing.
679+
add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' );
680+
// Cross-origin isolation for client-side media processing.
681+
add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' );
682+
add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' );
683+
add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' );
684+
add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' );
678685
// Nav menu.
679686
add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 );
680687
add_filter( 'nav_menu_css_class', 'wp_nav_menu_remove_menu_item_has_children_class', 10, 4 );

src/wp-includes/media-template.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ class="wp-video-shortcode {{ classes.join( ' ' ) }}"
156156
function wp_print_media_templates() {
157157
$class = 'media-modal wp-core-ui';
158158

159+
$is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled();
160+
161+
if ( $is_cross_origin_isolation_enabled ) {
162+
ob_start();
163+
}
164+
159165
$alt_text_description = sprintf(
160166
/* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */
161167
__( '<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.' ),
@@ -1582,4 +1588,42 @@ function wp_print_media_templates() {
15821588
* @since 3.5.0
15831589
*/
15841590
do_action( 'print_media_templates' );
1591+
1592+
if ( $is_cross_origin_isolation_enabled ) {
1593+
$html = (string) ob_get_clean();
1594+
1595+
/*
1596+
* The media templates are inside <script type="text/html"> tags,
1597+
* whose content is treated as raw text by the HTML Tag Processor.
1598+
* Extract each script block's content, process it separately,
1599+
* then reassemble the full output.
1600+
*/
1601+
$script_processor = new WP_HTML_Tag_Processor( $html );
1602+
while ( $script_processor->next_tag( 'SCRIPT' ) ) {
1603+
if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
1604+
continue;
1605+
}
1606+
/*
1607+
* Unlike wp_add_crossorigin_attributes(), this does not check whether
1608+
* URLs are actually cross-origin. Media templates use Underscore.js
1609+
* template expressions (e.g. {{ data.url }}) as placeholder URLs,
1610+
* so actual URLs are not available at parse time.
1611+
* The crossorigin attribute is added unconditionally to all relevant
1612+
* media tags to ensure cross-origin isolation works regardless of
1613+
* the final URL value at render time.
1614+
*/
1615+
$template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
1616+
while ( $template_processor->next_tag() ) {
1617+
if (
1618+
in_array( $template_processor->get_tag(), array( 'AUDIO', 'IMG', 'VIDEO' ), true )
1619+
&& ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
1620+
) {
1621+
$template_processor->set_attribute( 'crossorigin', 'anonymous' );
1622+
}
1623+
}
1624+
$script_processor->set_modifiable_text( $template_processor->get_updated_html() );
1625+
}
1626+
1627+
echo $script_processor->get_updated_html();
1628+
}
15851629
}

src/wp-includes/media.php

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6359,3 +6359,205 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) {
63596359
*/
63606360
return apply_filters( 'image_editor_output_format', $output_format, $filename, $mime_type );
63616361
}
6362+
6363+
/**
6364+
* Checks whether client-side media processing is enabled.
6365+
*
6366+
* Client-side media processing uses the browser's capabilities to handle
6367+
* tasks like image resizing and compression before uploading to the server.
6368+
*
6369+
* @since 7.0.0
6370+
*
6371+
* @return bool Whether client-side media processing is enabled.
6372+
*/
6373+
function wp_is_client_side_media_processing_enabled(): bool {
6374+
/**
6375+
* Filters whether client-side media processing is enabled.
6376+
*
6377+
* @since 7.0.0
6378+
*
6379+
* @param bool $enabled Whether client-side media processing is enabled. Default true.
6380+
*/
6381+
return (bool) apply_filters( 'wp_client_side_media_processing_enabled', true );
6382+
}
6383+
6384+
/**
6385+
* Sets a global JS variable to indicate that client-side media processing is enabled.
6386+
*
6387+
* @since 7.0.0
6388+
*/
6389+
function wp_set_client_side_media_processing_flag(): void {
6390+
if ( ! wp_is_client_side_media_processing_enabled() ) {
6391+
return;
6392+
}
6393+
6394+
wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' );
6395+
6396+
/*
6397+
* Register the @wordpress/vips/worker script module as a dynamic dependency
6398+
* of the wp-upload-media classic script. This ensures it is included in the
6399+
* import map so that the dynamic import() in upload-media.js can resolve it.
6400+
*/
6401+
wp_scripts()->add_data(
6402+
'wp-upload-media',
6403+
'module_dependencies',
6404+
array( '@wordpress/vips/worker' )
6405+
);
6406+
}
6407+
6408+
/**
6409+
* Enables cross-origin isolation in the block editor.
6410+
*
6411+
* Required for enabling SharedArrayBuffer for WebAssembly-based
6412+
* media processing in the editor.
6413+
*
6414+
* @since 7.0.0
6415+
*
6416+
* @link https://web.dev/coop-coep/
6417+
*/
6418+
function wp_set_up_cross_origin_isolation(): void {
6419+
if ( ! wp_is_client_side_media_processing_enabled() ) {
6420+
return;
6421+
}
6422+
6423+
$screen = get_current_screen();
6424+
6425+
if ( ! $screen ) {
6426+
return;
6427+
}
6428+
6429+
if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
6430+
return;
6431+
}
6432+
6433+
// Cross-origin isolation is not needed if users can't upload files anyway.
6434+
if ( ! current_user_can( 'upload_files' ) ) {
6435+
return;
6436+
}
6437+
6438+
wp_start_cross_origin_isolation_output_buffer();
6439+
}
6440+
6441+
/**
6442+
* Starts an output buffer to send cross-origin isolation headers.
6443+
*
6444+
* Sends headers and uses an output buffer to add crossorigin="anonymous"
6445+
* attributes where needed.
6446+
*
6447+
* @since 7.0.0
6448+
*
6449+
* @link https://web.dev/coop-coep/
6450+
*
6451+
* @global bool $is_safari
6452+
*/
6453+
function wp_start_cross_origin_isolation_output_buffer(): void {
6454+
global $is_safari;
6455+
6456+
$coep = $is_safari ? 'require-corp' : 'credentialless';
6457+
6458+
ob_start(
6459+
static function ( string $output ) use ( $coep ): string {
6460+
header( 'Cross-Origin-Opener-Policy: same-origin' );
6461+
header( "Cross-Origin-Embedder-Policy: $coep" );
6462+
6463+
return wp_add_crossorigin_attributes( $output );
6464+
}
6465+
);
6466+
}
6467+
6468+
/**
6469+
* Adds crossorigin="anonymous" to relevant tags in the given HTML string.
6470+
*
6471+
* @since 7.0.0
6472+
*
6473+
* @param string $html HTML input.
6474+
* @return string Modified HTML.
6475+
*/
6476+
function wp_add_crossorigin_attributes( string $html ): string {
6477+
$site_url = site_url();
6478+
6479+
$processor = new WP_HTML_Tag_Processor( $html );
6480+
6481+
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
6482+
$cross_origin_tag_attributes = array(
6483+
'AUDIO' => array( 'src' => false ),
6484+
'IMG' => array(
6485+
'src' => false,
6486+
'srcset' => true,
6487+
),
6488+
'LINK' => array(
6489+
'href' => false,
6490+
'imagesrcset' => true,
6491+
),
6492+
'SCRIPT' => array( 'src' => false ),
6493+
'VIDEO' => array(
6494+
'src' => false,
6495+
'poster' => false,
6496+
),
6497+
'SOURCE' => array( 'src' => false ),
6498+
);
6499+
6500+
while ( $processor->next_tag() ) {
6501+
$tag = $processor->get_tag();
6502+
6503+
if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
6504+
continue;
6505+
}
6506+
6507+
if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
6508+
$processor->set_bookmark( 'audio-video-parent' );
6509+
}
6510+
6511+
$processor->set_bookmark( 'resume' );
6512+
6513+
$sought = false;
6514+
6515+
$crossorigin = $processor->get_attribute( 'crossorigin' );
6516+
6517+
$is_cross_origin = false;
6518+
6519+
foreach ( $cross_origin_tag_attributes[ $tag ] as $attr => $is_srcset ) {
6520+
if ( $is_srcset ) {
6521+
$srcset = $processor->get_attribute( $attr );
6522+
if ( is_string( $srcset ) ) {
6523+
foreach ( explode( ',', $srcset ) as $candidate ) {
6524+
$candidate_url = strtok( trim( $candidate ), ' ' );
6525+
if ( is_string( $candidate_url ) && '' !== $candidate_url && ! str_starts_with( $candidate_url, $site_url ) && ! str_starts_with( $candidate_url, '/' ) ) {
6526+
$is_cross_origin = true;
6527+
break;
6528+
}
6529+
}
6530+
}
6531+
} else {
6532+
$url = $processor->get_attribute( $attr );
6533+
if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
6534+
$is_cross_origin = true;
6535+
}
6536+
}
6537+
6538+
if ( $is_cross_origin ) {
6539+
break;
6540+
}
6541+
}
6542+
6543+
if ( $is_cross_origin && ! is_string( $crossorigin ) ) {
6544+
if ( 'SOURCE' === $tag ) {
6545+
$sought = $processor->seek( 'audio-video-parent' );
6546+
6547+
if ( $sought ) {
6548+
$processor->set_attribute( 'crossorigin', 'anonymous' );
6549+
}
6550+
} else {
6551+
$processor->set_attribute( 'crossorigin', 'anonymous' );
6552+
}
6553+
6554+
if ( $sought ) {
6555+
$processor->seek( 'resume' );
6556+
$processor->release_bookmark( 'audio-video-parent' );
6557+
}
6558+
}
6559+
}
6560+
6561+
return $processor->get_updated_html();
6562+
}
6563+

src/wp-includes/rest-api/class-wp-rest-server.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,34 @@ public function get_index( $request ) {
13681368
'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
13691369
);
13701370

1371+
// Add media processing settings for users who can upload files.
1372+
if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) {
1373+
// Image sizes keyed by name for client-side media processing.
1374+
$available['image_sizes'] = array();
1375+
foreach ( wp_get_registered_image_subsizes() as $name => $size ) {
1376+
$available['image_sizes'][ $name ] = $size;
1377+
}
1378+
1379+
/** This filter is documented in wp-admin/includes/image.php */
1380+
$available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 );
1381+
1382+
// Image output formats.
1383+
$input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' );
1384+
$output_formats = array();
1385+
foreach ( $input_formats as $mime_type ) {
1386+
/** This filter is documented in wp-includes/class-wp-image-editor.php */
1387+
$output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type );
1388+
}
1389+
$available['image_output_formats'] = (object) $output_formats;
1390+
1391+
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
1392+
$available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' );
1393+
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
1394+
$available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' );
1395+
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
1396+
$available['gif_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' );
1397+
}
1398+
13711399
$response = new WP_REST_Response( $available );
13721400

13731401
$fields = $request['_fields'] ?? '';

0 commit comments

Comments
 (0)