Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,9 @@ module.exports = function(grunt) {
src: [
'**/*',
'!**/*.map',
'!vips/**',
// Skip non-minified VIPS files — they are ~16MB of inlined WASM
// with no debugging value over the minified versions.
'!vips/!(*.min).js',
],
dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/',
} ],
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 @@ -678,6 +678,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();
}
}
231 changes: 231 additions & 0 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -6400,3 +6400,234 @@ 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 {
// This is due to SharedArrayBuffer requiring a secure context.
$host = strtolower( (string) strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) );
$enabled = ( is_ssl() || 'localhost' === $host || str_ends_with( $host, '.localhost' ) );

/**
* Filters whether client-side media processing is enabled.
*
* @since 7.0.0
*
* @param bool $enabled Whether client-side media processing is enabled. Default true if the page is served in a secure context.
*/
return (bool) apply_filters( 'wp_client_side_media_processing_enabled', $enabled );
}

/**
* 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' );

$chromium_version = wp_get_chromium_major_version();

if ( null !== $chromium_version && $chromium_version >= 137 ) {
wp_add_inline_script( 'wp-block-editor', 'window.__documentIsolationPolicy = 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' )
);
}

/**
* Returns the major Chrome/Chromium version from the current request's User-Agent.
*
* Matches all Chromium-based browsers (Chrome, Edge, Opera, Brave).
*
* @since 7.0.0
*
* @return int|null The major Chrome version, or null if not a Chromium browser.
*/
function wp_get_chromium_major_version(): ?int {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return null;
}
if ( preg_match( '#Chrome/(\d+)#', $_SERVER['HTTP_USER_AGENT'], $matches ) ) {
return (int) $matches[1];
}
return null;
}

/**
* Enables cross-origin isolation in the block editor.
*
* Required for enabling SharedArrayBuffer for WebAssembly-based
* media processing in the editor. Uses Document-Isolation-Policy
* on supported browsers (Chromium 137+).
*
* Skips setup when a third-party page builder overrides the block
* editor via a custom `action` query parameter, as DIP would block
* same-origin iframe access that these editors rely on.
*
* @since 7.0.0
*/
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;
}

/*
* Skip when a third-party page builder overrides the block editor.
* DIP isolates the document into its own agent cluster,
* which blocks same-origin iframe access that these editors rely on.
*/
if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
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();
}

/**
* Sends the Document-Isolation-Policy header for cross-origin isolation.
*
* Uses an output buffer to add crossorigin="anonymous" where needed.
*
* @since 7.0.0
*/
function wp_start_cross_origin_isolation_output_buffer(): void {
$chromium_version = wp_get_chromium_major_version();

if ( null === $chromium_version || $chromium_version < 137 ) {
return;
}

ob_start(
static function ( string $output ): string {
header( 'Document-Isolation-Policy: isolate-and-credentialless' );

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 ),
'LINK' => array( 'href' => false ),
'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/media.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-gd.php */
$available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' );
/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
$available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' );
/** This filter is documented in wp-includes/class-wp-image-editor-gd.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