--- a/wp/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php Wed Sep 21 18:19:35 2022 +0200
+++ b/wp/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php Tue Sep 27 16:37:53 2022 +0200
@@ -33,9 +33,9 @@
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
- $this->namespace = 'wp/v2';
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
+ $this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
}
/**
@@ -68,17 +68,30 @@
// Lists/updates a single template based on the given id.
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P<id>[\/\w-]+)',
+ // The route.
+ 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 the template name.
+ '[\/\w-]+'
+ ),
array(
+ 'args' => array(
+ 'id' => array(
+ 'description' => __( 'The id of a template' ),
+ 'type' => 'string',
+ 'sanitize_callback' => array( $this, '_sanitize_template_id' ),
+ ),
+ ),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
- 'id' => array(
- 'description' => __( 'The id of a template' ),
- 'type' => 'string',
- ),
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
@@ -129,6 +142,41 @@
}
/**
+ * Requesting this endpoint for a template like 'twentytwentytwo//home'
+ * requires using a path like /wp/v2/templates/twentytwentytwo//home. There
+ * are special cases when WordPress routing corrects the name to contain
+ * only a single slash like 'twentytwentytwo/home'.
+ *
+ * This method doubles the last slash if it's not already doubled. It relies
+ * on the template ID format {theme_name}//{template_slug} and the fact that
+ * slugs cannot contain slashes.
+ *
+ * @since 5.9.0
+ * @see https://core.trac.wordpress.org/ticket/54507
+ *
+ * @param string $id Template ID.
+ * @return string Sanitized template ID.
+ */
+ public function _sanitize_template_id( $id ) {
+ $id = urldecode( $id );
+
+ $last_slash_pos = strrpos( $id, '/' );
+ if ( false === $last_slash_pos ) {
+ return $id;
+ }
+
+ $is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
+ if ( $is_double_slashed ) {
+ return $id;
+ }
+ return (
+ substr( $id, 0, $last_slash_pos )
+ . '/'
+ . substr( $id, $last_slash_pos )
+ );
+ }
+
+ /**
* Checks if a given request has access to read templates.
*
* @since 5.8.0
@@ -156,6 +204,9 @@
if ( isset( $request['area'] ) ) {
$query['area'] = $request['area'];
}
+ if ( isset( $request['post_type'] ) ) {
+ $query['post_type'] = $request['post_type'];
+ }
$templates = array();
foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
@@ -187,7 +238,11 @@
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
- $template = get_block_template( $request['id'], $this->post_type );
+ if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
+ $template = get_block_file_template( $request['id'], $this->post_type );
+ } else {
+ $template = get_block_template( $request['id'], $this->post_type );
+ }
if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
@@ -222,14 +277,39 @@
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}
+ $post_before = get_post( $template->wp_id );
+
+ if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
+ wp_delete_post( $template->wp_id, true );
+ $request->set_param( 'context', 'edit' );
+
+ $template = get_block_template( $request['id'], $this->post_type );
+ $response = $this->prepare_item_for_response( $template, $request );
+
+ return rest_ensure_response( $response );
+ }
+
$changes = $this->prepare_item_for_database( $request );
+ if ( is_wp_error( $changes ) ) {
+ return $changes;
+ }
+
if ( 'custom' === $template->source ) {
- $result = wp_update_post( wp_slash( (array) $changes ), true );
+ $update = true;
+ $result = wp_update_post( wp_slash( (array) $changes ), false );
} else {
- $result = wp_insert_post( wp_slash( (array) $changes ), true );
+ $update = false;
+ $post_before = null;
+ $result = wp_insert_post( wp_slash( (array) $changes ), false );
}
+
if ( is_wp_error( $result ) ) {
+ if ( 'db_update_error' === $result->get_error_code() ) {
+ $result->add_data( array( 'status' => 500 ) );
+ } else {
+ $result->add_data( array( 'status' => 400 ) );
+ }
return $result;
}
@@ -239,10 +319,17 @@
return $fields_update;
}
- return $this->prepare_item_for_response(
- get_block_template( $request['id'], $this->post_type ),
- $request
- );
+ $request->set_param( 'context', 'edit' );
+
+ $post = get_post( $template->wp_id );
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
+ do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
+
+ wp_after_insert_post( $post, $update, $post_before );
+
+ $response = $this->prepare_item_for_response( $template, $request );
+
+ return rest_ensure_response( $response );
}
/**
@@ -266,27 +353,47 @@
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
- $changes = $this->prepare_item_for_database( $request );
- $changes->post_name = $request['slug'];
- $result = wp_insert_post( wp_slash( (array) $changes ), true );
- if ( is_wp_error( $result ) ) {
- return $result;
+ $prepared_post = $this->prepare_item_for_database( $request );
+
+ if ( is_wp_error( $prepared_post ) ) {
+ return $prepared_post;
}
- $posts = get_block_templates( array( 'wp_id' => $result ), $this->post_type );
+
+ $prepared_post->post_name = $request['slug'];
+ $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
+ if ( is_wp_error( $post_id ) ) {
+ if ( 'db_insert_error' === $post_id->get_error_code() ) {
+ $post_id->add_data( array( 'status' => 500 ) );
+ } else {
+ $post_id->add_data( array( 'status' => 400 ) );
+ }
+
+ return $post_id;
+ }
+ $posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type );
if ( ! count( $posts ) ) {
- return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ) );
+ return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) );
}
$id = $posts[0]->id;
+ $post = get_post( $post_id );
$template = get_block_template( $id, $this->post_type );
$fields_update = $this->update_additional_fields_for_object( $template, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
- return $this->prepare_item_for_response(
- get_block_template( $id, $this->post_type ),
- $request
- );
+ /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
+ do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
+
+ wp_after_insert_post( $post, false, null );
+
+ $response = $this->prepare_item_for_response( $template, $request );
+ $response = rest_ensure_response( $response );
+
+ $response->set_status( 201 );
+ $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) );
+
+ return $response;
}
/**
@@ -321,10 +428,12 @@
$id = $template->wp_id;
$force = (bool) $request['force'];
+ $request->set_param( 'context', 'edit' );
+
// If we're forcing, then delete permanently.
if ( $force ) {
$previous = $this->prepare_item_for_response( $template, $request );
- wp_delete_post( $id, true );
+ $result = wp_delete_post( $id, true );
$response = new WP_REST_Response();
$response->set_data(
array(
@@ -332,22 +441,32 @@
'previous' => $previous->get_data(),
)
);
+ } else {
+ // Otherwise, only trash if we haven't already.
+ if ( 'trash' === $template->status ) {
+ return new WP_Error(
+ 'rest_template_already_trashed',
+ __( 'The template has already been deleted.' ),
+ array( 'status' => 410 )
+ );
+ }
- return $response;
+ // (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 );
}
- // Otherwise, only trash if we haven't already.
- if ( 'trash' === $template->status ) {
+ if ( ! $result ) {
return new WP_Error(
- 'rest_template_already_trashed',
- __( 'The template has already been deleted.' ),
- array( 'status' => 410 )
+ 'rest_cannot_delete',
+ __( 'The template cannot be deleted.' ),
+ array( 'status' => 500 )
);
}
- wp_trash_post( $id );
- $template->status = 'trash';
- return $this->prepare_item_for_response( $template, $request );
+ return $response;
}
/**
@@ -374,18 +493,29 @@
$changes->tax_input = array(
'wp_theme' => $template->theme,
);
+ $changes->meta_input = array(
+ 'origin' => $template->source,
+ );
} else {
$changes->post_name = $template->slug;
$changes->ID = $template->wp_id;
$changes->post_status = 'publish';
}
if ( isset( $request['content'] ) ) {
- $changes->post_content = $request['content'];
+ if ( is_string( $request['content'] ) ) {
+ $changes->post_content = $request['content'];
+ } elseif ( isset( $request['content']['raw'] ) ) {
+ $changes->post_content = $request['content']['raw'];
+ }
} elseif ( null !== $template && 'custom' !== $template->source ) {
$changes->post_content = $template->content;
}
if ( isset( $request['title'] ) ) {
- $changes->post_title = $request['title'];
+ if ( is_string( $request['title'] ) ) {
+ $changes->post_title = $request['title'];
+ } elseif ( ! empty( $request['title']['raw'] ) ) {
+ $changes->post_title = $request['title']['raw'];
+ }
} elseif ( null !== $template && 'custom' !== $template->source ) {
$changes->post_title = $template->title;
}
@@ -395,6 +525,34 @@
$changes->post_excerpt = $template->description;
}
+ 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 ) {
+ $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
+ }
+ }
+
+ if ( ! empty( $request['author'] ) ) {
+ $post_author = (int) $request['author'];
+
+ if ( get_current_user_id() !== $post_author ) {
+ $user_obj = get_userdata( $post_author );
+
+ if ( ! $user_obj ) {
+ return new WP_Error(
+ 'rest_invalid_author',
+ __( 'Invalid author ID.' ),
+ array( 'status' => 400 )
+ );
+ }
+ }
+
+ $changes->post_author = $post_author;
+ }
+
return $changes;
}
@@ -402,37 +560,109 @@
* Prepare a single template output for response
*
* @since 5.8.0
+ * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support.
*
- * @param WP_Block_Template $template Template instance.
+ * @param WP_Block_Template $item Template instance.
* @param WP_REST_Request $request Request object.
- * @return WP_REST_Response $data
+ * @return WP_REST_Response Response object.
*/
- public function prepare_item_for_response( $template, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
- $result = array(
- 'id' => $template->id,
- 'theme' => $template->theme,
- 'content' => array( 'raw' => $template->content ),
- 'slug' => $template->slug,
- 'source' => $template->source,
- 'type' => $template->type,
- 'description' => $template->description,
- 'title' => array(
- 'raw' => $template->title,
- 'rendered' => $template->title,
- ),
- 'status' => $template->status,
- 'wp_id' => $template->wp_id,
- 'has_theme_file' => $template->has_theme_file,
- );
+ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ // Restores the more descriptive, specific name for use within this method.
+ $template = $item;
+
+ $fields = $this->get_fields_for_response( $request );
+
+ // Base fields for every template.
+ $data = array();
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $template->id;
+ }
+
+ if ( rest_is_field_included( 'theme', $fields ) ) {
+ $data['theme'] = $template->theme;
+ }
- if ( 'wp_template_part' === $template->type ) {
- $result['area'] = $template->area;
+ if ( rest_is_field_included( 'content', $fields ) ) {
+ $data['content'] = array();
+ }
+ if ( rest_is_field_included( 'content.raw', $fields ) ) {
+ $data['content']['raw'] = $template->content;
+ }
+
+ if ( rest_is_field_included( 'content.block_version', $fields ) ) {
+ $data['content']['block_version'] = block_version( $template->content );
+ }
+
+ if ( rest_is_field_included( 'slug', $fields ) ) {
+ $data['slug'] = $template->slug;
+ }
+
+ if ( rest_is_field_included( 'source', $fields ) ) {
+ $data['source'] = $template->source;
+ }
+
+ if ( rest_is_field_included( 'origin', $fields ) ) {
+ $data['origin'] = $template->origin;
+ }
+
+ if ( rest_is_field_included( 'type', $fields ) ) {
+ $data['type'] = $template->type;
}
- $result = $this->add_additional_fields_to_object( $result, $request );
+ if ( rest_is_field_included( 'description', $fields ) ) {
+ $data['description'] = $template->description;
+ }
+
+ if ( rest_is_field_included( 'title', $fields ) ) {
+ $data['title'] = array();
+ }
+
+ if ( rest_is_field_included( 'title.raw', $fields ) ) {
+ $data['title']['raw'] = $template->title;
+ }
+
+ if ( rest_is_field_included( 'title.rendered', $fields ) ) {
+ if ( $template->wp_id ) {
+ /** This filter is documented in wp-includes/post-template.php */
+ $data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id );
+ } else {
+ $data['title']['rendered'] = $template->title;
+ }
+ }
+
+ if ( rest_is_field_included( 'status', $fields ) ) {
+ $data['status'] = $template->status;
+ }
- $response = rest_ensure_response( $result );
- $links = $this->prepare_links( $template->id );
+ if ( rest_is_field_included( 'wp_id', $fields ) ) {
+ $data['wp_id'] = (int) $template->wp_id;
+ }
+
+ if ( rest_is_field_included( 'has_theme_file', $fields ) ) {
+ $data['has_theme_file'] = (bool) $template->has_theme_file;
+ }
+
+ if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) {
+ $data['is_custom'] = $template->is_custom;
+ }
+
+ if ( rest_is_field_included( 'author', $fields ) ) {
+ $data['author'] = (int) $template->author;
+ }
+
+ if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) {
+ $data['area'] = $template->area;
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ // 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();
@@ -477,7 +707,7 @@
*
* @since 5.8.0
*
- * @return array List of link relations.
+ * @return string[] List of link relations.
*/
protected function get_available_actions() {
$rels = array();
@@ -499,16 +729,25 @@
* Retrieves the query params for the posts collection.
*
* @since 5.8.0
+ * @since 5.9.0 Added `'area'` and `'post_type'`.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
- 'context' => $this->get_context_param(),
- 'wp_id' => array(
+ 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+ 'wp_id' => array(
'description' => __( 'Limit to the specified post id.' ),
'type' => 'integer',
),
+ 'area' => array(
+ 'description' => __( 'Limit to the specified template part area.' ),
+ 'type' => 'string',
+ ),
+ 'post_type' => array(
+ 'description' => __( 'Post type to get the templates for.' ),
+ 'type' => 'string',
+ ),
);
}
@@ -516,6 +755,7 @@
* Retrieves the block type' schema, conforming to JSON Schema.
*
* @since 5.8.0
+ * @since 5.9.0 Added `'area'`.
*
* @return array Item schema data.
*/
@@ -541,30 +781,67 @@
'context' => array( 'embed', 'view', 'edit' ),
'required' => true,
'minLength' => 1,
- 'pattern' => '[a-zA-Z_\-]+',
+ 'pattern' => '[a-zA-Z0-9_\-]+',
),
'theme' => array(
'description' => __( 'Theme identifier for the template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
+ 'type' => array(
+ 'description' => __( 'Type of template.' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
'source' => array(
'description' => __( 'Source of template' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
+ 'origin' => array(
+ 'description' => __( 'Source of a customized template' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
'content' => array(
'description' => __( 'Content of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
+ 'properties' => array(
+ 'raw' => array(
+ 'description' => __( 'Content for the template, as it exists in the database.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'block_version' => array(
+ 'description' => __( 'Version of the content block format used by the template.' ),
+ 'type' => 'integer',
+ 'context' => array( 'edit' ),
+ 'readonly' => true,
+ ),
+ ),
),
'title' => array(
'description' => __( 'Title of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
+ 'properties' => array(
+ 'raw' => array(
+ 'description' => __( 'Title for the template, as it exists in the database.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'rendered' => array(
+ 'description' => __( 'HTML title for the template, transformed for display.' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ ),
),
'description' => array(
'description' => __( 'Description of template.' ),
@@ -575,6 +852,7 @@
'status' => array(
'description' => __( 'Status of template.' ),
'type' => 'string',
+ 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'default' => 'publish',
'context' => array( 'embed', 'view', 'edit' ),
),
@@ -590,9 +868,31 @@
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
+ 'author' => array(
+ 'description' => __( 'The ID for the author of the template.' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
),
);
+ if ( 'wp_template' === $this->post_type ) {
+ $schema['properties']['is_custom'] = array(
+ 'description' => __( 'Whether a template is a custom template.' ),
+ 'type' => 'bool',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ );
+ }
+
+ if ( 'wp_template_part' === $this->post_type ) {
+ $schema['properties']['area'] = array(
+ 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ );
+ }
+
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );