diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php index dd72bc1c15210..b92df383bc51f 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php @@ -601,7 +601,7 @@ public function prepare_item_for_response( $item, $request ) { $data['meta'] = $this->meta->get_value( $menu_item->ID, $request ); } - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; @@ -907,7 +907,7 @@ public function get_item_schema() { 'readonly' => true, ); - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies( $schema['properties'] ); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index ee3e6b4959869..bfbb1c1b55adf 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -47,6 +47,18 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { */ protected $allow_batch = array( 'v1' => true ); + /** + * Taxonomies exposed as properties on the REST API item resource. + * + * The list is initialized while building the item schema so taxonomy REST + * bases that conflict with existing properties are excluded consistently + * from item reads, writes, permissions, and action links. + * + * @since 7.2.0 + * @var WP_Taxonomy[]|null + */ + protected $rest_taxonomies = null; + /** * Constructor. * @@ -1690,6 +1702,61 @@ public function handle_template( $template, $post_id, $validate = false ) { update_post_meta( $post_id, '_wp_page_template', $template ); } + /** + * Retrieves taxonomies exposed as properties on the REST API item resource. + * + * When schema properties are provided, taxonomy REST bases that conflict + * with an existing property are excluded and the resulting list is cached. + * Calling this method without schema properties never triggers schema + * generation, avoiding recursion in controllers that override + * get_item_schema(). + * + * @since 7.2.0 + * + * @param array|null $properties Existing item schema properties, or null to use the cached list. + * @return WP_Taxonomy[] Taxonomies available as item resource properties. + */ + protected function get_rest_taxonomies( $properties = null ) { + if ( null !== $this->rest_taxonomies ) { + return $this->rest_taxonomies; + } + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + if ( null === $properties ) { + return $taxonomies; + } + + $rest_taxonomies = array(); + + foreach ( $taxonomies as $taxonomy_name => $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + if ( array_key_exists( $base, $properties ) ) { + $taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name'; + _doing_it_wrong( + 'register_taxonomy', + sprintf( + /* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */ + __( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ), + $taxonomy->name, + $taxonomy_field_name_with_conflict, + $base + ), + '5.4.0' + ); + continue; + } + + $rest_taxonomies[ $taxonomy_name ] = $taxonomy; + $properties[ $base ] = true; + } + + $this->rest_taxonomies = $rest_taxonomies; + + return $this->rest_taxonomies; + } + /** * Updates the post's terms from a REST request. * @@ -1700,7 +1767,8 @@ public function handle_template( $template, $post_id, $validate = false ) { * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null. */ protected function handle_terms( $post_id, $request ) { - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $this->get_item_schema(); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; @@ -1728,7 +1796,8 @@ protected function handle_terms( $post_id, $request ) { * @return bool Whether the current user can assign the provided terms. */ protected function check_assign_terms_permission( $request ) { - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $this->get_item_schema(); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; @@ -2103,7 +2172,7 @@ public function prepare_item_for_response( $item, $request ) { $data['meta'] = $this->meta->get_value( $post->ID, $request ); } - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; @@ -2369,7 +2438,7 @@ protected function get_available_actions( $post, $request ) { } } - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $tax ) { $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; @@ -2759,26 +2828,11 @@ public function get_item_schema() { ), ); - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies( $schema['properties'] ); foreach ( $taxonomies as $taxonomy ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - if ( array_key_exists( $base, $schema['properties'] ) ) { - $taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name'; - _doing_it_wrong( - 'register_taxonomy', - sprintf( - /* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */ - __( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ), - $taxonomy->name, - $taxonomy_field_name_with_conflict, - $base - ), - '5.4.0' - ); - } - $schema['properties'][ $base ] = array( /* translators: %s: Taxonomy name. */ 'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ), @@ -2914,7 +2968,7 @@ protected function get_schema_links() { ); } - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + $taxonomies = $this->get_rest_taxonomies(); foreach ( $taxonomies as $tax ) { $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 212ddde70dd83..193e47642125d 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -4918,6 +4918,62 @@ public function test_get_item_schema_issues_doing_it_wrong_when_taxonomy_name_is $controller->register_routes(); } + /** + * @ticket 65855 + */ + public function test_get_item_schema_does_not_overwrite_existing_property_when_taxonomy_name_conflicts() { + $this->setExpectedIncorrectUsage( 'register_taxonomy' ); + + register_taxonomy( + 'type', + 'post', + array( + 'show_in_rest' => true, + ) + ); + + $controller = new WP_REST_Posts_Controller( 'post' ); + $schema = $controller->get_item_schema(); + + unregister_taxonomy( 'type' ); + + $this->assertSame( 'string', $schema['properties']['type']['type'] ); + $this->assertTrue( $schema['properties']['type']['readonly'] ); + } + + /** + * @ticket 65855 + */ + public function test_prepare_item_for_response_does_not_overwrite_existing_property_when_taxonomy_name_conflicts() { + $this->setExpectedIncorrectUsage( 'register_taxonomy' ); + + register_taxonomy( + 'type', + 'post', + array( + 'show_in_rest' => true, + ) + ); + + $controller = new WP_REST_Posts_Controller( 'post' ); + $request = new WP_REST_Request( + 'GET', + sprintf( '/wp/v2/posts/%d', self::$post_id ) + ); + $request->set_param( 'context', 'view' ); + $request->set_param( '_fields', 'type' ); + + $response = $controller->prepare_item_for_response( + get_post( self::$post_id ), + $request + ); + $data = $response->get_data(); + + unregister_taxonomy( 'type' ); + + $this->assertSame( 'post', $data['type'] ); + } + /** * @ticket 39805 */ diff --git a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php index a5b76af3438d2..663e998a7b117 100644 --- a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php +++ b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php @@ -786,6 +786,29 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'invalid', $properties ); } + /** + * @ticket 65855 + * @covers ::get_item_schema + */ + public function test_get_item_schema_does_not_overwrite_existing_property_when_taxonomy_name_conflicts() { + $this->setExpectedIncorrectUsage( 'register_taxonomy' ); + + register_taxonomy( + 'type', + 'nav_menu_item', + array( + 'show_in_rest' => true, + ) + ); + + $controller = new WP_REST_Menu_Items_Controller( 'nav_menu_item' ); + $schema = $controller->get_item_schema(); + + unregister_taxonomy( 'type' ); + + $this->assertSame( 'string', $schema['properties']['type']['type'] ); + } + /** * @ticket 40878 * @covers ::get_items_permissions_check