diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 9b248c145394..923b46b740de 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -503,6 +503,8 @@ function _build_block_template_result_from_post( $post ) { $has_theme_file = wp_get_theme()->get_stylesheet() === $theme && null !== _get_block_template_file( $post->post_type, $post->post_name ); + $origin = get_post_meta( $post->ID, 'origin', true ); + $template = new WP_Block_Template(); $template->wp_id = $post->ID; $template->id = $theme . '//' . $post->post_name; @@ -510,12 +512,14 @@ function _build_block_template_result_from_post( $post ) { $template->content = $post->post_content; $template->slug = $post->post_name; $template->source = 'custom'; + $template->origin = ! empty( $origin ) ? $origin : null; $template->type = $post->post_type; $template->description = $post->post_excerpt; $template->title = $post->post_title; $template->status = $post->post_status; $template->has_theme_file = $has_theme_file; $template->is_custom = true; + $template->author = $post->post_author; if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) { $template->is_custom = false; diff --git a/src/wp-includes/class-wp-block-template.php b/src/wp-includes/class-wp-block-template.php index 3ea090830f51..1d73d281fe76 100644 --- a/src/wp-includes/class-wp-block-template.php +++ b/src/wp-includes/class-wp-block-template.php @@ -77,6 +77,16 @@ class WP_Block_Template { */ public $source = 'theme'; + /** + * Origin of the content when the content has been customized. + * When customized, origin takes on the value of source and source becomes + * 'custom'. + * + * @since 5.9.0 + * @var string + */ + public $origin; + /** * Post Id. * @@ -109,4 +119,14 @@ class WP_Block_Template { * @var bool */ public $is_custom = true; + + /** + * Author. + * + * A value of 0 means no author. + * + * @since 5.9.0 + * @var int + */ + public $author; } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index a51d616416e0..f7fe8d74516e 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -384,6 +384,7 @@ function create_initial_post_types() { 'excerpt', 'editor', 'revisions', + 'author', ), ) ); @@ -442,6 +443,7 @@ function create_initial_post_types() { 'excerpt', 'editor', 'revisions', + 'author', ), ) ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 951c8232dde9..35434920036e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -244,6 +244,10 @@ public function update_item( $request ) { $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 ); } else { @@ -294,6 +298,11 @@ public function create_item_permissions_check( $request ) { */ public function create_item( $request ) { $prepared_post = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $prepared_post ) ) { + return $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 ) ) { @@ -422,6 +431,9 @@ protected function prepare_item_for_database( $request ) { $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; @@ -461,6 +473,24 @@ protected function prepare_item_for_database( $request ) { } } + 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; } @@ -510,6 +540,10 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $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; } @@ -547,6 +581,10 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $data['has_theme_file'] = (bool) $template->has_theme_file; } + 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; } @@ -695,6 +733,12 @@ public function get_item_schema() { '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' ), @@ -758,6 +802,11 @@ public function get_item_schema() { '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' ), + ), ), ); diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 3d1699e16f4f..734d7a45216e 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -53,7 +53,6 @@ public static function wpTearDownAfterClass() { wp_delete_post( self::$post->ID ); } - public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/templates', $routes ); @@ -93,6 +92,7 @@ public function test_get_items() { 'theme' => 'default', 'slug' => 'my_template', 'source' => 'custom', + 'origin' => null, 'type' => 'wp_template', 'description' => 'Description of my template.', 'title' => array( @@ -102,6 +102,7 @@ public function test_get_items() { 'status' => 'publish', 'wp_id' => self::$post->ID, 'has_theme_file' => false, + 'author' => 0, ), $this->find_and_normalize_template_by_id( $data, 'default//my_template' ) ); @@ -134,6 +135,7 @@ public function test_get_item() { 'theme' => 'default', 'slug' => 'my_template', 'source' => 'custom', + 'origin' => null, 'type' => 'wp_template', 'description' => 'Description of my template.', 'title' => array( @@ -143,6 +145,7 @@ public function test_get_item() { 'status' => 'publish', 'wp_id' => self::$post->ID, 'has_theme_file' => false, + 'author' => 0, ), $data ); @@ -161,6 +164,7 @@ public function test_create_item() { 'description' => 'Just a description', 'title' => 'My Template', 'content' => 'Content', + 'author' => self::$admin_id, ) ); $response = rest_get_server()->dispatch( $request ); @@ -177,6 +181,7 @@ public function test_create_item() { ), 'slug' => 'my_custom_template', 'source' => 'custom', + 'origin' => null, 'type' => 'wp_template', 'description' => 'Just a description', 'title' => array( @@ -185,6 +190,7 @@ public function test_create_item() { ), 'status' => 'publish', 'has_theme_file' => false, + 'author' => self::$admin_id, ), $data ); @@ -207,6 +213,7 @@ public function test_create_item_raw() { 'content' => array( 'raw' => 'Content', ), + 'author' => self::$admin_id, ) ); $response = rest_get_server()->dispatch( $request ); @@ -223,6 +230,7 @@ public function test_create_item_raw() { ), 'slug' => 'my_custom_template_raw', 'source' => 'custom', + 'origin' => null, 'type' => 'wp_template', 'description' => 'Just a description', 'title' => array( @@ -231,11 +239,28 @@ public function test_create_item_raw() { ), 'status' => 'publish', 'has_theme_file' => false, + 'author' => self::$admin_id, ), $data ); } + public function test_create_item_invalid_author() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template_invalid_author', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'author' => -1, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_author', $response, 400 ); + } + /** * @covers WP_REST_Templates_Controller::update_item */ @@ -370,19 +395,21 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 11, $properties ); + $this->assertCount( 13, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'slug', $properties ); $this->assertArrayHasKey( 'theme', $properties ); $this->assertArrayHasKey( 'type', $properties ); $this->assertArrayHasKey( 'source', $properties ); + $this->assertArrayHasKey( 'origin', $properties ); $this->assertArrayHasKey( 'content', $properties ); $this->assertArrayHasKey( 'title', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'status', $properties ); $this->assertArrayHasKey( 'wp_id', $properties ); $this->assertArrayHasKey( 'has_theme_file', $properties ); + $this->assertArrayHasKey( 'author', $properties ); } protected function find_and_normalize_template_by_id( $templates, $id ) { diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 3c6f4906af68..5d36b184abca 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5117,6 +5117,11 @@ mockedApiResponse.Schema = { "private" ], "required": false + }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false } } } @@ -5262,6 +5267,11 @@ mockedApiResponse.Schema = { "private" ], "required": false + }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false } } }, @@ -5571,6 +5581,11 @@ mockedApiResponse.Schema = { "private" ], "required": false + }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false } } } @@ -5750,6 +5765,11 @@ mockedApiResponse.Schema = { ], "required": false }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false + }, "area": { "description": "Where the template part is intended for use (header, footer, etc.)", "type": "string", @@ -5900,6 +5920,11 @@ mockedApiResponse.Schema = { ], "required": false }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false + }, "area": { "description": "Where the template part is intended for use (header, footer, etc.)", "type": "string", @@ -6214,6 +6239,11 @@ mockedApiResponse.Schema = { ], "required": false }, + "author": { + "description": "The ID for the author of the template.", + "type": "integer", + "required": false + }, "area": { "description": "Where the template part is intended for use (header, footer, etc.)", "type": "string",