--- a/wp/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php Fri Sep 05 18:40:08 2025 +0200
@@ -42,6 +42,7 @@
* Registers the controllers routes.
*
* @since 5.8.0
+ * @since 6.1.0 Endpoint for fallback template content.
*/
public function register_routes() {
// Lists all templates.
@@ -65,6 +66,34 @@
)
);
+ // Get fallback template content.
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/lookup',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_template_fallback' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'slug' => array(
+ 'description' => __( 'The slug of the template to get the fallback for' ),
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ 'is_custom' => array(
+ 'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ),
+ 'type' => 'boolean',
+ ),
+ 'template_prefix' => array(
+ 'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ),
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ )
+ );
+
// Lists/updates a single template based on the given id.
register_rest_route(
$this->namespace,
@@ -72,11 +101,13 @@
sprintf(
'/%s/(?P<id>%s%s)',
$this->rest_base,
- // Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
- // Excludes invalid directory name characters: `/:<>*?"|`.
+ /*
+ * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
+ * Excludes invalid directory name characters: `/:<>*?"|`.
+ */
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
- '[\/\w-]+'
+ '[\/\w%-]+'
),
array(
'args' => array(
@@ -118,6 +149,29 @@
}
/**
+ * Returns the fallback template for the given slug.
+ *
+ * @since 6.1.0
+ * @since 6.3.0 Ignore empty templates.
+ *
+ * @param WP_REST_Request $request The request instance.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_template_fallback( $request ) {
+ $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );
+
+ do {
+ $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' );
+ array_shift( $hierarchy );
+ } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) );
+
+ // To maintain original behavior, return an empty object rather than a 404 error when no template is found.
+ $response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass();
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
* Checks if the user has permissions to make the request.
*
* @since 5.8.0
@@ -126,8 +180,10 @@
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
protected function permissions_check( $request ) {
- // Verify if the current user has edit_theme_options capability.
- // This capability is required to edit/view/delete templates.
+ /*
+ * Verify if the current user has edit_theme_options capability.
+ * This capability is required to edit/view/delete templates.
+ */
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_templates',
@@ -180,12 +236,28 @@
* Checks if a given request has access to read templates.
*
* @since 5.8.0
+ * @since 6.6.0 Allow users with edit_posts capability to read templates.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
- return $this->permissions_check( $request );
+ if ( current_user_can( 'edit_posts' ) ) {
+ return true;
+ }
+ foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
+ if ( current_user_can( $post_type->cap->edit_posts ) ) {
+ return true;
+ }
+ }
+
+ return new WP_Error(
+ 'rest_cannot_manage_templates',
+ __( 'Sorry, you are not allowed to access the templates on this site.' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
}
/**
@@ -221,12 +293,28 @@
* Checks if a given request has access to read a single template.
*
* @since 5.8.0
+ * @since 6.6.0 Allow users with edit_posts capability to read individual templates.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
- return $this->permissions_check( $request );
+ if ( current_user_can( 'edit_posts' ) ) {
+ return true;
+ }
+ foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
+ if ( current_user_can( $post_type->cap->edit_posts ) ) {
+ return true;
+ }
+ }
+
+ return new WP_Error(
+ 'rest_cannot_manage_templates',
+ __( 'Sorry, you are not allowed to access the templates on this site.' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
}
/**
@@ -451,8 +539,10 @@
);
}
- // (Note that internally this falls through to `wp_delete_post()`
- // if the Trash is disabled.)
+ /*
+ * (Note that internally this falls through to `wp_delete_post()`
+ * if the Trash is disabled.)
+ */
$result = wp_trash_post( $id );
$template->status = 'trash';
$response = $this->prepare_item_for_response( $template, $request );
@@ -475,7 +565,7 @@
* @since 5.8.0
*
* @param WP_REST_Request $request Request object.
- * @return stdClass Changes to pass to wp_update_post.
+ * @return stdClass|WP_Error Changes to pass to wp_update_post.
*/
protected function prepare_item_for_database( $request ) {
$template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
@@ -484,7 +574,7 @@
$changes->post_type = $this->post_type;
$changes->post_status = 'publish';
$changes->tax_input = array(
- 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : wp_get_theme()->get_stylesheet(),
+ 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
);
} elseif ( 'custom' !== $template->source ) {
$changes->post_name = $template->slug;
@@ -525,12 +615,21 @@
$changes->post_excerpt = $template->description;
}
+ if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) {
+ $changes->meta_input = wp_parse_args(
+ array(
+ 'is_wp_suggestion' => $request['is_wp_suggestion'],
+ ),
+ $changes->meta_input = array()
+ );
+ }
+
if ( 'wp_template_part' === $this->post_type ) {
if ( isset( $request['area'] ) ) {
$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
} elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
- } elseif ( ! $template->area ) {
+ } elseif ( empty( $template->area ) ) {
$changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
}
}
@@ -553,7 +652,8 @@
$changes->post_author = $post_author;
}
- return $changes;
+ /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
+ return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request );
}
/**
@@ -561,12 +661,19 @@
*
* @since 5.8.0
* @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support.
+ * @since 6.3.0 Added `modified` property to the response.
*
* @param WP_Block_Template $item Template instance.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
- public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ public function prepare_item_for_response( $item, $request ) {
+ // Resolve pattern blocks so they don't need to be resolved client-side
+ // in the editor, improving performance.
+ $blocks = parse_blocks( $item->content );
+ $blocks = resolve_pattern_blocks( $blocks );
+ $item->content = serialize_blocks( $blocks );
+
// Restores the more descriptive, specific name for use within this method.
$template = $item;
@@ -655,6 +762,18 @@
$data['area'] = $template->area;
}
+ if ( rest_is_field_included( 'modified', $fields ) ) {
+ $data['modified'] = mysql_to_rfc3339( $template->modified );
+ }
+
+ if ( rest_is_field_included( 'author_text', $fields ) ) {
+ $data['author_text'] = self::get_wp_templates_author_text_field( $template );
+ }
+
+ if ( rest_is_field_included( 'original_source', $fields ) ) {
+ $data['original_source'] = self::get_wp_templates_original_source_field( $template );
+ }
+
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
@@ -662,19 +781,98 @@
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
- $links = $this->prepare_links( $template->id );
- $response->add_links( $links );
- if ( ! empty( $links['self']['href'] ) ) {
- $actions = $this->get_available_actions();
- $self = $links['self']['href'];
- foreach ( $actions as $rel ) {
- $response->add_link( $rel, $self );
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = $this->prepare_links( $template->id );
+ $response->add_links( $links );
+ if ( ! empty( $links['self']['href'] ) ) {
+ $actions = $this->get_available_actions();
+ $self = $links['self']['href'];
+ foreach ( $actions as $rel ) {
+ $response->add_link( $rel, $self );
+ }
}
}
return $response;
}
+ /**
+ * Returns the source from where the template originally comes from.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Block_Template $template_object Template instance.
+ * @return string Original source of the template one of theme, plugin, site, or user.
+ */
+ private static function get_wp_templates_original_source_field( $template_object ) {
+ if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) {
+ // Added by theme.
+ // Template originally provided by a theme, but customized by a user.
+ // Templates originally didn't have the 'origin' field so identify
+ // older customized templates by checking for no origin and a 'theme'
+ // or 'custom' source.
+ if ( $template_object->has_theme_file &&
+ ( 'theme' === $template_object->origin || (
+ empty( $template_object->origin ) && in_array(
+ $template_object->source,
+ array(
+ 'theme',
+ 'custom',
+ ),
+ true
+ ) )
+ )
+ ) {
+ return 'theme';
+ }
+
+ // Added by plugin.
+ if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) {
+ return 'plugin';
+ }
+
+ // Added by site.
+ // Template was created from scratch, but has no author. Author support
+ // was only added to templates in WordPress 5.9. Fallback to showing the
+ // site logo and title.
+ if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) {
+ return 'site';
+ }
+ }
+
+ // Added by user.
+ return 'user';
+ }
+
+ /**
+ * Returns a human readable text for the author of the template.
+ *
+ * @since 6.5.0
+ *
+ * @param WP_Block_Template $template_object Template instance.
+ * @return string Human readable text for the author.
+ */
+ private static function get_wp_templates_author_text_field( $template_object ) {
+ $original_source = self::get_wp_templates_original_source_field( $template_object );
+ switch ( $original_source ) {
+ case 'theme':
+ $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' );
+ return empty( $theme_name ) ? $template_object->theme : $theme_name;
+ case 'plugin':
+ $plugins = get_plugins();
+ $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ];
+ return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name'];
+ case 'site':
+ return get_bloginfo( 'name' );
+ case 'user':
+ $author = get_user_by( 'id', $template_object->author );
+ if ( ! $author ) {
+ return __( 'Unknown author' );
+ }
+ return $author->get( 'display_name' );
+ }
+ }
+
/**
* Prepares links for the request.
@@ -685,20 +883,39 @@
* @return array Links for the given post.
*/
protected function prepare_links( $id ) {
- $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
-
$links = array(
'self' => array(
- 'href' => rest_url( trailingslashit( $base ) . $id ),
+ 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ),
),
'collection' => array(
- 'href' => rest_url( $base ),
+ 'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
),
'about' => array(
'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
),
);
+ 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;
}
@@ -769,43 +986,43 @@
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
- 'id' => array(
+ 'id' => array(
'description' => __( 'ID of template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'slug' => array(
+ 'slug' => array(
'description' => __( 'Unique slug identifying the template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'required' => true,
'minLength' => 1,
- 'pattern' => '[a-zA-Z0-9_\-]+',
+ 'pattern' => '[a-zA-Z0-9_\%-]+',
),
- 'theme' => array(
+ 'theme' => array(
'description' => __( 'Theme identifier for the template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
- 'type' => array(
+ 'type' => array(
'description' => __( 'Type of template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
- 'source' => array(
+ 'source' => array(
'description' => __( 'Source of template' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'origin' => array(
+ 'origin' => array(
'description' => __( 'Source of a customized template' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'content' => array(
+ 'content' => array(
'description' => __( 'Content of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
@@ -824,7 +1041,7 @@
),
),
),
- 'title' => array(
+ 'title' => array(
'description' => __( 'Title of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
@@ -843,36 +1060,61 @@
),
),
),
- 'description' => array(
+ 'description' => array(
'description' => __( 'Description of template.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
),
- 'status' => array(
+ 'status' => array(
'description' => __( 'Status of template.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'default' => 'publish',
'context' => array( 'embed', 'view', 'edit' ),
),
- 'wp_id' => array(
+ 'wp_id' => array(
'description' => __( 'Post ID.' ),
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'has_theme_file' => array(
+ 'has_theme_file' => array(
'description' => __( 'Theme file exists.' ),
'type' => 'bool',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'author' => array(
+ 'author' => array(
'description' => __( 'The ID for the author of the template.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
+ 'modified' => array(
+ 'description' => __( "The date the template was last modified, in the site's timezone." ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'author_text' => array(
+ 'type' => 'string',
+ 'description' => __( 'Human readable text for the author.' ),
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'original_source' => array(
+ 'description' => __( 'Where the template originally comes from e.g. \'theme\'' ),
+ 'type' => 'string',
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'enum' => array(
+ 'theme',
+ 'plugin',
+ 'site',
+ 'user',
+ ),
+ ),
),
);