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 1 commit
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,350 @@
<?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();

$args = $this->get_endpoint_args_for_item_schema();
$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 support server-side format conversion.', 'gutenberg' ),
);
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $args,
),
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
),
true
);

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',
'required' => true,
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* 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 {
$fields = $this->get_fields_for_response( $request );
$response = parent::prepare_item_for_response( $item, $request );

$data = $response->get_data();

if ( rest_is_field_included( 'missing_image_sizes', $fields ) ) {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
$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
);

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;
}
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
}

$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 ( false === $request['generate_sub_sizes'] ) {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 );
add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 );

}

if ( false === $request['convert_format'] ) {
// Prevent image conversion as that is done client-side.
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
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 ) {
$post = $this->get_post( $request['id'] );

if ( is_wp_error( $post ) ) {
return $post;
}

if ( ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this post.', 'gutenberg' ),
array( 'status' => rest_authorization_required_code() )
);
}

if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to upload media on this site.', 'gutenberg' ),
array( 'status' => 400 )
);
}

return true;
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
}

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

if ( 'attachment' !== get_post_type( $attachment_id ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'Invalid parent type.', 'gutenberg' ),
array( 'status' => 400 )
);
}
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
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 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 ? basename( $attachment_filename ) : null;

/**
* 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.
*/
$filter_filename = 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;
};

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'] = 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' => basename( $path ),
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
'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_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) )
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
);

$response_request->set_param( 'context', 'edit' );
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

if ( isset( $request['_fields'] ) ) {
$response_request['_fields'] = $request['_fields'];
}

$response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request );

$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

return $response;
}
}
Loading
Loading