Skip to content

Commit 51a5f4d

Browse files
Media: Re-introduce client-side media processing feature.
Reverts the removal in [62081] now that WordPress 7.1 has forked. Restores all PHP functions, REST API endpoints, cross-origin isolation infrastructure, VIPS script module handling, build configuration, and associated tests. Follow-up to [62081]. Props adamsilverstein, jorbin, westonruter. Fixes #64919. See #64906. git-svn-id: https://develop.svn.wordpress.org/trunk@62428 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b226691 commit 51a5f4d

16 files changed

Lines changed: 1633 additions & 24 deletions

Gruntfile.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,9 @@ module.exports = function(grunt) {
702702
src: [
703703
'**/*',
704704
'!**/*.map',
705-
'!vips/**',
705+
// Skip non-minified VIPS files — they are ~16MB of inlined WASM
706+
// with no debugging value over the minified versions.
707+
'!vips/!(*.min).js',
706708
],
707709
dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/',
708710
} ],

src/wp-includes/assets/script-loader-packages.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,10 @@
843843
'wp-url'
844844
),
845845
'module_dependencies' => array(
846-
846+
array(
847+
'id' => '@wordpress/vips/worker',
848+
'import' => 'dynamic'
849+
)
847850
),
848851
'version' => 'd359c2cccf866d7082d2'
849852
),

src/wp-includes/assets/script-modules-packages.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,24 @@
284284
),
285285
'version' => 'c5843b6c5e84b352f43b'
286286
),
287+
'vips/loader.js' => array(
288+
'dependencies' => array(
289+
290+
),
291+
'module_dependencies' => array(
292+
array(
293+
'id' => '@wordpress/vips/worker',
294+
'import' => 'dynamic'
295+
)
296+
),
297+
'version' => '07c9acb45d3e5d81829a'
298+
),
299+
'vips/worker.js' => array(
300+
'dependencies' => array(
301+
302+
),
303+
'version' => 'aff5e5c5b28ae6b73aaa'
304+
),
287305
'workflow/index.js' => array(
288306
'dependencies' => array(
289307
'react',

src/wp-includes/default-filters.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,13 @@
684684
add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 );
685685
add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' );
686686

687+
// Client-side media processing.
688+
add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' );
689+
// Cross-origin isolation for client-side media processing.
690+
add_action( 'load-post.php', 'wp_set_up_cross_origin_isolation' );
691+
add_action( 'load-post-new.php', 'wp_set_up_cross_origin_isolation' );
692+
add_action( 'load-site-editor.php', 'wp_set_up_cross_origin_isolation' );
693+
add_action( 'load-widgets.php', 'wp_set_up_cross_origin_isolation' );
687694
// Nav menu.
688695
add_filter( 'nav_menu_item_id', '_nav_menu_item_id_use_once', 10, 2 );
689696
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: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6447,3 +6447,220 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) {
64476447
return apply_filters( 'image_editor_output_format', $output_format, $filename, $mime_type );
64486448
}
64496449

6450+
/**
6451+
* Checks whether client-side media processing is enabled.
6452+
*
6453+
* Client-side media processing uses the browser's capabilities to handle
6454+
* tasks like image resizing and compression before uploading to the server.
6455+
*
6456+
* @since 7.1.0
6457+
*
6458+
* @return bool Whether client-side media processing is enabled.
6459+
*/
6460+
function wp_is_client_side_media_processing_enabled(): bool {
6461+
// This is due to SharedArrayBuffer requiring a secure context.
6462+
$host = strtolower( (string) strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) );
6463+
$enabled = ( is_ssl() || 'localhost' === $host || str_ends_with( $host, '.localhost' ) );
6464+
6465+
/**
6466+
* Filters whether client-side media processing is enabled.
6467+
*
6468+
* @since 7.1.0
6469+
*
6470+
* @param bool $enabled Whether client-side media processing is enabled. Default true if the page is served in a secure context.
6471+
*/
6472+
return (bool) apply_filters( 'wp_client_side_media_processing_enabled', $enabled );
6473+
}
6474+
6475+
/**
6476+
* Sets a global JS variable to indicate that client-side media processing is enabled.
6477+
*
6478+
* @since 7.1.0
6479+
*/
6480+
function wp_set_client_side_media_processing_flag(): void {
6481+
if ( ! wp_is_client_side_media_processing_enabled() ) {
6482+
return;
6483+
}
6484+
6485+
wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true;', 'before' );
6486+
6487+
$chromium_version = wp_get_chromium_major_version();
6488+
6489+
if ( null !== $chromium_version && $chromium_version >= 137 ) {
6490+
wp_add_inline_script( 'wp-block-editor', 'window.__documentIsolationPolicy = true;', 'before' );
6491+
}
6492+
6493+
/*
6494+
* Register the @wordpress/vips/worker script module as a dynamic dependency
6495+
* of the wp-upload-media classic script. This ensures it is included in the
6496+
* import map so that the dynamic import() in upload-media.js can resolve it.
6497+
*/
6498+
wp_scripts()->add_data(
6499+
'wp-upload-media',
6500+
'module_dependencies',
6501+
array( '@wordpress/vips/worker' )
6502+
);
6503+
}
6504+
6505+
/**
6506+
* Returns the major Chrome/Chromium version from the current request's User-Agent.
6507+
*
6508+
* Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).
6509+
*
6510+
* @since 7.1.0
6511+
*
6512+
* @return int|null The major Chrome version, or null if not a Chromium browser.
6513+
*/
6514+
function wp_get_chromium_major_version(): ?int {
6515+
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
6516+
return null;
6517+
}
6518+
if ( preg_match( '#Chrome/(\d+)#', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {
6519+
return (int) $matches[1];
6520+
}
6521+
return null;
6522+
}
6523+
6524+
/**
6525+
* Enables cross-origin isolation in the block editor.
6526+
*
6527+
* Required for enabling SharedArrayBuffer for WebAssembly-based
6528+
* media processing in the editor. Uses Document-Isolation-Policy
6529+
* on supported browsers (Chromium 137+).
6530+
*
6531+
* Skips setup when a third-party page builder overrides the block
6532+
* editor via a custom `action` query parameter, as DIP would block
6533+
* same-origin iframe access that these editors rely on.
6534+
*
6535+
* @since 7.1.0
6536+
*/
6537+
function wp_set_up_cross_origin_isolation(): void {
6538+
if ( ! wp_is_client_side_media_processing_enabled() ) {
6539+
return;
6540+
}
6541+
6542+
$screen = get_current_screen();
6543+
6544+
if ( ! $screen ) {
6545+
return;
6546+
}
6547+
6548+
if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
6549+
return;
6550+
}
6551+
6552+
/*
6553+
* Skip when a third-party page builder overrides the block editor.
6554+
* DIP isolates the document into its own agent cluster,
6555+
* which blocks same-origin iframe access that these editors rely on.
6556+
*/
6557+
if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
6558+
return;
6559+
}
6560+
6561+
// Cross-origin isolation is not needed if users can't upload files anyway.
6562+
if ( ! current_user_can( 'upload_files' ) ) {
6563+
return;
6564+
}
6565+
6566+
wp_start_cross_origin_isolation_output_buffer();
6567+
}
6568+
6569+
/**
6570+
* Sends the Document-Isolation-Policy header for cross-origin isolation.
6571+
*
6572+
* Uses an output buffer to add crossorigin="anonymous" where needed.
6573+
*
6574+
* @since 7.1.0
6575+
*/
6576+
function wp_start_cross_origin_isolation_output_buffer(): void {
6577+
$chromium_version = wp_get_chromium_major_version();
6578+
6579+
if ( null === $chromium_version || $chromium_version < 137 ) {
6580+
return;
6581+
}
6582+
6583+
ob_start(
6584+
static function ( string $output ): string {
6585+
header( 'Document-Isolation-Policy: isolate-and-credentialless' );
6586+
6587+
return wp_add_crossorigin_attributes( $output );
6588+
}
6589+
);
6590+
}
6591+
6592+
/**
6593+
* Adds crossorigin="anonymous" to relevant tags in the given HTML string.
6594+
*
6595+
* @since 7.1.0
6596+
*
6597+
* @param string $html HTML input.
6598+
* @return string Modified HTML.
6599+
*/
6600+
function wp_add_crossorigin_attributes( string $html ): string {
6601+
$site_url = site_url();
6602+
6603+
$processor = new WP_HTML_Tag_Processor( $html );
6604+
6605+
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
6606+
$cross_origin_tag_attributes = array(
6607+
'AUDIO' => array( 'src' ),
6608+
'LINK' => array( 'href' ),
6609+
'SCRIPT' => array( 'src' ),
6610+
'VIDEO' => array( 'src', 'poster' ),
6611+
'SOURCE' => array( 'src' ),
6612+
);
6613+
6614+
while ( $processor->next_tag() ) {
6615+
$tag = $processor->get_tag();
6616+
6617+
if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
6618+
continue;
6619+
}
6620+
$crossorigin = $processor->get_attribute( 'crossorigin' );
6621+
if ( null !== $crossorigin ) {
6622+
continue;
6623+
}
6624+
6625+
if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
6626+
$processor->set_bookmark( 'audio-video-parent' );
6627+
}
6628+
6629+
$processor->set_bookmark( 'resume' );
6630+
6631+
$sought = false;
6632+
6633+
$is_cross_origin = false;
6634+
6635+
foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
6636+
$url = $processor->get_attribute( $attr );
6637+
if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
6638+
$is_cross_origin = true;
6639+
}
6640+
6641+
if ( $is_cross_origin ) {
6642+
break;
6643+
}
6644+
}
6645+
6646+
if ( $is_cross_origin ) {
6647+
if ( 'SOURCE' === $tag ) {
6648+
$sought = $processor->seek( 'audio-video-parent' );
6649+
6650+
if ( $sought ) {
6651+
$processor->set_attribute( 'crossorigin', 'anonymous' );
6652+
}
6653+
} else {
6654+
$processor->set_attribute( 'crossorigin', 'anonymous' );
6655+
}
6656+
6657+
if ( $sought ) {
6658+
$processor->seek( 'resume' );
6659+
$processor->release_bookmark( 'audio-video-parent' );
6660+
}
6661+
}
6662+
}
6663+
6664+
return $processor->get_updated_html();
6665+
}
6666+

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/media.php */
1387+
$output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type );
1388+
}
1389+
$available['image_output_formats'] = (object) $output_formats;
1390+
1391+
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1392+
$available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' );
1393+
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1394+
$available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' );
1395+
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1396+
$available['gif_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' );
1397+
}
1398+
13711399
$response = new WP_REST_Response( $available );
13721400

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

0 commit comments

Comments
 (0)