summaryrefslogtreecommitdiffstats
path: root/wp-includes/rest-api
diff options
context:
space:
mode:
Diffstat (limited to 'wp-includes/rest-api')
-rw-r--r--wp-includes/rest-api/class-wp-rest-server.php51
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php65
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php85
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php4
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php322
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php950
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-font-families-controller.php564
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-global-styles-revisions-controller.php79
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php5
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php14
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-search-controller.php2
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php134
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php6
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-url-details-controller.php2
-rw-r--r--wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php6
-rw-r--r--wp-includes/rest-api/fields/class-wp-rest-meta-fields.php19
-rw-r--r--wp-includes/rest-api/search/class-wp-rest-post-format-search-handler.php24
-rw-r--r--wp-includes/rest-api/search/class-wp-rest-post-search-handler.php28
-rw-r--r--wp-includes/rest-api/search/class-wp-rest-term-search-handler.php21
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 );