Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experiment for client-side media processing #64650

Merged
merged 33 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
07699c3
Add experiment and PHP changes for client-side media processing
swissspidy Aug 20, 2024
d382ba5
Rephrase description
swissspidy Aug 23, 2024
e53b5e5
Rephrase label
swissspidy Aug 23, 2024
46bd626
Add explanatory comment
swissspidy Aug 23, 2024
4608113
Remove comment
swissspidy Aug 23, 2024
4e1f6ff
Use `edit_media_item_permissions_check`
swissspidy Aug 23, 2024
de817ea
Use `wp_basename`
swissspidy Aug 23, 2024
bf57db2
Wrap require call in `gutenberg_is_experiment_enabled`
swissspidy Aug 23, 2024
84530f6
Don't send 201 status
swissspidy Aug 23, 2024
d70a39f
Add `wp_attachment_is` safeguards
swissspidy Aug 23, 2024
08eb4fd
Use `get_post()` method
swissspidy Aug 23, 2024
4ad56ae
Move filter to private method
swissspidy Aug 23, 2024
c665ce6
Override `get_endpoint_args_for_item_schema` instead
swissspidy Aug 23, 2024
71af56f
Use `empty`
swissspidy Aug 23, 2024
ce396de
Fix condition
swissspidy Aug 23, 2024
1296a8f
Lint fixes
swissspidy Aug 23, 2024
c2d6da0
Merge branch 'trunk' into fix/61447-add-experiment
swissspidy Aug 23, 2024
9e27064
Merge branch 'trunk' into fix/61447-add-experiment
swissspidy Aug 25, 2024
159c541
Undo now obsolete change
swissspidy Aug 25, 2024
7278d79
use `rest_get_route_for_post`
swissspidy Aug 25, 2024
b0afa6a
Update error messa
swissspidy Aug 26, 2024
0677e7b
Use array access
swissspidy Aug 26, 2024
250b2c1
Use 400 status
swissspidy Aug 26, 2024
90920a9
Use route path, not URL
swissspidy Aug 26, 2024
c399660
Add image sizes enum
swissspidy Aug 26, 2024
226b693
No strict comparison
swissspidy Aug 26, 2024
824bf3c
Add comment
swissspidy Aug 26, 2024
84040cf
Expand explanatory comment
swissspidy Aug 26, 2024
1c36d96
Remove errant trailing comma
swissspidy Aug 26, 2024
3d3f3b7
Add comment to filter
swissspidy Aug 26, 2024
9403688
Update enum
swissspidy Aug 26, 2024
39fb4f8
Merge branch 'trunk' into fix/61447-add-experiment
swissspidy Aug 26, 2024
a0ddc82
Use class method in closure instead
swissspidy Aug 27, 2024
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
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings-ui', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindingsUI = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-media-processing', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalMediaProcessing = true', 'before' );
}
}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
<?php
/**
* Class Gutenberg_REST_Attachments_Controller.
*
* @package MediaExperiments
*/

/**
* Class Gutenberg_REST_Attachments_Controller.
*/
class Gutenberg_REST_Attachments_Controller extends WP_REST_Attachments_Controller {
/**
* Registers the routes for attachments.
*
* @see register_rest_route()
*/
public function register_routes(): void {
parent::register_routes();

$valid_image_sizes = array_keys( wp_get_registered_image_subsizes() );

// Special case to set 'original_image' in attachment metadata.
$valid_image_sizes[] = 'original';

register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/sideload',
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
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.', 'gutenberg' ),
'type' => 'integer',
),
'image_size' => array(
'description' => __( 'Image size.', 'gutenberg' ),
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
'type' => 'string',
'enum' => $valid_image_sizes,
'required' => true,
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Retrieves an array of endpoint arguments from the item schema for the controller.
*
* @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, this is not done
* on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
$args = rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );

if ( WP_REST_Server::CREATABLE === $method ) {
$args['generate_sub_sizes'] = array(
'type' => 'boolean',
'default' => true,
'description' => __( 'Whether to generate image sub sizes.', 'gutenberg' ),
);
$args['convert_format'] = array(
'type' => 'boolean',
'default' => true,
'description' => __( 'Whether to convert image formats.', 'gutenberg' ),
);
}

return $args;
}

/**
* Prepares a single attachment output for response.
*
* Ensures 'missing_image_sizes' is set for PDFs and not just images.
*
* @param WP_Post $item Attachment object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ): WP_REST_Response {
$response = parent::prepare_item_for_response( $item, $request );

$data = $response->get_data();

// Handle missing image sizes for PDFs.

$fields = $this->get_fields_for_response( $request );

if (
rest_is_field_included( 'missing_image_sizes', $fields ) &&
empty( $data['missing_image_sizes'] )
) {
$mime_type = get_post_mime_type( $item );

if ( 'application/pdf' === $mime_type ) {
$metadata = wp_get_attachment_metadata( $item->ID, true );

if ( ! is_array( $metadata ) ) {
$metadata = array();
}

$metadata['sizes'] = $metadata['sizes'] ?? array();

$fallback_sizes = array(
'thumbnail',
'medium',
'large',
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
);

// The filter might have been added by ::create_item().
remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

/** This filter is documented in wp-admin/includes/image.php */
$fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

$registered_sizes = wp_get_registered_image_subsizes();
$merged_sizes = array_keys( array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ) );

$missing_image_sizes = array_diff( $merged_sizes, array_keys( $metadata['sizes'] ) );
$data['missing_image_sizes'] = $missing_image_sizes;
}
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

$links = $response->get_links();

$response = rest_ensure_response( $data );

foreach ( $links as $rel => $rel_links ) {
foreach ( $rel_links as $link ) {
$response->add_link( $rel, $link['href'], $link['attributes'] );
}
}

return $response;
}

/**
* Creates a single attachment.
*
* @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 create_item( $request ) {
if ( ! $request['generate_sub_sizes'] ) {
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );

}

if ( ! $request['convert_format'] ) {
add_filter( 'image_editor_output_format', '__return_empty_array', 100 );
}

$response = parent::create_item( $request );

remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );
remove_filter( 'image_editor_output_format', '__return_empty_array', 100 );
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

return $response;
}


/**
* Checks if a given request has access to sideload a file.
*
* Sideloading a file for an existing attachment
* requires both update and create permissions.
*
* @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 );
}
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns a closure for filtering {@see 'wp_unique_filename'} during sideloads.
*
* {@see 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.
*
* @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582
*
* @param string $attachment_filename Attachment file name.
* @return callable Function to add to the filter.
*/
private function get_wp_unique_filename_filter( $attachment_filename ) {
/**
* Filters the result when generating a unique file name.
*
* @param string $filename Unique file name.
* @param string $ext File extension. Example: ".png".
* @param string $dir Directory path.
* @param callable|null $unique_filename_callback Callback function that generates the unique file name.
* @param string[] $alt_filenames Array of alternate file names that were checked for collisions.
* @param int|string $number The highest number that was used to make the file name unique
* or an empty string if unused.
*
* @return string Filtered file name.
*/
return static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
if ( empty( $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+)-' . $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;
};
}

/**
* Side-loads a media file without creating an attachment.
*
* @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 = $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.', 'gutenberg' ),
array( 'status' => 400 )
);
}

if ( ! $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 https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582
* 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 = $this->get_wp_unique_filename_filter( $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().
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
// The post date doesn't usually matter for pages, so don't backdate this upload.
if ( $parent_post && 'page' !== $parent_post->post_type && substr( $parent_post->post_date, 0, 4 ) > 0 ) {
$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 );
} 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;
}
}
Loading
Loading