diff --git a/wp-includes/block-template-utils.php b/wp-includes/block-template-utils.php index 0ce64a659d..c5953e1d4c 100644 --- a/wp-includes/block-template-utils.php +++ b/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/wp-includes/class-wp-post-type.php b/wp-includes/class-wp-post-type.php index ce6d9d348b..7a2769ed88 100644 --- a/wp-includes/class-wp-post-type.php +++ b/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 @@ 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. * diff --git a/wp-includes/post.php b/wp-includes/post.php index a85d8fbdb5..c3910d2b3a 100644 --- a/wp-includes/post.php +++ b/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/wp-includes/rest-api.php b/wp-includes/rest-api.php index 71539b084c..19d29d051f 100644 --- a/wp-includes/rest-api.php +++ b/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/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index d14119133a..5545625df4 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -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; diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-template-autosaves-controller.php new file mode 100644 index 0000000000..c996894a59 --- /dev/null +++ b/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/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-template-revisions-controller.php new file mode 100644 index 0000000000..8d32ecb7c0 --- /dev/null +++ b/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/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index e9904ff234..53f8faa755 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -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; } diff --git a/wp-includes/version.php b/wp-includes/version.php index f4fc6ae597..ff83c5fa16 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -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. diff --git a/wp-settings.php b/wp-settings.php index 5b1e1205d2..38b03ecf72 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -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';