diff options
Diffstat (limited to 'wp-includes/rest-api')
19 files changed, 2290 insertions, 91 deletions
diff --git a/wp-includes/rest-api/class-wp-rest-server.php b/wp-includes/rest-api/class-wp-rest-server.php index a1bc4b9..b85c020 100644 --- a/wp-includes/rest-api/class-wp-rest-server.php +++ b/wp-includes/rest-api/class-wp-rest-server.php @@ -88,6 +88,14 @@ class WP_REST_Server { protected $embed_cache = array(); /** + * Stores request objects that are currently being handled. + * + * @since 6.5.0 + * @var array + */ + protected $dispatching_requests = array(); + + /** * Instantiates the REST server. * * @since 4.4.0 @@ -467,18 +475,20 @@ class WP_REST_Server { $this->set_status( $code ); /** - * Filters whether to send nocache headers on a REST API request. + * Filters whether to send no-cache headers on a REST API request. * * @since 4.4.0 - * @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from rest-api.php + * @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from wp-includes/rest-api.php. * * @param bool $rest_send_nocache_headers Whether to send no-cache headers. */ $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() ); - // send no cache headers if the $send_no_cache_headers is true - // OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4x response code. - if ( $send_no_cache_headers || ( true === $method_overridden && strpos( $code, '4' ) === 0 ) ) { + /* + * Send no-cache headers if $send_no_cache_headers is true, + * OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code. + */ + if ( $send_no_cache_headers || ( true === $method_overridden && str_starts_with( $code, '4' ) ) ) { foreach ( wp_get_nocache_headers() as $header => $header_value ) { if ( empty( $header_value ) ) { $this->remove_header( $header ); @@ -738,6 +748,13 @@ class WP_REST_Server { $request['context'] = 'embed'; } + if ( empty( $request['per_page'] ) ) { + $matched = $this->match_request_to_handler( $request ); + if ( ! is_wp_error( $matched ) && isset( $matched[1]['args']['per_page']['maximum'] ) ) { + $request['per_page'] = (int) $matched[1]['args']['per_page']['maximum']; + } + } + $response = $this->dispatch( $request ); /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ @@ -981,6 +998,8 @@ class WP_REST_Server { * @return WP_REST_Response Response returned by the callback. */ public function dispatch( $request ) { + $this->dispatching_requests[] = $request; + /** * Filters the pre-calculated result of a REST API dispatch request. * @@ -1006,6 +1025,7 @@ class WP_REST_Server { $result = $this->error_to_response( $result ); } + array_pop( $this->dispatching_requests ); return $result; } @@ -1013,7 +1033,9 @@ class WP_REST_Server { $matched = $this->match_request_to_handler( $request ); if ( is_wp_error( $matched ) ) { - return $this->error_to_response( $matched ); + $response = $this->error_to_response( $matched ); + array_pop( $this->dispatching_requests ); + return $response; } list( $route, $handler ) = $matched; @@ -1038,7 +1060,22 @@ class WP_REST_Server { } } - return $this->respond_to_request( $request, $route, $handler, $error ); + $response = $this->respond_to_request( $request, $route, $handler, $error ); + array_pop( $this->dispatching_requests ); + return $response; + } + + /** + * Returns whether the REST server is currently dispatching / responding to a request. + * + * This may be a standalone REST API request, or an internal request dispatched from within a regular page load. + * + * @since 6.5.0 + * + * @return bool Whether the REST server is currently handling a request. + */ + public function is_dispatching() { + return (bool) $this->dispatching_requests; } /** diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 7367c0f..804d22a 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -171,6 +171,14 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) ); } + if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + $thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment_id ); + + if ( is_wp_error( $thumbnail_update ) ) { + return $thumbnail_update; + } + } + if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { $meta_update = $this->meta->update_value( $request['meta'], $attachment_id ); @@ -186,6 +194,12 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { return $fields_update; } + $terms_update = $this->handle_terms( $attachment_id, $request ); + + if ( is_wp_error( $terms_update ) ) { + return $terms_update; + } + $request->set_param( 'context', 'edit' ); /** @@ -201,7 +215,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { wp_after_insert_post( $attachment, false, null ); - if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + if ( wp_is_serving_rest_request() ) { /* * Set a custom header with the attachment_id. * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. @@ -317,6 +331,43 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { } /** + * Determines the featured media based on a request param. + * + * @since 6.5.0 + * + * @param int $featured_media Featured Media ID. + * @param int $post_id Post ID. + * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error. + */ + protected function handle_featured_media( $featured_media, $post_id ) { + $post_type = get_post_type( $post_id ); + $thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' ); + + // Similar check as in wp_insert_post(). + if ( ! $thumbnail_support && get_post_mime_type( $post_id ) ) { + if ( wp_attachment_is( 'audio', $post_id ) ) { + $thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' ); + } elseif ( wp_attachment_is( 'video', $post_id ) ) { + $thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' ); + } + } + + if ( $thumbnail_support ) { + return parent::handle_featured_media( $featured_media, $post_id ); + } + + return new WP_Error( + 'rest_no_featured_media', + sprintf( + /* translators: %s: attachment mime type */ + __( 'This site does not support post thumbnails on attachments with MIME type %s.' ), + get_post_mime_type( $post_id ) + ), + array( 'status' => 400 ) + ); + } + + /** * Updates a single attachment. * * @since 4.7.0 @@ -349,6 +400,14 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { $attachment = get_post( $request['id'] ); + if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + $thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment->ID ); + + if ( is_wp_error( $thumbnail_update ) ) { + return $thumbnail_update; + } + } + $fields_update = $this->update_additional_fields_for_object( $attachment, $request ); if ( is_wp_error( $fields_update ) ) { @@ -450,7 +509,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { ); } - $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ); + $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' ); $mime_type = get_post_mime_type( $attachment_id ); if ( ! in_array( $mime_type, $supported_types, true ) ) { return new WP_Error( @@ -630,7 +689,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) ); } - if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + if ( wp_is_serving_rest_request() ) { /* * Set a custom header with the attachment_id. * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error. diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php index 852143d..38ac0e1 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php @@ -240,6 +240,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { * @since 5.5.0 * @since 5.9.0 Renamed `$block_type` to `$item` to match parent class for PHP 8 named parameter support. * @since 6.3.0 Added `selectors` field. + * @since 6.5.0 Added `view_script_module_ids` field. * * @param WP_Block_Type $item Block type data. * @param WP_REST_Request $request Full details about the request. @@ -280,6 +281,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'keywords', 'parent', 'ancestor', + 'allowed_blocks', 'provides_context', 'uses_context', 'selectors', @@ -290,8 +292,10 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'editor_script_handles', 'script_handles', 'view_script_handles', + 'view_script_module_ids', 'editor_style_handles', 'style_handles', + 'view_style_handles', 'variations', 'block_hooks', ), @@ -466,21 +470,21 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'title' => 'block-type', 'type' => 'object', 'properties' => array( - 'api_version' => array( + 'api_version' => array( 'description' => __( 'Version of block API.' ), 'type' => 'integer', 'default' => 1, 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'title' => array( + 'title' => array( 'description' => __( 'Title of block type.' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'Unique name identifying the block type.' ), 'type' => 'string', 'pattern' => self::NAME_PATTERN, @@ -488,15 +492,15 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'description' => array( + 'description' => array( 'description' => __( 'Description of block type.' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'icon' => $icon_definition, - 'attributes' => array( + 'icon' => $icon_definition, + 'attributes' => array( 'description' => __( 'Block attributes.' ), 'type' => array( 'object', 'null' ), 'properties' => array(), @@ -507,7 +511,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'provides_context' => array( + 'provides_context' => array( 'description' => __( 'Context provided by blocks of this type.' ), 'type' => 'object', 'properties' => array(), @@ -518,7 +522,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'uses_context' => array( + 'uses_context' => array( 'description' => __( 'Context values inherited by blocks of this type.' ), 'type' => 'array', 'default' => array(), @@ -528,7 +532,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'selectors' => array( + 'selectors' => array( 'description' => __( 'Custom CSS selectors.' ), 'type' => 'object', 'default' => array(), @@ -536,7 +540,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'supports' => array( + 'supports' => array( 'description' => __( 'Block supports.' ), 'type' => 'object', 'default' => array(), @@ -544,15 +548,15 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'category' => $category_definition, - 'is_dynamic' => array( + 'category' => $category_definition, + 'is_dynamic' => array( 'description' => __( 'Is the block dynamically rendered.' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'editor_script_handles' => array( + 'editor_script_handles' => array( 'description' => __( 'Editor script handles.' ), 'type' => array( 'array' ), 'default' => array(), @@ -562,7 +566,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'script_handles' => array( + 'script_handles' => array( 'description' => __( 'Public facing and editor script handles.' ), 'type' => array( 'array' ), 'default' => array(), @@ -572,7 +576,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'view_script_handles' => array( + 'view_script_handles' => array( 'description' => __( 'Public facing script handles.' ), 'type' => array( 'array' ), 'default' => array(), @@ -582,7 +586,17 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'editor_style_handles' => array( + 'view_script_module_ids' => array( + 'description' => __( 'Public facing script module IDs.' ), + 'type' => array( 'array' ), + 'default' => array(), + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'editor_style_handles' => array( 'description' => __( 'Editor style handles.' ), 'type' => array( 'array' ), 'default' => array(), @@ -592,7 +606,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'style_handles' => array( + 'style_handles' => array( 'description' => __( 'Public facing and editor style handles.' ), 'type' => array( 'array' ), 'default' => array(), @@ -602,7 +616,17 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'styles' => array( + 'view_style_handles' => array( + 'description' => __( 'Public facing style handles.' ), + 'type' => array( 'array' ), + 'default' => array(), + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'styles' => array( 'description' => __( 'Block style variations.' ), 'type' => 'array', 'items' => array( @@ -631,7 +655,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'variations' => array( + 'variations' => array( 'description' => __( 'Block variations.' ), 'type' => 'array', 'items' => array( @@ -683,14 +707,14 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'default' => null, ), - 'textdomain' => array( + 'textdomain' => array( 'description' => __( 'Public text domain.' ), 'type' => array( 'string', 'null' ), 'default' => null, 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'parent' => array( + 'parent' => array( 'description' => __( 'Parent blocks.' ), 'type' => array( 'array', 'null' ), 'items' => array( @@ -701,7 +725,7 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'ancestor' => array( + 'ancestor' => array( 'description' => __( 'Ancestor blocks.' ), 'type' => array( 'array', 'null' ), 'items' => array( @@ -712,9 +736,20 @@ class WP_REST_Block_Types_Controller extends WP_REST_Controller { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'keywords' => $keywords_definition, - 'example' => $example_definition, - 'block_hooks' => array( + 'allowed_blocks' => array( + 'description' => __( 'Allowed child block types.' ), + 'type' => array( 'array', 'null' ), + 'items' => array( + 'type' => 'string', + 'pattern' => self::NAME_PATTERN, + ), + 'default' => null, + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'keywords' => $keywords_definition, + 'example' => $example_definition, + 'block_hooks' => array( 'description' => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.' ), 'type' => 'object', 'patternProperties' => array( diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index 1169b6b..c129ef6 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -300,7 +300,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { $prepared_args['orderby'] = 'none'; $total_comments = $query->query( $prepared_args ); - $max_pages = ceil( $total_comments / $request['per_page'] ); + $max_pages = (int) ceil( $total_comments / $request['per_page'] ); } $response = rest_ensure_response( $comments ); @@ -1091,7 +1091,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { if ( in_array( 'content', $fields, true ) ) { $data['content'] = array( /** This filter is documented in wp-includes/comment-template.php */ - 'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment ), + 'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ), 'raw' => $comment->comment_content, ); } diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php new file mode 100644 index 0000000..b409af4 --- /dev/null +++ b/wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php @@ -0,0 +1,322 @@ +<?php +/** + * Rest Font Collections Controller. + * + * This file contains the class for the REST API Font Collections Controller. + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + */ + +/** + * Font Library Controller class. + * + * @since 6.5.0 + */ +class WP_REST_Font_Collections_Controller extends WP_REST_Controller { + + /** + * Constructor. + * + * @since 6.5.0 + */ + public function __construct() { + $this->rest_base = 'font-collections'; + $this->namespace = 'wp/v2'; + } + + /** + * Registers the routes for the objects of the controller. + * + * @since 6.5.0 + */ + public function register_routes() { + 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(), + + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<slug>[\/\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Gets the font collections available. + * + * @since 6.5.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $collections_all = WP_Font_Library::get_instance()->get_font_collections(); + + $page = $request['page']; + $per_page = $request['per_page']; + $total_items = count( $collections_all ); + $max_pages = (int) ceil( $total_items / $per_page ); + + if ( $page > $max_pages && $total_items > 0 ) { + return new WP_Error( + 'rest_post_invalid_page_number', + __( 'The page number requested is larger than the number of pages available.' ), + array( 'status' => 400 ) + ); + } + + $collections_page = array_slice( $collections_all, ( $page - 1 ) * $per_page, $per_page ); + + $items = array(); + foreach ( $collections_page as $collection ) { + $item = $this->prepare_item_for_response( $collection, $request ); + + // If there's an error loading a collection, skip it and continue loading valid collections. + if ( is_wp_error( $item ) ) { + continue; + } + $item = $this->prepare_response_for_collection( $item ); + $items[] = $item; + } + + $response = rest_ensure_response( $items ); + + $response->header( 'X-WP-Total', (int) $total_items ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + $request_params = $request->get_query_params(); + $collection_url = rest_url( $this->namespace . '/' . $this->rest_base ); + $base = add_query_arg( urlencode_deep( $request_params ), $collection_url ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Gets a font collection. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $slug = $request->get_param( 'slug' ); + $collection = WP_Font_Library::get_instance()->get_font_collection( $slug ); + + if ( ! $collection ) { + return new WP_Error( 'rest_font_collection_not_found', __( 'Font collection not found.' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $collection, $request ); + } + + /** + * Prepare a single collection output for response. + * + * @since 6.5.0 + * + * @param WP_Font_Collection $item Font collection object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'slug', $fields ) ) { + $data['slug'] = $item->slug; + } + + // If any data fields are requested, get the collection data. + $data_fields = array( 'name', 'description', 'font_families', 'categories' ); + if ( ! empty( array_intersect( $fields, $data_fields ) ) ) { + $collection_data = $item->get_data(); + if ( is_wp_error( $collection_data ) ) { + $collection_data->add_data( array( 'status' => 500 ) ); + return $collection_data; + } + + foreach ( $data_fields as $field ) { + if ( rest_is_field_included( $field, $fields ) ) { + $data[ $field ] = $collection_data[ $field ]; + } + } + } + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) ) { + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $response->data = $this->add_additional_fields_to_object( $response->data, $request ); + $response->data = $this->filter_response_by_context( $response->data, $context ); + + /** + * Filters the font collection data for a REST API response. + * + * @since 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Font_Collection $item The font collection object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'rest_prepare_font_collection', $response, $item, $request ); + } + + /** + * Retrieves the font collection's schema, conforming to JSON Schema. + * + * @since 6.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'font-collection', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Unique identifier for the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'The name for the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'description' => array( + 'description' => __( 'The description for the font collection.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'font_families' => array( + 'description' => __( 'The font families for the font collection.' ), + 'type' => 'array', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'categories' => array( + 'description' => __( 'The categories for the font collection.' ), + 'type' => 'array', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Prepares links for the request. + * + * @since 6.5.0 + * + * @param WP_Font_Collection $collection Font collection data + * @return array Links for the given font collection. + */ + protected function prepare_links( $collection ) { + return array( + 'self' => array( + 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + } + + /** + * Retrieves the search params for the font collections. + * + * @since 6.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + + unset( $query_params['search'] ); + + /** + * Filters REST API collection parameters for the font collections controller. + * + * @since 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_font_collections_collection_params', $query_params ); + } + + /** + * Checks whether the user has permissions to use the Fonts Collections. + * + * @since 6.5.0 + * + * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( current_user_can( 'edit_theme_options' ) ) { + return true; + } + + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font collections.' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } +} diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php new file mode 100644 index 0000000..c7f72d4 --- /dev/null +++ b/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php @@ -0,0 +1,950 @@ +<?php +/** + * REST API: WP_REST_Font_Faces_Controller class + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + */ + +/** + * Class to access font faces through the REST API. + */ +class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller { + + /** + * The latest version of theme.json schema supported by the controller. + * + * @since 6.5.0 + * @var int + */ + const LATEST_THEME_JSON_VERSION_SUPPORTED = 2; + + /** + * Whether the controller supports batching. + * + * @since 6.5.0 + * @var false + */ + protected $allow_batch = false; + + /** + * Registers the routes for posts. + * + * @since 6.5.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'font_family_id' => array( + 'description' => __( 'The ID for the parent font family of the font face.' ), + 'type' => 'integer', + 'required' => true, + ), + ), + 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' => $this->get_create_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\d]+)', + array( + 'args' => array( + 'font_family_id' => array( + 'description' => __( 'The ID for the parent font family of the font face.' ), + 'type' => 'integer', + 'required' => true, + ), + 'id' => array( + 'description' => __( 'Unique identifier for the font face.' ), + 'type' => 'integer', + 'required' => true, + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'type' => 'boolean', + 'default' => false, + 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to font faces. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->read ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font faces.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to a font face. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( ! current_user_can( 'read_post', $post->ID ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access this font face.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Validates settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @param WP_REST_Request $request Request object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. + */ + public function validate_create_font_face_settings( $value, $request ) { + $settings = json_decode( $value, true ); + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + __( 'font_face_settings parameter must be a valid JSON string.' ), + array( 'status' => 400 ) + ); + } + + // Check that the font face settings match the theme.json schema. + $schema = $this->get_item_schema()['properties']['font_face_settings']; + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' ); + + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; + } + + // Check that none of the required settings are empty values. + $required = $schema['required']; + foreach ( $required as $key ) { + if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Name of the missing font face settings parameter, e.g. "font_face_settings[src]". */ + sprintf( __( '%s cannot be empty.' ), "font_face_setting[ $key ]" ), + array( 'status' => 400 ) + ); + } + } + + $srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] ); + $files = $request->get_file_params(); + + foreach ( $srcs as $src ) { + // Check that each src is a non-empty string. + $src = ltrim( $src ); + if ( empty( $src ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Font face source parameter name: "font_face_settings[src]". */ + sprintf( __( '%s values must be non-empty strings.' ), 'font_face_settings[src]' ), + array( 'status' => 400 ) + ); + } + + // Check that srcs are valid URLs or file references. + if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: 1: Font face source parameter name: "font_face_settings[src]", 2: The invalid src value. */ + sprintf( __( '%1$s value "%2$s" must be a valid URL or file reference.' ), 'font_face_settings[src]', $src ), + array( 'status' => 400 ) + ); + } + } + + // Check that each file in the request references a src in the settings. + foreach ( array_keys( $files ) as $file ) { + if ( ! in_array( $file, $srcs, true ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: 1: File key (e.g. "file-0") in the request data, 2: Font face source parameter name: "font_face_settings[src]". */ + sprintf( __( 'File %1$s must be used in %2$s.' ), $file, 'font_face_settings[src]' ), + array( 'status' => 400 ) + ); + } + } + + return true; + } + + /** + * Sanitizes the font face settings when creating a font face. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font face settings. + * @return array Decoded and sanitized array of font face settings. + */ + public function sanitize_font_face_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. + $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_face_settings']['properties']; + + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); + } + + return $settings; + } + + /** + * Retrieves a collection of font faces within the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + return parent::get_items( $request ); + } + + /** + * Retrieves a single font face within the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + // Check that the font face has a valid parent font family. + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + if ( (int) $font_family->ID !== (int) $post->post_parent ) { + return new WP_Error( + 'rest_font_face_parent_id_mismatch', + /* translators: %d: A post id. */ + sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ), + array( 'status' => 404 ) + ); + } + + return parent::get_item( $request ); + } + + /** + * Creates a font face for the parent font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); + $file_params = $request->get_file_params(); + + // Check that the necessary font face properties are unique. + $query = new WP_Query( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'title' => WP_Font_Utils::get_font_face_slug( $settings ), + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + if ( ! empty( $query->posts ) ) { + return new WP_Error( + 'rest_duplicate_font_face', + __( 'A font face matching those settings already exists.' ), + array( 'status' => 400 ) + ); + } + + // Move the uploaded font asset from the temp folder to the fonts directory. + if ( ! function_exists( 'wp_handle_upload' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src']; + $processed_srcs = array(); + $font_file_meta = array(); + + foreach ( $srcs as $src ) { + // If src not a file reference, use it as is. + if ( ! isset( $file_params[ $src ] ) ) { + $processed_srcs[] = $src; + continue; + } + + $file = $file_params[ $src ]; + $font_file = $this->handle_font_file_upload( $file ); + if ( is_wp_error( $font_file ) ) { + return $font_file; + } + + $processed_srcs[] = $font_file['url']; + $font_file_meta[] = $this->relative_fonts_path( $font_file['file'] ); + } + + // Store the updated settings for prepare_item_for_database to use. + $settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs; + $request->set_param( 'font_face_settings', $settings ); + + // Ensure that $settings data is slashed, so values with quotes are escaped. + // WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content. + $font_face_post = parent::create_item( $request ); + + if ( is_wp_error( $font_face_post ) ) { + return $font_face_post; + } + + $font_face_id = $font_face_post->data['id']; + + foreach ( $font_file_meta as $font_file_path ) { + add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path ); + } + + return $font_face_post; + } + + /** + * Deletes a single font face. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $font_family = $this->get_parent_font_family_post( $request['font_family_id'] ); + if ( is_wp_error( $font_family ) ) { + return $font_family; + } + + if ( (int) $font_family->ID !== (int) $post->post_parent ) { + return new WP_Error( + 'rest_font_face_parent_id_mismatch', + /* translators: %d: A post id. */ + sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ), + array( 'status' => 404 ) + ); + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for font faces. + if ( ! $force ) { + return new WP_Error( + 'rest_trash_not_supported', + /* translators: %s: force=true */ + sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ), + array( 'status' => 501 ) + ); + } + + return parent::delete_item( $request ); + } + + /** + * Prepares a single font face output for response. + * + * @since 6.5.0 + * + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item->ID; + } + if ( rest_is_field_included( 'theme_json_version', $fields ) ) { + $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED; + } + + if ( rest_is_field_included( 'parent', $fields ) ) { + $data['parent'] = $item->post_parent; + } + + if ( rest_is_field_included( 'font_face_settings', $fields ) ) { + $data['font_face_settings'] = $this->get_settings_from_post( $item ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + } + + /** + * Filters the font face data for a REST API response. + * + * @since 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Font face post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request ); + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @since 6.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + // Base properties for every Post. + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'default' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the typography settings.' ), + 'type' => 'integer', + 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, + 'minimum' => 2, + 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent font family of the font face.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + // Font face settings come directly from theme.json schema + // See https://schemas.wp.org/trunk/theme.json + 'font_face_settings' => array( + 'description' => __( 'font-face declaration in theme.json format.' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'properties' => array( + 'fontFamily' => array( + 'description' => __( 'CSS font-family value.' ), + 'type' => 'string', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), + ), + 'fontStyle' => array( + 'description' => __( 'CSS font-style value.' ), + 'type' => 'string', + 'default' => 'normal', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'fontWeight' => array( + 'description' => __( 'List of available font weights, separated by a space.' ), + 'default' => '400', + // Changed from `oneOf` to avoid errors from loose type checking. + // e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check. + 'type' => array( 'string', 'integer' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'fontDisplay' => array( + 'description' => __( 'CSS font-display value.' ), + 'type' => 'string', + 'default' => 'fallback', + 'enum' => array( + 'auto', + 'block', + 'fallback', + 'swap', + 'optional', + ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'src' => array( + 'description' => __( 'Paths or URLs to the font files.' ), + // Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array, + // and causing a "matches more than one of the expected formats" error. + 'anyOf' => array( + array( + 'type' => 'string', + ), + array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + 'default' => array(), + 'arg_options' => array( + 'sanitize_callback' => function ( $value ) { + return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value ); + }, + ), + ), + 'fontStretch' => array( + 'description' => __( 'CSS font-stretch value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'ascentOverride' => array( + 'description' => __( 'CSS ascent-override value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'descentOverride' => array( + 'description' => __( 'CSS descent-override value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'fontVariant' => array( + 'description' => __( 'CSS font-variant value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'fontFeatureSettings' => array( + 'description' => __( 'CSS font-feature-settings value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'fontVariationSettings' => array( + 'description' => __( 'CSS font-variation-settings value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'lineGapOverride' => array( + 'description' => __( 'CSS line-gap-override value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'sizeAdjust' => array( + 'description' => __( 'CSS size-adjust value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'unicodeRange' => array( + 'description' => __( 'CSS unicode-range value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'preview' => array( + 'description' => __( 'URL to a preview image of the font face.' ), + 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), + ), + ), + 'required' => array( 'fontFamily', 'src' ), + 'additionalProperties' => false, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + + /** + * Retrieves the query params for the font face collection. + * + * @since 6.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + // Remove unneeded params. + unset( + $query_params['after'], + $query_params['modified_after'], + $query_params['before'], + $query_params['modified_before'], + $query_params['search'], + $query_params['search_columns'], + $query_params['slug'], + $query_params['status'] + ); + + $query_params['orderby']['default'] = 'id'; + $query_params['orderby']['enum'] = array( 'id', 'include' ); + + /** + * Filters collection parameters for the font face controller. + * + * @since 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_wp_font_face_collection_params', $query_params ); + } + + /** + * Get the params used when creating a new font face. + * + * @since 6.5.0 + * + * @return array Font face create arguments. + */ + public function get_create_params() { + $properties = $this->get_item_schema()['properties']; + return array( + 'theme_json_version' => $properties['theme_json_version'], + // When creating, font_face_settings is stringified JSON, to work with multipart/form-data used + // when uploading font files. + 'font_face_settings' => array( + 'description' => __( 'font-face declaration in theme.json format, encoded as a string.' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_create_font_face_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ), + ), + ); + } + + /** + * Get the parent font family, if the ID is valid. + * + * @since 6.5.0 + * + * @param int $font_family_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent_font_family_post( $font_family_id ) { + $error = new WP_Error( + 'rest_post_invalid_parent', + __( 'Invalid post parent ID.', 'default' ), + array( 'status' => 404 ) + ); + + if ( (int) $font_family_id <= 0 ) { + return $error; + } + + $font_family_post = get_post( (int) $font_family_id ); + + if ( empty( $font_family_post ) || empty( $font_family_post->ID ) + || 'wp_font_family' !== $font_family_post->post_type + ) { + return $error; + } + + return $font_family_post; + } + + /** + * Prepares links for the request. + * + * @since 6.5.0 + * + * @param WP_Post $post Post object. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + // Entity meta. + return array( + 'self' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), + ), + 'collection' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ), + ), + 'parent' => array( + 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), + ), + ); + } + + /** + * Prepares a single font face post for creation. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Request object. + * @return stdClass Post object. + */ + protected function prepare_item_for_database( $request ) { + $prepared_post = new stdClass(); + + // Settings have already been decoded by ::sanitize_font_face_settings(). + $settings = $request->get_param( 'font_face_settings' ); + + // Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting, + // which may contain multibyte characters. + $title = WP_Font_Utils::get_font_face_slug( $settings ); + + $prepared_post->post_type = $this->post_type; + $prepared_post->post_parent = $request['font_family_id']; + $prepared_post->post_status = 'publish'; + $prepared_post->post_title = $title; + $prepared_post->post_name = sanitize_title( $title ); + $prepared_post->post_content = wp_json_encode( $settings ); + + return $prepared_post; + } + + /** + * Sanitizes a single src value for a font face. + * + * @since 6.5.0 + * + * @param string $value Font face src that is a URL or the key for a $_FILES array item. + * @return string Sanitized value. + */ + protected function sanitize_src( $value ) { + $value = ltrim( $value ); + return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value ); + } + + /** + * Handles the upload of a font file using wp_handle_upload(). + * + * @since 6.5.0 + * + * @param array $file Single file item from $_FILES. + * @return array|WP_Error Array containing uploaded file attributes on success, or WP_Error object on failure. + */ + protected function handle_font_file_upload( $file ) { + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); + // Filter the upload directory to return the fonts directory. + add_filter( 'upload_dir', '_wp_filter_font_directory' ); + + $overrides = array( + 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), + // Not testing a form submission. + 'test_form' => false, + // Only allow uploading font files for this request. + 'mimes' => WP_Font_Utils::get_allowed_font_mime_types(), + ); + + // Bypasses is_uploaded_file() when running unit tests. + if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) { + $overrides['action'] = 'wp_handle_mock_upload'; + } + + $uploaded_file = wp_handle_upload( $file, $overrides ); + + remove_filter( 'upload_dir', '_wp_filter_font_directory' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); + + return $uploaded_file; + } + + /** + * Handles file upload error. + * + * @since 6.5.0 + * + * @param array $file File upload data. + * @param string $message Error message from wp_handle_upload(). + * @return WP_Error WP_Error object. + */ + public function handle_font_file_upload_error( $file, $message ) { + $status = 500; + $code = 'rest_font_upload_unknown_error'; + + if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) { + $status = 400; + $code = 'rest_font_upload_invalid_file_type'; + } + + return new WP_Error( $code, $message, array( 'status' => $status ) ); + } + + /** + * Returns relative path to an uploaded font file. + * + * The path is relative to the current fonts directory. + * + * @since 6.5.0 + * @access private + * + * @param string $path Full path to the file. + * @return string Relative path on success, unchanged path on failure. + */ + protected function relative_fonts_path( $path ) { + $new_path = $path; + + $fonts_dir = wp_get_font_dir(); + if ( str_starts_with( $new_path, $fonts_dir['path'] ) ) { + $new_path = str_replace( $fonts_dir, '', $new_path ); + $new_path = ltrim( $new_path, '/' ); + } + + return $new_path; + } + + /** + * Gets the font face's settings from the post. + * + * @since 6.5.0 + * + * @param WP_Post $post Font face post object. + * @return array Font face settings array. + */ + protected function get_settings_from_post( $post ) { + $settings = json_decode( $post->post_content, true ); + $properties = $this->get_item_schema()['properties']['font_face_settings']['properties']; + + // Provide required, empty settings if needed. + if ( null === $settings ) { + $settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + } + + // Only return the properties defined in the schema. + return array_intersect_key( $settings, $properties ); + } +} diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php new file mode 100644 index 0000000..184b42d --- /dev/null +++ b/wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php @@ -0,0 +1,564 @@ +<?php +/** + * REST API: WP_REST_Font_Families_Controller class + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + */ + +/** + * Font Families Controller class. + * + * @since 6.5.0 + */ +class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller { + + /** + * The latest version of theme.json schema supported by the controller. + * + * @since 6.5.0 + * @var int + */ + const LATEST_THEME_JSON_VERSION_SUPPORTED = 2; + + /** + * Whether the controller supports batching. + * + * @since 6.5.0 + * @var false + */ + protected $allow_batch = false; + + /** + * Checks if a given request has access to font families. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->read ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access font families.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks if a given request has access to a font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( ! current_user_can( 'read_post', $post->ID ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to access this font family.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Validates settings when creating or updating a font family. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font family settings. + * @param WP_REST_Request $request Request object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. + */ + public function validate_font_family_settings( $value, $request ) { + $settings = json_decode( $value, true ); + + // Check settings string is valid JSON. + if ( null === $settings ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Parameter name: "font_family_settings". */ + sprintf( __( '%s parameter must be a valid JSON string.' ), 'font_family_settings' ), + array( 'status' => 400 ) + ); + } + + $schema = $this->get_item_schema()['properties']['font_family_settings']; + $required = $schema['required']; + + if ( isset( $request['id'] ) ) { + // Allow sending individual properties if we are updating an existing font family. + unset( $schema['required'] ); + + // But don't allow updating the slug, since it is used as a unique identifier. + if ( isset( $settings['slug'] ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Name of parameter being updated: font_family_settings[slug]". */ + sprintf( __( '%s cannot be updated.' ), 'font_family_settings[slug]' ), + array( 'status' => 400 ) + ); + } + } + + // Check that the font face settings match the theme.json schema. + $has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' ); + + if ( is_wp_error( $has_valid_settings ) ) { + $has_valid_settings->add_data( array( 'status' => 400 ) ); + return $has_valid_settings; + } + + // Check that none of the required settings are empty values. + foreach ( $required as $key ) { + if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: Name of the empty font family setting parameter, e.g. "font_family_settings[slug]". */ + sprintf( __( '%s cannot be empty.' ), "font_family_settings[ $key ]" ), + array( 'status' => 400 ) + ); + } + } + + return true; + } + + /** + * Sanitizes the font family settings when creating or updating a font family. + * + * @since 6.5.0 + * + * @param string $value Encoded JSON string of font family settings. + * @return array Decoded array of font family settings. + */ + public function sanitize_font_family_settings( $value ) { + // Settings arrive as stringified JSON, since this is a multipart/form-data request. + $settings = json_decode( $value, true ); + $schema = $this->get_item_schema()['properties']['font_family_settings']['properties']; + + // Sanitize settings based on callbacks in the schema. + foreach ( $settings as $key => $value ) { + $sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback']; + $settings[ $key ] = call_user_func( $sanitize_callback, $value ); + } + + return $settings; + } + + /** + * Creates a single font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function create_item( $request ) { + $settings = $request->get_param( 'font_family_settings' ); + + // Check that the font family slug is unique. + $query = new WP_Query( + array( + 'post_type' => $this->post_type, + 'posts_per_page' => 1, + 'name' => $settings['slug'], + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + if ( ! empty( $query->posts ) ) { + return new WP_Error( + 'rest_duplicate_font_family', + /* translators: %s: Font family slug. */ + sprintf( __( 'A font family with slug "%s" already exists.' ), $settings['slug'] ), + array( 'status' => 400 ) + ); + } + + return parent::create_item( $request ); + } + + /** + * Deletes a single font family. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function delete_item( $request ) { + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for font families. + if ( ! $force ) { + return new WP_Error( + 'rest_trash_not_supported', + /* translators: %s: force=true */ + sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ), + array( 'status' => 501 ) + ); + } + + return parent::delete_item( $request ); + } + + /** + * Prepares a single font family output for response. + * + * @since 6.5.0 + * + * @param WP_Post $item Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $item->ID; + } + + if ( rest_is_field_included( 'theme_json_version', $fields ) ) { + $data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED; + } + + if ( rest_is_field_included( 'font_faces', $fields ) ) { + $data['font_faces'] = $this->get_font_face_ids( $item->ID ); + } + + if ( rest_is_field_included( 'font_family_settings', $fields ) ) { + $data['font_family_settings'] = $this->get_settings_from_post( $item ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) ) { + $links = $this->prepare_links( $item ); + $response->add_links( $links ); + } + + /** + * Filters the font family data for a REST API response. + * + * @since 6.5.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Font family post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request ); + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @since 6.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + // Base properties for every Post. + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the post.', 'default' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'theme_json_version' => array( + 'description' => __( 'Version of the theme.json schema used for the typography settings.' ), + 'type' => 'integer', + 'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, + 'minimum' => 2, + 'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'font_faces' => array( + 'description' => __( 'The IDs of the child font faces in the font family.' ), + 'type' => 'array', + 'context' => array( 'view', 'edit', 'embed' ), + 'items' => array( + 'type' => 'integer', + ), + ), + // Font family settings come directly from theme.json schema + // See https://schemas.wp.org/trunk/theme.json + 'font_family_settings' => array( + 'description' => __( 'font-face definition in theme.json format.' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'properties' => array( + 'name' => array( + 'description' => __( 'Name of the font family preset, translatable.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'Kebab-case unique identifier for the font family preset.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'fontFamily' => array( + 'description' => __( 'CSS font-family value.' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ), + ), + ), + 'preview' => array( + 'description' => __( 'URL to a preview image of the font family.' ), + 'type' => 'string', + 'format' => 'uri', + 'default' => '', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_url', + ), + ), + ), + 'required' => array( 'name', 'slug', 'fontFamily' ), + 'additionalProperties' => false, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @since 6.5.0 + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + + $schema = parent::get_public_item_schema(); + + // Also remove `arg_options' from child font_family_settings properties, since the parent + // controller only handles the top level properties. + foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) { + unset( $property['arg_options'] ); + } + + return $schema; + } + + /** + * Retrieves the query params for the font family collection. + * + * @since 6.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + // Remove unneeded params. + unset( + $query_params['after'], + $query_params['modified_after'], + $query_params['before'], + $query_params['modified_before'], + $query_params['search'], + $query_params['search_columns'], + $query_params['status'] + ); + + $query_params['orderby']['default'] = 'id'; + $query_params['orderby']['enum'] = array( 'id', 'include' ); + + /** + * Filters collection parameters for the font family controller. + * + * @since 6.5.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_wp_font_family_collection_params', $query_params ); + } + + /** + * Get the arguments used when creating or updating a font family. + * + * @since 6.5.0 + * + * @return array Font family create/edit arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) { + $properties = $this->get_item_schema()['properties']; + return array( + 'theme_json_version' => $properties['theme_json_version'], + // When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data. + // Font families don't currently support file uploads, but may accept preview files in the future. + 'font_family_settings' => array( + 'description' => __( 'font-family declaration in theme.json format, encoded as a string.' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'validate_font_family_settings' ), + 'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ), + ), + ); + } + + return parent::get_endpoint_args_for_item_schema( $method ); + } + + /** + * Get the child font face post IDs. + * + * @since 6.5.0 + * + * @param int $font_family_id Font family post ID. + * @return int[] Array of child font face post IDs. + */ + protected function get_font_face_ids( $font_family_id ) { + $query = new WP_Query( + array( + 'fields' => 'ids', + 'post_parent' => $font_family_id, + 'post_type' => 'wp_font_face', + 'posts_per_page' => 99, + 'order' => 'ASC', + 'orderby' => 'id', + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + return $query->posts; + } + + /** + * Prepares font family links for the request. + * + * @since 6.5.0 + * + * @param WP_Post $post Post object. + * @return array Links for the given post. + */ + protected function prepare_links( $post ) { + // Entity meta. + $links = parent::prepare_links( $post ); + + return array( + 'self' => $links['self'], + 'collection' => $links['collection'], + 'font_faces' => $this->prepare_font_face_links( $post->ID ), + ); + } + + /** + * Prepares child font face links for the request. + * + * @param int $font_family_id Font family post ID. + * @return array Links for the child font face posts. + */ + protected function prepare_font_face_links( $font_family_id ) { + $font_face_ids = $this->get_font_face_ids( $font_family_id ); + $links = array(); + foreach ( $font_face_ids as $font_face_id ) { + $links[] = array( + 'embeddable' => true, + 'href' => rest_url( sprintf( '%s/%s/%s/font-faces/%s', $this->namespace, $this->rest_base, $font_family_id, $font_face_id ) ), + ); + } + return $links; + } + + /** + * Prepares a single font family post for create or update. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Request object. + * @return stdClass|WP_Error Post object or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + $prepared_post = new stdClass(); + // Settings have already been decoded by ::sanitize_font_family_settings(). + $settings = $request->get_param( 'font_family_settings' ); + + // This is an update and we merge with the existing font family. + if ( isset( $request['id'] ) ) { + $existing_post = $this->get_post( $request['id'] ); + if ( is_wp_error( $existing_post ) ) { + return $existing_post; + } + + $prepared_post->ID = $existing_post->ID; + $existing_settings = $this->get_settings_from_post( $existing_post ); + $settings = array_merge( $existing_settings, $settings ); + } + + $prepared_post->post_type = $this->post_type; + $prepared_post->post_status = 'publish'; + $prepared_post->post_title = $settings['name']; + $prepared_post->post_name = sanitize_title( $settings['slug'] ); + + // Remove duplicate information from settings. + unset( $settings['name'] ); + unset( $settings['slug'] ); + + $prepared_post->post_content = wp_json_encode( $settings ); + + return $prepared_post; + } + + /** + * Gets the font family's settings from the post. + * + * @since 6.5.0 + * + * @param WP_Post $post Font family post object. + * @return array Font family settings array. + */ + protected function get_settings_from_post( $post ) { + $settings_json = json_decode( $post->post_content, true ); + + // Default to empty strings if the settings are missing. + return array( + 'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '', + 'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '', + 'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '', + 'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '', + ); + } +} diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php index 9ce7e2b..5f5ae84 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php @@ -47,6 +47,7 @@ class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Controller { * Registers the controller's routes. * * @since 6.3.0 + * @since 6.5.0 Added route to fetch individual global styles revisions. */ public function register_routes() { register_rest_route( @@ -68,6 +69,32 @@ class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Controller { 'schema' => array( $this, 'get_public_item_schema' ), ) ); + + register_rest_route( + $this->namespace, + '/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)', + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent of the global styles revision.' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the global styles revision.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); } /** @@ -179,7 +206,7 @@ class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Controller { } if ( $revisions_query->query_vars['posts_per_page'] > 0 ) { - $max_pages = ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); + $max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); } else { $max_pages = $total_revisions > 0 ? 1 : 0; } @@ -242,6 +269,56 @@ class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Controller { } /** + * Retrieves one global styles revision from the collection. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + $revision = $this->get_revision( $request['id'] ); + if ( is_wp_error( $revision ) ) { + return $revision; + } + + $response = $this->prepare_item_for_response( $revision, $request ); + return rest_ensure_response( $response ); + } + + /** + * Gets the global styles revision, if the ID is valid. + * + * @since 6.5.0 + * + * @param int $id Supplied ID. + * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise. + */ + protected function get_revision( $id ) { + $error = new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid global styles revision ID.' ), + array( 'status' => 404 ) + ); + + if ( (int) $id <= 0 ) { + return $error; + } + + $revision = get_post( (int) $id ); + if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) { + return $error; + } + + return $revision; + } + + /** * Checks the post_date_gmt or modified_gmt and prepare any post or * modified date for single post output. * diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index abfef4e..4501e47 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -403,7 +403,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { $total_posts = $count_query->found_posts; } - $max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] ); + $max_pages = (int) ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] ); if ( $page > $max_pages && $total_posts > 0 ) { return new WP_Error( @@ -1742,6 +1742,8 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { * @since 4.7.0 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support. * + * @global WP_Post $post Global post object. + * * @param WP_Post $item Post object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. @@ -2375,6 +2377,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { 'comments', 'revisions', 'custom-fields', + 'thumbnail', ), ); diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php index 5501c19..fb5fa29 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php @@ -308,7 +308,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { } if ( $revisions_query->query_vars['posts_per_page'] > 0 ) { - $max_pages = ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); + $max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); } else { $max_pages = $total_revisions > 0 ? 1 : 0; } @@ -387,6 +387,7 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { * Retrieves one revision from the collection. * * @since 4.7.0 + * @since 6.5.0 Added a condition to check that parent id matches revision parent id. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. @@ -402,6 +403,15 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { return $revision; } + if ( (int) $parent->ID !== (int) $revision->post_parent ) { + return new WP_Error( + 'rest_revision_parent_id_mismatch', + /* translators: %d: A post id. */ + sprintf( __( 'The revision does not belong to the specified parent with id of "%d"' ), $parent->ID ), + array( 'status' => 404 ) + ); + } + $response = $this->prepare_item_for_response( $revision, $request ); return rest_ensure_response( $response ); } @@ -550,6 +560,8 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller { * @since 4.7.0 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support. * + * @global WP_Post $post Global post object. + * * @param WP_Post $item Post revision object. * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php index 4c44f67..cc1c302 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php @@ -152,7 +152,7 @@ class WP_REST_Search_Controller extends WP_REST_Controller { $total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ]; $page = (int) $request['page']; $per_page = (int) $request['per_page']; - $max_pages = ceil( $total / $per_page ); + $max_pages = (int) ceil( $total / $per_page ); if ( $page > $max_pages && $total > 0 ) { return new WP_Error( diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 53f8faa..cec8a77 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -619,7 +619,8 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { $changes->post_author = $post_author; } - return $changes; + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request ); } /** @@ -726,6 +727,14 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { $data['modified'] = mysql_to_rfc3339( $template->modified ); } + if ( rest_is_field_included( 'author_text', $fields ) ) { + $data['author_text'] = self::get_wp_templates_author_text_field( $template ); + } + + if ( rest_is_field_included( 'original_source', $fields ) ) { + $data['original_source'] = self::get_wp_templates_original_source_field( $template ); + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -748,6 +757,83 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { return $response; } + /** + * Returns the source from where the template originally comes from. + * + * @since 6.5.0 + * + * @param WP_Block_Template $template_object Template instance. + * @return string Original source of the template one of theme, plugin, site, or user. + */ + private static function get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { + // Added by theme. + // Template originally provided by a theme, but customized by a user. + // Templates originally didn't have the 'origin' field so identify + // older customized templates by checking for no origin and a 'theme' + // or 'custom' source. + if ( $template_object->has_theme_file && + ( 'theme' === $template_object->origin || ( + empty( $template_object->origin ) && in_array( + $template_object->source, + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) { + return 'plugin'; + } + + // Added by site. + // Template was created from scratch, but has no author. Author support + // was only added to templates in WordPress 5.9. Fallback to showing the + // site logo and title. + if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; + } + + /** + * Returns a human readable text for the author of the template. + * + * @since 6.5.0 + * + * @param WP_Block_Template $template_object Template instance. + * @return string Human readable text for the author. + */ + private static function get_wp_templates_author_text_field( $template_object ) { + $original_source = self::get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); + return empty( $theme_name ) ? $template_object->theme : $theme_name; + case 'plugin': + $plugins = get_plugins(); + $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ]; + return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name']; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author' ); + } + return $author->get( 'display_name' ); + } + } + /** * Prepares links for the request. @@ -861,13 +947,13 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { 'title' => $this->post_type, 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'ID of template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'slug' => array( + 'slug' => array( 'description' => __( 'Unique slug identifying the template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), @@ -875,29 +961,29 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { 'minLength' => 1, 'pattern' => '[a-zA-Z0-9_\%-]+', ), - 'theme' => array( + 'theme' => array( 'description' => __( 'Theme identifier for the template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), - 'type' => array( + 'type' => array( 'description' => __( 'Type of template.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), ), - 'source' => array( + 'source' => array( 'description' => __( 'Source of template' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'origin' => array( + 'origin' => array( 'description' => __( 'Source of a customized template' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'content' => array( + 'content' => array( 'description' => __( 'Content of template.' ), 'type' => array( 'object', 'string' ), 'default' => '', @@ -916,7 +1002,7 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { ), ), ), - 'title' => array( + 'title' => array( 'description' => __( 'Title of template.' ), 'type' => array( 'object', 'string' ), 'default' => '', @@ -935,43 +1021,61 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { ), ), ), - 'description' => array( + 'description' => array( 'description' => __( 'Description of template.' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), ), - 'status' => array( + 'status' => array( 'description' => __( 'Status of template.' ), 'type' => 'string', 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 'default' => 'publish', 'context' => array( 'embed', 'view', 'edit' ), ), - 'wp_id' => array( + 'wp_id' => array( 'description' => __( 'Post ID.' ), 'type' => 'integer', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'has_theme_file' => array( + 'has_theme_file' => array( 'description' => __( 'Theme file exists.' ), 'type' => 'bool', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), - 'author' => array( + 'author' => array( 'description' => __( 'The ID for the author of the template.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), ), - 'modified' => array( + 'modified' => array( 'description' => __( "The date the template was last modified, in the site's timezone." ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), + 'author_text' => array( + 'type' => 'string', + 'description' => __( 'Human readable text for the author.' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'original_source' => array( + 'description' => __( 'Where the template originally comes from e.g. \'theme\'' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + 'enum' => array( + 'theme', + 'plugin', + 'site', + 'user', + ), + ), ), ); diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index cf9dc91..8ffc6ad 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -348,13 +348,13 @@ class WP_REST_Terms_Controller extends WP_REST_Controller { // Store pagination values for headers. $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + $page = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $response->header( 'X-WP-Total', (int) $total_terms ); - $max_pages = ceil( $total_terms / $per_page ); + $max_pages = (int) ceil( $total_terms / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); + $response->header( 'X-WP-TotalPages', $max_pages ); $request_params = $request->get_query_params(); $collection_url = rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) ); diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-url-details-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-url-details-controller.php index c9ac667..7dcc4d7 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-url-details-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-url-details-controller.php @@ -128,7 +128,7 @@ class WP_REST_URL_Details_Controller extends WP_REST_Controller { * * @since 5.9.0 * - * @param WP_REST_REQUEST $request Full details about the request. + * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error The parsed details as a response object. WP_Error if there are errors. */ public function parse_url_details( $request ) { diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 9b38470..c2e0db8 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -348,7 +348,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller { // Store pagination values for headers then unset for count query. $per_page = (int) $prepared_args['number']; - $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + $page = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); $prepared_args['fields'] = 'ID'; @@ -363,9 +363,9 @@ class WP_REST_Users_Controller extends WP_REST_Controller { $response->header( 'X-WP-Total', (int) $total_users ); - $max_pages = ceil( $total_users / $per_page ); + $max_pages = (int) ceil( $total_users / $per_page ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); + $response->header( 'X-WP-TotalPages', $max_pages ); $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); if ( $page > 1 ) { diff --git a/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php index 60c3c3a..7b46905 100644 --- a/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php +++ b/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php @@ -141,6 +141,7 @@ abstract class WP_REST_Meta_Fields { */ public function update_value( $meta, $object_id ) { $fields = $this->get_registered_fields(); + $error = new WP_Error(); foreach ( $fields as $meta_key => $args ) { $name = $args['name']; @@ -163,35 +164,38 @@ abstract class WP_REST_Meta_Fields { $current = get_metadata( $this->get_meta_type(), $object_id, $meta_key, true ); if ( is_wp_error( rest_validate_value_from_schema( $current, $args['schema'] ) ) ) { - return new WP_Error( + $error->add( 'rest_invalid_stored_value', /* translators: %s: Custom field key. */ sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 ) ); + continue; } } $result = $this->delete_meta_value( $object_id, $meta_key, $name ); if ( is_wp_error( $result ) ) { - return $result; + $error->merge_from( $result ); } continue; } if ( ! $args['single'] && is_array( $value ) && count( array_filter( $value, 'is_null' ) ) ) { - return new WP_Error( + $error->add( 'rest_invalid_stored_value', /* translators: %s: Custom field key. */ sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 ) ); + continue; } $is_valid = rest_validate_value_from_schema( $value, $args['schema'], 'meta.' . $name ); if ( is_wp_error( $is_valid ) ) { $is_valid->add_data( array( 'status' => 400 ) ); - return $is_valid; + $error->merge_from( $is_valid ); + continue; } $value = rest_sanitize_value_from_schema( $value, $args['schema'] ); @@ -203,10 +207,15 @@ abstract class WP_REST_Meta_Fields { } if ( is_wp_error( $result ) ) { - return $result; + $error->merge_from( $result ); + continue; } } + if ( $error->has_errors() ) { + return $error; + } + return null; } diff --git a/wp-includes/rest-api/search/class-wp-rest-post-format-search-handler.php b/wp-includes/rest-api/search/class-wp-rest-post-format-search-handler.php index b219281..8d815b1 100644 --- a/wp-includes/rest-api/search/class-wp-rest-post-format-search-handler.php +++ b/wp-includes/rest-api/search/class-wp-rest-post-format-search-handler.php @@ -26,14 +26,17 @@ class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler { } /** - * Searches the object type content for a given search request. + * Searches the post formats for a given search request. * * @since 5.6.0 * * @param WP_REST_Request $request Full REST request. - * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing - * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the - * total count for the matching search results. + * @return array { + * Associative array containing found IDs and total count for the matching search results. + * + * @type string[] $ids Array containing slugs for the matching post formats. + * @type int $total Total count for the matching search results. + * } */ public function search_items( WP_REST_Request $request ) { $format_strings = get_post_format_strings(); @@ -46,7 +49,7 @@ class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler { } /** - * Filters the query arguments for a REST API search request. + * Filters the query arguments for a REST API post format search request. * * Enables adding extra arguments or setting defaults for a post format search request. * @@ -84,13 +87,20 @@ class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler { } /** - * Prepares the search result for a given ID. + * Prepares the search result for a given post format. * * @since 5.6.0 * * @param string $id Item ID, the post format slug. * @param array $fields Fields to include for the item. - * @return array Associative array containing all fields for the item. + * @return array { + * Associative array containing fields for the post format based on the `$fields` parameter. + * + * @type string $id Optional. Post format slug. + * @type string $title Optional. Post format name. + * @type string $url Optional. Post format permalink URL. + * @type string $type Optional. String 'post-format'. + *} */ public function prepare_item( $id, array $fields ) { $data = array(); diff --git a/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php b/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php index fb85fce..1d47723 100644 --- a/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php +++ b/wp-includes/rest-api/search/class-wp-rest-post-search-handler.php @@ -40,14 +40,17 @@ class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler { } /** - * Searches the object type content for a given search request. + * Searches posts for a given search request. * * @since 5.0.0 * * @param WP_REST_Request $request Full REST request. - * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing - * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the - * total count for the matching search results. + * @return array { + * Associative array containing found IDs and total count for the matching search results. + * + * @type int[] $ids Array containing the matching post IDs. + * @type int $total Total count for the matching search results. + * } */ public function search_items( WP_REST_Request $request ) { @@ -78,7 +81,7 @@ class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler { } /** - * Filters the query arguments for a REST API search request. + * Filters the query arguments for a REST API post search request. * * Enables adding extra arguments or setting defaults for a post search request. * @@ -102,13 +105,20 @@ class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler { } /** - * Prepares the search result for a given ID. + * Prepares the search result for a given post ID. * * @since 5.0.0 * - * @param int $id Item ID. - * @param array $fields Fields to include for the item. - * @return array Associative array containing all fields for the item. + * @param int $id Post ID. + * @param array $fields Fields to include for the post. + * @return array { + * Associative array containing fields for the post based on the `$fields` parameter. + * + * @type int $id Optional. Post ID. + * @type string $title Optional. Post title. + * @type string $url Optional. Post permalink URL. + * @type string $type Optional. Post type. + * } */ public function prepare_item( $id, array $fields ) { $post = get_post( $id ); diff --git a/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php b/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php index eed35e7..6527697 100644 --- a/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php +++ b/wp-includes/rest-api/search/class-wp-rest-term-search-handler.php @@ -36,7 +36,7 @@ class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler { } /** - * Searches the object type content for a given search request. + * Searches terms for a given search request. * * @since 5.6.0 * @@ -44,7 +44,7 @@ class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler { * @return array { * Associative array containing found IDs and total count for the matching search results. * - * @type int[] $ids Found IDs. + * @type int[] $ids Found term IDs. * @type string|int|WP_Error $total Numeric string containing the number of terms in that * taxonomy, 0 if there are no results, or WP_Error if * the requested taxonomy does not exist. @@ -79,7 +79,7 @@ class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler { } /** - * Filters the query arguments for a REST API search request. + * Filters the query arguments for a REST API term search request. * * Enables adding extra arguments or setting defaults for a term search request. * @@ -110,13 +110,20 @@ class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler { } /** - * Prepares the search result for a given ID. + * Prepares the search result for a given term ID. * * @since 5.6.0 * - * @param int $id Item ID. - * @param array $fields Fields to include for the item. - * @return array Associative array containing all fields for the item. + * @param int $id Term ID. + * @param array $fields Fields to include for the term. + * @return array { + * Associative array containing fields for the term based on the `$fields` parameter. + * + * @type int $id Optional. Term ID. + * @type string $title Optional. Term name. + * @type string $url Optional. Term permalink URL. + * @type string $type Optional. Term taxonomy name. + * } */ public function prepare_item( $id, array $fields ) { $term = get_term( $id ); |