From b566abe3639a5a657197dc24067b636af87d954e Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 13 Aug 2019 10:22:59 -0700 Subject: [PATCH 1/2] Revert "Revert "FSE: Add API endpoint to side-load images (#34823)" (#35328)" This reverts commit 8aff6a8356cfaa8bac5276409ca3a515f8fc5237. --- .../class-starter-page-templates.php | 23 ++ ...lass-wp-rest-sideload-image-controller.php | 284 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php diff --git a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-starter-page-templates.php b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-starter-page-templates.php index ba8907b5449cd..5abf04545e148 100644 --- a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-starter-page-templates.php +++ b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-starter-page-templates.php @@ -25,7 +25,9 @@ class Starter_Page_Templates { private function __construct() { add_action( 'init', [ $this, 'register_scripts' ] ); add_action( 'init', [ $this, 'register_meta_field' ] ); + add_action( 'rest_api_init', [ $this, 'register_rest_api' ] ); add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_assets' ] ); + add_action( 'delete_attachment', [ $this, 'clear_sideloaded_image_cache' ] ); } /** @@ -71,6 +73,15 @@ public function register_meta_field() { register_meta( 'post', '_starter_page_template', $args ); } + /** + * Register rest api endpoint for side-loading images. + */ + public function register_rest_api() { + require_once __DIR__ . '/class-wp-rest-sideload-image-controller.php'; + + ( new WP_REST_Sideload_Image_Controller() )->register_routes(); + } + /** * Pass error message to frontend JavaScript console. * @@ -193,6 +204,18 @@ public function fetch_vertical_data() { return $vertical_templates; } + /** + * Deletes cached attachment data when attachment gets deleted. + * + * @param int $id Attachment ID of the attachment to be deleted. + */ + public function clear_sideloaded_image_cache( $id ) { + $url = get_post_meta( $id, '_sideloaded_url', true ); + if ( ! empty( $url ) ) { + delete_transient( 'fse_sideloaded_image_' . md5( $url ) ); + } + } + /** * Returns ISO 639 conforming locale string. * diff --git a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php new file mode 100644 index 0000000000000..c625a4156cc89 --- /dev/null +++ b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php @@ -0,0 +1,284 @@ +namespace = 'fse/v1'; + $this->rest_base = 'sideload/image'; + } + + /** + * Register available routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'create_item' ], + 'permission_callback' => [ $this, 'create_item_permissions_check' ], + 'show_in_index' => false, + 'args' => $this->get_collection_params(), + ], + 'schema' => [ $this, 'get_item_schema' ], + ] + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + [ + [ + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'create_items' ], + 'show_in_index' => false, + 'args' => [ + 'resources' => [ + 'description' => 'URL to the image to be side-loaded.', + 'type' => 'array', + 'required' => true, + 'items' => [ + 'type' => 'object', + 'properties' => $this->get_collection_params(), + ], + ], + ], + ], + ] + ); + } + + /** + * Creates a single attachment. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. + */ + public function create_item( $request ) { + if ( ! empty( $request['post_id'] ) && in_array( get_post_type( $request['post_id'] ), [ 'revision', 'attachment' ], true ) ) { + return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), [ 'status' => 400 ] ); + } + + $inserted = false; + $attachment = $this->get_attachment( $request->get_param( 'url' ) ); + if ( ! $attachment ) { + // Include image functions to get access to wp_read_image_metadata(). + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + // The post ID on success, WP_Error on failure. + $id = media_sideload_image( + $request->get_param( 'url' ), + $request->get_param( 'post_id' ), + null, + 'id' + ); + + if ( is_wp_error( $id ) ) { + if ( 'db_update_error' === $id->get_error_code() ) { + $id->add_data( [ 'status' => 500 ] ); + } else { + $id->add_data( [ 'status' => 400 ] ); + } + + return rest_ensure_response( $id ); // Return error. + } + + $attachment = get_post( $id ); + + /** + * Fires after a single attachment is created or updated via the REST API. + * + * @param WP_Post $attachment Inserted or updated attachment object. + * @param WP_REST_Request $request The request sent to the API. + * @param bool $creating True when creating an attachment, false when updating. + */ + do_action( 'rest_insert_attachment', $attachment, $request, true ); + + if ( isset( $request['alt_text'] ) ) { + update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) ); + } + + update_post_meta( $id, '_sideloaded_url', $request->get_param( 'url' ) ); + + $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); + + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $inserted = true; + $request->set_param( 'context', 'edit' ); + + /** + * Fires after a single attachment is completely created or updated via the REST API. + * + * @param WP_Post $attachment Inserted or updated attachment object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating an attachment, false when updating. + */ + do_action( 'rest_after_insert_attachment', $attachment, $request, true ); + } + + $response = $this->prepare_item_for_response( $attachment, $request ); + $response = rest_ensure_response( $response ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', 'wp/v2', 'media', $attachment->ID ) ) ); + + if ( $inserted ) { + $response->set_status( 201 ); + } + + return $response; + } + + /** + * Creates a batch of attachments. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. + */ + public function create_items( $request ) { + $data = []; + + // Foreach request specified in the requests param, run the endpoint. + foreach ( $request['resources'] as $resource ) { + $request = new WP_REST_Request( 'POST', "/{$this->namespace}/{$this->rest_base}" ); + + // Add specified request parameters into the request. + foreach ( $resource as $param_name => $param_value ) { + $request->set_param( $param_name, $param_value ); + } + + $response = rest_do_request( $request ); + $data[] = $this->prepare_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a response for inserting into a collection of responses. + * + * @param WP_REST_Response $response Response object. + * @return array|WP_REST_Response Response data, ready for insertion into collection data. + */ + public function prepare_for_collection( $response ) { + if ( ! ( $response instanceof WP_REST_Response ) ) { + return $response; + } + + $data = (array) $response->get_data(); + $server = rest_get_server(); + + if ( method_exists( $server, 'get_compact_response_links' ) ) { + $links = call_user_func( [ $server, 'get_compact_response_links' ], $response ); + } else { + $links = call_user_func( [ $server, 'get_response_links' ], $response ); + } + + if ( ! empty( $links ) ) { + $data['_links'] = $links; + } + + return $data; + } + + /** + * Prepares a single attachment output for response. + * + * @param WP_Post $post Attachment object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $post, $request ) { + $response = parent::prepare_item_for_response( $post, $request ); + $base = 'wp/v2/media'; + + foreach ( [ 'self', 'collection', 'about' ] as $link ) { + $response->remove_link( $link ); + + } + + $response->add_link( 'self', rest_url( trailingslashit( $base ) . $post->ID ) ); + $response->add_link( 'collection', rest_url( $base ) ); + $response->add_link( 'about', rest_url( 'wp/v2/types/' . $post->post_type ) ); + + return $response; + } + + /** + * Gets the attachment if an image has been sideloaded previously. + * + * @param string $url URL of the image to sideload. + * @return object|bool Attachment object on success, false on failure. + */ + public function get_attachment( $url ) { + $cache_key = 'fse_sideloaded_image_' . md5( $url ); + $attachment = get_transient( $cache_key ); + + if ( false === $attachment ) { + $attachments = new WP_Query( + [ + 'no_found_rows' => true, + 'posts_per_page' => 1, + 'post_status' => 'inherit', + 'post_type' => 'attachment', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => [ + [ + 'key' => '_sideloaded_url', + 'value' => $url, + ], + ], + ] + ); + + if ( $attachments->have_posts() ) { + $attachment = $attachments->post; + set_transient( $cache_key, $attachment ); + } + } + + return $attachment; + } + + /** + * Returns the endpoints request parameters. + * + * @return array Request parameters. + */ + public function get_collection_params() { + return [ + 'url' => [ + 'description' => 'URL to the image to be side-loaded.', + 'type' => 'string', + 'required' => true, + 'format' => 'uri', + 'sanitize_callback' => function( $url ) { + return esc_url_raw( strtok( $url, '?' ) ); + }, + ], + 'post_id' => [ + 'description' => 'ID of the post to associate the image with', + 'type' => 'integer', + 'default' => 0, + ], + ]; + } +} From 97eb273fe17e2f241d5007cded191ca1f0a239ad Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 13 Aug 2019 10:31:47 -0700 Subject: [PATCH 2/2] Add proper namespacing --- ...lass-wp-rest-sideload-image-controller.php | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php index c625a4156cc89..c547eaaa88c26 100644 --- a/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php +++ b/apps/full-site-editing/full-site-editing-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php @@ -2,13 +2,15 @@ /** * WP_REST_Sideload_Image_Controller file. * - * @package full-site-editing + * @package A8C\FSE */ +namespace A8C\FSE; + /** * Class WP_REST_Sideload_Image_Controller. */ -class WP_REST_Sideload_Image_Controller extends WP_REST_Attachments_Controller { +class WP_REST_Sideload_Image_Controller extends \WP_REST_Attachments_Controller { /** * WP_REST_Sideload_Image_Controller constructor. @@ -29,7 +31,7 @@ public function register_routes() { '/' . $this->rest_base, [ [ - 'methods' => WP_REST_Server::CREATABLE, + 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_item' ], 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'show_in_index' => false, @@ -44,7 +46,7 @@ public function register_routes() { '/' . $this->rest_base . '/batch', [ [ - 'methods' => WP_REST_Server::CREATABLE, + 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_items' ], 'show_in_index' => false, 'args' => [ @@ -66,12 +68,12 @@ public function register_routes() { /** * Creates a single attachment. * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_Error|\WP_REST_Response Response object on success, WP_Error object on failure. */ public function create_item( $request ) { if ( ! empty( $request['post_id'] ) && in_array( get_post_type( $request['post_id'] ), [ 'revision', 'attachment' ], true ) ) { - return new WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), [ 'status' => 400 ] ); + return new \WP_Error( 'rest_invalid_param', __( 'Invalid parent type.' ), [ 'status' => 400 ] ); } $inserted = false; @@ -150,15 +152,15 @@ public function create_item( $request ) { /** * Creates a batch of attachments. * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response Response object on success, WP_Error object on failure. + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_Error|\WP_REST_Response Response object on success, WP_Error object on failure. */ public function create_items( $request ) { $data = []; // Foreach request specified in the requests param, run the endpoint. foreach ( $request['resources'] as $resource ) { - $request = new WP_REST_Request( 'POST', "/{$this->namespace}/{$this->rest_base}" ); + $request = new \WP_REST_Request( 'POST', "/{$this->namespace}/{$this->rest_base}" ); // Add specified request parameters into the request. foreach ( $resource as $param_name => $param_value ) { @@ -175,11 +177,11 @@ public function create_items( $request ) { /** * Prepare a response for inserting into a collection of responses. * - * @param WP_REST_Response $response Response object. - * @return array|WP_REST_Response Response data, ready for insertion into collection data. + * @param \WP_REST_Response $response Response object. + * @return array|\WP_REST_Response Response data, ready for insertion into collection data. */ public function prepare_for_collection( $response ) { - if ( ! ( $response instanceof WP_REST_Response ) ) { + if ( ! ( $response instanceof \WP_REST_Response ) ) { return $response; } @@ -202,9 +204,9 @@ public function prepare_for_collection( $response ) { /** * Prepares a single attachment output for response. * - * @param WP_Post $post Attachment object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. + * @param \WP_Post $post Attachment object. + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response Response object. */ public function prepare_item_for_response( $post, $request ) { $response = parent::prepare_item_for_response( $post, $request ); @@ -233,7 +235,7 @@ public function get_attachment( $url ) { $attachment = get_transient( $cache_key ); if ( false === $attachment ) { - $attachments = new WP_Query( + $attachments = new \WP_Query( [ 'no_found_rows' => true, 'posts_per_page' => 1,