mirror of
https://github.com/WordPress/WordPress.git
synced 2026-06-19 07:37:07 +00:00
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. Built from https://develop.svn.wordpress.org/trunk@62428 git-svn-id: http://core.svn.wordpress.org/trunk@61709 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
@@ -843,7 +843,10 @@
|
||||
'wp-url'
|
||||
),
|
||||
'module_dependencies' => array(
|
||||
|
||||
array(
|
||||
'id' => '@wordpress/vips/worker',
|
||||
'import' => 'dynamic'
|
||||
)
|
||||
),
|
||||
'version' => 'd359c2cccf866d7082d2'
|
||||
),
|
||||
|
||||
@@ -284,6 +284,24 @@
|
||||
),
|
||||
'version' => 'c5843b6c5e84b352f43b'
|
||||
),
|
||||
'vips/loader.js' => array(
|
||||
'dependencies' => array(
|
||||
|
||||
),
|
||||
'module_dependencies' => array(
|
||||
array(
|
||||
'id' => '@wordpress/vips/worker',
|
||||
'import' => 'dynamic'
|
||||
)
|
||||
),
|
||||
'version' => '07c9acb45d3e5d81829a'
|
||||
),
|
||||
'vips/worker.js' => array(
|
||||
'dependencies' => array(
|
||||
|
||||
),
|
||||
'version' => 'aff5e5c5b28ae6b73aaa'
|
||||
),
|
||||
'workflow/index.js' => array(
|
||||
'dependencies' => array(
|
||||
'react',
|
||||
|
||||
@@ -684,6 +684,13 @@ add_action( 'customize_controls_enqueue_scripts', 'wp_plupload_default_settings'
|
||||
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 );
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'module_dependencies' => array(array('id' => '@wordpress/vips/worker', 'import' => 'dynamic')), 'version' => '07c9acb45d3e5d81829a');
|
||||
@@ -0,0 +1 @@
|
||||
function r(){return import("@wordpress/vips/worker")}export{r as default};
|
||||
@@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array(), 'version' => 'aff5e5c5b28ae6b73aaa');
|
||||
+5255
File diff suppressed because one or more lines are too long
@@ -156,6 +156,12 @@ function wp_underscore_video_template() {
|
||||
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.' ),
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6447,3 +6447,220 @@ 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.1.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.1.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.1.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.1.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.1.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.1.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.1.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' ),
|
||||
'LINK' => array( 'href' ),
|
||||
'SCRIPT' => array( 'src' ),
|
||||
'VIDEO' => array( 'src', 'poster' ),
|
||||
'SOURCE' => array( 'src' ),
|
||||
);
|
||||
|
||||
while ( $processor->next_tag() ) {
|
||||
$tag = $processor->get_tag();
|
||||
|
||||
if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
|
||||
continue;
|
||||
}
|
||||
$crossorigin = $processor->get_attribute( 'crossorigin' );
|
||||
if ( null !== $crossorigin ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
|
||||
$processor->set_bookmark( 'audio-video-parent' );
|
||||
}
|
||||
|
||||
$processor->set_bookmark( 'resume' );
|
||||
|
||||
$sought = false;
|
||||
|
||||
$is_cross_origin = false;
|
||||
|
||||
foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
|
||||
$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 ) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1368,6 +1368,34 @@ class WP_REST_Server {
|
||||
'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'] ?? '';
|
||||
|
||||
@@ -63,6 +63,97 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
'args' => $this->get_edit_media_item_args(),
|
||||
)
|
||||
);
|
||||
|
||||
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';
|
||||
// Client-side big image threshold: sideload the scaled version.
|
||||
$valid_image_sizes[] = 'scaled';
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\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' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\d]+)/finalize',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'finalize_item' ),
|
||||
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Unique identifier for the attachment.' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
),
|
||||
),
|
||||
'allow_batch' => $this->allow_batch,
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the query params for the attachments collection.
|
||||
*
|
||||
* @since 7.1.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<string, array<string, mixed>> 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 && wp_is_client_side_media_processing_enabled() ) {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +252,12 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
*/
|
||||
$prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, $files['file']['type'] ?? null );
|
||||
|
||||
// When the client handles image processing (generate_sub_sizes is false),
|
||||
// skip the server-side image editor support check.
|
||||
if ( false === $request['generate_sub_sizes'] ) {
|
||||
$prevent_unsupported_uploads = false;
|
||||
}
|
||||
|
||||
// If the upload is an image, check if the server can handle the mime type.
|
||||
if (
|
||||
$prevent_unsupported_uploads &&
|
||||
@@ -192,6 +289,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
* Creates a single attachment.
|
||||
*
|
||||
* @since 4.7.0
|
||||
* @since 7.1.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 +303,24 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle generate_sub_sizes parameter.
|
||||
if ( false === $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 ( false === $request['convert_format'] ) {
|
||||
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
|
||||
}
|
||||
|
||||
$insert = $this->insert_attachment( $request );
|
||||
|
||||
if ( is_wp_error( $insert ) ) {
|
||||
$this->remove_client_side_media_processing_filters();
|
||||
return $insert;
|
||||
}
|
||||
|
||||
@@ -225,6 +338,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -233,6 +347,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
@@ -241,12 +356,14 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -283,6 +400,8 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
*/
|
||||
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
|
||||
|
||||
$this->remove_client_side_media_processing_filters();
|
||||
|
||||
$response = $this->prepare_item_for_response( $attachment, $request );
|
||||
$response = rest_ensure_response( $response );
|
||||
$response->set_status( 201 );
|
||||
@@ -291,6 +410,18 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes filters added for client-side media processing.
|
||||
*
|
||||
* @since 7.1.0
|
||||
*/
|
||||
private function remove_client_side_media_processing_filters(): void {
|
||||
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.
|
||||
*
|
||||
@@ -1856,4 +1987,272 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
|
||||
|
||||
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 7.1.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 7.1.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 = (int) $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 ( false === $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 /wp-includes/functions.php.
|
||||
* 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 = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
|
||||
return self::filter_wp_unique_filename( $filename, $dir, $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 && ! str_starts_with( $parent_post->post_date, '0000-00-00' ) ) {
|
||||
$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 );
|
||||
} elseif ( 'scaled' === $image_size ) {
|
||||
// The current attached file is the original; record it as original_image.
|
||||
$current_file = get_attached_file( $attachment_id, true );
|
||||
|
||||
if ( ! $current_file ) {
|
||||
return new WP_Error(
|
||||
'rest_sideload_no_attached_file',
|
||||
__( 'Unable to retrieve the attached file for this attachment.' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$metadata['original_image'] = wp_basename( $current_file );
|
||||
|
||||
// Validate the scaled image before updating the attached file.
|
||||
$size = wp_getimagesize( $path );
|
||||
$filesize = wp_filesize( $path );
|
||||
|
||||
if ( ! $size || ! $filesize ) {
|
||||
return new WP_Error(
|
||||
'rest_sideload_invalid_image',
|
||||
__( 'Unable to read the scaled image file.' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
// Update the attached file to point to the scaled version.
|
||||
if (
|
||||
get_attached_file( $attachment_id, true ) !== $path &&
|
||||
! update_attached_file( $attachment_id, $path )
|
||||
) {
|
||||
return new WP_Error(
|
||||
'rest_sideload_update_attached_file_failed',
|
||||
__( 'Unable to update the attached file for this attachment.' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
$metadata['width'] = $size[0];
|
||||
$metadata['height'] = $size[1];
|
||||
$metadata['filesize'] = $filesize;
|
||||
$metadata['file'] = _wp_relative_upload_path( $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 7.1.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 $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, $dir, $number, $attachment_filename ) {
|
||||
if ( ! is_int( $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+|scaled)-' . $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes an attachment after client-side media processing.
|
||||
*
|
||||
* Triggers the 'wp_generate_attachment_metadata' filter so that
|
||||
* server-side plugins can process the attachment after all client-side
|
||||
* operations (upload, thumbnail generation, sideloads) are complete.
|
||||
*
|
||||
* @since 7.1.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 finalize_item( WP_REST_Request $request ) {
|
||||
$attachment_id = (int) $request['id'];
|
||||
|
||||
$post = $this->get_post( $attachment_id );
|
||||
if ( is_wp_error( $post ) ) {
|
||||
return $post;
|
||||
}
|
||||
|
||||
$metadata = wp_get_attachment_metadata( $attachment_id );
|
||||
if ( ! is_array( $metadata ) ) {
|
||||
$metadata = array();
|
||||
}
|
||||
|
||||
/** This filter is documented in wp-admin/includes/image.php */
|
||||
$metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' );
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
return $this->prepare_item_for_response( $post, $response_request );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,11 @@ function wp_default_script_modules() {
|
||||
wp_interactivity()->add_client_navigation_support_to_script_module( $script_module_id );
|
||||
}
|
||||
|
||||
if ( '' !== $suffix ) {
|
||||
// VIPS files are always minified — the non-minified versions are not
|
||||
// shipped because they are ~10MB of inlined WASM with no debugging value.
|
||||
if ( str_starts_with( $file_name, 'vips/' ) ) {
|
||||
$file_name = str_replace( '.js', '.min.js', $file_name );
|
||||
} elseif ( '' !== $suffix ) {
|
||||
$file_name = str_replace( '.js', $suffix . '.js', $file_name );
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* @global string $wp_version
|
||||
*/
|
||||
$wp_version = '7.1-alpha-62427';
|
||||
$wp_version = '7.1-alpha-62428';
|
||||
|
||||
/**
|
||||
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
|
||||
|
||||
Reference in New Issue
Block a user