wp/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
--- 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',
+					),
+				),
 			),
 		);