: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* REST API: WP_REST_Templates_Controller class
* Base Templates REST API Controller.
* @see WP_REST_Controller
class WP_REST_Templates_Controller extends WP_REST_Controller {
* @param string $post_type Post type.
public function __construct( $post_type ) {
$this->post_type = $post_type;
$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';
* Registers the controllers routes.
* @since 6.1.0 Endpoint for fallback template content.
public function register_routes() {
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
'schema' => array( $this, 'get_public_item_schema' ),
// Get fallback template content.
'/' . $this->rest_base . '/lookup',
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_template_fallback' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'description' => __( 'The slug of the template to get the fallback for' ),
'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ),
'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`' ),
// Lists/updates a single template based on the given id.
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'description' => __( 'The id of a template' ),
'sanitize_callback' => array( $this, '_sanitize_template_id' ),
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'description' => __( 'Whether to bypass Trash and force deletion.' ),
'schema' => array( $this, 'get_public_item_schema' ),
* Returns the fallback template for the given slug.
* @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'] );
$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.
* @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.
protected function permissions_check( $request ) {
* 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' ) ) {
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.' ),
'status' => rest_authorization_required_code(),
* 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.
* @see https://core.trac.wordpress.org/ticket/54507
* @param string $id Template ID.
* @return string Sanitized template ID.
public function _sanitize_template_id( $id ) {
$last_slash_pos = strrpos( $id, '/' );
if ( false === $last_slash_pos ) {
$is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
if ( $is_double_slashed ) {
substr( $id, 0, $last_slash_pos )
. substr( $id, $last_slash_pos )
* Checks if a given request has access to read templates.
* @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 ) {
if ( current_user_can( 'edit_posts' ) ) {
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.' ),
'status' => rest_authorization_required_code(),
* Returns a list of templates.
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response
public function get_items( $request ) {
if ( isset( $request['wp_id'] ) ) {
$query['wp_id'] = $request['wp_id'];
if ( isset( $request['area'] ) ) {
$query['area'] = $request['area'];
if ( isset( $request['post_type'] ) ) {
$query['post_type'] = $request['post_type'];
foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
$data = $this->prepare_item_for_response( $template, $request );
$templates[] = $this->prepare_response_for_collection( $data );
return rest_ensure_response( $templates );
* Checks if a given request has access to read a single template.
* @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 ) {
if ( current_user_can( 'edit_posts' ) ) {
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.' ),
'status' => rest_authorization_required_code(),
* Returns the given template
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
public function get_item( $request ) {
if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
$template = get_block_file_template( $request['id'], $this->post_type );
$template = get_block_template( $request['id'], $this->post_type );
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
return $this->prepare_item_for_response( $template, $request );
* Checks if a given request has access to write a single template.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
public function update_item_permissions_check( $request ) {
return $this->permissions_check( $request );
* Updates a single template.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
public function update_item( $request ) {
$template = get_block_template( $request['id'], $this->post_type );
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 ) ) {
if ( 'custom' === $template->source ) {
$result = wp_update_post( wp_slash( (array) $changes ), false );
$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 ) );
$result->add_data( array( 'status' => 400 ) );
$template = get_block_template( $request['id'], $this->post_type );
$fields_update = $this->update_additional_fields_for_object( $template, $request );
if ( is_wp_error( $fields_update ) ) {
$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 );
* Checks if a given request has access to create a template.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
public function create_item_permissions_check( $request ) {
return $this->permissions_check( $request );
* Creates a single template.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
public function create_item( $request ) {
$prepared_post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_post ) ) {
$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 ) );
$post_id->add_data( array( 'status' => 400 ) );
$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.' ), array( 'status' => 400 ) );
$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 ) ) {
/** 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 ) ) );
* Checks if a given request has access to delete a single template.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
public function delete_item_permissions_check( $request ) {
return $this->permissions_check( $request );
* Deletes a single template.