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.
Built from https://develop.svn.wordpress.org/trunk@56819


git-svn-id: http://core.svn.wordpress.org/trunk@56331 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
spacedmonkey 2023-10-10 14:05:21 +00:00
parent b44637c00c
commit 6e774df98c
10 changed files with 942 additions and 187 deletions

View File

@ -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.

View File

@ -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 @@ final class WP_Post_Type {
* - `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 @@ final class WP_Post_Type {
// 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 @@ final class WP_Post_Type {
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.
*

View File

@ -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.

View File

@ -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.

View File

@ -65,8 +65,13 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {
$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 @@ class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {
*/
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;

View File

@ -0,0 +1,276 @@
<?php
/**
* REST API: WP_REST_Template_Autosaves_Controller class.
*
* @package WordPress
* @subpackage REST_API
* @since 6.4.0
*/
/**
* Core class used to access template autosaves via the REST API.
*
* @since 6.4.0
*
* @see WP_REST_Autosaves_Controller
*/
class WP_REST_Template_Autosaves_Controller extends WP_REST_Autosaves_Controller {
/**
* Parent post type.
*
* @since 6.4.0
* @var string
*/
private $parent_post_type;
/**
* Parent post controller.
*
* @since 6.4.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* Revision controller.
*
* @since 6.4.0
* @var WP_REST_Revisions_Controller
*/
private $revisions_controller;
/**
* The base of the parent controller's route.
*
* @since 6.4.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 6.4.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
parent::__construct( $parent_post_type );
$this->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<id>%s%s)/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* 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<parent>%s%s)/%s/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base,
'(?P<id>[\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 );
}
}

View File

@ -0,0 +1,297 @@
<?php
/**
* REST API: WP_REST_Template_Revisions_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.4.0
*/
/**
* Core class used to access template revisions via the REST API.
*
* @since 6.4.0
*
* @see WP_REST_Controller
*/
class WP_REST_Template_Revisions_Controller extends WP_REST_Revisions_Controller {
/**
* Parent post type.
*
* @since 6.4.0
* @var string
*/
private $parent_post_type;
/**
* Parent controller.
*
* @since 6.4.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* The base of the parent controller's route.
*
* @since 6.4.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 6.4.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
parent::__construct( $parent_post_type );
$this->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<parent>%s%s)/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* 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<parent>%s%s)/%s/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base,
'(?P<id>[\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 );
}
}

View File

@ -760,7 +760,7 @@ class WP_REST_Templates_Controller extends WP_REST_Controller {
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 @@ class WP_REST_Templates_Controller extends WP_REST_Controller {
),
);
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;
}

View File

@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.4-beta2-56818';
$wp_version = '6.4-beta2-56819';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -273,7 +273,9 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-global-styles-revis
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';