From 1f51e1f4f6c81238fffed706ff165795ea34d522 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 10 Oct 2023 14:03:03 +0000 Subject: [PATCH] REST API: Fix issue with Template and Template Part Revision/Autosave REST API controllers. The Template and Template Part REST API controllers have unique characteristics compared to other post type REST API controllers. They do not rely on integer IDs to reference objects; instead, they use a combination of the theme name and slug of the template, like 'twentytwentyfour//home.' Consequently, when the post types template and template part were introduced in [52062], it led to the registration of REST API endpoints for autosaves and revisions with invalid URL structures. In this commit, we introduce new functionality to enable custom autosave and revisions endpoints to be registered at the post type level. Similar to the 'rest_controller_class' parameter, developers can now define 'revisions_rest_controller' and 'autosave_rest_controller.' This empowers developers to create custom controllers for these functionalities. Additionally, we introduce a 'late_route_registration' parameter, which proves helpful when dealing with custom URL patterns and regex pattern matching issues. This commit registers new classes for template and template part autosave and revisions controllers, differentiating them from standard controllers in the following ways: * The response shape now matches that of the template controller. * Permission checks align with the template controller. * A custom URL pattern is introduced to support slug-based identification of templates. Furthermore, we've updated the utility function '_build_block_template_result_from_post' to support passing revision post objects. This enhancement ensures compatibility with the custom revisions controller. Props spacedmonkey, revgeorge, andraganescu, hellofromTonya, antonvlasenko, kadamwhite, ironprogrammer, costdev, mukesh27, timothyblynjacobs, adamsilverstein. Fixes 56922. git-svn-id: https://develop.svn.wordpress.org/trunk@56819 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/block-template-utils.php | 26 +- src/wp-includes/class-wp-post-type.php | 193 +- src/wp-includes/post.php | 281 +- src/wp-includes/rest-api.php | 16 +- .../class-wp-rest-autosaves-controller.php | 13 +- ...-wp-rest-template-autosaves-controller.php | 276 ++ ...-wp-rest-template-revisions-controller.php | 297 ++ .../class-wp-rest-templates-controller.php | 23 +- src/wp-settings.php | 2 + tests/phpunit/tests/post/wpPostType.php | 210 ++ .../tests/rest-api/rest-schema-setup.php | 16 +- .../wpRestTemplateAutosavesController.php | 413 ++ .../wpRestTemplateRevisionsController.php | 511 +++ tests/qunit/fixtures/wp-api-generated.js | 3350 ++++++++--------- 14 files changed, 3758 insertions(+), 1869 deletions(-) create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php create mode 100644 tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php create mode 100644 tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 0ce64a659d6a6..c5953e1d4ce2c 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -724,6 +724,7 @@ function _wp_build_title_and_description_for_taxonomy_block_template( $taxonomy, * * @since 5.9.0 * @since 6.3.0 Added `modified` property to template objects. + * @since 6.4.0 Added support for a revision post to be passed to this function. * @access private * * @param WP_Post $post Template post. @@ -731,7 +732,14 @@ function _wp_build_title_and_description_for_taxonomy_block_template( $taxonomy, */ function _build_block_template_result_from_post( $post ) { $default_template_types = get_default_block_template_types(); - $terms = get_the_terms( $post, 'wp_theme' ); + + $post_id = wp_is_post_revision( $post ); + if ( ! $post_id ) { + $post_id = $post; + } + $parent_post = get_post( $post_id ); + + $terms = get_the_terms( $parent_post, 'wp_theme' ); if ( is_wp_error( $terms ) ) { return $terms; @@ -745,12 +753,12 @@ function _build_block_template_result_from_post( $post ) { $template_file = _get_block_template_file( $post->post_type, $post->post_name ); $has_theme_file = get_stylesheet() === $theme && null !== $template_file; - $origin = get_post_meta( $post->ID, 'origin', true ); - $is_wp_suggestion = get_post_meta( $post->ID, 'is_wp_suggestion', true ); + $origin = get_post_meta( $parent_post->ID, 'origin', true ); + $is_wp_suggestion = get_post_meta( $parent_post->ID, 'is_wp_suggestion', true ); $template = new WP_Block_Template(); $template->wp_id = $post->ID; - $template->id = $theme . '//' . $post->post_name; + $template->id = $theme . '//' . $parent_post->post_name; $template->theme = $theme; $template->content = $post->post_content; $template->slug = $post->post_name; @@ -765,23 +773,23 @@ function _build_block_template_result_from_post( $post ) { $template->author = $post->post_author; $template->modified = $post->post_modified; - if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { + if ( 'wp_template' === $parent_post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { $template->post_types = $template_file['postTypes']; } - if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) { + if ( 'wp_template' === $parent_post->post_type && isset( $default_template_types[ $template->slug ] ) ) { $template->is_custom = false; } - if ( 'wp_template_part' === $post->post_type ) { - $type_terms = get_the_terms( $post, 'wp_template_part_area' ); + if ( 'wp_template_part' === $parent_post->post_type ) { + $type_terms = get_the_terms( $parent_post, 'wp_template_part_area' ); if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { $template->area = $type_terms[0]->name; } } // Check for a block template without a description and title or with a title equal to the slug. - if ( 'wp_template' === $post->post_type && empty( $template->description ) && ( empty( $template->title ) || $template->title === $template->slug ) ) { + if ( 'wp_template' === $parent_post->post_type && empty( $template->description ) && ( empty( $template->title ) || $template->title === $template->slug ) ) { $matches = array(); // Check for a block template for a single author, page, post, tag, category, custom post type, or custom taxonomy. diff --git a/src/wp-includes/class-wp-post-type.php b/src/wp-includes/class-wp-post-type.php index ce6d9d348b480..7a2769ed88327 100644 --- a/src/wp-includes/class-wp-post-type.php +++ b/src/wp-includes/class-wp-post-type.php @@ -396,6 +396,54 @@ final class WP_Post_Type { */ public $rest_controller; + /** + * The controller for this post type's revisions REST API endpoints. + * + * Custom controllers must extend WP_REST_Controller. + * + * @since 6.4.0 + * @var string|bool $revisions_rest_controller_class + */ + public $revisions_rest_controller_class; + + /** + * The controller instance for this post type's revisions REST API endpoints. + * + * Lazily computed. Should be accessed using {@see WP_Post_Type::get_revisions_rest_controller()}. + * + * @since 6.4.0 + * @var WP_REST_Controller $revisions_rest_controller + */ + public $revisions_rest_controller; + + /** + * The controller for this post type's autosave REST API endpoints. + * + * Custom controllers must extend WP_REST_Controller. + * + * @since 6.4.0 + * @var string|bool $autosave_rest_controller_class + */ + public $autosave_rest_controller_class; + + /** + * The controller instance for this post type's autosave REST API endpoints. + * + * Lazily computed. Should be accessed using {@see WP_Post_Type::get_autosave_rest_controller()}. + * + * @since 6.4.0 + * @var WP_REST_Controller $autosave_rest_controller + */ + public $autosave_rest_controller; + + /** + * A flag to register the post type REST API controller after its associated autosave / revisions controllers, instead of before. Registration order affects route matching priority. + * + * @since 6.4.0 + * @var bool $late_route_registration + */ + public $late_route_registration; + /** * Constructor. * @@ -455,6 +503,7 @@ public function set_props( $args ) { * - `register_page_post_type_args` * * @since 6.0.0 + * @since 6.4.0 Added `late_route_registration`, `autosave_rest_controller_class` and `revisions_rest_controller_class` arguments. * * @param array $args Array of arguments for registering a post type. * See the register_post_type() function for accepted arguments. @@ -466,37 +515,40 @@ public function set_props( $args ) { // Args prefixed with an underscore are reserved for internal use. $defaults = array( - 'labels' => array(), - 'description' => '', - 'public' => false, - 'hierarchical' => false, - 'exclude_from_search' => null, - 'publicly_queryable' => null, - 'show_ui' => null, - 'show_in_menu' => null, - 'show_in_nav_menus' => null, - 'show_in_admin_bar' => null, - 'menu_position' => null, - 'menu_icon' => null, - 'capability_type' => 'post', - 'capabilities' => array(), - 'map_meta_cap' => null, - 'supports' => array(), - 'register_meta_box_cb' => null, - 'taxonomies' => array(), - 'has_archive' => false, - 'rewrite' => true, - 'query_var' => true, - 'can_export' => true, - 'delete_with_user' => null, - 'show_in_rest' => false, - 'rest_base' => false, - 'rest_namespace' => false, - 'rest_controller_class' => false, - 'template' => array(), - 'template_lock' => false, - '_builtin' => false, - '_edit_link' => 'post.php?post=%d', + 'labels' => array(), + 'description' => '', + 'public' => false, + 'hierarchical' => false, + 'exclude_from_search' => null, + 'publicly_queryable' => null, + 'show_ui' => null, + 'show_in_menu' => null, + 'show_in_nav_menus' => null, + 'show_in_admin_bar' => null, + 'menu_position' => null, + 'menu_icon' => null, + 'capability_type' => 'post', + 'capabilities' => array(), + 'map_meta_cap' => null, + 'supports' => array(), + 'register_meta_box_cb' => null, + 'taxonomies' => array(), + 'has_archive' => false, + 'rewrite' => true, + 'query_var' => true, + 'can_export' => true, + 'delete_with_user' => null, + 'show_in_rest' => false, + 'rest_base' => false, + 'rest_namespace' => false, + 'rest_controller_class' => false, + 'autosave_rest_controller_class' => false, + 'revisions_rest_controller_class' => false, + 'late_route_registration' => false, + 'template' => array(), + 'template_lock' => false, + '_builtin' => false, + '_edit_link' => 'post.php?post=%d', ); $args = array_merge( $defaults, $args ); @@ -816,6 +868,85 @@ public function get_rest_controller() { return $this->rest_controller; } + /** + * Gets the REST API revisions controller for this post type. + * + * Will only instantiate the controller class once per request. + * + * @since 6.4.0 + * + * @return WP_REST_Controller|null The controller instance, or null if the post type + * is set not to show in rest. + */ + public function get_revisions_rest_controller() { + if ( ! $this->show_in_rest ) { + return null; + } + + if ( ! post_type_supports( $this->name, 'revisions' ) ) { + return null; + } + + $class = $this->revisions_rest_controller_class ? $this->revisions_rest_controller_class : WP_REST_Revisions_Controller::class; + if ( ! class_exists( $class ) ) { + return null; + } + + if ( ! is_subclass_of( $class, WP_REST_Controller::class ) ) { + return null; + } + + if ( ! $this->revisions_rest_controller ) { + $this->revisions_rest_controller = new $class( $this->name ); + } + + if ( ! ( $this->revisions_rest_controller instanceof $class ) ) { + return null; + } + + return $this->revisions_rest_controller; + } + + /** + * Gets the REST API autosave controller for this post type. + * + * Will only instantiate the controller class once per request. + * + * @since 6.4.0 + * + * @return WP_REST_Controller|null The controller instance, or null if the post type + * is set not to show in rest. + */ + public function get_autosave_rest_controller() { + if ( ! $this->show_in_rest ) { + return null; + } + + if ( 'attachment' === $this->name ) { + return null; + } + + $class = $this->autosave_rest_controller_class ? $this->autosave_rest_controller_class : WP_REST_Autosaves_Controller::class; + + if ( ! class_exists( $class ) ) { + return null; + } + + if ( ! is_subclass_of( $class, WP_REST_Controller::class ) ) { + return null; + } + + if ( ! $this->autosave_rest_controller ) { + $this->autosave_rest_controller = new $class( $this->name ); + } + + if ( ! ( $this->autosave_rest_controller instanceof $class ) ) { + return null; + } + + return $this->autosave_rest_controller; + } + /** * Returns the default labels for post types. * diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index a85d8fbdb5e87..c3910d2b3a2d3 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -346,7 +346,7 @@ function create_initial_post_types() { register_post_type( 'wp_template', array( - 'labels' => array( + 'labels' => array( 'name' => _x( 'Templates', 'post type general name' ), 'singular_name' => _x( 'Template', 'post type singular name' ), 'add_new' => __( 'Add New Template' ), @@ -366,19 +366,22 @@ function create_initial_post_types() { 'items_list_navigation' => __( 'Templates list navigation' ), 'items_list' => __( 'Templates list' ), ), - 'description' => __( 'Templates to include in your theme.' ), - 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ - 'has_archive' => false, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_rest' => true, - 'rewrite' => false, - 'rest_base' => 'templates', - 'rest_controller_class' => 'WP_REST_Templates_Controller', - 'capability_type' => array( 'template', 'templates' ), - 'capabilities' => array( + 'description' => __( 'Templates to include in your theme.' ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ + 'has_archive' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'rest_base' => 'templates', + 'rest_controller_class' => 'WP_REST_Templates_Controller', + 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller', + 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller', + 'late_route_registration' => true, + 'capability_type' => array( 'template', 'templates' ), + 'capabilities' => array( 'create_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', @@ -392,8 +395,8 @@ function create_initial_post_types() { 'read' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', ), - 'map_meta_cap' => true, - 'supports' => array( + 'map_meta_cap' => true, + 'supports' => array( 'title', 'slug', 'excerpt', @@ -407,7 +410,7 @@ function create_initial_post_types() { register_post_type( 'wp_template_part', array( - 'labels' => array( + 'labels' => array( 'name' => _x( 'Template Parts', 'post type general name' ), 'singular_name' => _x( 'Template Part', 'post type singular name' ), 'add_new' => __( 'Add New Template Part' ), @@ -427,19 +430,22 @@ function create_initial_post_types() { 'items_list_navigation' => __( 'Template parts list navigation' ), 'items_list' => __( 'Template parts list' ), ), - 'description' => __( 'Template parts to include in your templates.' ), - 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ - 'has_archive' => false, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_rest' => true, - 'rewrite' => false, - 'rest_base' => 'template-parts', - 'rest_controller_class' => 'WP_REST_Templates_Controller', - 'map_meta_cap' => true, - 'capabilities' => array( + 'description' => __( 'Template parts to include in your templates.' ), + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + '_edit_link' => $template_edit_link, /* internal use only. don't use this when registering your own post type. */ + 'has_archive' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'rest_base' => 'template-parts', + 'rest_controller_class' => 'WP_REST_Templates_Controller', + 'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller', + 'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller', + 'late_route_registration' => true, + 'map_meta_cap' => true, + 'capabilities' => array( 'create_posts' => 'edit_theme_options', 'delete_posts' => 'edit_theme_options', 'delete_others_posts' => 'edit_theme_options', @@ -453,7 +459,7 @@ function create_initial_post_types() { 'read' => 'edit_theme_options', 'read_private_posts' => 'edit_theme_options', ), - 'supports' => array( + 'supports' => array( 'title', 'slug', 'excerpt', @@ -1575,85 +1581,88 @@ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) * @param array|string $args { * Array or string of arguments for registering a post type. * - * @type string $label Name of the post type shown in the menu. Usually plural. - * Default is value of $labels['name']. - * @type string[] $labels An array of labels for this post type. If not set, post - * labels are inherited for non-hierarchical types and page - * labels for hierarchical ones. See get_post_type_labels() for a full - * list of supported labels. - * @type string $description A short descriptive summary of what the post type is. - * Default empty. - * @type bool $public Whether a post type is intended for use publicly either via - * the admin interface or by front-end users. While the default - * settings of $exclude_from_search, $publicly_queryable, $show_ui, - * and $show_in_nav_menus are inherited from $public, each does not - * rely on this relationship and controls a very specific intention. - * Default false. - * @type bool $hierarchical Whether the post type is hierarchical (e.g. page). Default false. - * @type bool $exclude_from_search Whether to exclude posts with this post type from front end search - * results. Default is the opposite value of $public. - * @type bool $publicly_queryable Whether queries can be performed on the front end for the post type - * as part of parse_request(). Endpoints would include: - * * ?post_type={post_type_key} - * * ?{post_type_key}={single_post_slug} - * * ?{post_type_query_var}={single_post_slug} - * If not set, the default is inherited from $public. - * @type bool $show_ui Whether to generate and allow a UI for managing this post type in the - * admin. Default is value of $public. - * @type bool|string $show_in_menu Where to show the post type in the admin menu. To work, $show_ui - * must be true. If true, the post type is shown in its own top level - * menu. If false, no menu is shown. If a string of an existing top - * level menu ('tools.php' or 'edit.php?post_type=page', for example), the - * post type will be placed as a sub-menu of that. - * Default is value of $show_ui. - * @type bool $show_in_nav_menus Makes this post type available for selection in navigation menus. - * Default is value of $public. - * @type bool $show_in_admin_bar Makes this post type available via the admin bar. Default is value - * of $show_in_menu. - * @type bool $show_in_rest Whether to include the post type in the REST API. Set this to true - * for the post type to be available in the block editor. - * @type string $rest_base To change the base URL of REST API route. Default is $post_type. - * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. - * @type string $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'. - * @type int $menu_position The position in the menu order the post type should appear. To work, - * $show_in_menu must be true. Default null (at the bottom). - * @type string $menu_icon The URL to the icon to be used for this menu. Pass a base64-encoded - * SVG using a data URI, which will be colored to match the color scheme - * -- this should begin with 'data:image/svg+xml;base64,'. Pass the name - * of a Dashicons helper class to use a font icon, e.g. - * 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty - * so an icon can be added via CSS. Defaults to use the posts icon. - * @type string|array $capability_type The string to use to build the read, edit, and delete capabilities. - * May be passed as an array to allow for alternative plurals when using - * this argument as a base to construct the capabilities, e.g. - * array('story', 'stories'). Default 'post'. - * @type string[] $capabilities Array of capabilities for this post type. $capability_type is used - * as a base to construct capabilities by default. - * See get_post_type_capabilities(). - * @type bool $map_meta_cap Whether to use the internal default meta capability handling. - * Default false. - * @type array $supports Core feature(s) the post type supports. Serves as an alias for calling - * add_post_type_support() directly. Core features include 'title', - * 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', - * 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'. - * Additionally, the 'revisions' feature dictates whether the post type - * will store revisions, and the 'comments' feature dictates whether the - * comments count will show on the edit screen. A feature can also be - * specified as an array of arguments to provide additional information - * about supporting that feature. - * Example: `array( 'my_feature', array( 'field' => 'value' ) )`. - * Default is an array containing 'title' and 'editor'. - * @type callable $register_meta_box_cb Provide a callback function that sets up the meta boxes for the - * edit form. Do remove_meta_box() and add_meta_box() calls in the - * callback. Default null. - * @type string[] $taxonomies An array of taxonomy identifiers that will be registered for the - * post type. Taxonomies can be registered later with register_taxonomy() - * or register_taxonomy_for_object_type(). - * Default empty array. - * @type bool|string $has_archive Whether there should be post type archives, or if a string, the - * archive slug to use. Will generate the proper rewrite rules if - * $rewrite is enabled. Default false. - * @type bool|array $rewrite { + * @type string $label Name of the post type shown in the menu. Usually plural. + * Default is value of $labels['name']. + * @type string[] $labels An array of labels for this post type. If not set, post + * labels are inherited for non-hierarchical types and page + * labels for hierarchical ones. See get_post_type_labels() for a full + * list of supported labels. + * @type string $description A short descriptive summary of what the post type is. + * Default empty. + * @type bool $public Whether a post type is intended for use publicly either via + * the admin interface or by front-end users. While the default + * settings of $exclude_from_search, $publicly_queryable, $show_ui, + * and $show_in_nav_menus are inherited from $public, each does not + * rely on this relationship and controls a very specific intention. + * Default false. + * @type bool $hierarchical Whether the post type is hierarchical (e.g. page). Default false. + * @type bool $exclude_from_search Whether to exclude posts with this post type from front end search + * results. Default is the opposite value of $public. + * @type bool $publicly_queryable Whether queries can be performed on the front end for the post type + * as part of parse_request(). Endpoints would include: + * * ?post_type={post_type_key} + * * ?{post_type_key}={single_post_slug} + * * ?{post_type_query_var}={single_post_slug} + * If not set, the default is inherited from $public. + * @type bool $show_ui Whether to generate and allow a UI for managing this post type in the + * admin. Default is value of $public. + * @type bool|string $show_in_menu Where to show the post type in the admin menu. To work, $show_ui + * must be true. If true, the post type is shown in its own top level + * menu. If false, no menu is shown. If a string of an existing top + * level menu ('tools.php' or 'edit.php?post_type=page', for example), the + * post type will be placed as a sub-menu of that. + * Default is value of $show_ui. + * @type bool $show_in_nav_menus Makes this post type available for selection in navigation menus. + * Default is value of $public. + * @type bool $show_in_admin_bar Makes this post type available via the admin bar. Default is value + * of $show_in_menu. + * @type bool $show_in_rest Whether to include the post type in the REST API. Set this to true + * for the post type to be available in the block editor. + * @type string $rest_base To change the base URL of REST API route. Default is $post_type. + * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. + * @type string $rest_controller_class REST API controller class name. Default is 'WP_REST_Posts_Controller'. + * @type string|bool $autosave_rest_controller_class REST API controller class name. Default is 'WP_REST_Autosaves_Controller'. + * @type string|bool $revisions_rest_controller_class REST API controller class name. Default is 'WP_REST_Revisions_Controller'. + * @type bool $late_route_registration A flag to direct the REST API controllers for autosave / revisions should be registered before/after the post type controller. + * @type int $menu_position The position in the menu order the post type should appear. To work, + * $show_in_menu must be true. Default null (at the bottom). + * @type string $menu_icon The URL to the icon to be used for this menu. Pass a base64-encoded + * SVG using a data URI, which will be colored to match the color scheme + * -- this should begin with 'data:image/svg+xml;base64,'. Pass the name + * of a Dashicons helper class to use a font icon, e.g. + * 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty + * so an icon can be added via CSS. Defaults to use the posts icon. + * @type string|array $capability_type The string to use to build the read, edit, and delete capabilities. + * May be passed as an array to allow for alternative plurals when using + * this argument as a base to construct the capabilities, e.g. + * array('story', 'stories'). Default 'post'. + * @type string[] $capabilities Array of capabilities for this post type. $capability_type is used + * as a base to construct capabilities by default. + * See get_post_type_capabilities(). + * @type bool $map_meta_cap Whether to use the internal default meta capability handling. + * Default false. + * @type array $supports Core feature(s) the post type supports. Serves as an alias for calling + * add_post_type_support() directly. Core features include 'title', + * 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', + * 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'. + * Additionally, the 'revisions' feature dictates whether the post type + * will store revisions, and the 'comments' feature dictates whether the + * comments count will show on the edit screen. A feature can also be + * specified as an array of arguments to provide additional information + * about supporting that feature. + * Example: `array( 'my_feature', array( 'field' => 'value' ) )`. + * Default is an array containing 'title' and 'editor'. + * @type callable $register_meta_box_cb Provide a callback function that sets up the meta boxes for the + * edit form. Do remove_meta_box() and add_meta_box() calls in the + * callback. Default null. + * @type string[] $taxonomies An array of taxonomy identifiers that will be registered for the + * post type. Taxonomies can be registered later with register_taxonomy() + * or register_taxonomy_for_object_type(). + * Default empty array. + * @type bool|string $has_archive Whether there should be post type archives, or if a string, the + * archive slug to use. Will generate the proper rewrite rules if + * $rewrite is enabled. Default false. + * @type bool|array $rewrite { * Triggers the handling of rewrites for this post type. To prevent rewrite, set to false. * Defaults to true, using $post_type as slug. To specify rewrite rules, an array can be * passed with any of these keys: @@ -1668,32 +1677,32 @@ function get_post_types( $args = array(), $output = 'names', $operator = 'and' ) * inherits from $permalink_epmask. If not specified and permalink_epmask * is not set, defaults to EP_PERMALINK. * } - * @type string|bool $query_var Sets the query_var key for this post type. Defaults to $post_type - * key. If false, a post type cannot be loaded at - * ?{query_var}={post_slug}. If specified as a string, the query - * ?{query_var_string}={post_slug} will be valid. - * @type bool $can_export Whether to allow this post type to be exported. Default true. - * @type bool $delete_with_user Whether to delete posts of this type when deleting a user. - * * If true, posts of this type belonging to the user will be moved - * to Trash when the user is deleted. - * * If false, posts of this type belonging to the user will *not* - * be trashed or deleted. - * * If not set (the default), posts are trashed if post type supports - * the 'author' feature. Otherwise posts are not trashed or deleted. - * Default null. - * @type array $template Array of blocks to use as the default initial state for an editor - * session. Each item should be an array containing block name and - * optional attributes. Default empty array. - * @type string|false $template_lock Whether the block template should be locked if $template is set. - * * If set to 'all', the user is unable to insert new blocks, - * move existing blocks and delete blocks. - * * If set to 'insert', the user is able to move existing blocks - * but is unable to insert new blocks and delete blocks. - * Default false. - * @type bool $_builtin FOR INTERNAL USE ONLY! True if this post type is a native or - * "built-in" post_type. Default false. - * @type string $_edit_link FOR INTERNAL USE ONLY! URL segment to use for edit link of - * this post type. Default 'post.php?post=%d'. + * @type string|bool $query_var Sets the query_var key for this post type. Defaults to $post_type + * key. If false, a post type cannot be loaded at + * ?{query_var}={post_slug}. If specified as a string, the query + * ?{query_var_string}={post_slug} will be valid. + * @type bool $can_export Whether to allow this post type to be exported. Default true. + * @type bool $delete_with_user Whether to delete posts of this type when deleting a user. + * * If true, posts of this type belonging to the user will be moved + * to Trash when the user is deleted. + * * If false, posts of this type belonging to the user will *not* + * be trashed or deleted. + * * If not set (the default), posts are trashed if post type supports + * the 'author' feature. Otherwise posts are not trashed or deleted. + * Default null. + * @type array $template Array of blocks to use as the default initial state for an editor + * session. Each item should be an array containing block name and + * optional attributes. Default empty array. + * @type string|false $template_lock Whether the block template should be locked if $template is set. + * * If set to 'all', the user is unable to insert new blocks, + * move existing blocks and delete blocks. + * * If set to 'insert', the user is able to move existing blocks + * but is unable to insert new blocks and delete blocks. + * Default false. + * @type bool $_builtin FOR INTERNAL USE ONLY! True if this post type is a native or + * "built-in" post_type. Default false. + * @type string $_edit_link FOR INTERNAL USE ONLY! URL segment to use for edit link of + * this post type. Default 'post.php?post=%d'. * } * @return WP_Post_Type|WP_Error The registered post type object on success, * WP_Error object on failure. diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 71539b084c26d..19d29d051f5fe 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -241,17 +241,23 @@ function create_initial_rest_routes() { continue; } - $controller->register_routes(); + if ( ! $post_type->late_route_registration ) { + $controller->register_routes(); + } - if ( post_type_supports( $post_type->name, 'revisions' ) ) { - $revisions_controller = new WP_REST_Revisions_Controller( $post_type->name ); + $revisions_controller = $post_type->get_revisions_rest_controller(); + if ( $revisions_controller ) { $revisions_controller->register_routes(); } - if ( 'attachment' !== $post_type->name ) { - $autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name ); + $autosaves_controller = $post_type->get_autosave_rest_controller(); + if ( $autosaves_controller ) { $autosaves_controller->register_routes(); } + + if ( $post_type->late_route_registration ) { + $controller->register_routes(); + } } // Post types. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index d14119133aff3..5545625df4609 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -65,8 +65,13 @@ public function __construct( $parent_post_type ) { $parent_controller = new WP_REST_Posts_Controller( $parent_post_type ); } - $this->parent_controller = $parent_controller; - $this->revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type ); + $this->parent_controller = $parent_controller; + + $revisions_controller = $post_type_object->get_revisions_rest_controller(); + if ( ! $revisions_controller ) { + $revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type ); + } + $this->revisions_controller = $revisions_controller; $this->rest_base = 'autosaves'; $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; @@ -205,11 +210,11 @@ public function create_item_permissions_check( $request ) { */ public function create_item( $request ) { - if ( ! defined( 'DOING_AUTOSAVE' ) ) { + if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) { define( 'DOING_AUTOSAVE', true ); } - $post = get_post( $request['id'] ); + $post = $this->get_parent( $request['id'] ); if ( is_wp_error( $post ) ) { return $post; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php new file mode 100644 index 0000000000000..c996894a5933c --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php @@ -0,0 +1,276 @@ +parent_post_type = $parent_post_type; + $post_type_object = get_post_type_object( $parent_post_type ); + $parent_controller = $post_type_object->get_rest_controller(); + + if ( ! $parent_controller ) { + $parent_controller = new WP_REST_Templates_Controller( $parent_post_type ); + } + + $this->parent_controller = $parent_controller; + + $revisions_controller = $post_type_object->get_revisions_rest_controller(); + if ( ! $revisions_controller ) { + $revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type ); + } + $this->revisions_controller = $revisions_controller; + $this->rest_base = 'autosaves'; + $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; + $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; + } + + /** + * Registers the routes for autosaves. + * + * @since 6.4.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->namespace, + sprintf( + '/%s/(?P%s%s)/%s', + $this->parent_base, + /* + * Matches theme's directory: `/themes///` or `/themes//`. + * Excludes invalid directory name characters: `/:<>*?"|`. + */ + '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + // Matches the template name. + '[\/\w%-]+', + $this->rest_base + ), + array( + 'args' => array( + 'id' => array( + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ), + ), + ), + 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->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + sprintf( + '/%s/(?P%s%s)/%s/%s', + $this->parent_base, + /* + * Matches theme's directory: `/themes///` or `/themes//`. + * Excludes invalid directory name characters: `/:<>*?"|`. + */ + '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + // Matches the template name. + '[\/\w%-]+', + $this->rest_base, + '(?P[\d]+)' + ), + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ), + ), + 'id' => array( + 'description' => __( 'The ID for the autosave.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Prepares the item for the REST response. + * + * @since 6.4.0 + * + * @param WP_Post $item Post revision object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $template = _build_block_template_result_from_post( $item ); + $response = $this->parent_controller->prepare_item_for_response( $template, $request ); + + $fields = $this->get_fields_for_response( $request ); + $data = $response->get_data(); + + if ( in_array( 'parent', $fields, true ) ) { + $data['parent'] = (int) $item->post_parent; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = new WP_REST_Response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $template ); + $response->add_links( $links ); + } + + return $response; + } + + /** + * Gets the autosave, if the ID is valid. + * + * @since 6.4.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Post|WP_Error Autosave post object if ID is valid, WP_Error otherwise. + */ + public function get_item( $request ) { + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + $autosave = wp_get_post_autosave( $parent->ID ); + + if ( ! $autosave ) { + return new WP_Error( + 'rest_post_no_autosave', + __( 'There is no autosave revision for this template.' ), + array( 'status' => 404 ) + ); + } + + $response = $this->prepare_item_for_response( $autosave, $request ); + return $response; + } + + /** + * Get the parent post. + * + * @since 6.4.0 + * + * @param int $parent_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent( $parent_id ) { + return $this->revisions_controller->get_parent( $parent_id ); + } + + /** + * Prepares links for the request. + * + * @since 6.4.0 + * + * @param WP_Block_Template $template Template. + * @return array Links for the given post. + */ + protected function prepare_links( $template ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ), + ), + 'parent' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ), + ), + ); + + return $links; + } + + /** + * Retrieves the autosave's schema, conforming to JSON Schema. + * + * @since 6.4.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = $this->revisions_controller->get_item_schema(); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php new file mode 100644 index 0000000000000..8d32ecb7c0904 --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php @@ -0,0 +1,297 @@ +parent_post_type = $parent_post_type; + $post_type_object = get_post_type_object( $parent_post_type ); + $parent_controller = $post_type_object->get_rest_controller(); + + if ( ! $parent_controller ) { + $parent_controller = new WP_REST_Templates_Controller( $parent_post_type ); + } + + $this->parent_controller = $parent_controller; + $this->rest_base = 'revisions'; + $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; + $this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2'; + } + + /** + * Registers the routes for revisions based on post types supporting revisions. + * + * @since 6.4.0 + * + * @see register_rest_route() + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + sprintf( + '/%s/(?P%s%s)/%s', + $this->parent_base, + /* + * Matches theme's directory: `/themes///` or `/themes//`. + * Excludes invalid directory name characters: `/:<>*?"|`. + */ + '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + // Matches the template name. + '[\/\w%-]+', + $this->rest_base + ), + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ), + ), + ), + 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, + sprintf( + '/%s/(?P%s%s)/%s/%s', + $this->parent_base, + /* + * Matches theme's directory: `/themes///` or `/themes//`. + * Excludes invalid directory name characters: `/:<>*?"|`. + */ + '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + // Matches the template name. + '[\/\w%-]+', + $this->rest_base, + '(?P[\d]+)' + ), + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ), + ), + 'id' => array( + 'description' => __( 'Unique identifier for the 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' ) ), + ), + ), + 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' => __( 'Required to be true, as revisions do not support trashing.' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Gets the parent post, if the ID is valid. + * + * @since 6.4.0 + * + * @param int $parent_post_id Supplied ID. + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent( $parent_post_id ) { + $template = get_block_template( $parent_post_id, $this->parent_post_type ); + + if ( ! $template ) { + return new WP_Error( + 'rest_post_invalid_parent', + __( 'Invalid template parent ID.' ), + array( 'status' => 404 ) + ); + } + + return get_post( $template->wp_id ); + } + + /** + * Prepares the item for the REST response. + * + * @since 6.4.0 + * + * @param WP_Post $item Post revision object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $template = _build_block_template_result_from_post( $item ); + $response = $this->parent_controller->prepare_item_for_response( $template, $request ); + + $fields = $this->get_fields_for_response( $request ); + $data = $response->get_data(); + + if ( in_array( 'parent', $fields, true ) ) { + $data['parent'] = (int) $item->post_parent; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = new WP_REST_Response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $template ); + $response->add_links( $links ); + } + + return $response; + } + + /** + * Checks if a given request has access to delete a revision. + * + * @since 6.4.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise. + */ + public function delete_item_permissions_check( $request ) { + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + if ( ! current_user_can( 'delete_post', $parent->ID ) ) { + return new WP_Error( + 'rest_cannot_delete', + __( 'Sorry, you are not allowed to delete revisions of this post.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + $revision = $this->get_revision( $request['id'] ); + if ( is_wp_error( $revision ) ) { + return $revision; + } + + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'rest_cannot_delete', + __( 'Sorry, you are not allowed to delete this revision.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Prepares links for the request. + * + * @since 6.4.0 + * + * @param WP_Block_Template $template Template. + * @return array Links for the given post. + */ + protected function prepare_links( $template ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ), + ), + 'parent' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ), + ), + ); + + return $links; + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * @since 6.4.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = $this->parent_controller->get_item_schema(); + + $schema['properties']['parent'] = array( + 'description' => __( 'The ID for the parent of the revision.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index e9904ff234912..53f8faa75595b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -760,7 +760,7 @@ public function prepare_item_for_response( $item, $request ) { protected function prepare_links( $id ) { $links = array( 'self' => array( - 'href' => rest_url( rest_get_route_for_post( $id ) ), + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ), ), 'collection' => array( 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ), @@ -770,6 +770,27 @@ protected function prepare_links( $id ) { ), ); + if ( post_type_supports( $this->post_type, 'revisions' ) ) { + $template = get_block_template( $id, $this->post_type ); + if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) { + $revisions = wp_get_latest_revision_id_and_total_count( $template->wp_id ); + $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; + $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id ); + + $links['version-history'] = array( + 'href' => rest_url( $revisions_base ), + 'count' => $revisions_count, + ); + + if ( $revisions_count > 0 ) { + $links['predecessor-version'] = array( + 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), + 'id' => $revisions['latest_id'], + ); + } + } + } + return $links; } diff --git a/src/wp-settings.php b/src/wp-settings.php index 5b1e1205d2719..38b03ecf7268f 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -273,7 +273,9 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-types-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-post-statuses-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-revisions-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-template-revisions-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-autosaves-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-taxonomies-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-terms-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-items-controller.php'; diff --git a/tests/phpunit/tests/post/wpPostType.php b/tests/phpunit/tests/post/wpPostType.php index 8443c804ad868..2a8ad42f0a2ca 100644 --- a/tests/phpunit/tests/post/wpPostType.php +++ b/tests/phpunit/tests/post/wpPostType.php @@ -229,4 +229,214 @@ public function test_applies_registration_args_filters() { $this->assertSame( 3, $action->get_call_count() ); } + + /** + * @ticket 56922 + * + * @dataProvider data_should_have_correct_custom_revisions_and_autosaves_controllers_properties + * + * @covers WP_Post_Type::set_props + * + * @param string $property_name Property name. + * @param string $property_value Property value. + * @param string|bool $expected_property_value Expected property value. + */ + public function test_should_have_correct_custom_revisions_and_autosaves_controllers_properties( $property_name, $property_value, $expected_property_value ) { + $properties = null === $property_value ? array() : array( $property_name => $property_value ); + + $post_type = new WP_Post_Type( 'test_post_type', $properties ); + + $this->assertObjectHasProperty( $property_name, $post_type, "The WP_Post_Type object does not have the expected {$property_name} property." ); + $this->assertSame( + $expected_property_value, + $post_type->$property_name, + sprintf( 'Expected the property "%s" to have the %s value.', $property_name, var_export( $expected_property_value, true ) ) + ); + } + + /** + * Data provider for test_should_allow_to_set_custom_revisions_and_autosaves_controllers_properties. + * + * @return array[] Arguments { + * @type string $property_name Property name. + * @type string $property_value Property value. + * @type string|bool $expected_property_value Expected property value. + * } + */ + public function data_should_have_correct_custom_revisions_and_autosaves_controllers_properties() { + return array( + 'autosave_rest_controller_class property' => array( + 'autosave_rest_controller_class', + 'My_Custom_Template_Autosaves_Controller', + 'My_Custom_Template_Autosaves_Controller', + ), + 'autosave_rest_controller_class property (null value)' => array( + 'autosave_rest_controller_class', + null, + false, + ), + 'revisions_rest_controller_class property' => array( + 'revisions_rest_controller_class', + 'My_Custom_Template_Revisions_Controller', + 'My_Custom_Template_Revisions_Controller', + ), + 'revisions_rest_controller_class property (null value)' => array( + 'revisions_rest_controller_class', + null, + false, + ), + ); + } + + /** + * @ticket 56922 + * + * @covers WP_Post_Type::get_revisions_rest_controller + * + * @dataProvider data_get_revisions_rest_controller_should_return_correct_values + * + * @param bool $show_in_rest Enables "show_in_rest" support. + * @param bool $supports_revisions Enables revisions support. + * @param string|bool $revisions_rest_controller_class Custom revisions REST controller class. + * @param string|null $expected_value Expected value. + */ + public function test_get_revisions_rest_controller_should_return_correct_values( $show_in_rest, $supports_revisions, $revisions_rest_controller_class, $expected_value ) { + $post_type = 'test_post_type'; + $properties = array( + 'show_in_rest' => $show_in_rest, + 'supports' => $supports_revisions ? array( 'revisions' ) : array(), + 'revisions_rest_controller_class' => $revisions_rest_controller_class, + ); + register_post_type( $post_type, $properties ); + $post_type = get_post_type_object( $post_type ); + + $controller = $post_type->get_revisions_rest_controller(); + if ( $expected_value ) { + $this->assertInstanceOf( $expected_value, $controller ); + + return; + } + + $this->assertSame( $expected_value, $controller ); + } + + /** + * Data provider for test_get_revisions_rest_controller_should_return_correct_values. + * + * @return array[] Arguments { + * @type bool $show_in_rest Enables "show_in_rest" support. + * @type bool $supports_revisions Enables revisions support. + * @type string|bool $revisions_rest_controller_class Custom revisions REST controller class. + * @type string|null $expected_value Expected value. + * } + */ + public function data_get_revisions_rest_controller_should_return_correct_values() { + return array( + 'disable show_in_rest' => array( + false, + false, + false, + null, + ), + 'disable revisions support' => array( + true, + false, + false, + null, + ), + 'default rest revisions controller' => array( + true, + true, + false, + WP_REST_Revisions_Controller::class, + ), + 'incorrect rest revisions controller' => array( + true, + true, + stdClass::class, + null, + ), + 'correct rest revisions controller' => array( + true, + true, + WP_REST_Template_Revisions_Controller::class, + WP_REST_Template_Revisions_Controller::class, + ), + ); + } + + /** + * @ticket 56922 + * + * @covers WP_Post_Type::get_autosave_rest_controller + * + * @dataProvider data_get_autosave_rest_controller_should_return_correct_values + * + * @param bool $show_in_rest Enables "show_in_rest" support. + * @param string $post_type Post type. + * @param string|bool $autosave_rest_controller_class Custom autosave REST controller class. + * @param string|null $expected_value Expected value. + */ + public function test_get_autosave_rest_controller_should_return_correct_values( $show_in_rest, $post_type, $autosave_rest_controller_class, $expected_value ) { + $properties = array( + 'show_in_rest' => $show_in_rest, + 'autosave_rest_controller_class' => $autosave_rest_controller_class, + ); + register_post_type( $post_type, $properties ); + $post_type = get_post_type_object( $post_type ); + + $controller = $post_type->get_autosave_rest_controller(); + if ( $expected_value ) { + $this->assertInstanceOf( $expected_value, $controller ); + + return; + } + + $this->assertSame( $expected_value, $controller ); + } + + /** + * Data provider for test_get_autosave_rest_controller_should_return_correct_values. + * + * @return array[] Arguments { + * @type bool $show_in_rest Enables "show_in_rest" support. + * @type string $post_type Post type. + * @type string|bool $autosave_rest_controller_class Custom autosave REST controller class. + * @type string|null $expected_value Expected value. + * } + */ + public function data_get_autosave_rest_controller_should_return_correct_values() { + return array( + 'disable show_in_rest' => array( + false, + 'attachment', + false, + null, + ), + 'invalid post type' => array( + true, + 'attachment', + false, + null, + ), + 'default rest autosave controller' => array( + true, + 'test_post_type', + false, + WP_REST_Autosaves_Controller::class, + ), + 'incorrect rest autosave controller' => array( + true, + 'test_post_type', + stdClass::class, + null, + ), + 'correct rest autosave controller' => array( + true, + 'test_post_type', + WP_REST_Template_Autosaves_Controller::class, + WP_REST_Template_Autosaves_Controller::class, + ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index dfd98877d8ed3..c53f887bc822c 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -144,18 +144,18 @@ public function test_expected_routes_in_schema() { '/wp/v2/block-types/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', '/wp/v2/settings', '/wp/v2/template-parts', - '/wp/v2/template-parts/(?P[\d]+)/autosaves', '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)', - '/wp/v2/template-parts/(?P[\d]+)/autosaves/(?P[\d]+)', - '/wp/v2/template-parts/(?P[\d]+)/revisions', - '/wp/v2/template-parts/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', '/wp/v2/template-parts/lookup', '/wp/v2/templates', - '/wp/v2/templates/(?P[\d]+)/autosaves', '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)', - '/wp/v2/templates/(?P[\d]+)/autosaves/(?P[\d]+)', - '/wp/v2/templates/(?P[\d]+)/revisions', - '/wp/v2/templates/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', '/wp/v2/templates/lookup', '/wp/v2/themes', '/wp/v2/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php new file mode 100644 index 0000000000000..9452a188433be --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -0,0 +1,413 @@ +user->create( + array( + 'role' => 'contributor', + ) + ); + + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( self::$admin_id ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => self::PARENT_POST_TYPE, + 'post_name' => self::TEMPLATE_NAME, + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::register_routes + * @ticket 56922 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + $routes, + 'Template autosaves route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + $routes, + 'Single template autosave based on the given ID route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + $routes, + 'Template part autosaves route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + $routes, + 'Single template part autosave based on the given ID route does not exist.' + ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::get_context_param + * @ticket 56922 + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + // Collection. + $this->assertCount( + 2, + $data['endpoints'], + 'Failed to assert that the collection autosave endpoints count is 2.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the GET collection endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + "Failed to assert that the enum values for the GET collection endpoint are 'view', 'embed', and 'edit'." + ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves/1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( + 1, + $data['endpoints'], + 'Failed to assert that the single autosave endpoints count is 1.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the single autosave endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + "Failed to assert that the enum values for the single autosave endpoint are 'view', 'embed', and 'edit'." + ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::get_items + * @ticket 56922 + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( + 'GET', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves' + ); + $response = rest_get_server()->dispatch( $request ); + $autosaves = $response->get_data(); + + $this->assertCount( + 1, + $autosaves, + 'Failed asserting that the response data contains exactly 1 item.' + ); + + $this->assertSame( + $autosave_post_id, + $autosaves[0]['wp_id'], + 'Failed asserting that the ID of the autosave matches the expected autosave post ID.' + ); + $this->assertSame( + self::$template_post->ID, + $autosaves[0]['parent'], + 'Failed asserting that the parent ID of the autosave matches the template post ID.' + ); + $this->assertSame( + 'Autosave content.', + $autosaves[0]['content']['raw'], + 'Failed asserting that the content of the autosave is "Autosave content.".' + ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::get_item + * @ticket 56922 + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves/' . $autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + $autosave = $response->get_data(); + + $this->assertIsArray( $autosave, 'Failed asserting that the autosave is an array.' ); + $this->assertSame( + $autosave_post_id, + $autosave['wp_id'], + "Failed asserting that the autosave id is the same as $autosave_post_id." + ); + $this->assertSame( + self::$template_post->ID, + $autosave['parent'], + sprintf( + 'Failed asserting that the parent id of the autosave is the same as %s.', + self::$template_post->ID + ) + ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::prepare_item_for_response + * @ticket 56922 + */ + public function test_prepare_item() { + wp_set_current_user( self::$admin_id ); + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + $autosave_db_post = get_post( $autosave_post_id ); + $template_id = self::TEST_THEME . '//' . self::TEMPLATE_NAME; + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . $template_id . '/autosaves/' . $autosave_db_post->ID ); + $controller = new WP_REST_Template_Autosaves_Controller( self::PARENT_POST_TYPE ); + $response = $controller->prepare_item_for_response( $autosave_db_post, $request ); + $this->assertInstanceOf( + WP_REST_Response::class, + $response, + 'Failed asserting that the response object is an instance of WP_REST_Response.' + ); + + $autosave = $response->get_data(); + $this->assertIsArray( $autosave, 'Failed asserting that the autosave is an array.' ); + $this->assertSame( + $autosave_db_post->ID, + $autosave['wp_id'], + "Failed asserting that the autosave id is the same as $autosave_db_post->ID." + ); + $this->assertSame( + self::$template_post->ID, + $autosave['parent'], + sprintf( + 'Failed asserting that the parent id of the autosave is the same as %s.', + self::$template_post->ID + ) + ); + + $links = $response->get_links(); + $this->assertIsArray( $links, 'Failed asserting that the links are an array.' ); + + $this->assertStringEndsWith( + $template_id . '/autosaves/' . $autosave_db_post->ID, + $links['self'][0]['href'], + "Failed asserting that the self link ends with $template_id . '/autosaves/' . $autosave_db_post->ID." + ); + + $this->assertStringEndsWith( + $template_id, + $links['parent'][0]['href'], + "Failed asserting that the parent link ends with %$template_id." + ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::get_item_schema + * @ticket 56922 + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $properties = $data['schema']['properties']; + + $this->assertCount( 16, $properties ); + $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); + $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); + $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); + $this->assertArrayHasKey( 'source', $properties, 'Source key should exist in properties.' ); + $this->assertArrayHasKey( 'origin', $properties, 'Origin key should exist in properties.' ); + $this->assertArrayHasKey( 'content', $properties, 'Content key should exist in properties.' ); + $this->assertArrayHasKey( 'title', $properties, 'Title key should exist in properties.' ); + $this->assertArrayHasKey( 'description', $properties, 'description key should exist in properties.' ); + $this->assertArrayHasKey( 'status', $properties, 'status key should exist in properties.' ); + $this->assertArrayHasKey( 'wp_id', $properties, 'wp_id key should exist in properties.' ); + $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); + $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); + $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'is_custom', $properties, 'is_custom key should exist in properties.' ); + $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::create_item + * @ticket 56922 + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + + $template_id = self::TEST_THEME . '/' . self::TEMPLATE_NAME; + $request = new WP_REST_Request( 'POST', '/wp/v2/templates/' . $template_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + + $request_parameters = array( + 'title' => 'Post Title', + 'content' => 'Post content', + 'excerpt' => 'Post excerpt', + 'name' => 'test', + 'id' => $template_id, + ); + + $request->set_body_params( $request_parameters ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, 'The response from this request should not return a WP_Error object' ); + $response = rest_ensure_response( $response ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'content', $data, 'Response should contain a key called content' ); + $this->assertSame( $request_parameters['content'], $data['content']['raw'], 'Response data should match for field content' ); + + $this->assertArrayHasKey( 'title', $data, 'Response should contain a key called title' ); + $this->assertSame( $request_parameters['title'], $data['title']['raw'], 'Response data should match for field title' ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::delete_item + * @ticket 56922 + */ + public function test_create_item_incorrect_permission() { + wp_set_current_user( self::$contributor_id ); + $template_id = self::TEST_THEME . '/' . self::TEMPLATE_NAME; + $request = new WP_REST_Request( 'POST', '/wp/v2/templates/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, WP_Http::FORBIDDEN ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::delete_item + * @ticket 56922 + */ + public function test_create_item_no_permission() { + wp_set_current_user( 0 ); + $template_id = self::TEST_THEME . '/' . self::TEMPLATE_NAME; + $request = new WP_REST_Request( 'POST', '/wp/v2/templates/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_update_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to update template autosaves.", + WP_REST_Template_Autosaves_Controller::class + ) + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_delete_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to delete template autosaves.", + WP_REST_Template_Autosaves_Controller::class + ) + ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php new file mode 100644 index 0000000000000..91370b21c726b --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -0,0 +1,511 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( self::$admin_id ); + + self::$contributor_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => self::PARENT_POST_TYPE, + 'post_name' => self::TEMPLATE_NAME, + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Update post to create a new revisions. + self::$revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_post->ID, + 'post_content' => 'Content revision #2', + ) + ); + + // Update post to create a new revisions. + self::$revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_post->ID, + 'post_content' => 'Content revision #3', + ) + ); + + // Update post to create a new revisions. + self::$revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_post->ID, + 'post_content' => 'Content revision #4', + ) + ); + + // Update post to create a new revisions. + self::$revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_post->ID, + 'post_content' => 'Content revision #5', + ) + ); + } + + /** + * Remove revisions when tests are complete. + */ + public static function wpTearDownAfterClass() { + // Also deletes revisions. + foreach ( self::$revisions as $revision ) { + wp_delete_post( $revision, true ); + } + } + + /** + * @covers WP_REST_Template_Revisions_Controller::register_routes + * @ticket 56922 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + $routes, + 'Template revisions route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + $routes, + 'Single template revision based on the given ID route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + $routes, + 'Template part revisions route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + $routes, + 'Single template part revision based on the given ID route does not exist.' + ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_context_param + * @ticket 56922 + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the collection endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + 'Failed to assert correct enum values for the collection endpoint.' + ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( + 2, + $data['endpoints'], + 'Failed to assert that the single revision endpoint count is 2.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the single revision endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + 'Failed to assert correct enum values for the single revision endpoint.' + ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_items + * @ticket 56922 + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( + 'GET', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' + ); + $response = rest_get_server()->dispatch( $request ); + $revisions = $response->get_data(); + + $this->assertCount( + 4, + $revisions, + 'Failed asserting that the response data contains exactly 4 items.' + ); + + $this->assertSame( + self::$template_post->ID, + $revisions[0]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #5', + $revisions[0]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #5".' + ); + + $this->assertSame( + self::$template_post->ID, + $revisions[1]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #4', + $revisions[1]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #4".' + ); + + $this->assertSame( + self::$template_post->ID, + $revisions[2]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #3', + $revisions[2]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #3".' + ); + + $this->assertSame( + self::$template_post->ID, + $revisions[3]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #2', + $revisions[3]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #2".' + ); + } + + + /** + * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check + * @ticket 56922 + */ + public function test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check + * @ticket 56922 + */ + public function test_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions() { + wp_set_current_user( self::$contributor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::FORBIDDEN ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $revisions = wp_get_post_revisions( self::$template_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $revision = $response->get_data(); + + $this->assertIsArray( $revision, 'Failed asserting that the revision is an array.' ); + $this->assertSame( + $revision_id, + $revision['wp_id'], + "Failed asserting that the revision id is the same as $revision_id" + ); + $this->assertSame( + self::$template_post->ID, + $revision['parent'], + sprintf( + 'Failed asserting that the parent id of the revision is the same as %s.', + self::$template_post->ID + ) + ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + */ + public function test_get_item_not_found() { + wp_set_current_user( self::$admin_id ); + + $revisions = wp_get_post_revisions( self::$template_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/invalid//parent/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, WP_Http::NOT_FOUND ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::prepare_item_for_response + * @ticket 56922 + */ + public function test_prepare_item() { + $revisions = wp_get_post_revisions( self::$template_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + $post = get_post( $revision_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $controller = new WP_REST_Template_Revisions_Controller( self::PARENT_POST_TYPE ); + $response = $controller->prepare_item_for_response( $post, $request ); + $this->assertInstanceOf( + WP_REST_Response::class, + $response, + 'Failed asserting that the response object is an instance of WP_REST_Response.' + ); + + $revision = $response->get_data(); + $this->assertIsArray( $revision, 'Failed asserting that the revision is an array.' ); + $this->assertSame( + $revision_id, + $revision['wp_id'], + "Failed asserting that the revision id is the same as $revision_id." + ); + $this->assertSame( + self::$template_post->ID, + $revision['parent'], + sprintf( + 'Failed asserting that the parent id of the revision is the same as %s.', + self::$template_post->ID + ) + ); + + $links = $response->get_links(); + $this->assertIsArray( $links, 'Failed asserting that the links are an array.' ); + + $this->assertStringEndsWith( + self::TEST_THEME . '//' . self::TEMPLATE_NAME . '/revisions/' . $revision_id, + $links['self'][0]['href'], + sprintf( + 'Failed asserting that the self link ends with %s.', + self::TEST_THEME . '//' . self::TEMPLATE_NAME . '/revisions/' . $revision_id + ) + ); + + $this->assertStringEndsWith( + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + $links['parent'][0]['href'], + sprintf( + 'Failed asserting that the parent link ends with %s.', + self::TEST_THEME . '//' . self::TEMPLATE_NAME + ) + ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_item_schema + * @ticket 56922 + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertCount( 16, $properties ); + $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); + $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); + $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); + $this->assertArrayHasKey( 'source', $properties, 'Source key should exist in properties.' ); + $this->assertArrayHasKey( 'origin', $properties, 'Origin key should exist in properties.' ); + $this->assertArrayHasKey( 'content', $properties, 'Content key should exist in properties.' ); + $this->assertArrayHasKey( 'title', $properties, 'Title key should exist in properties.' ); + $this->assertArrayHasKey( 'description', $properties, 'description key should exist in properties.' ); + $this->assertArrayHasKey( 'status', $properties, 'status key should exist in properties.' ); + $this->assertArrayHasKey( 'wp_id', $properties, 'wp_id key should exist in properties.' ); + $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); + $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); + $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'is_custom', $properties, 'is_custom key should exist in properties.' ); + $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_create_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to create template revisions.", + WP_REST_Template_Revisions_Controller::class + ) + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_update_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to update template revisions.", + WP_REST_Template_Revisions_Controller::class + ) + ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + */ + public function test_delete_item() { + wp_set_current_user( self::$admin_id ); + + $revision_id = _wp_put_post_revision( self::$template_post ); + self::$revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Failed asserting that the response status is 200.' ); + $this->assertNull( get_post( $revision_id ), 'Failed asserting that the post with the given revision ID is deleted.' ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + */ + public function test_delete_item_incorrect_permission() { + wp_set_current_user( self::$contributor_id ); + $revision_id = _wp_put_post_revision( self::$template_post ); + self::$revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, WP_Http::FORBIDDEN ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + */ + public function test_delete_item_no_permission() { + wp_set_current_user( 0 ); + $revision_id = _wp_put_post_revision( self::$template_post ); + self::$revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + */ + public function test_delete_item_not_found() { + wp_set_current_user( self::$admin_id ); + + $revision_id = _wp_put_post_revision( self::$template_post ); + self::$revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/invalid//parent/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, WP_Http::NOT_FOUND ); + } +} diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 8341830452a1f..d212facf26006 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -2707,7 +2707,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/media": { + "/wp/v2/menu-items": { "namespace": "wp/v2", "methods": [ "GET", @@ -2718,6 +2718,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -2740,7 +2743,7 @@ mockedApiResponse.Schema = { "per_page": { "description": "Maximum number of items to be returned in result set.", "type": "integer", - "default": 10, + "default": 100, "minimum": 1, "maximum": 100, "required": false @@ -2762,24 +2765,6 @@ mockedApiResponse.Schema = { "format": "date-time", "required": false }, - "author": { - "description": "Limit result set to posts assigned to specific authors.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "author_exclude": { - "description": "Ensure result set excludes posts assigned to specific authors.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, "before": { "description": "Limit response to posts published before a given ISO8601 compliant date.", "type": "string", @@ -2818,7 +2803,7 @@ mockedApiResponse.Schema = { "order": { "description": "Order sort attribute ascending or descending.", "type": "string", - "default": "desc", + "default": "asc", "enum": [ "asc", "desc" @@ -2826,9 +2811,9 @@ mockedApiResponse.Schema = { "required": false }, "orderby": { - "description": "Sort collection by post attribute.", + "description": "Sort collection by object attribute.", "type": "string", - "default": "date", + "default": "menu_order", "enum": [ "author", "date", @@ -2839,28 +2824,11 @@ mockedApiResponse.Schema = { "relevance", "slug", "include_slugs", - "title" + "title", + "menu_order" ], "required": false }, - "parent": { - "description": "Limit result set to items with particular parent IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "parent_exclude": { - "description": "Limit result set to all items except those of a particular parent ID.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, "search_columns": { "default": [], "description": "Array of column names to be searched.", @@ -2884,36 +2852,118 @@ mockedApiResponse.Schema = { "required": false }, "status": { - "default": "inherit", + "default": "publish", "description": "Limit result set to posts assigned one or more statuses.", "type": "array", "items": { "enum": [ - "inherit", + "publish", + "future", + "draft", + "pending", "private", - "trash" + "trash", + "auto-draft", + "inherit", + "request-pending", + "request-confirmed", + "request-failed", + "request-completed", + "any" ], "type": "string" }, "required": false }, - "media_type": { - "default": null, - "description": "Limit result set to attachments of a particular media type.", + "tax_relation": { + "description": "Limit result set based on relationship between multiple taxonomies.", "type": "string", "enum": [ - "image", - "video", - "text", - "application", - "audio" + "AND", + "OR" ], "required": false }, - "mime_type": { - "default": null, - "description": "Limit result set to attachments of a particular MIME type.", - "type": "string", + "menus": { + "description": "Limit result set to items with specific terms assigned in the menus taxonomy.", + "type": [ + "object", + "array" + ], + "oneOf": [ + { + "title": "Term ID List", + "description": "Match terms with the listed IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "title": "Term ID Taxonomy Query", + "description": "Perform an advanced term query.", + "type": "object", + "properties": { + "terms": { + "description": "Term IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [] + }, + "operator": { + "description": "Whether items must be assigned all or any of the specified terms.", + "type": "string", + "enum": [ + "AND", + "OR" + ], + "default": "OR" + } + }, + "additionalProperties": false + } + ], + "required": false + }, + "menus_exclude": { + "description": "Limit result set to items except those with specific terms assigned in the menus taxonomy.", + "type": [ + "object", + "array" + ], + "oneOf": [ + { + "title": "Term ID List", + "description": "Match terms with the listed IDs.", + "type": "array", + "items": { + "type": "integer" + } + }, + { + "title": "Term ID Taxonomy Query", + "description": "Perform an advanced term query.", + "type": "object", + "properties": { + "terms": { + "description": "Term IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [] + } + }, + "additionalProperties": false + } + ], + "required": false + }, + "menu_order": { + "description": "Limit result set to posts with a specific menu_order value.", + "type": "integer", "required": false } } @@ -2922,55 +2972,26 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", + "title": { + "description": "The title for the object.", "type": [ "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" + "object" ], - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", "properties": { "raw": { - "description": "Title for the post, as it exists in the database.", + "description": "Title for the object, as it exists in the database.", "type": "string", "context": [ "edit" ] }, "rendered": { - "description": "HTML title for the post, transformed for display.", + "description": "HTML title for the object, transformed for display.", "type": "string", "context": [ "view", @@ -2982,817 +3003,40 @@ mockedApiResponse.Schema = { }, "required": false }, - "author": { - "description": "The ID for the author of the post.", - "type": "integer", - "required": false - }, - "comment_status": { - "description": "Whether or not comments are open on the post.", + "type": { + "default": "custom", + "description": "The family of objects originally represented, such as \"post_type\" or \"taxonomy\".", "type": "string", "enum": [ - "open", - "closed" + "taxonomy", + "post_type", + "post_type_archive", + "custom" ], "required": false }, - "ping_status": { - "description": "Whether or not the post can be pinged.", + "status": { + "default": "publish", + "description": "A named status for the object.", "type": "string", "enum": [ - "open", - "closed" + "publish", + "future", + "draft", + "pending", + "private" ], "required": false }, - "meta": { - "description": "Meta fields.", - "type": "object", - "properties": [], - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", + "parent": { + "default": 0, + "description": "The ID for the parent of the object.", + "type": "integer", + "minimum": 0, "required": false }, - "alt_text": { - "description": "Alternative text to display when attachment is not displayed.", - "type": "string", - "required": false - }, - "caption": { - "description": "The attachment caption.", - "type": "object", - "properties": { - "raw": { - "description": "Caption for the attachment, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML caption for the attachment, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "description": { - "description": "The attachment description.", - "type": "object", - "properties": { - "raw": { - "description": "Description for the attachment, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML description for the attachment, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - } - }, - "required": false - }, - "post": { - "description": "The ID for the associated post of the attachment.", - "type": "integer", - "required": false - } - } - } - ], - "_links": { - "self": "http://example.org/index.php?rest_route=/wp/v2/media" - } - }, - "/wp/v2/media/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - }, - { - "methods": [ - "POST", - "PUT", - "PATCH" - ], - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", - "type": "string", - "required": false - }, - "status": { - "description": "A named status for the post.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "title": { - "description": "The title for the post.", - "type": "object", - "properties": { - "raw": { - "description": "Title for the post, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the post, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "author": { - "description": "The ID for the author of the post.", - "type": "integer", - "required": false - }, - "comment_status": { - "description": "Whether or not comments are open on the post.", - "type": "string", - "enum": [ - "open", - "closed" - ], - "required": false - }, - "ping_status": { - "description": "Whether or not the post can be pinged.", - "type": "string", - "enum": [ - "open", - "closed" - ], - "required": false - }, - "meta": { - "description": "Meta fields.", - "type": "object", - "properties": [], - "required": false - }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", - "required": false - }, - "alt_text": { - "description": "Alternative text to display when attachment is not displayed.", - "type": "string", - "required": false - }, - "caption": { - "description": "The attachment caption.", - "type": "object", - "properties": { - "raw": { - "description": "Caption for the attachment, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML caption for the attachment, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "description": { - "description": "The attachment description.", - "type": "object", - "properties": { - "raw": { - "description": "Description for the attachment, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML description for the attachment, transformed for display.", - "type": "string", - "context": [ - "view", - "edit" - ], - "readonly": true - } - }, - "required": false - }, - "post": { - "description": "The ID for the associated post of the attachment.", - "type": "integer", - "required": false - } - } - }, - { - "methods": [ - "DELETE" - ], - "args": { - "id": { - "description": "Unique identifier for the post.", - "type": "integer", - "required": false - }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", - "required": false - } - } - } - ] - }, - "/wp/v2/media/(?P[\\d]+)/post-process": { - "namespace": "wp/v2", - "methods": [ - "POST" - ], - "endpoints": [ - { - "methods": [ - "POST" - ], - "args": { - "id": { - "description": "Unique identifier for the attachment.", - "type": "integer", - "required": false - }, - "action": { - "type": "string", - "enum": [ - "create-image-subsizes" - ], - "required": true - } - } - } - ] - }, - "/wp/v2/media/(?P[\\d]+)/edit": { - "namespace": "wp/v2", - "methods": [ - "POST" - ], - "endpoints": [ - { - "methods": [ - "POST" - ], - "args": { - "src": { - "description": "URL to the edited image file.", - "type": "string", - "format": "uri", - "required": true - }, - "modifiers": { - "description": "Array of image edits.", - "type": "array", - "minItems": 1, - "items": { - "description": "Image edit.", - "type": "object", - "required": [ - "type", - "args" - ], - "oneOf": [ - { - "title": "Rotation", - "properties": { - "type": { - "description": "Rotation type.", - "type": "string", - "enum": [ - "rotate" - ] - }, - "args": { - "description": "Rotation arguments.", - "type": "object", - "required": [ - "angle" - ], - "properties": { - "angle": { - "description": "Angle to rotate clockwise in degrees.", - "type": "number" - } - } - } - } - }, - { - "title": "Crop", - "properties": { - "type": { - "description": "Crop type.", - "type": "string", - "enum": [ - "crop" - ] - }, - "args": { - "description": "Crop arguments.", - "type": "object", - "required": [ - "left", - "top", - "width", - "height" - ], - "properties": { - "left": { - "description": "Horizontal position from the left to begin the crop as a percentage of the image width.", - "type": "number" - }, - "top": { - "description": "Vertical position from the top to begin the crop as a percentage of the image height.", - "type": "number" - }, - "width": { - "description": "Width of the crop as a percentage of the image width.", - "type": "number" - }, - "height": { - "description": "Height of the crop as a percentage of the image height.", - "type": "number" - } - } - } - } - } - ] - }, - "required": false - }, - "rotation": { - "description": "The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.", - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true, - "maximum": 360, - "exclusiveMaximum": true, - "required": false - }, - "x": { - "description": "As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.", - "type": "number", - "minimum": 0, - "maximum": 100, - "required": false - }, - "y": { - "description": "As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.", - "type": "number", - "minimum": 0, - "maximum": 100, - "required": false - }, - "width": { - "description": "As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.", - "type": "number", - "minimum": 0, - "maximum": 100, - "required": false - }, - "height": { - "description": "As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.", - "type": "number", - "minimum": 0, - "maximum": 100, - "required": false - } - } - } - ] - }, - "/wp/v2/menu-items": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "allow_batch": { - "v1": true - }, - "args": { - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "page": { - "description": "Current page of the collection.", - "type": "integer", - "default": 1, - "minimum": 1, - "required": false - }, - "per_page": { - "description": "Maximum number of items to be returned in result set.", - "type": "integer", - "default": 100, - "minimum": 1, - "maximum": 100, - "required": false - }, - "search": { - "description": "Limit results to those matching a string.", - "type": "string", - "required": false - }, - "after": { - "description": "Limit response to posts published after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_after": { - "description": "Limit response to posts modified after a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "before": { - "description": "Limit response to posts published before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "modified_before": { - "description": "Limit response to posts modified before a given ISO8601 compliant date.", - "type": "string", - "format": "date-time", - "required": false - }, - "exclude": { - "description": "Ensure result set excludes specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "include": { - "description": "Limit result set to specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "offset": { - "description": "Offset the result set by a specific number of items.", - "type": "integer", - "required": false - }, - "order": { - "description": "Order sort attribute ascending or descending.", - "type": "string", - "default": "asc", - "enum": [ - "asc", - "desc" - ], - "required": false - }, - "orderby": { - "description": "Sort collection by object attribute.", - "type": "string", - "default": "menu_order", - "enum": [ - "author", - "date", - "id", - "include", - "modified", - "parent", - "relevance", - "slug", - "include_slugs", - "title", - "menu_order" - ], - "required": false - }, - "search_columns": { - "default": [], - "description": "Array of column names to be searched.", - "type": "array", - "items": { - "enum": [ - "post_title", - "post_content", - "post_excerpt" - ], - "type": "string" - }, - "required": false - }, - "slug": { - "description": "Limit result set to posts with one or more specific slugs.", - "type": "array", - "items": { - "type": "string" - }, - "required": false - }, - "status": { - "default": "publish", - "description": "Limit result set to posts assigned one or more statuses.", - "type": "array", - "items": { - "enum": [ - "publish", - "future", - "draft", - "pending", - "private", - "trash", - "auto-draft", - "inherit", - "request-pending", - "request-confirmed", - "request-failed", - "request-completed", - "any" - ], - "type": "string" - }, - "required": false - }, - "tax_relation": { - "description": "Limit result set based on relationship between multiple taxonomies.", - "type": "string", - "enum": [ - "AND", - "OR" - ], - "required": false - }, - "menus": { - "description": "Limit result set to items with specific terms assigned in the menus taxonomy.", - "type": [ - "object", - "array" - ], - "oneOf": [ - { - "title": "Term ID List", - "description": "Match terms with the listed IDs.", - "type": "array", - "items": { - "type": "integer" - } - }, - { - "title": "Term ID Taxonomy Query", - "description": "Perform an advanced term query.", - "type": "object", - "properties": { - "terms": { - "description": "Term IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [] - }, - "operator": { - "description": "Whether items must be assigned all or any of the specified terms.", - "type": "string", - "enum": [ - "AND", - "OR" - ], - "default": "OR" - } - }, - "additionalProperties": false - } - ], - "required": false - }, - "menus_exclude": { - "description": "Limit result set to items except those with specific terms assigned in the menus taxonomy.", - "type": [ - "object", - "array" - ], - "oneOf": [ - { - "title": "Term ID List", - "description": "Match terms with the listed IDs.", - "type": "array", - "items": { - "type": "integer" - } - }, - { - "title": "Term ID Taxonomy Query", - "description": "Perform an advanced term query.", - "type": "object", - "properties": { - "terms": { - "description": "Term IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [] - } - }, - "additionalProperties": false - } - ], - "required": false - }, - "menu_order": { - "description": "Limit result set to posts with a specific menu_order value.", - "type": "integer", - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "allow_batch": { - "v1": true - }, - "args": { - "title": { - "description": "The title for the object.", - "type": [ - "string", - "object" - ], - "properties": { - "raw": { - "description": "Title for the object, as it exists in the database.", - "type": "string", - "context": [ - "edit" - ] - }, - "rendered": { - "description": "HTML title for the object, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "type": { - "default": "custom", - "description": "The family of objects originally represented, such as \"post_type\" or \"taxonomy\".", - "type": "string", - "enum": [ - "taxonomy", - "post_type", - "post_type_archive", - "custom" - ], - "required": false - }, - "status": { - "default": "publish", - "description": "A named status for the object.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "parent": { - "default": 0, - "description": "The ID for the parent of the object.", - "type": "integer", - "minimum": 0, - "required": false - }, - "attr_title": { - "description": "Text for the title attribute of the link element for this menu item.", + "attr_title": { + "description": "Text for the title attribute of the link element for this menu item.", "type": "string", "required": false }, @@ -4631,33 +3875,316 @@ mockedApiResponse.Schema = { "properties": [], "required": false }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + }, + "wp_pattern_category": { + "description": "The terms assigned to the post in the wp_pattern_category taxonomy.", + "type": "array", + "items": { + "type": "integer" + }, + "required": false + } + } + } + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/blocks" + } + }, + "/wp/v2/blocks/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "password": { + "description": "The password for the post if it is password protected.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "POST", + "PUT", + "PATCH" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "view", + "edit" + ] + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "view", + "edit" + ] + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "meta": { + "description": "Meta fields.", + "type": "object", + "properties": [], + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + }, + "wp_pattern_category": { + "description": "The terms assigned to the post in the wp_pattern_category taxonomy.", + "type": "array", + "items": { + "type": "integer" + }, + "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", + "required": false + } + } + } + ] + }, + "/wp/v2/blocks/(?P[\\d]+)/revisions": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "required": false + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string", + "required": false + }, + "exclude": { + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], "required": false }, - "wp_pattern_category": { - "description": "The terms assigned to the post in the wp_pattern_category taxonomy.", + "include": { + "description": "Limit result set to specific IDs.", "type": "array", "items": { "type": "integer" }, + "default": [], + "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by object attribute.", + "type": "string", + "default": "date", + "enum": [ + "date", + "id", + "include", + "relevance", + "slug", + "include_slugs", + "title" + ], "required": false } } } - ], - "_links": { - "self": "http://example.org/index.php?rest_route=/wp/v2/blocks" - } + ] }, - "/wp/v2/blocks/(?P[\\d]+)": { + "/wp/v2/blocks/(?P[\\d]+)/revisions/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET", - "POST", - "PUT", - "PATCH", "DELETE" ], "endpoints": [ @@ -4665,12 +4192,14 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], - "allow_batch": { - "v1": true - }, "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false + }, "id": { - "description": "Unique identifier for the post.", + "description": "Unique identifier for the revision.", "type": "integer", "required": false }, @@ -4684,26 +4213,71 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false }, - "password": { - "description": "The password for the post if it is password protected.", + "id": { + "description": "Unique identifier for the revision.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Required to be true, as revisions do not support trashing.", + "required": false + } + } + } + ] + }, + "/wp/v2/blocks/(?P[\\d]+)/autosaves": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false } } }, { "methods": [ - "POST", - "PUT", - "PATCH" + "POST" ], - "allow_batch": { - "v1": true - }, "args": { - "id": { - "description": "Unique identifier for the post.", + "parent": { + "description": "The ID for the parent of the autosave.", "type": "integer", "required": false }, @@ -4815,31 +4389,46 @@ mockedApiResponse.Schema = { "required": false } } - }, + } + ] + }, + "/wp/v2/blocks/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ { "methods": [ - "DELETE" + "GET" ], - "allow_batch": { - "v1": true - }, "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, "id": { - "description": "Unique identifier for the post.", + "description": "The ID for the autosave.", "type": "integer", "required": false }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false } } } ] }, - "/wp/v2/blocks/(?P[\\d]+)/revisions": { + "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/revisions": { "namespace": "wp/v2", "methods": [ "GET" @@ -4851,8 +4440,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "context": { @@ -4937,7 +4526,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/blocks/(?P[\\d]+)/revisions/(?P[\\d]+)": { + "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/revisions/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -4950,8 +4539,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "id": { @@ -4978,8 +4567,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "id": { @@ -4997,7 +4586,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/blocks/(?P[\\d]+)/autosaves": { + "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/autosaves": { "namespace": "wp/v2", "methods": [ "GET", @@ -5009,9 +4598,9 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", + "id": { + "description": "The id of a template", + "type": "string", "required": false }, "context": { @@ -5032,89 +4621,73 @@ mockedApiResponse.Schema = { "POST" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "date": { - "description": "The date the post was published, in the site's timezone.", - "type": [ - "string", - "null" - ], - "format": "date-time", - "required": false - }, - "date_gmt": { - "description": "The date the post was published, as GMT.", - "type": [ - "string", - "null" - ], - "format": "date-time", + "id": { + "description": "The id of a template", + "type": "string", "required": false }, "slug": { - "description": "An alphanumeric identifier for the post unique to its type.", + "description": "Unique slug identifying the template.", "type": "string", + "minLength": 1, + "pattern": "[a-zA-Z0-9_\\%-]+", "required": false }, - "status": { - "description": "A named status for the post.", + "theme": { + "description": "Theme identifier for the template.", "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], "required": false }, - "password": { - "description": "A password to protect access to the content and excerpt.", + "type": { + "description": "Type of template.", "type": "string", "required": false }, - "title": { - "description": "The title for the post.", - "type": "object", + "content": { + "description": "Content of template.", + "type": [ + "object", + "string" + ], "properties": { "raw": { - "description": "Title for the post, as it exists in the database.", + "description": "Content for the template, as it exists in the database.", "type": "string", "context": [ "view", "edit" ] + }, + "block_version": { + "description": "Version of the content block format used by the template.", + "type": "integer", + "context": [ + "edit" + ], + "readonly": true } }, "required": false }, - "content": { - "description": "The content for the post.", - "type": "object", + "title": { + "description": "Title of template.", + "type": [ + "object", + "string" + ], "properties": { "raw": { - "description": "Content for the post, as it exists in the database.", + "description": "Title for the template, as it exists in the database.", "type": "string", "context": [ "view", - "edit" + "edit", + "embed" ] }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", + "rendered": { + "description": "HTML title for the template, transformed for display.", + "type": "string", "context": [ "view", "edit", @@ -5125,30 +4698,33 @@ mockedApiResponse.Schema = { }, "required": false }, - "meta": { - "description": "Meta fields.", - "type": "object", - "properties": [], + "description": { + "description": "Description of template.", + "type": "string", "required": false }, - "template": { - "description": "The theme file to use to display the post.", + "status": { + "description": "Status of template.", "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], "required": false }, - "wp_pattern_category": { - "description": "The terms assigned to the post in the wp_pattern_category taxonomy.", - "type": "array", - "items": { - "type": "integer" - }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", "required": false } } } ] }, - "/wp/v2/blocks/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/autosaves/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET" @@ -5160,8 +4736,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "id": { @@ -5536,7 +5112,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/templates/(?P[\\d]+)/revisions": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/revisions": { "namespace": "wp/v2", "methods": [ "GET" @@ -5548,8 +5124,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "context": { @@ -5634,7 +5210,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/templates/(?P[\\d]+)/revisions/(?P[\\d]+)": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/revisions/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -5647,8 +5223,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "id": { @@ -5675,8 +5251,8 @@ mockedApiResponse.Schema = { ], "args": { "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, "id": { @@ -5694,7 +5270,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/templates/(?P[\\d]+)/autosaves": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/autosaves": { "namespace": "wp/v2", "methods": [ "GET", @@ -5706,204 +5282,20 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - }, - { - "methods": [ - "POST" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, - "slug": { - "description": "Unique slug identifying the template.", - "type": "string", - "minLength": 1, - "pattern": "[a-zA-Z0-9_\\%-]+", - "required": false - }, - "theme": { - "description": "Theme identifier for the template.", - "type": "string", - "required": false - }, - "type": { - "description": "Type of template.", - "type": "string", - "required": false - }, - "content": { - "description": "Content of template.", - "type": [ - "object", - "string" - ], - "properties": { - "raw": { - "description": "Content for the template, as it exists in the database.", - "type": "string", - "context": [ - "view", - "edit" - ] - }, - "block_version": { - "description": "Version of the content block format used by the template.", - "type": "integer", - "context": [ - "edit" - ], - "readonly": true - } - }, - "required": false - }, - "title": { - "description": "Title of template.", - "type": [ - "object", - "string" - ], - "properties": { - "raw": { - "description": "Title for the template, as it exists in the database.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ] - }, - "rendered": { - "description": "HTML title for the template, transformed for display.", - "type": "string", - "context": [ - "view", - "edit", - "embed" - ], - "readonly": true - } - }, - "required": false - }, - "description": { - "description": "Description of template.", - "type": "string", - "required": false - }, - "status": { - "description": "Status of template.", - "type": "string", - "enum": [ - "publish", - "future", - "draft", - "pending", - "private" - ], - "required": false - }, - "author": { - "description": "The ID for the author of the template.", - "type": "integer", - "required": false - } - } - } - ] - }, - "/wp/v2/templates/(?P[\\d]+)/autosaves/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, "id": { - "description": "The ID for the autosave.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", + "description": "The id of a template", "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", "required": false - } - } - } - ] - }, - "/wp/v2/template-parts": { - "namespace": "wp/v2", - "methods": [ - "GET", - "POST" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { + }, "context": { "description": "Scope under which the request is made; determines fields present in response.", "type": "string", "enum": [ "view", "embed", - "edit" - ], - "default": "view", - "required": false - }, - "wp_id": { - "description": "Limit to the specified post id.", - "type": "integer", - "required": false - }, - "area": { - "description": "Limit to the specified template part area.", - "type": "string", - "required": false - }, - "post_type": { - "description": "Post type to get the templates for.", - "type": "string", + "edit" + ], + "default": "view", "required": false } } @@ -5913,12 +5305,17 @@ mockedApiResponse.Schema = { "POST" ], "args": { + "id": { + "description": "The id of a template", + "type": "string", + "required": false + }, "slug": { "description": "Unique slug identifying the template.", "type": "string", "minLength": 1, "pattern": "[a-zA-Z0-9_\\%-]+", - "required": true + "required": false }, "theme": { "description": "Theme identifier for the template.", @@ -5931,7 +5328,6 @@ mockedApiResponse.Schema = { "required": false }, "content": { - "default": "", "description": "Content of template.", "type": [ "object", @@ -5958,7 +5354,6 @@ mockedApiResponse.Schema = { "required": false }, "title": { - "default": "", "description": "Title of template.", "type": [ "object", @@ -5988,13 +5383,11 @@ mockedApiResponse.Schema = { "required": false }, "description": { - "default": "", "description": "Description of template.", "type": "string", "required": false }, "status": { - "default": "publish", "description": "Status of template.", "type": "string", "enum": [ @@ -6018,16 +5411,9 @@ mockedApiResponse.Schema = { } } } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts" - } - ] - } + ] }, - "/wp/v2/template-parts/lookup": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)/autosaves/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET" @@ -6038,40 +5424,36 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "slug": { - "description": "The slug of the template to get the fallback for", + "parent": { + "description": "The id of a template", "type": "string", - "required": true + "required": false }, - "is_custom": { - "description": "Indicates if a template is custom or part of the template hierarchy", - "type": "boolean", + "id": { + "description": "The ID for the autosave.", + "type": "integer", "required": false }, - "template_prefix": { - "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "context": { + "description": "Scope under which the request is made; determines fields present in response.", "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false } } } - ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts/lookup" - } - ] - } + ] }, - "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)": { + "/wp/v2/template-parts": { "namespace": "wp/v2", "methods": [ "GET", - "POST", - "PUT", - "PATCH", - "DELETE" + "POST" ], "endpoints": [ { @@ -6079,11 +5461,6 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "id": { - "description": "The id of a template", - "type": "string", - "required": false - }, "context": { "description": "Scope under which the request is made; determines fields present in response.", "type": "string", @@ -6094,27 +5471,35 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "wp_id": { + "description": "Limit to the specified post id.", + "type": "integer", + "required": false + }, + "area": { + "description": "Limit to the specified template part area.", + "type": "string", + "required": false + }, + "post_type": { + "description": "Post type to get the templates for.", + "type": "string", + "required": false } } }, { "methods": [ - "POST", - "PUT", - "PATCH" + "POST" ], "args": { - "id": { - "description": "The id of a template", - "type": "string", - "required": false - }, "slug": { "description": "Unique slug identifying the template.", "type": "string", "minLength": 1, "pattern": "[a-zA-Z0-9_\\%-]+", - "required": false + "required": true }, "theme": { "description": "Theme identifier for the template.", @@ -6127,6 +5512,7 @@ mockedApiResponse.Schema = { "required": false }, "content": { + "default": "", "description": "Content of template.", "type": [ "object", @@ -6153,6 +5539,7 @@ mockedApiResponse.Schema = { "required": false }, "title": { + "default": "", "description": "Title of template.", "type": [ "object", @@ -6182,11 +5569,13 @@ mockedApiResponse.Schema = { "required": false }, "description": { + "default": "", "description": "Description of template.", "type": "string", "required": false }, "status": { + "default": "publish", "description": "Status of template.", "type": "string", "enum": [ @@ -6209,190 +5598,61 @@ mockedApiResponse.Schema = { "required": false } } - }, - { - "methods": [ - "DELETE" - ], - "args": { - "id": { - "description": "The id of a template", - "type": "string", - "required": false - }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", - "required": false - } - } } - ] - }, - "/wp/v2/template-parts/(?P[\\d]+)/revisions": { - "namespace": "wp/v2", - "methods": [ - "GET" ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - }, - "page": { - "description": "Current page of the collection.", - "type": "integer", - "default": 1, - "minimum": 1, - "required": false - }, - "per_page": { - "description": "Maximum number of items to be returned in result set.", - "type": "integer", - "minimum": 1, - "maximum": 100, - "required": false - }, - "search": { - "description": "Limit results to those matching a string.", - "type": "string", - "required": false - }, - "exclude": { - "description": "Ensure result set excludes specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "include": { - "description": "Limit result set to specific IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "default": [], - "required": false - }, - "offset": { - "description": "Offset the result set by a specific number of items.", - "type": "integer", - "required": false - }, - "order": { - "description": "Order sort attribute ascending or descending.", - "type": "string", - "default": "desc", - "enum": [ - "asc", - "desc" - ], - "required": false - }, - "orderby": { - "description": "Sort collection by object attribute.", - "type": "string", - "default": "date", - "enum": [ - "date", - "id", - "include", - "relevance", - "slug", - "include_slugs", - "title" - ], - "required": false - } + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts" } - } - ] + ] + } }, - "/wp/v2/template-parts/(?P[\\d]+)/revisions/(?P[\\d]+)": { + "/wp/v2/template-parts/lookup": { "namespace": "wp/v2", "methods": [ - "GET", - "DELETE" + "GET" ], "endpoints": [ { "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", - "required": false - }, - "id": { - "description": "Unique identifier for the revision.", - "type": "integer", - "required": false - }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", - "required": false - } - } - }, - { - "methods": [ - "DELETE" - ], - "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", - "required": false + "GET" + ], + "args": { + "slug": { + "description": "The slug of the template to get the fallback for", + "type": "string", + "required": true }, - "id": { - "description": "Unique identifier for the revision.", - "type": "integer", + "is_custom": { + "description": "Indicates if a template is custom or part of the template hierarchy", + "type": "boolean", "required": false }, - "force": { - "type": "boolean", - "default": false, - "description": "Required to be true, as revisions do not support trashing.", + "template_prefix": { + "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "type": "string", "required": false } } } - ] + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts/lookup" + } + ] + } }, - "/wp/v2/template-parts/(?P[\\d]+)/autosaves": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w%-]+)": { "namespace": "wp/v2", "methods": [ "GET", - "POST" + "POST", + "PUT", + "PATCH", + "DELETE" ], "endpoints": [ { @@ -6400,9 +5660,9 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", + "id": { + "description": "The id of a template", + "type": "string", "required": false }, "context": { @@ -6420,12 +5680,14 @@ mockedApiResponse.Schema = { }, { "methods": [ - "POST" + "POST", + "PUT", + "PATCH" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", + "id": { + "description": "The id of a template", + "type": "string", "required": false }, "slug": { @@ -6528,39 +5790,21 @@ mockedApiResponse.Schema = { "required": false } } - } - ] - }, - "/wp/v2/template-parts/(?P[\\d]+)/autosaves/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET" - ], - "endpoints": [ + }, { "methods": [ - "GET" + "DELETE" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, "id": { - "description": "The ID for the autosave.", - "type": "integer", + "description": "The id of a template", + "type": "string", "required": false }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", - "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", "required": false } } @@ -6702,48 +5946,229 @@ mockedApiResponse.Schema = { }, "required": false }, - "slug": { - "description": "Limit result set to posts with one or more specific slugs.", - "type": "array", - "items": { - "type": "string" - }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + }, + "required": false + }, + "status": { + "default": "publish", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "publish", + "future", + "draft", + "pending", + "private", + "trash", + "auto-draft", + "inherit", + "request-pending", + "request-confirmed", + "request-failed", + "request-completed", + "any" + ], + "type": "string" + }, + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "allow_batch": { + "v1": true + }, + "args": { + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "password": { + "description": "A password to protect access to the content and excerpt.", + "type": "string", + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit", + "embed" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "content": { + "description": "The content for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Content for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit", + "embed" + ] + }, + "rendered": { + "description": "HTML content for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + }, + "block_version": { + "description": "Version of the content block format used by the post.", + "type": "integer", + "context": [ + "edit", + "embed" + ], + "readonly": true + }, + "protected": { + "description": "Whether the content is protected with a password.", + "type": "boolean", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/navigation" + } + ] + } + }, + "/wp/v2/navigation/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false }, - "status": { - "default": "publish", - "description": "Limit result set to posts assigned one or more statuses.", - "type": "array", - "items": { - "enum": [ - "publish", - "future", - "draft", - "pending", - "private", - "trash", - "auto-draft", - "inherit", - "request-pending", - "request-confirmed", - "request-failed", - "request-completed", - "any" - ], - "type": "string" - }, + "password": { + "description": "The password for the post if it is password protected.", + "type": "string", "required": false } } }, { "methods": [ - "POST" + "POST", + "PUT", + "PATCH" ], "allow_batch": { "v1": true }, "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, "date": { "description": "The date the post was published, in the site's timezone.", "type": [ @@ -6859,23 +6284,132 @@ mockedApiResponse.Schema = { "required": false } } + }, + { + "methods": [ + "DELETE" + ], + "allow_batch": { + "v1": true + }, + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", + "required": false + } + } } + ] + }, + "/wp/v2/navigation/(?P[\\d]+)/revisions": { + "namespace": "wp/v2", + "methods": [ + "GET" ], - "_links": { - "self": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/navigation" + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + }, + "page": { + "description": "Current page of the collection.", + "type": "integer", + "default": 1, + "minimum": 1, + "required": false + }, + "per_page": { + "description": "Maximum number of items to be returned in result set.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "required": false + }, + "search": { + "description": "Limit results to those matching a string.", + "type": "string", + "required": false + }, + "exclude": { + "description": "Ensure result set excludes specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "include": { + "description": "Limit result set to specific IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by object attribute.", + "type": "string", + "default": "date", + "enum": [ + "date", + "id", + "include", + "relevance", + "slug", + "include_slugs", + "title" + ], + "required": false + } } - ] - } + } + ] }, - "/wp/v2/navigation/(?P[\\d]+)": { + "/wp/v2/navigation/(?P[\\d]+)/revisions/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET", - "POST", - "PUT", - "PATCH", "DELETE" ], "endpoints": [ @@ -6883,12 +6417,14 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], - "allow_batch": { - "v1": true - }, "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false + }, "id": { - "description": "Unique identifier for the post.", + "description": "Unique identifier for the revision.", "type": "integer", "required": false }, @@ -6902,26 +6438,71 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "parent": { + "description": "The ID for the parent of the revision.", + "type": "integer", + "required": false }, - "password": { - "description": "The password for the post if it is password protected.", + "id": { + "description": "Unique identifier for the revision.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Required to be true, as revisions do not support trashing.", + "required": false + } + } + } + ] + }, + "/wp/v2/navigation/(?P[\\d]+)/autosaves": { + "namespace": "wp/v2", + "methods": [ + "GET", + "POST" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false } } }, { "methods": [ - "POST", - "PUT", - "PATCH" + "POST" ], - "allow_batch": { - "v1": true - }, "args": { - "id": { - "description": "Unique identifier for the post.", + "parent": { + "description": "The ID for the parent of the autosave.", "type": "integer", "required": false }, @@ -7040,34 +6621,50 @@ mockedApiResponse.Schema = { "required": false } } - }, + } + ] + }, + "/wp/v2/navigation/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ { "methods": [ - "DELETE" + "GET" ], - "allow_batch": { - "v1": true - }, "args": { + "parent": { + "description": "The ID for the parent of the autosave.", + "type": "integer", + "required": false + }, "id": { - "description": "Unique identifier for the post.", + "description": "The ID for the autosave.", "type": "integer", "required": false }, - "force": { - "type": "boolean", - "default": false, - "description": "Whether to bypass Trash and force deletion.", + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", "required": false } } } ] }, - "/wp/v2/navigation/(?P[\\d]+)/revisions": { + "/wp/v2/media": { "namespace": "wp/v2", "methods": [ - "GET" + "GET", + "POST" ], "endpoints": [ { @@ -7075,11 +6672,6 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", - "required": false - }, "context": { "description": "Scope under which the request is made; determines fields present in response.", "type": "string", @@ -7101,6 +6693,7 @@ mockedApiResponse.Schema = { "per_page": { "description": "Maximum number of items to be returned in result set.", "type": "integer", + "default": 10, "minimum": 1, "maximum": 100, "required": false @@ -7110,6 +6703,48 @@ mockedApiResponse.Schema = { "type": "string", "required": false }, + "after": { + "description": "Limit response to posts published after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_after": { + "description": "Limit response to posts modified after a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "author": { + "description": "Limit result set to posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "author_exclude": { + "description": "Ensure result set excludes posts assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "before": { + "description": "Limit response to posts published before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, + "modified_before": { + "description": "Limit response to posts modified before a given ISO8601 compliant date.", + "type": "string", + "format": "date-time", + "required": false + }, "exclude": { "description": "Ensure result set excludes specific IDs.", "type": "array", @@ -7125,108 +6760,287 @@ mockedApiResponse.Schema = { "items": { "type": "integer" }, - "default": [], + "default": [], + "required": false + }, + "offset": { + "description": "Offset the result set by a specific number of items.", + "type": "integer", + "required": false + }, + "order": { + "description": "Order sort attribute ascending or descending.", + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ], + "required": false + }, + "orderby": { + "description": "Sort collection by post attribute.", + "type": "string", + "default": "date", + "enum": [ + "author", + "date", + "id", + "include", + "modified", + "parent", + "relevance", + "slug", + "include_slugs", + "title" + ], + "required": false + }, + "parent": { + "description": "Limit result set to items with particular parent IDs.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "parent_exclude": { + "description": "Limit result set to all items except those of a particular parent ID.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false + }, + "search_columns": { + "default": [], + "description": "Array of column names to be searched.", + "type": "array", + "items": { + "enum": [ + "post_title", + "post_content", + "post_excerpt" + ], + "type": "string" + }, + "required": false + }, + "slug": { + "description": "Limit result set to posts with one or more specific slugs.", + "type": "array", + "items": { + "type": "string" + }, + "required": false + }, + "status": { + "default": "inherit", + "description": "Limit result set to posts assigned one or more statuses.", + "type": "array", + "items": { + "enum": [ + "inherit", + "private", + "trash" + ], + "type": "string" + }, + "required": false + }, + "media_type": { + "default": null, + "description": "Limit result set to attachments of a particular media type.", + "type": "string", + "enum": [ + "image", + "video", + "text", + "application", + "audio" + ], + "required": false + }, + "mime_type": { + "default": null, + "description": "Limit result set to attachments of a particular MIME type.", + "type": "string", + "required": false + } + } + }, + { + "methods": [ + "POST" + ], + "args": { + "date": { + "description": "The date the post was published, in the site's timezone.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "date_gmt": { + "description": "The date the post was published, as GMT.", + "type": [ + "string", + "null" + ], + "format": "date-time", + "required": false + }, + "slug": { + "description": "An alphanumeric identifier for the post unique to its type.", + "type": "string", + "required": false + }, + "status": { + "description": "A named status for the post.", + "type": "string", + "enum": [ + "publish", + "future", + "draft", + "pending", + "private" + ], + "required": false + }, + "title": { + "description": "The title for the post.", + "type": "object", + "properties": { + "raw": { + "description": "Title for the post, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML title for the post, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, "required": false }, - "offset": { - "description": "Offset the result set by a specific number of items.", + "author": { + "description": "The ID for the author of the post.", "type": "integer", "required": false }, - "order": { - "description": "Order sort attribute ascending or descending.", + "comment_status": { + "description": "Whether or not comments are open on the post.", "type": "string", - "default": "desc", "enum": [ - "asc", - "desc" + "open", + "closed" ], "required": false }, - "orderby": { - "description": "Sort collection by object attribute.", + "ping_status": { + "description": "Whether or not the post can be pinged.", "type": "string", - "default": "date", "enum": [ - "date", - "id", - "include", - "relevance", - "slug", - "include_slugs", - "title" + "open", + "closed" ], "required": false - } - } - } - ] - }, - "/wp/v2/navigation/(?P[\\d]+)/revisions/(?P[\\d]+)": { - "namespace": "wp/v2", - "methods": [ - "GET", - "DELETE" - ], - "endpoints": [ - { - "methods": [ - "GET" - ], - "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + }, + "meta": { + "description": "Meta fields.", + "type": "object", + "properties": [], "required": false }, - "id": { - "description": "Unique identifier for the revision.", - "type": "integer", + "template": { + "description": "The theme file to use to display the post.", + "type": "string", "required": false }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", + "alt_text": { + "description": "Alternative text to display when attachment is not displayed.", "type": "string", - "enum": [ - "view", - "embed", - "edit" - ], - "default": "view", "required": false - } - } - }, - { - "methods": [ - "DELETE" - ], - "args": { - "parent": { - "description": "The ID for the parent of the revision.", - "type": "integer", + }, + "caption": { + "description": "The attachment caption.", + "type": "object", + "properties": { + "raw": { + "description": "Caption for the attachment, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML caption for the attachment, transformed for display.", + "type": "string", + "context": [ + "view", + "edit", + "embed" + ], + "readonly": true + } + }, "required": false }, - "id": { - "description": "Unique identifier for the revision.", - "type": "integer", + "description": { + "description": "The attachment description.", + "type": "object", + "properties": { + "raw": { + "description": "Description for the attachment, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] + }, + "rendered": { + "description": "HTML description for the attachment, transformed for display.", + "type": "string", + "context": [ + "view", + "edit" + ], + "readonly": true + } + }, "required": false }, - "force": { - "type": "boolean", - "default": false, - "description": "Required to be true, as revisions do not support trashing.", + "post": { + "description": "The ID for the associated post of the attachment.", + "type": "integer", "required": false } } } - ] + ], + "_links": { + "self": "http://example.org/index.php?rest_route=/wp/v2/media" + } }, - "/wp/v2/navigation/(?P[\\d]+)/autosaves": { + "/wp/v2/media/(?P[\\d]+)": { "namespace": "wp/v2", "methods": [ "GET", - "POST" + "POST", + "PUT", + "PATCH", + "DELETE" ], "endpoints": [ { @@ -7234,8 +7048,8 @@ mockedApiResponse.Schema = { "GET" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", + "id": { + "description": "Unique identifier for the post.", "type": "integer", "required": false }, @@ -7254,11 +7068,13 @@ mockedApiResponse.Schema = { }, { "methods": [ - "POST" + "POST", + "PUT", + "PATCH" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", + "id": { + "description": "Unique identifier for the post.", "type": "integer", "required": false }, @@ -7297,11 +7113,6 @@ mockedApiResponse.Schema = { ], "required": false }, - "password": { - "description": "A password to protect access to the content and excerpt.", - "type": "string", - "required": false - }, "title": { "description": "The title for the post.", "type": "object", @@ -7310,8 +7121,7 @@ mockedApiResponse.Schema = { "description": "Title for the post, as it exists in the database.", "type": "string", "context": [ - "edit", - "embed" + "edit" ] }, "rendered": { @@ -7327,20 +7137,58 @@ mockedApiResponse.Schema = { }, "required": false }, - "content": { - "description": "The content for the post.", + "author": { + "description": "The ID for the author of the post.", + "type": "integer", + "required": false + }, + "comment_status": { + "description": "Whether or not comments are open on the post.", + "type": "string", + "enum": [ + "open", + "closed" + ], + "required": false + }, + "ping_status": { + "description": "Whether or not the post can be pinged.", + "type": "string", + "enum": [ + "open", + "closed" + ], + "required": false + }, + "meta": { + "description": "Meta fields.", + "type": "object", + "properties": [], + "required": false + }, + "template": { + "description": "The theme file to use to display the post.", + "type": "string", + "required": false + }, + "alt_text": { + "description": "Alternative text to display when attachment is not displayed.", + "type": "string", + "required": false + }, + "caption": { + "description": "The attachment caption.", "type": "object", "properties": { "raw": { - "description": "Content for the post, as it exists in the database.", + "description": "Caption for the attachment, as it exists in the database.", "type": "string", "context": [ - "edit", - "embed" + "edit" ] }, "rendered": { - "description": "HTML content for the post, transformed for display.", + "description": "HTML caption for the attachment, transformed for display.", "type": "string", "context": [ "view", @@ -7348,68 +7196,220 @@ mockedApiResponse.Schema = { "embed" ], "readonly": true + } + }, + "required": false + }, + "description": { + "description": "The attachment description.", + "type": "object", + "properties": { + "raw": { + "description": "Description for the attachment, as it exists in the database.", + "type": "string", + "context": [ + "edit" + ] }, - "block_version": { - "description": "Version of the content block format used by the post.", - "type": "integer", - "context": [ - "edit", - "embed" - ], - "readonly": true - }, - "protected": { - "description": "Whether the content is protected with a password.", - "type": "boolean", + "rendered": { + "description": "HTML description for the attachment, transformed for display.", + "type": "string", "context": [ "view", - "edit", - "embed" + "edit" ], "readonly": true } }, "required": false }, - "template": { - "description": "The theme file to use to display the post.", - "type": "string", + "post": { + "description": "The ID for the associated post of the attachment.", + "type": "integer", + "required": false + } + } + }, + { + "methods": [ + "DELETE" + ], + "args": { + "id": { + "description": "Unique identifier for the post.", + "type": "integer", + "required": false + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to bypass Trash and force deletion.", "required": false } } } ] }, - "/wp/v2/navigation/(?P[\\d]+)/autosaves/(?P[\\d]+)": { + "/wp/v2/media/(?P[\\d]+)/post-process": { "namespace": "wp/v2", "methods": [ - "GET" + "POST" ], "endpoints": [ { "methods": [ - "GET" + "POST" ], "args": { - "parent": { - "description": "The ID for the parent of the autosave.", - "type": "integer", - "required": false - }, "id": { - "description": "The ID for the autosave.", + "description": "Unique identifier for the attachment.", "type": "integer", "required": false }, - "context": { - "description": "Scope under which the request is made; determines fields present in response.", + "action": { "type": "string", "enum": [ - "view", - "embed", - "edit" + "create-image-subsizes" ], - "default": "view", + "required": true + } + } + } + ] + }, + "/wp/v2/media/(?P[\\d]+)/edit": { + "namespace": "wp/v2", + "methods": [ + "POST" + ], + "endpoints": [ + { + "methods": [ + "POST" + ], + "args": { + "src": { + "description": "URL to the edited image file.", + "type": "string", + "format": "uri", + "required": true + }, + "modifiers": { + "description": "Array of image edits.", + "type": "array", + "minItems": 1, + "items": { + "description": "Image edit.", + "type": "object", + "required": [ + "type", + "args" + ], + "oneOf": [ + { + "title": "Rotation", + "properties": { + "type": { + "description": "Rotation type.", + "type": "string", + "enum": [ + "rotate" + ] + }, + "args": { + "description": "Rotation arguments.", + "type": "object", + "required": [ + "angle" + ], + "properties": { + "angle": { + "description": "Angle to rotate clockwise in degrees.", + "type": "number" + } + } + } + } + }, + { + "title": "Crop", + "properties": { + "type": { + "description": "Crop type.", + "type": "string", + "enum": [ + "crop" + ] + }, + "args": { + "description": "Crop arguments.", + "type": "object", + "required": [ + "left", + "top", + "width", + "height" + ], + "properties": { + "left": { + "description": "Horizontal position from the left to begin the crop as a percentage of the image width.", + "type": "number" + }, + "top": { + "description": "Vertical position from the top to begin the crop as a percentage of the image height.", + "type": "number" + }, + "width": { + "description": "Width of the crop as a percentage of the image width.", + "type": "number" + }, + "height": { + "description": "Height of the crop as a percentage of the image height.", + "type": "number" + } + } + } + } + } + ] + }, + "required": false + }, + "rotation": { + "description": "The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.", + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 360, + "exclusiveMaximum": true, + "required": false + }, + "x": { + "description": "As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.", + "type": "number", + "minimum": 0, + "maximum": 100, + "required": false + }, + "y": { + "description": "As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.", + "type": "number", + "minimum": 0, + "maximum": 100, + "required": false + }, + "width": { + "description": "As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.", + "type": "number", + "minimum": 0, + "maximum": 100, + "required": false + }, + "height": { + "description": "As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.", + "type": "number", + "minimum": 0, + "maximum": 100, "required": false } } @@ -8780,12 +8780,12 @@ mockedApiResponse.Schema = { "enum": { "post": "post", "page": "page", - "attachment": "attachment", "nav_menu_item": "nav_menu_item", "wp_block": "wp_block", "wp_template": "wp_template", "wp_template_part": "wp_template_part", - "wp_navigation": "wp_navigation" + "wp_navigation": "wp_navigation", + "attachment": "attachment" } }, "required": false @@ -12352,36 +12352,6 @@ mockedApiResponse.TypesCollection = { ] } }, - "attachment": { - "description": "", - "hierarchical": false, - "has_archive": false, - "name": "Media", - "slug": "attachment", - "icon": "dashicons-admin-media", - "taxonomies": [], - "rest_base": "media", - "rest_namespace": "wp/v2", - "_links": { - "collection": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/types" - } - ], - "wp:items": [ - { - "href": "http://example.org/index.php?rest_route=/wp/v2/media" - } - ], - "curies": [ - { - "name": "wp", - "href": "https://api.w.org/{rel}", - "templated": true - } - ] - } - }, "nav_menu_item": { "description": "", "hierarchical": false, @@ -12535,6 +12505,36 @@ mockedApiResponse.TypesCollection = { } ] } + }, + "attachment": { + "description": "", + "hierarchical": false, + "has_archive": false, + "name": "Media", + "slug": "attachment", + "icon": "dashicons-admin-media", + "taxonomies": [], + "rest_base": "media", + "rest_namespace": "wp/v2", + "_links": { + "collection": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/types" + } + ], + "wp:items": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/media" + } + ], + "curies": [ + { + "name": "wp", + "href": "https://api.w.org/{rel}", + "templated": true + } + ] + } } };