This commit is contained in:
Hanson.xyz Dev
2026-01-04 17:50:08 -06:00
parent 7e45ce0756
commit acc8ac87a0
4131 changed files with 232562 additions and 250244 deletions
@@ -535,7 +535,7 @@ class WP_REST_Request implements ArrayAccess {
*
* @since 4.4.0
*
* @return array Parameter map of key to value
* @return array Parameter map of key to value.
*/
public function get_query_params() {
return $this->params['GET'];
@@ -587,7 +587,7 @@ class WP_REST_Request implements ArrayAccess {
*
* @since 4.4.0
*
* @return array Parameter map of key to value
* @return array Parameter map of key to value.
*/
public function get_file_params() {
return $this->params['FILES'];
@@ -613,7 +613,7 @@ class WP_REST_Request implements ArrayAccess {
*
* @since 4.4.0
*
* @return array Parameter map of key to value
* @return array Parameter map of key to value.
*/
public function get_default_params() {
return $this->params['defaults'];
@@ -43,7 +43,7 @@ class WP_REST_Response extends WP_HTTP_Response {
/**
* Adds a link to the response.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
* {@internal The $rel parameter is first, as this looks nicer when sending multiple.}
*
* @since 4.4.0
*
@@ -76,9 +76,9 @@ class WP_REST_Response extends WP_HTTP_Response {
*
* @since 4.4.0
*
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string $href Optional. Only remove links for the relation matching the given href.
* Default null.
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string|null $href Optional. Only remove links for the relation matching the given href.
* Default null.
*/
public function remove_link( $rel, $href = null ) {
if ( ! isset( $this->links[ $rel ] ) ) {
@@ -135,7 +135,7 @@ class WP_REST_Response extends WP_HTTP_Response {
/**
* Sets a single link header.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
* {@internal The $rel parameter is first, as this looks nicer when sending multiple.}
*
* @since 4.4.0
*
+26 -20
View File
@@ -166,8 +166,8 @@ class WP_REST_Server {
*
* @since 4.4.0
*
* @return WP_Error|null|true WP_Error indicates unsuccessful login, null indicates successful
* or no authentication provided
* @return WP_Error|null|true WP_Error if authentication error occurred, null if authentication
* method wasn't used, true if authentication succeeded.
*/
public function check_authentication() {
/**
@@ -191,7 +191,7 @@ class WP_REST_Server {
*
* @since 4.4.0
*
* @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
* @param WP_Error|null|true $errors WP_Error if authentication error occurred, null if authentication
* method wasn't used, true if authentication succeeded.
*/
return apply_filters( 'rest_authentication_errors', null );
@@ -224,10 +224,10 @@ class WP_REST_Server {
*
* @since 4.4.0
*
* @param string $code WP_Error-style code.
* @param string $message Human-readable message.
* @param int $status Optional. HTTP status code to send. Default null.
* @return string JSON representation of the error
* @param string $code WP_Error-style code.
* @param string $message Human-readable message.
* @param int|null $status Optional. HTTP status code to send. Default null.
* @return string JSON representation of the error.
*/
protected function json_error( $code, $message, $status = null ) {
if ( $status ) {
@@ -278,8 +278,8 @@ class WP_REST_Server {
*
* @global WP_User $current_user The currently authenticated user.
*
* @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @param string|null $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @return null|false Null if not served and a HEAD request, false otherwise.
*/
public function serve_request( $path = null ) {
@@ -655,12 +655,11 @@ class WP_REST_Server {
}
/**
* Gets the target links for a REST API Link.
* Gets the target hints for a REST API Link.
*
* @since 6.7.0
*
* @param array $link
*
* @param array $link The link to get target hints for.
* @return array|null
*/
protected static function get_target_hints_for_link( $link ) {
@@ -764,6 +763,7 @@ class WP_REST_Server {
*
* @param array $data Data from the request.
* @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
* Default true.
* @return array {
* Data with sub-requests embedded.
*
@@ -1339,9 +1339,7 @@ class WP_REST_Server {
* @return false|string Boolean false or string error message.
*/
protected function get_json_last_error() {
$last_error_code = json_last_error();
if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
if ( JSON_ERROR_NONE === json_last_error() ) {
return false;
}
@@ -1355,11 +1353,7 @@ class WP_REST_Server {
*
* @since 4.4.0
*
* @param array $request {
* Request.
*
* @type string $context Context.
* }
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response The API root index data.
*/
public function get_index( $request ) {
@@ -1753,6 +1747,12 @@ class WP_REST_Server {
$has_error = false;
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$validation[] = $single_request;
continue;
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
$error = null;
@@ -1823,6 +1823,12 @@ class WP_REST_Server {
}
foreach ( $requests as $i => $single_request ) {
if ( is_wp_error( $single_request ) ) {
$result = $this->error_to_response( $single_request );
$responses[] = $this->envelope_response( $result, false )->get_data();
continue;
}
$clean_request = clone $single_request;
$clean_request->set_url_params( array() );
$clean_request->set_attributes( array() );
@@ -0,0 +1,293 @@
<?php
/**
* REST API ability categories controller for Abilities API.
*
* @package WordPress
* @subpackage Abilities_API
* @since 6.9.0
*/
declare( strict_types = 1 );
/**
* Core controller used to access ability categories via the REST API.
*
* @since 6.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Abilities_V1_Categories_Controller extends WP_REST_Controller {
/**
* REST API namespace.
*
* @since 6.9.0
* @var string
*/
protected $namespace = 'wp-abilities/v1';
/**
* REST API base route.
*
* @since 6.9.0
* @var string
*/
protected $rest_base = 'categories';
/**
* Registers the routes for ability categories.
*
* @since 6.9.0
*
* @see register_rest_route()
*/
public function register_routes(): void {
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>[a-z0-9]+(?:-[a-z0-9]+)*)',
array(
'args' => array(
'slug' => array(
'description' => __( 'Unique identifier for the ability category.' ),
'type' => 'string',
'pattern' => '^[a-z0-9]+(?:-[a-z0-9]+)*$',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Retrieves all ability categories.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success.
*/
public function get_items( $request ) {
$categories = wp_get_ability_categories();
$page = $request['page'];
$per_page = $request['per_page'];
$offset = ( $page - 1 ) * $per_page;
$total_categories = count( $categories );
$max_pages = (int) ceil( $total_categories / $per_page );
if ( $request->get_method() === 'HEAD' ) {
$response = new WP_REST_Response( array() );
} else {
$categories = array_slice( $categories, $offset, $per_page );
$data = array();
foreach ( $categories as $category ) {
$item = $this->prepare_item_for_response( $category, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
}
$response->header( 'X-WP-Total', (string) $total_categories );
$response->header( 'X-WP-TotalPages', (string) $max_pages );
$query_params = $request->get_query_params();
$base = add_query_arg(
urlencode_deep( $query_params ),
rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) )
);
if ( $page > 1 ) {
$prev_page = $page - 1;
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $page < $max_pages ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Retrieves a specific ability category.
*
* @since 6.9.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 ) {
$category = wp_get_ability_category( $request['slug'] );
if ( ! $category ) {
return new WP_Error(
'rest_ability_category_not_found',
__( 'Ability category not found.' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $category, $request );
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read ability categories.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the request has read access.
*/
public function get_items_permissions_check( $request ) {
return current_user_can( 'read' );
}
/**
* Checks if a given request has access to read an ability category.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the request has read access.
*/
public function get_item_permissions_check( $request ) {
return current_user_can( 'read' );
}
/**
* Prepares an ability category for response.
*
* @since 6.9.0
*
* @param WP_Ability_Category $category The ability category object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $category, $request ) {
$data = array(
'slug' => $category->get_slug(),
'label' => $category->get_label(),
'description' => $category->get_description(),
'meta' => $category->get_meta(),
);
$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 );
$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $category->get_slug() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'abilities' => array(
'href' => rest_url( sprintf( '%s/abilities?category=%s', $this->namespace, $category->get_slug() ) ),
),
);
$response->add_links( $links );
}
return $response;
}
/**
* Retrieves the ability category's schema, conforming to JSON Schema.
*
* @since 6.9.0
*
* @return array<string, mixed> Item schema data.
*/
public function get_item_schema(): array {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'ability-category',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Unique identifier for the ability category.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Display label for the category.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the category.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta' => array(
'description' => __( 'Meta information about the category.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Retrieves the query params for collections.
*
* @since 6.9.0
*
* @return array<string, mixed> Collection parameters.
*/
public function get_collection_params(): array {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'page' => array(
'description' => __( 'Current page of the collection.' ),
'type' => 'integer',
'default' => 1,
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.' ),
'type' => 'integer',
'default' => 50,
'minimum' => 1,
'maximum' => 100,
),
);
}
}
@@ -0,0 +1,364 @@
<?php
/**
* REST API list controller for Abilities API.
*
* @package WordPress
* @subpackage Abilities_API
* @since 6.9.0
*/
declare( strict_types = 1 );
/**
* Core controller used to access abilities via the REST API.
*
* @since 6.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Abilities_V1_List_Controller extends WP_REST_Controller {
/**
* REST API namespace.
*
* @since 6.9.0
* @var string
*/
protected $namespace = 'wp-abilities/v1';
/**
* REST API base route.
*
* @since 6.9.0
* @var string
*/
protected $rest_base = 'abilities';
/**
* Registers the routes for abilities.
*
* @since 6.9.0
*
* @see register_rest_route()
*/
public function register_routes(): void {
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<name>[a-zA-Z0-9\-\/]+)',
array(
'args' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.' ),
'type' => 'string',
'pattern' => '^[a-zA-Z0-9\-\/]+$',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Retrieves all abilities.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success.
*/
public function get_items( $request ) {
$abilities = array_filter(
wp_get_abilities(),
static function ( $ability ) {
return $ability->get_meta_item( 'show_in_rest' );
}
);
// Filter by ability category if specified.
$category = $request['category'];
if ( ! empty( $category ) ) {
$abilities = array_filter(
$abilities,
static function ( $ability ) use ( $category ) {
return $ability->get_category() === $category;
}
);
// Reset array keys after filtering.
$abilities = array_values( $abilities );
}
$page = $request['page'];
$per_page = $request['per_page'];
$offset = ( $page - 1 ) * $per_page;
$total_abilities = count( $abilities );
$max_pages = (int) ceil( $total_abilities / $per_page );
if ( $request->get_method() === 'HEAD' ) {
$response = new WP_REST_Response( array() );
} else {
$abilities = array_slice( $abilities, $offset, $per_page );
$data = array();
foreach ( $abilities as $ability ) {
$item = $this->prepare_item_for_response( $ability, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
}
$response->header( 'X-WP-Total', (string) $total_abilities );
$response->header( 'X-WP-TotalPages', (string) $max_pages );
$query_params = $request->get_query_params();
$base = add_query_arg( urlencode_deep( $query_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $page < $max_pages ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Retrieves a specific ability.
*
* @since 6.9.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 ) {
$ability = wp_get_ability( $request['name'] );
if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
return new WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $ability, $request );
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read ability items.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the request has read access.
*/
public function get_items_permissions_check( $request ) {
return current_user_can( 'read' );
}
/**
* Checks if a given request has access to read an ability item.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the request has read access.
*/
public function get_item_permissions_check( $request ) {
return current_user_can( 'read' );
}
/**
* Normalizes schema empty object defaults.
*
* Converts empty array defaults to objects when the schema type is 'object'
* to ensure proper JSON serialization as {} instead of [].
*
* @since 6.9.0
*
* @param array<string, mixed> $schema The schema array.
* @return array<string, mixed> The normalized schema.
*/
private function normalize_schema_empty_object_defaults( array $schema ): array {
if ( isset( $schema['type'] ) && 'object' === $schema['type'] && isset( $schema['default'] ) ) {
$default = $schema['default'];
if ( is_array( $default ) && empty( $default ) ) {
$schema['default'] = (object) $default;
}
}
return $schema;
}
/**
* Prepares an ability for response.
*
* @since 6.9.0
*
* @param WP_Ability $ability The ability object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $ability, $request ) {
$data = array(
'name' => $ability->get_name(),
'label' => $ability->get_label(),
'description' => $ability->get_description(),
'category' => $ability->get_category(),
'input_schema' => $this->normalize_schema_empty_object_defaults( $ability->get_input_schema() ),
'output_schema' => $this->normalize_schema_empty_object_defaults( $ability->get_output_schema() ),
'meta' => $ability->get_meta(),
);
$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 );
$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $ability->get_name() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
$links['wp:action-run'] = array(
'href' => rest_url( sprintf( '%s/%s/%s/run', $this->namespace, $this->rest_base, $ability->get_name() ) ),
);
$response->add_links( $links );
}
return $response;
}
/**
* Retrieves the ability's schema, conforming to JSON Schema.
*
* @since 6.9.0
*
* @return array<string, mixed> Item schema data.
*/
public function get_item_schema(): array {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'ability',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Display label for the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'category' => array(
'description' => __( 'Ability category this ability belongs to.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'input_schema' => array(
'description' => __( 'JSON Schema for the ability input.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'output_schema' => array(
'description' => __( 'JSON Schema for the ability output.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta' => array(
'description' => __( 'Meta information about the ability.' ),
'type' => 'object',
'properties' => array(
'annotations' => array(
'description' => __( 'Annotations for the ability.' ),
'type' => array( 'boolean', 'null' ),
'default' => null,
),
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Retrieves the query params for collections.
*
* @since 6.9.0
*
* @return array<string, mixed> Collection parameters.
*/
public function get_collection_params(): array {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'page' => array(
'description' => __( 'Current page of the collection.' ),
'type' => 'integer',
'default' => 1,
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.' ),
'type' => 'integer',
'default' => 50,
'minimum' => 1,
'maximum' => 100,
),
'category' => array(
'description' => __( 'Limit results to abilities in specific ability category.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
}
@@ -0,0 +1,244 @@
<?php
/**
* REST API run controller for Abilities API.
*
* @package WordPress
* @subpackage Abilities_API
* @since 6.9.0
*/
declare( strict_types = 1 );
/**
* Core controller used to execute abilities via the REST API.
*
* @since 6.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Abilities_V1_Run_Controller extends WP_REST_Controller {
/**
* REST API namespace.
*
* @since 6.9.0
* @var string
*/
protected $namespace = 'wp-abilities/v1';
/**
* REST API base route.
*
* @since 6.9.0
* @var string
*/
protected $rest_base = 'abilities';
/**
* Registers the routes for ability execution.
*
* @since 6.9.0
*
* @see register_rest_route()
*/
public function register_routes(): void {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<name>[a-zA-Z0-9\-\/]+?)/run',
array(
'args' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.' ),
'type' => 'string',
'pattern' => '^[a-zA-Z0-9\-\/]+$',
),
),
// TODO: We register ALLMETHODS because at route registration time, we don't know which abilities
// exist or their annotations (`destructive`, `idempotent`, `readonly`). This is due to WordPress
// load order - routes are registered early, before plugins have registered their abilities.
// This approach works but could be improved with lazy route registration or a different
// architecture that allows type-specific routes after abilities are registered.
// This was the same issue that we ended up seeing with the Feature API.
array(
'methods' => WP_REST_Server::ALLMETHODS,
'callback' => array( $this, 'execute_ability' ),
'permission_callback' => array( $this, 'check_ability_permissions' ),
'args' => $this->get_run_args(),
),
'schema' => array( $this, 'get_run_schema' ),
)
);
}
/**
* Executes an ability.
*
* @since 6.9.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 execute_ability( $request ) {
$ability = wp_get_ability( $request['name'] );
if ( ! $ability ) {
return new WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
array( 'status' => 404 )
);
}
$input = $this->get_input_from_request( $request );
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
return $result;
}
return rest_ensure_response( $result );
}
/**
* Validates if the HTTP method matches the expected method for the ability based on its annotations.
*
* @since 6.9.0
*
* @param string $request_method The HTTP method of the request.
* @param array<string, (null|bool)> $annotations The ability annotations.
* @return true|WP_Error True on success, or WP_Error object on failure.
*/
public function validate_request_method( string $request_method, array $annotations ) {
$expected_method = 'POST';
if ( ! empty( $annotations['readonly'] ) ) {
$expected_method = 'GET';
} elseif ( ! empty( $annotations['destructive'] ) && ! empty( $annotations['idempotent'] ) ) {
$expected_method = 'DELETE';
}
if ( $expected_method === $request_method ) {
return true;
}
$error_message = __( 'Abilities that perform updates require POST method.' );
if ( 'GET' === $expected_method ) {
$error_message = __( 'Read-only abilities require GET method.' );
} elseif ( 'DELETE' === $expected_method ) {
$error_message = __( 'Abilities that perform destructive actions require DELETE method.' );
}
return new WP_Error(
'rest_ability_invalid_method',
$error_message,
array( 'status' => 405 )
);
}
/**
* Checks if a given request has permission to execute a specific ability.
*
* @since 6.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has execution permission, WP_Error object otherwise.
*/
public function check_ability_permissions( $request ) {
$ability = wp_get_ability( $request['name'] );
if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
return new WP_Error(
'rest_ability_not_found',
__( 'Ability not found.' ),
array( 'status' => 404 )
);
}
$is_valid = $this->validate_request_method(
$request->get_method(),
$ability->get_meta_item( 'annotations' )
);
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
$input = $this->get_input_from_request( $request );
$input = $ability->normalize_input( $input );
$is_valid = $ability->validate_input( $input );
if ( is_wp_error( $is_valid ) ) {
$is_valid->add_data( array( 'status' => 400 ) );
return $is_valid;
}
$result = $ability->check_permissions( $input );
if ( is_wp_error( $result ) ) {
$result->add_data( array( 'status' => rest_authorization_required_code() ) );
return $result;
}
if ( ! $result ) {
return new WP_Error(
'rest_ability_cannot_execute',
__( 'Sorry, you are not allowed to execute this ability.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Extracts input parameters from the request.
*
* @since 6.9.0
*
* @param WP_REST_Request $request The request object.
* @return mixed|null The input parameters.
*/
private function get_input_from_request( $request ) {
if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ), true ) ) {
// For GET and DELETE requests, look for 'input' query parameter.
$query_params = $request->get_query_params();
return $query_params['input'] ?? null;
}
// For POST requests, look for 'input' in JSON body.
$json_params = $request->get_json_params();
return $json_params['input'] ?? null;
}
/**
* Retrieves the arguments for ability execution endpoint.
*
* @since 6.9.0
*
* @return array<string, mixed> Arguments for the run endpoint.
*/
public function get_run_args(): array {
return array(
'input' => array(
'description' => __( 'Input parameters for the ability execution.' ),
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
'default' => null,
),
);
}
/**
* Retrieves the schema for ability execution endpoint.
*
* @since 6.9.0
*
* @return array<string, mixed> Schema for the run endpoint.
*/
public function get_run_schema(): array {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'ability-execution',
'type' => 'object',
'properties' => array(
'result' => array(
'description' => __( 'The result of the ability execution.' ),
'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
}
}
@@ -802,7 +802,16 @@ class WP_REST_Application_Passwords_Controller extends WP_REST_Controller {
'app_id' => array(
'description' => __( 'A UUID provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.' ),
'type' => 'string',
'format' => 'uuid',
'oneOf' => array(
array(
'type' => 'string',
'format' => 'uuid',
),
array(
'type' => 'string',
'enum' => array( '' ),
),
),
'context' => array( 'view', 'edit', 'embed' ),
),
'name' => array(
@@ -70,6 +70,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
* prepares for WP_Query.
*
* @since 4.7.0
* @since 6.9.0 Extends the `media_type` and `mime_type` request arguments to support array values.
*
* @param array $prepared_args Optional. Array of prepared arguments. Default empty array.
* @param WP_REST_Request $request Optional. Request to prepare items for.
@@ -82,19 +83,30 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
$query_args['post_status'] = 'inherit';
}
$media_types = $this->get_media_types();
$all_mime_types = array();
$media_types = $this->get_media_types();
if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
if ( ! empty( $request['media_type'] ) && is_array( $request['media_type'] ) ) {
foreach ( $request['media_type'] as $type ) {
if ( isset( $media_types[ $type ] ) ) {
$all_mime_types = array_merge( $all_mime_types, $media_types[ $type ] );
}
}
}
if ( ! empty( $request['mime_type'] ) ) {
$parts = explode( '/', $request['mime_type'] );
if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
$query_args['post_mime_type'] = $request['mime_type'];
if ( ! empty( $request['mime_type'] ) && is_array( $request['mime_type'] ) ) {
foreach ( $request['mime_type'] as $mime_type ) {
$parts = explode( '/', $mime_type );
if ( isset( $media_types[ $parts[0] ] ) && in_array( $mime_type, $media_types[ $parts[0] ], true ) ) {
$all_mime_types[] = $mime_type;
}
}
}
if ( ! empty( $all_mime_types ) ) {
$query_args['post_mime_type'] = array_values( array_unique( $all_mime_types ) );
}
// Filter query clauses to include filenames.
if ( isset( $query_args['s'] ) ) {
add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
@@ -543,6 +555,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
* Applies edits to a media item and creates a new attachment record.
*
* @since 5.5.0
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
@@ -584,6 +597,20 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
} else {
$modifiers = array();
if ( isset( $request['flip']['horizontal'] ) || isset( $request['flip']['vertical'] ) ) {
$flip_args = array(
'vertical' => isset( $request['flip']['vertical'] ) ? (bool) $request['flip']['vertical'] : false,
'horizontal' => isset( $request['flip']['horizontal'] ) ? (bool) $request['flip']['horizontal'] : false,
);
$modifiers[] = array(
'type' => 'flip',
'args' => array(
'flip' => $flip_args,
),
);
}
if ( ! empty( $request['rotation'] ) ) {
$modifiers[] = array(
'type' => 'rotate',
@@ -637,6 +664,21 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
foreach ( $modifiers as $modifier ) {
$args = $modifier['args'];
switch ( $modifier['type'] ) {
case 'flip':
/*
* Flips the current image.
* The vertical flip is the first argument (flip along horizontal axis), the horizontal flip is the second argument (flip along vertical axis).
* See: WP_Image_Editor::flip()
*/
$result = $image_editor->flip( $args['flip']['vertical'], $args['flip']['horizontal'] );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'rest_image_flip_failed',
__( 'Unable to flip this image.' ),
array( 'status' => 500 )
);
}
break;
case 'rotate':
// Rotation direction: clockwise vs. counterclockwise.
$rotate = 0 - $args['angle'];
@@ -711,24 +753,31 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
return $saved;
}
// Create new attachment post.
$new_attachment_post = array(
'post_mime_type' => $saved['mime-type'],
'guid' => $uploads['url'] . "/$filename",
'post_title' => $image_name,
'post_content' => '',
);
// Grab original attachment post so we can use it to set defaults.
$original_attachment_post = get_post( $attachment_id );
// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
$attachment_post = get_post( $attachment_id );
// Check request fields and assign default values.
$new_attachment_post = $this->prepare_item_for_database( $request );
$new_attachment_post->post_mime_type = $saved['mime-type'];
$new_attachment_post->guid = $uploads['url'] . "/$filename";
if ( $attachment_post ) {
$new_attachment_post['post_content'] = $attachment_post->post_content;
$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
$new_attachment_post['post_title'] = $attachment_post->post_title;
}
// Unset ID so wp_insert_attachment generates a new ID.
unset( $new_attachment_post->ID );
$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );
// Set new attachment post title with fallbacks.
$new_attachment_post->post_title = $new_attachment_post->post_title ?? $original_attachment_post->post_title ?? $image_name;
// Set new attachment post caption (post_excerpt).
$new_attachment_post->post_excerpt = $new_attachment_post->post_excerpt ?? $original_attachment_post->post_excerpt ?? '';
// Set new attachment post description (post_content) with fallbacks.
$new_attachment_post->post_content = $new_attachment_post->post_content ?? $original_attachment_post->post_content ?? '';
// Set post parent if set in request, else the default of `0` (no parent).
$new_attachment_post->post_parent = $new_attachment_post->post_parent ?? 0;
// Insert the new attachment post.
$new_attachment_id = wp_insert_attachment( wp_slash( (array) $new_attachment_post ), $saved['path'], 0, true );
if ( is_wp_error( $new_attachment_id ) ) {
if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
@@ -740,8 +789,8 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
return $new_attachment_id;
}
// Copy the image alt text from the edited image.
$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
// First, try to use the alt text from the request. If not set, copy the image alt text from the original attachment.
$image_alt = isset( $request['alt_text'] ) ? sanitize_text_field( $request['alt_text'] ) : get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
if ( ! empty( $image_alt ) ) {
// update_post_meta() expects slashed.
@@ -970,6 +1019,33 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
}
/**
* Prepares attachment links for the request.
*
* @since 6.9.0
*
* @param WP_Post $post Post object.
* @return array Links for the given attachment.
*/
protected function prepare_links( $post ) {
$links = parent::prepare_links( $post );
if ( ! empty( $post->post_parent ) ) {
$post = get_post( $post->post_parent );
if ( ! empty( $post ) ) {
$links['https://api.w.org/attached-to'] = array(
'href' => rest_url( rest_get_route_for_post( $post ) ),
'embeddable' => true,
'post_type' => $post->post_type,
'id' => $post->ID,
);
}
}
return $links;
}
/**
* Retrieves the attachment's schema, conforming to JSON Schema.
*
@@ -1278,6 +1354,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
* Retrieves the query params for collections of attachments.
*
* @since 4.7.0
* @since 6.9.0 Extends the `media_type` and `mime_type` request arguments to support array values.
*
* @return array Query parameters for the attachment collection as an array.
*/
@@ -1285,19 +1362,25 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
$params = parent::get_collection_params();
$params['status']['default'] = 'inherit';
$params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' );
$media_types = $this->get_media_types();
$media_types = array_keys( $this->get_media_types() );
$params['media_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular media type.' ),
'type' => 'string',
'enum' => array_keys( $media_types ),
'description' => __( 'Limit result set to attachments of a particular media type or media types.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => $media_types,
),
);
$params['mime_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
'type' => 'string',
'description' => __( 'Limit result set to attachments of a particular MIME type or MIME types.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
return $params;
@@ -1453,17 +1536,19 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
* Gets the request args for the edit item route.
*
* @since 5.5.0
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
*
* @return array
*/
protected function get_edit_media_item_args() {
return array(
$args = array(
'src' => array(
'description' => __( 'URL to the edited image file.' ),
'type' => 'string',
'format' => 'uri',
'required' => true,
),
// The `modifiers` param takes precedence over the older format.
'modifiers' => array(
'description' => __( 'Array of image edits.' ),
'type' => 'array',
@@ -1476,6 +1561,43 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
'args',
),
'oneOf' => array(
array(
'title' => __( 'Flip' ),
'properties' => array(
'type' => array(
'description' => __( 'Flip type.' ),
'type' => 'string',
'enum' => array( 'flip' ),
),
'args' => array(
'description' => __( 'Flip arguments.' ),
'type' => 'object',
'required' => array(
'flip',
),
'properties' => array(
'flip' => array(
'description' => __( 'Flip direction.' ),
'type' => 'object',
'required' => array(
'horizontal',
'vertical',
),
'properties' => array(
'horizontal' => array(
'description' => __( 'Whether to flip in the horizontal direction.' ),
'type' => 'boolean',
),
'vertical' => array(
'description' => __( 'Whether to flip in the vertical direction.' ),
'type' => 'boolean',
),
),
),
),
),
),
),
array(
'title' => __( 'Rotation' ),
'properties' => array(
@@ -1573,5 +1695,33 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
'maximum' => 100,
),
);
/*
* Get the args based on the post schema. This calls `rest_get_endpoint_args_for_schema()`,
* which also takes care of sanitization and validation.
*/
$update_item_args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );
if ( isset( $update_item_args['caption'] ) ) {
$args['caption'] = $update_item_args['caption'];
}
if ( isset( $update_item_args['description'] ) ) {
$args['description'] = $update_item_args['description'];
}
if ( isset( $update_item_args['title'] ) ) {
$args['title'] = $update_item_args['title'];
}
if ( isset( $update_item_args['post'] ) ) {
$args['post'] = $update_item_args['post'];
}
if ( isset( $update_item_args['alt_text'] ) ) {
$args['alt_text'] = $update_item_args['alt_text'];
}
return $args;
}
}
@@ -123,6 +123,10 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
* @return true|WP_Error True if the request has read access, error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$is_note = 'note' === $request['type'];
$is_edit_context = 'edit' === $request['context'];
$protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
$forbidden_params = array();
if ( ! empty( $request['post'] ) ) {
foreach ( (array) $request['post'] as $post_id ) {
@@ -141,10 +145,51 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
array( 'status' => rest_authorization_required_code() )
);
}
if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
if ( current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error(
'rest_comment_not_supported_post_type',
__( 'Sorry, this post type does not support notes.' ),
array( 'status' => 403 )
);
}
foreach ( $protected_params as $param ) {
if ( 'status' === $param ) {
if ( 'approve' !== $request[ $param ] ) {
$forbidden_params[] = $param;
}
} elseif ( 'type' === $param ) {
if ( 'comment' !== $request[ $param ] ) {
$forbidden_params[] = $param;
}
} elseif ( ! empty( $request[ $param ] ) ) {
$forbidden_params[] = $param;
}
}
return new WP_Error(
'rest_forbidden_param',
/* translators: %s: List of forbidden parameters. */
sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ),
array( 'status' => rest_authorization_required_code() )
);
}
}
}
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
// Re-map edit context capabilities when requesting `note` for a post.
if ( $is_edit_context && $is_note && ! empty( $request['post'] ) ) {
foreach ( (array) $request['post'] as $post_id ) {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit comments.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
} elseif ( $is_edit_context && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit comments.' ),
@@ -153,9 +198,6 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
}
if ( ! current_user_can( 'edit_posts' ) ) {
$protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
$forbidden_params = array();
foreach ( $protected_params as $param ) {
if ( 'status' === $param ) {
if ( 'approve' !== $request[ $param ] ) {
@@ -302,12 +344,13 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
$max_pages = (int) $query->max_num_pages;
if ( $total_comments < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
// Out-of-bounds, run the query without pagination/offset to get the total count.
unset( $prepared_args['number'], $prepared_args['offset'] );
$query = new WP_Comment_Query();
$prepared_args['count'] = true;
$prepared_args['orderby'] = 'none';
$query = new WP_Comment_Query();
$prepared_args['count'] = true;
$prepared_args['orderby'] = 'none';
$prepared_args['update_comment_meta_cache'] = false;
$total_comments = $query->query( $prepared_args );
$max_pages = (int) ceil( $total_comments / $request['per_page'] );
@@ -394,7 +437,9 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
return $comment;
}
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
// Re-map edit context capabilities when requesting `note` type.
$edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' );
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit comments.' ),
@@ -452,6 +497,16 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
* @return true|WP_Error True if the request has access to create items, error object otherwise.
*/
public function create_item_permissions_check( $request ) {
$is_note = ! empty( $request['type'] ) && 'note' === $request['type'];
if ( ! is_user_logged_in() && $is_note ) {
return new WP_Error(
'rest_comment_login_required',
__( 'Sorry, you must be logged in to comment.' ),
array( 'status' => 401 )
);
}
if ( ! is_user_logged_in() ) {
if ( get_option( 'comment_registration' ) ) {
return new WP_Error(
@@ -505,7 +560,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
}
}
if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
$edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' );
if ( isset( $request['status'] ) && ! current_user_can( ...$edit_cap ) ) {
return new WP_Error(
'rest_comment_invalid_status',
/* translators: %s: Request parameter. */
@@ -532,7 +588,15 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
);
}
if ( 'draft' === $post->post_status ) {
if ( $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
return new WP_Error(
'rest_comment_not_supported_post_type',
__( 'Sorry, this post type does not support notes.' ),
array( 'status' => 403 )
);
}
if ( 'draft' === $post->post_status && ! $is_note ) {
return new WP_Error(
'rest_comment_draft_post',
__( 'Sorry, you are not allowed to create a comment on this post.' ),
@@ -556,7 +620,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
);
}
if ( ! comments_open( $post->ID ) ) {
if ( ! comments_open( $post->ID ) && ! $is_note ) {
return new WP_Error(
'rest_comment_closed',
__( 'Sorry, comments are closed for this item.' ),
@@ -584,8 +648,8 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
);
}
// Do not allow comments to be created with a non-default type.
if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) {
// Do not allow comments to be created with a non-core type.
if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note' ), true ) ) {
return new WP_Error(
'rest_invalid_comment_type',
__( 'Cannot create a comment with that type.' ),
@@ -598,12 +662,17 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
return $prepared_comment;
}
$prepared_comment['comment_type'] = 'comment';
$prepared_comment['comment_type'] = $request['type'];
if ( ! isset( $prepared_comment['comment_content'] ) ) {
$prepared_comment['comment_content'] = '';
}
// Include note metadata into check_is_comment_content_allowed.
if ( isset( $request['meta']['_wp_note_status'] ) ) {
$prepared_comment['meta']['_wp_note_status'] = $request['meta']['_wp_note_status'];
}
if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) {
return new WP_Error(
'rest_comment_content_invalid',
@@ -666,7 +735,11 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
);
}
$prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );
// Don't check for duplicates or flooding for notes.
$prepared_comment['comment_approved'] =
'note' === $prepared_comment['comment_type'] ?
'1' :
wp_allow_comment( $prepared_comment, true );
if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
$error_code = $prepared_comment['comment_approved']->get_error_code();
@@ -859,8 +932,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
if ( is_wp_error( $prepared_args ) ) {
return $prepared_args;
}
if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
if ( ! $this->check_is_comment_content_allowed( $prepared_args ) ) {
return new WP_Error(
'rest_comment_content_invalid',
__( 'Invalid comment content.' ),
@@ -1207,6 +1279,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
array(
'count' => true,
'orderby' => 'none',
'type' => 'all',
)
);
@@ -1223,6 +1296,22 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
);
}
// Embedding children for notes requires `type` and `status` inheritance.
if ( isset( $links['children'] ) && 'note' === $comment->comment_type ) {
$args = array(
'parent' => $comment->comment_ID,
'type' => $comment->comment_type,
'status' => 'all',
);
$rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
$links['children'] = array(
'href' => $rest_url,
'embeddable' => true,
);
}
return $links;
}
@@ -1520,6 +1609,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
'default' => 'comment',
),
),
);
@@ -1821,7 +1911,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
* @return bool Whether the comment can be read.
*/
protected function check_read_permission( $comment, $request ) {
if ( ! empty( $comment->comment_post_ID ) ) {
if ( 'note' !== $comment->comment_type && ! empty( $comment->comment_post_ID ) ) {
$post = get_post( $comment->comment_post_ID );
if ( $post ) {
if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
@@ -1903,6 +1993,10 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
* @return bool True if the content is allowed, false otherwise.
*/
protected function check_is_comment_content_allowed( $prepared_comment ) {
if ( ! isset( $prepared_comment['comment_content'] ) ) {
return true;
}
$check = wp_parse_args(
$prepared_comment,
array(
@@ -1922,10 +2016,42 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
return true;
}
// Allow empty notes only when resolution metadata is valid.
if (
isset( $check['comment_type'] ) &&
'note' === $check['comment_type'] &&
isset( $check['meta']['_wp_note_status'] ) &&
in_array( $check['meta']['_wp_note_status'], array( 'resolved', 'reopen' ), true )
) {
return true;
}
/*
* Do not allow a comment to be created with missing or empty
* comment_content. See wp_handle_comment_submission().
*/
return '' !== $check['comment_content'];
}
/**
* Check if post type supports notes.
*
* @param string $post_type Post type name.
* @return bool True if post type supports notes, false otherwise.
*/
private function check_post_type_supports_notes( $post_type ) {
$supports = get_all_post_type_supports( $post_type );
if ( ! isset( $supports['editor'] ) ) {
return false;
}
if ( ! is_array( $supports['editor'] ) ) {
return false;
}
foreach ( $supports['editor'] as $item ) {
if ( ! empty( $item['notes'] ) ) {
return true;
}
}
return false;
}
}
@@ -662,11 +662,11 @@ abstract class WP_REST_Controller {
/**
* Sanitizes the slug value.
*
* @since 4.7.0
*
* @internal We can't use sanitize_title() directly, as the second
* {@internal We can't use sanitize_title() directly, as the second
* parameter is the fallback title, which would end up being set to the
* request object.
* request object.}
*
* @since 4.7.0
*
* @see https://github.com/WP-API/WP-API/issues/1585
*
@@ -69,6 +69,7 @@ class WP_REST_Font_Collections_Controller extends WP_REST_Controller {
*
* @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 ) {
@@ -88,7 +88,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
// Lists/updates a single global style variation based on the given id.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\/\w-]+)',
'/' . $this->rest_base . '/(?P<id>[\/\d+]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
@@ -96,9 +96,8 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
'description' => __( 'ID of global styles config.' ),
'type' => 'integer',
),
),
),
@@ -115,17 +114,17 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
}
/**
* Sanitize the global styles ID or stylesheet to decode endpoint.
* Sanitize the global styles stylesheet to decode endpoint.
* For example, `wp/v2/global-styles/twentytwentytwo%200.4.0`
* would be decoded to `twentytwentytwo 0.4.0`.
*
* @since 5.9.0
*
* @param string $id_or_stylesheet Global styles ID or stylesheet.
* @return string Sanitized global styles ID or stylesheet.
* @param string $stylesheet Global styles stylesheet.
* @return string Sanitized global styles stylesheet.
*/
public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
return urldecode( $id_or_stylesheet );
public function _sanitize_global_styles_callback( $stylesheet ) {
return urldecode( $stylesheet );
}
/**
@@ -139,7 +138,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
protected function get_post( $id ) {
$error = new WP_Error(
'rest_global_styles_not_found',
__( 'No global styles config exist with that id.' ),
__( 'No global styles config exists with that ID.' ),
array( 'status' => 404 )
);
@@ -464,7 +463,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
'properties' => array(
'id' => array(
'description' => __( 'ID of global styles config.' ),
'type' => 'string',
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
@@ -203,9 +203,15 @@ class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Revisions_Contr
$total_revisions = $revisions_query->found_posts;
if ( $total_revisions < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
// Out-of-bounds, run the query without pagination/offset to get the total count.
unset( $query_args['paged'], $query_args['offset'] );
$count_query = new WP_Query();
$count_query = new WP_Query();
$query_args['fields'] = 'ids';
$query_args['posts_per_page'] = 1;
$query_args['update_post_meta_cache'] = false;
$query_args['update_post_term_cache'] = false;
$count_query->query( $query_args );
$total_revisions = $count_query->found_posts;
@@ -88,7 +88,7 @@ class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {
* @param bool $read_only_access Whether the current user has read access to menu items
* via the REST API.
* @param WP_REST_Request $request Full details about the request.
* @param WP_REST_Controller $this The current instance of the controller.
* @param WP_REST_Controller $controller The current instance of the controller.
*/
$read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
if ( $read_only_access ) {
@@ -487,10 +487,15 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
$total_posts = $posts_query->found_posts;
if ( $total_posts < 1 && $page > 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
// Out-of-bounds, run the query without pagination/offset to get the total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query = new WP_Query();
$query_args['fields'] = 'ids';
$query_args['posts_per_page'] = 1;
$query_args['update_post_meta_cache'] = false;
$query_args['update_post_term_cache'] = false;
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
@@ -308,12 +308,16 @@ class WP_REST_Revisions_Controller extends WP_REST_Controller {
$total_revisions = $revisions_query->found_posts;
if ( $total_revisions < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
// Out-of-bounds, run the query without pagination/offset to get the total count.
unset( $query_args['paged'], $query_args['offset'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$count_query = new WP_Query();
$query_args['fields'] = 'ids';
$query_args['posts_per_page'] = 1;
$query_args['update_post_meta_cache'] = false;
$query_args['update_post_term_cache'] = false;
$count_query->query( $query_args );
$total_revisions = $count_query->found_posts;
}
@@ -414,6 +414,15 @@ class WP_REST_Themes_Controller extends WP_REST_Controller {
);
}
if ( $theme->is_block_theme() && $this->is_same_theme( $theme, wp_get_theme() ) ) {
$links['https://api.w.org/export-theme'] = array(
'href' => rest_url( 'wp-block-editor/v1/export' ),
'targetHints' => array(
'allow' => current_user_can( 'export' ) ? array( 'GET' ) : array(),
),
);
}
return $links;
}
@@ -399,10 +399,13 @@ class WP_REST_Users_Controller extends WP_REST_Controller {
$total_users = $query->get_total();
if ( $total_users < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
// Out-of-bounds, run the query without pagination/offset to get the total count.
unset( $prepared_args['number'], $prepared_args['offset'] );
$count_query = new WP_User_Query( $prepared_args );
$total_users = $count_query->get_total();
$prepared_args['number'] = 1;
$prepared_args['fields'] = 'ID';
$count_query = new WP_User_Query( $prepared_args );
$total_users = $count_query->get_total();
}
$response->header( 'X-WP-Total', (int) $total_users );
@@ -100,7 +100,7 @@ class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler {
* @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();