--- a/wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php Tue Dec 15 15:52:01 2020 +0100
+++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php Wed Sep 21 18:19:35 2022 +0200
@@ -32,6 +32,14 @@
protected $meta;
/**
+ * Passwordless post access permitted.
+ *
+ * @since 5.7.1
+ * @var int[]
+ */
+ protected $password_check_passed = array();
+
+ /**
* Constructor.
*
* @since 4.7.0
@@ -48,7 +56,7 @@
}
/**
- * Registers the routes for the objects of the controller.
+ * Registers the routes for posts.
*
* @since 4.7.0
*
@@ -92,7 +100,7 @@
array(
'args' => array(
'id' => array(
- 'description' => __( 'Unique identifier for the object.' ),
+ 'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
),
),
@@ -149,6 +157,38 @@
}
/**
+ * Override the result of the post password check for REST requested posts.
+ *
+ * Allow users to read the content of password protected posts if they have
+ * previously passed a permission check or if they have the `edit_post` capability
+ * for the post being checked.
+ *
+ * @since 5.7.1
+ *
+ * @param bool $required Whether the post requires a password check.
+ * @param WP_Post $post The post been password checked.
+ * @return bool Result of password check taking in to account REST API considerations.
+ */
+ public function check_password_required( $required, $post ) {
+ if ( ! $required ) {
+ return $required;
+ }
+
+ $post = get_post( $post );
+
+ if ( ! $post ) {
+ return $required;
+ }
+
+ if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) {
+ // Password previously checked and approved.
+ return false;
+ }
+
+ return ! current_user_can( 'edit_post', $post->ID );
+ }
+
+ /**
* Retrieves a collection of posts.
*
* @since 4.7.0
@@ -216,14 +256,32 @@
// Check for & assign any parameters which require special handling or setting.
$args['date_query'] = array();
- // Set before into date query. Date query must be specified as an array of an array.
if ( isset( $registered['before'], $request['before'] ) ) {
- $args['date_query'][0]['before'] = $request['before'];
+ $args['date_query'][] = array(
+ 'before' => $request['before'],
+ 'column' => 'post_date',
+ );
+ }
+
+ if ( isset( $registered['modified_before'], $request['modified_before'] ) ) {
+ $args['date_query'][] = array(
+ 'before' => $request['modified_before'],
+ 'column' => 'post_modified',
+ );
}
- // Set after into date query. Date query must be specified as an array of an array.
if ( isset( $registered['after'], $request['after'] ) ) {
- $args['date_query'][0]['after'] = $request['after'];
+ $args['date_query'][] = array(
+ 'after' => $request['after'],
+ 'column' => 'post_date',
+ );
+ }
+
+ if ( isset( $registered['modified_after'], $request['modified_after'] ) ) {
+ $args['date_query'][] = array(
+ 'after' => $request['modified_after'],
+ 'column' => 'post_modified',
+ );
}
// Ensure our per_page parameter overrides any provided posts_per_page filter.
@@ -262,60 +320,41 @@
}
}
+ $args = $this->prepare_tax_query( $args, $request );
+
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
/**
- * Filters the query arguments for a request.
+ * Filters WP_Query arguments when querying posts via the REST API.
+ *
+ * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
+ *
+ * Possible hook names include:
+ *
+ * - `rest_post_query`
+ * - `rest_page_query`
+ * - `rest_attachment_query`
*
* Enables adding extra arguments or setting defaults for a post collection request.
*
* @since 4.7.0
+ * @since 5.7.0 Moved after the `tax_query` query arg is generated.
*
* @link https://developer.wordpress.org/reference/classes/wp_query/
*
- * @param array $args Key value array of query var to query value.
- * @param WP_REST_Request $request The request used.
+ * @param array $args Array of arguments for WP_Query.
+ * @param WP_REST_Request $request The REST API request.
*/
$args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
$query_args = $this->prepare_items_query( $args, $request );
- $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
-
- if ( ! empty( $request['tax_relation'] ) ) {
- $query_args['tax_query'] = array( 'relation' => $request['tax_relation'] );
- }
-
- foreach ( $taxonomies as $taxonomy ) {
- $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
- $tax_exclude = $base . '_exclude';
-
- if ( ! empty( $request[ $base ] ) ) {
- $query_args['tax_query'][] = array(
- 'taxonomy' => $taxonomy->name,
- 'field' => 'term_id',
- 'terms' => $request[ $base ],
- 'include_children' => false,
- );
- }
-
- if ( ! empty( $request[ $tax_exclude ] ) ) {
- $query_args['tax_query'][] = array(
- 'taxonomy' => $taxonomy->name,
- 'field' => 'term_id',
- 'terms' => $request[ $tax_exclude ],
- 'include_children' => false,
- 'operator' => 'NOT IN',
- );
- }
- }
-
$posts_query = new WP_Query();
$query_result = $posts_query->query( $query_args );
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
- add_filter( 'post_password_required', '__return_false' );
+ add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
$posts = array();
@@ -331,7 +370,7 @@
// Reset filter.
if ( 'edit' === $request['context'] ) {
- remove_filter( 'post_password_required', '__return_false' );
+ remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
$page = (int) $query_args['paged'];
@@ -417,7 +456,7 @@
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
- * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise.
+ * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
@@ -446,7 +485,7 @@
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
- add_filter( 'post_password_required', '__return_false' );
+ add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
if ( $post ) {
@@ -474,8 +513,14 @@
return false;
}
- // Edit context always gets access to password-protected posts.
- if ( 'edit' === $request['context'] ) {
+ /*
+ * Users always gets access to password protected content in the edit
+ * context if they have the `edit_post` meta capability.
+ */
+ if (
+ 'edit' === $request['context'] &&
+ current_user_can( 'edit_post', $post->ID )
+ ) {
return true;
}
@@ -591,7 +636,7 @@
$prepared_post->post_type = $this->post_type;
- $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
+ $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false );
if ( is_wp_error( $post_id ) ) {
@@ -611,6 +656,12 @@
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
+ * Possible hook names include:
+ *
+ * - `rest_insert_post`
+ * - `rest_insert_page`
+ * - `rest_insert_attachment`
+ *
* @since 4.7.0
*
* @param WP_Post $post Inserted or updated post object.
@@ -669,6 +720,12 @@
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
+ * Possible hook names include:
+ *
+ * - `rest_after_insert_post`
+ * - `rest_after_insert_page`
+ * - `rest_after_insert_attachment`
+ *
* @since 5.0.0
*
* @param WP_Post $post Inserted or updated post object.
@@ -677,6 +734,8 @@
*/
do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
+ wp_after_insert_post( $post, false, null );
+
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
@@ -751,14 +810,15 @@
return $valid_check;
}
- $post = $this->prepare_item_for_database( $request );
+ $post_before = get_post( $request['id'] );
+ $post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
- $post_id = wp_update_post( wp_slash( (array) $post ), true );
+ $post_id = wp_update_post( wp_slash( (array) $post ), true, false );
if ( is_wp_error( $post_id ) ) {
if ( 'db_update_error' === $post_id->get_error_code() ) {
@@ -828,6 +888,8 @@
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
+ wp_after_insert_post( $post, true, $post_before );
+
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
@@ -886,6 +948,12 @@
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
+ * Possible hook names include:
+ *
+ * - `rest_post_trashable`
+ * - `rest_page_trashable`
+ * - `rest_attachment_trashable`
+ *
* Pass false to disable Trash support for the post.
*
* @since 4.7.0
@@ -1048,7 +1116,8 @@
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
- $prepared_post = new stdClass();
+ $prepared_post = new stdClass();
+ $current_status = '';
// Post ID.
if ( isset( $request['id'] ) ) {
@@ -1058,6 +1127,7 @@
}
$prepared_post->ID = $existing_post->ID;
+ $current_status = $existing_post->post_status;
}
$schema = $this->get_item_schema();
@@ -1101,7 +1171,11 @@
$post_type = get_post_type_object( $prepared_post->post_type );
// Post status.
- if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
+ if (
+ ! empty( $schema['properties']['status'] ) &&
+ isset( $request['status'] ) &&
+ ( ! $current_status || $current_status !== $request['status'] )
+ ) {
$status = $this->handle_status_param( $request['status'], $post_type );
if ( is_wp_error( $status ) ) {
@@ -1241,6 +1315,12 @@
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
+ * Possible hook names include:
+ *
+ * - `rest_pre_insert_post`
+ * - `rest_pre_insert_page`
+ * - `rest_pre_insert_attachment`
+ *
* @since 4.7.0
*
* @param stdClass $prepared_post An object representing a single post prepared
@@ -1252,6 +1332,32 @@
}
/**
+ * Checks whether the status is valid for the given post.
+ *
+ * Allows for sending an update request with the current status, even if that status would not be acceptable.
+ *
+ * @since 5.6.0
+ *
+ * @param string $status The provided status.
+ * @param WP_REST_Request $request The request object.
+ * @param string $param The parameter name.
+ * @return true|WP_Error True if the status is valid, or WP_Error if not.
+ */
+ public function check_status( $status, $request, $param ) {
+ if ( $request['id'] ) {
+ $post = $this->get_post( $request['id'] );
+
+ if ( ! is_wp_error( $post ) && $post->post_status === $status ) {
+ return true;
+ }
+ }
+
+ $args = $request->get_attributes()['args'][ $param ];
+
+ return rest_validate_value_from_schema( $status, $args, $param );
+ }
+
+ /**
* Determines validity and normalizes the given status parameter.
*
* @since 4.7.0
@@ -1340,8 +1446,10 @@
}
if ( $request['id'] ) {
+ $post = get_post( $request['id'] );
$current_template = get_page_template_slug( $request['id'] );
} else {
+ $post = null;
$current_template = '';
}
@@ -1351,7 +1459,7 @@
}
// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
- $allowed_templates = wp_get_theme()->get_page_templates( get_post( $request['id'] ), $this->post_type );
+ $allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type );
if ( isset( $allowed_templates[ $template ] ) ) {
return true;
@@ -1370,9 +1478,9 @@
* @since 4.7.0
* @since 4.9.0 Added the `$validate` parameter.
*
- * @param string $template Page template filename.
- * @param integer $post_id Post ID.
- * @param bool $validate Whether to validate that the template selected is valid.
+ * @param string $template Page template filename.
+ * @param int $post_id Post ID.
+ * @param bool $validate Whether to validate that the template selected is valid.
*/
public function handle_template( $template, $post_id, $validate = false ) {
@@ -1666,8 +1774,9 @@
$has_password_filter = false;
if ( $this->can_access_password_content( $post, $request ) ) {
+ $this->password_check_passed[ $post->ID ] = true;
// Allow access to the post, permissions already checked before.
- add_filter( 'post_password_required', '__return_false' );
+ add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
$has_password_filter = true;
}
@@ -1705,7 +1814,7 @@
if ( $has_password_filter ) {
// Reset filter.
- remove_filter( 'post_password_required', '__return_false' );
+ remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
if ( rest_is_field_included( 'author', $fields ) ) {
@@ -1812,10 +1921,16 @@
}
/**
- * Filters the post data for a response.
+ * Filters the post data for a REST API response.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
+ * Possible hook names include:
+ *
+ * - `rest_prepare_post`
+ * - `rest_prepare_page`
+ * - `rest_prepare_attachment`
+ *
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
@@ -2038,31 +2153,31 @@
// Base properties for every Post.
'properties' => array(
'date' => array(
- 'description' => __( "The date the object was published, in the site's timezone." ),
+ 'description' => __( "The date the post was published, in the site's timezone." ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
- 'description' => __( 'The date the object was published, as GMT.' ),
+ 'description' => __( 'The date the post was published, as GMT.' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'guid' => array(
- 'description' => __( 'The globally unique identifier for the object.' ),
+ 'description' => __( 'The globally unique identifier for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
'raw' => array(
- 'description' => __( 'GUID for the object, as it exists in the database.' ),
+ 'description' => __( 'GUID for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'rendered' => array(
- 'description' => __( 'GUID for the object, transformed for display.' ),
+ 'description' => __( 'GUID for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
@@ -2070,34 +2185,34 @@
),
),
'id' => array(
- 'description' => __( 'Unique identifier for the object.' ),
+ 'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'link' => array(
- 'description' => __( 'URL to the object.' ),
+ 'description' => __( 'URL to the post.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'modified' => array(
- 'description' => __( "The date the object was last modified, in the site's timezone." ),
+ 'description' => __( "The date the post was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'modified_gmt' => array(
- 'description' => __( 'The date the object was last modified, as GMT.' ),
+ 'description' => __( 'The date the post was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
- 'description' => __( 'An alphanumeric identifier for the object unique to its type.' ),
+ 'description' => __( 'An alphanumeric identifier for the post unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
@@ -2105,13 +2220,16 @@
),
),
'status' => array(
- 'description' => __( 'A named status for the object.' ),
+ 'description' => __( 'A named status for the post.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'context' => array( 'view', 'edit' ),
+ 'arg_options' => array(
+ 'validate_callback' => array( $this, 'check_status' ),
+ ),
),
'type' => array(
- 'description' => __( 'Type of Post for the object.' ),
+ 'description' => __( 'Type of post.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
@@ -2127,14 +2245,14 @@
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$schema['properties']['permalink_template'] = array(
- 'description' => __( 'Permalink template for the object.' ),
+ 'description' => __( 'Permalink template for the post.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['generated_slug'] = array(
- 'description' => __( 'Slug automatically generated from the object title.' ),
+ 'description' => __( 'Slug automatically generated from the post title.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
@@ -2143,7 +2261,7 @@
if ( $post_type_obj->hierarchical ) {
$schema['properties']['parent'] = array(
- 'description' => __( 'The ID for the parent of the object.' ),
+ 'description' => __( 'The ID for the parent of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
@@ -2204,7 +2322,7 @@
case 'title':
$schema['properties']['title'] = array(
- 'description' => __( 'The title for the object.' ),
+ 'description' => __( 'The title for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
@@ -2213,12 +2331,12 @@
),
'properties' => array(
'raw' => array(
- 'description' => __( 'Title for the object, as it exists in the database.' ),
+ 'description' => __( 'Title for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
- 'description' => __( 'HTML title for the object, transformed for display.' ),
+ 'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
@@ -2229,7 +2347,7 @@
case 'editor':
$schema['properties']['content'] = array(
- 'description' => __( 'The content for the object.' ),
+ 'description' => __( 'The content for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
@@ -2238,18 +2356,18 @@
),
'properties' => array(
'raw' => array(
- 'description' => __( 'Content for the object, as it exists in the database.' ),
+ 'description' => __( 'Content for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
- 'description' => __( 'HTML content for the object, transformed for display.' ),
+ 'description' => __( 'HTML content for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'block_version' => array(
- 'description' => __( 'Version of the content block format used by the object.' ),
+ 'description' => __( 'Version of the content block format used by the post.' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
@@ -2266,7 +2384,7 @@
case 'author':
$schema['properties']['author'] = array(
- 'description' => __( 'The ID for the author of the object.' ),
+ 'description' => __( 'The ID for the author of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
@@ -2274,7 +2392,7 @@
case 'excerpt':
$schema['properties']['excerpt'] = array(
- 'description' => __( 'The excerpt for the object.' ),
+ 'description' => __( 'The excerpt for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
@@ -2283,12 +2401,12 @@
),
'properties' => array(
'raw' => array(
- 'description' => __( 'Excerpt for the object, as it exists in the database.' ),
+ 'description' => __( 'Excerpt for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
- 'description' => __( 'HTML excerpt for the object, transformed for display.' ),
+ 'description' => __( 'HTML excerpt for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
@@ -2305,7 +2423,7 @@
case 'thumbnail':
$schema['properties']['featured_media'] = array(
- 'description' => __( 'The ID of the featured media for the object.' ),
+ 'description' => __( 'The ID of the featured media for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
@@ -2313,13 +2431,13 @@
case 'comments':
$schema['properties']['comment_status'] = array(
- 'description' => __( 'Whether or not comments are open on the object.' ),
+ 'description' => __( 'Whether or not comments are open on the post.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['ping_status'] = array(
- 'description' => __( 'Whether or not the object can be pinged.' ),
+ 'description' => __( 'Whether or not the post can be pinged.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
@@ -2328,7 +2446,7 @@
case 'page-attributes':
$schema['properties']['menu_order'] = array(
- 'description' => __( 'The order of the object in relation to other object of its type.' ),
+ 'description' => __( 'The order of the post in relation to other posts.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
@@ -2339,7 +2457,7 @@
$formats = array_values( get_post_format_slugs() );
$schema['properties']['format'] = array(
- 'description' => __( 'The format for the object.' ),
+ 'description' => __( 'The format for the post.' ),
'type' => 'string',
'enum' => $formats,
'context' => array( 'view', 'edit' ),
@@ -2355,14 +2473,14 @@
if ( 'post' === $this->post_type ) {
$schema['properties']['sticky'] = array(
- 'description' => __( 'Whether or not the object should be treated as sticky.' ),
+ 'description' => __( 'Whether or not the post should be treated as sticky.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
);
}
$schema['properties']['template'] = array(
- 'description' => __( 'The theme file to use to display the object.' ),
+ 'description' => __( 'The theme file to use to display the post.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
@@ -2380,7 +2498,7 @@
_doing_it_wrong(
'register_taxonomy',
sprintf(
- /* translators: 1. The taxonomy name, 2. The property name, either 'rest_base' or 'name', 3. The conflicting value. */
+ /* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */
__( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ),
$taxonomy->name,
$taxonomy_field_name_with_conflict,
@@ -2392,7 +2510,7 @@
$schema['properties'][ $base ] = array(
/* translators: %s: Taxonomy name. */
- 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
+ 'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ),
'type' => 'array',
'items' => array(
'type' => 'integer',
@@ -2411,7 +2529,7 @@
$schema_fields = array_keys( $schema['properties'] );
/**
- * Filter the post's schema.
+ * Filters the post's schema.
*
* The dynamic portion of the filter, `$this->post_type`, refers to the
* post type slug for the controller.
@@ -2571,6 +2689,8 @@
* Retrieves the query params for the posts collection.
*
* @since 4.7.0
+ * @since 5.4.0 The `tax_relation` query parameter was added.
+ * @since 5.7.0 The `modified_after` and `modified_before` query parameters were added.
*
* @return array Collection parameters.
*/
@@ -2585,6 +2705,12 @@
'format' => 'date-time',
);
+ $query_params['modified_after'] = array(
+ 'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ );
+
if ( post_type_supports( $this->post_type, 'author' ) ) {
$query_params['author'] = array(
'description' => __( 'Limit result set to posts assigned to specific authors.' ),
@@ -2610,6 +2736,12 @@
'format' => 'date-time',
);
+ $query_params['modified_before'] = array(
+ 'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ );
+
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
@@ -2648,7 +2780,7 @@
);
$query_params['orderby'] = array(
- 'description' => __( 'Sort collection by object attribute.' ),
+ 'description' => __( 'Sort collection by post attribute.' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
@@ -2710,39 +2842,7 @@
'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
);
- $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
-
- if ( ! empty( $taxonomies ) ) {
- $query_params['tax_relation'] = array(
- 'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
- 'type' => 'string',
- 'enum' => array( 'AND', 'OR' ),
- );
- }
-
- foreach ( $taxonomies as $taxonomy ) {
- $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
-
- $query_params[ $base ] = array(
- /* translators: %s: Taxonomy name. */
- 'description' => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
- 'type' => 'array',
- 'items' => array(
- 'type' => 'integer',
- ),
- 'default' => array(),
- );
-
- $query_params[ $base . '_exclude' ] = array(
- /* translators: %s: Taxonomy name. */
- 'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
- 'type' => 'array',
- 'items' => array(
- 'type' => 'integer',
- ),
- 'default' => array(),
- );
- }
+ $query_params = $this->prepare_taxonomy_limit_schema( $query_params );
if ( 'post' === $this->post_type ) {
$query_params['sticky'] = array(
@@ -2752,7 +2852,7 @@
}
/**
- * Filter collection parameters for the posts controller.
+ * Filters collection parameters for the posts controller.
*
* The dynamic part of the filter `$this->post_type` refers to the post
* type slug for the controller.
@@ -2810,4 +2910,182 @@
return $statuses;
}
+
+ /**
+ * Prepares the 'tax_query' for a collection of posts.
+ *
+ * @since 5.7.0
+ *
+ * @param array $args WP_Query arguments.
+ * @param WP_REST_Request $request Full details about the request.
+ * @return array Updated query arguments.
+ */
+ private function prepare_tax_query( array $args, WP_REST_Request $request ) {
+ $relation = $request['tax_relation'];
+
+ if ( $relation ) {
+ $args['tax_query'] = array( 'relation' => $relation );
+ }
+
+ $taxonomies = wp_list_filter(
+ get_object_taxonomies( $this->post_type, 'objects' ),
+ array( 'show_in_rest' => true )
+ );
+
+ foreach ( $taxonomies as $taxonomy ) {
+ $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
+
+ $tax_include = $request[ $base ];
+ $tax_exclude = $request[ $base . '_exclude' ];
+
+ if ( $tax_include ) {
+ $terms = array();
+ $include_children = false;
+ $operator = 'IN';
+
+ if ( rest_is_array( $tax_include ) ) {
+ $terms = $tax_include;
+ } elseif ( rest_is_object( $tax_include ) ) {
+ $terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
+ $include_children = ! empty( $tax_include['include_children'] );
+
+ if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) {
+ $operator = 'AND';
+ }
+ }
+
+ if ( $terms ) {
+ $args['tax_query'][] = array(
+ 'taxonomy' => $taxonomy->name,
+ 'field' => 'term_id',
+ 'terms' => $terms,
+ 'include_children' => $include_children,
+ 'operator' => $operator,
+ );
+ }
+ }
+
+ if ( $tax_exclude ) {
+ $terms = array();
+ $include_children = false;
+
+ if ( rest_is_array( $tax_exclude ) ) {
+ $terms = $tax_exclude;
+ } elseif ( rest_is_object( $tax_exclude ) ) {
+ $terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
+ $include_children = ! empty( $tax_exclude['include_children'] );
+ }
+
+ if ( $terms ) {
+ $args['tax_query'][] = array(
+ 'taxonomy' => $taxonomy->name,
+ 'field' => 'term_id',
+ 'terms' => $terms,
+ 'include_children' => $include_children,
+ 'operator' => 'NOT IN',
+ );
+ }
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Prepares the collection schema for including and excluding items by terms.
+ *
+ * @since 5.7.0
+ *
+ * @param array $query_params Collection schema.
+ * @return array Updated schema.
+ */
+ private function prepare_taxonomy_limit_schema( array $query_params ) {
+ $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
+
+ if ( ! $taxonomies ) {
+ return $query_params;
+ }
+
+ $query_params['tax_relation'] = array(
+ 'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
+ 'type' => 'string',
+ 'enum' => array( 'AND', 'OR' ),
+ );
+
+ $limit_schema = array(
+ 'type' => array( 'object', 'array' ),
+ 'oneOf' => array(
+ array(
+ 'title' => __( 'Term ID List' ),
+ 'description' => __( 'Match terms with the listed IDs.' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+ ),
+ array(
+ 'title' => __( 'Term ID Taxonomy Query' ),
+ 'description' => __( 'Perform an advanced term query.' ),
+ 'type' => 'object',
+ 'properties' => array(
+ 'terms' => array(
+ 'description' => __( 'Term IDs.' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'integer',
+ ),
+ 'default' => array(),
+ ),
+ 'include_children' => array(
+ 'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
+ 'type' => 'boolean',
+ 'default' => false,
+ ),
+ ),
+ 'additionalProperties' => false,
+ ),
+ ),
+ );
+
+ $include_schema = array_merge(
+ array(
+ /* translators: %s: Taxonomy name. */
+ 'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
+ ),
+ $limit_schema
+ );
+ // 'operator' is supported only for 'include' queries.
+ $include_schema['oneOf'][1]['properties']['operator'] = array(
+ 'description' => __( 'Whether items must be assigned all or any of the specified terms.' ),
+ 'type' => 'string',
+ 'enum' => array( 'AND', 'OR' ),
+ 'default' => 'OR',
+ );
+
+ $exclude_schema = array_merge(
+ array(
+ /* translators: %s: Taxonomy name. */
+ 'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
+ ),
+ $limit_schema
+ );
+
+ foreach ( $taxonomies as $taxonomy ) {
+ $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
+ $base_exclude = $base . '_exclude';
+
+ $query_params[ $base ] = $include_schema;
+ $query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );
+
+ $query_params[ $base_exclude ] = $exclude_schema;
+ $query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );
+
+ if ( ! $taxonomy->hierarchical ) {
+ unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
+ unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
+ }
+ }
+
+ return $query_params;
+ }
}