diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php index 9db7403809..21c954ad05 100644 --- a/src/wp-includes/class-wp-taxonomy.php +++ b/src/wp-includes/class-wp-taxonomy.php @@ -199,6 +199,14 @@ final class WP_Taxonomy { */ public $rest_base; + /** + * The namespace for this taxonomy's REST API endpoints. + * + * @since 5.9 + * @var string|bool $rest_namespace + */ + public $rest_namespace; + /** * The controller for this taxonomy's REST API endpoints. * @@ -319,6 +327,7 @@ final class WP_Taxonomy { 'update_count_callback' => '', 'show_in_rest' => false, 'rest_base' => false, + 'rest_namespace' => false, 'rest_controller_class' => false, 'default_term' => null, 'sort' => null, @@ -384,6 +393,11 @@ final class WP_Taxonomy { $args['show_in_quick_edit'] = $args['show_ui']; } + // If not set, default rest_namespace to wp/v2 if show_in_rest is true. + if ( false === $args['rest_namespace'] && ! empty( $args['show_in_rest'] ) ) { + $args['rest_namespace'] = 'wp/v2'; + } + $default_caps = array( 'manage_terms' => 'manage_categories', 'edit_terms' => 'manage_categories', diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a30a246d95..03860a915c 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -3115,24 +3115,12 @@ function rest_get_route_for_term( $term ) { return ''; } - $taxonomy = get_taxonomy( $term->taxonomy ); - if ( ! $taxonomy ) { + $taxonomy_route = rest_get_route_for_taxonomy_items( $term->taxonomy ); + if ( ! $taxonomy_route ) { return ''; } - $controller = $taxonomy->get_rest_controller(); - if ( ! $controller ) { - return ''; - } - - $route = ''; - - // The only controller that works is the Terms controller. - if ( $controller instanceof WP_REST_Terms_Controller ) { - $namespace = 'wp/v2'; - $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id ); - } + $route = sprintf( '%s/%d', $taxonomy_route, $term->term_id ); /** * Filters the REST API route for a term. @@ -3145,6 +3133,39 @@ function rest_get_route_for_term( $term ) { return apply_filters( 'rest_route_for_term', $route, $term ); } +/** + * Gets the REST API route for a taxonomy. + * + * @since 5.9.0 + * + * @param string $taxonomy Name of taxonomy. + * @return string The route path with a leading slash for the given taxonomy. + */ +function rest_get_route_for_taxonomy_items( $taxonomy ) { + $taxonomy = get_taxonomy( $taxonomy ); + if ( ! $taxonomy ) { + return ''; + } + + if ( ! $taxonomy->show_in_rest ) { + return ''; + } + + $namespace = ! empty( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2'; + $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $route = sprintf( '/%s/%s', $namespace, $rest_base ); + + /** + * Filters the REST API route for a taxonomy. + * + * @since 5.9.0 + * + * @param string $route The route path. + * @param WP_Taxonomy $taxonomy The taxonomy object. + */ + return apply_filters( 'rest_route_for_taxonomy_items', $route, $taxonomy ); +} + /** * Gets the REST route for the currently queried object. * 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 0cc548b0b6..ffb4466892 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 @@ -2060,19 +2060,16 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { $links['https://api.w.org/term'] = array(); foreach ( $taxonomies as $tax ) { - $taxonomy_obj = get_taxonomy( $tax ); + $taxonomy_route = rest_get_route_for_taxonomy_items( $tax ); // Skip taxonomies that are not public. - if ( empty( $taxonomy_obj->show_in_rest ) ) { + if ( empty( $taxonomy_route ) ) { continue; } - - $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax; - $terms_url = add_query_arg( 'post', $post->ID, - rest_url( 'wp/v2/' . $tax_base ) + rest_url( $taxonomy_route ) ); $links['https://api.w.org/term'][] = array( diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php index 9d43ca2874..d05abcbcea 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php @@ -250,6 +250,10 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller { $data['rest_base'] = $base; } + if ( in_array( 'rest_namespace', $fields, true ) ) { + $data['rest_namespace'] = $taxonomy->rest_namespace; + } + if ( in_array( 'visibility', $fields, true ) ) { $data['visibility'] = array( 'public' => (bool) $taxonomy->public, @@ -274,7 +278,7 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller { 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), 'https://api.w.org/items' => array( - 'href' => rest_url( sprintf( 'wp/v2/%s', $base ) ), + 'href' => rest_url( rest_get_route_for_taxonomy_items( $taxonomy->name ) ), ), ) ); @@ -310,49 +314,49 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller { 'title' => 'taxonomy', 'type' => 'object', 'properties' => array( - 'capabilities' => array( + 'capabilities' => array( 'description' => __( 'All capabilities used by the taxonomy.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'description' => array( + 'description' => array( 'description' => __( 'A human-readable description of the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'hierarchical' => array( + 'hierarchical' => array( 'description' => __( 'Whether or not the taxonomy should have children.' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'labels' => array( + 'labels' => array( 'description' => __( 'Human-readable labels for the taxonomy for various contexts.' ), 'type' => 'object', 'context' => array( 'edit' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'The title for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'slug' => array( + 'slug' => array( 'description' => __( 'An alphanumeric identifier for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'show_cloud' => array( + 'show_cloud' => array( 'description' => __( 'Whether or not the term cloud should be displayed.' ), 'type' => 'boolean', 'context' => array( 'edit' ), 'readonly' => true, ), - 'types' => array( + 'types' => array( 'description' => __( 'Types associated with the taxonomy.' ), 'type' => 'array', 'items' => array( @@ -361,13 +365,19 @@ class WP_REST_Taxonomies_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'rest_base' => array( + 'rest_base' => array( 'description' => __( 'REST base route for the taxonomy.' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'visibility' => array( + 'rest_namespace' => array( + 'description' => __( 'REST namespace route for the taxonomy.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'visibility' => array( 'description' => __( 'The visibility settings for the taxonomy.' ), 'type' => 'object', 'context' => array( 'edit' ), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 183b7c7ffa..3a92b774f0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -57,9 +57,9 @@ class WP_REST_Terms_Controller extends WP_REST_Controller { */ public function __construct( $taxonomy ) { $this->taxonomy = $taxonomy; - $this->namespace = 'wp/v2'; $tax_obj = get_taxonomy( $taxonomy ); $this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name; + $this->namespace = ! empty( $tax_obj->rest_namespace ) ? $tax_obj->rest_namespace : 'wp/v2'; $this->meta = new WP_REST_Term_Meta_Fields( $taxonomy ); } diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index f81af0351e..0273a4aac7 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -387,6 +387,7 @@ function is_taxonomy_hierarchical( $taxonomy ) { * @type bool $show_in_rest Whether to include the taxonomy in the REST API. Set this to true * for the taxonomy to be available in the block editor. * @type string $rest_base To change the base url of REST API route. Default is $taxonomy. + * @type string $rest_namespace To change the namespace URL of REST API route. Default is wp/v2. * @type string $rest_controller_class REST API Controller class name. Default is 'WP_REST_Terms_Controller'. * @type bool $show_tagcloud Whether to list the taxonomy in the Tag Cloud Widget controls. If not set, * the default is inherited from `$show_ui` (default true). diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 5770f68841..34b414903d 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -1962,6 +1962,50 @@ class Tests_REST_API extends WP_UnitTestCase { $this->assertSame( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) ); } + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_custom_namespace() { + register_taxonomy( + 'ct', + 'post', + array( + 'show_in_rest' => true, + 'rest_base' => 'ct', + 'rest_namespace' => 'wordpress/v1', + ) + ); + $term = self::factory()->term->create_and_get( array( 'taxonomy' => 'ct' ) ); + + $this->assertSame( '/wordpress/v1/ct/' . $term->term_id, rest_get_route_for_term( $term ) ); + unregister_taxonomy( 'ct' ); + } + + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_items() { + $this->assertSame( '/wp/v2/categories', rest_get_route_for_taxonomy_items( 'category' ) ); + } + + /** + * @ticket 54267 + */ + public function test_rest_get_route_for_taxonomy_items_custom_namespace() { + register_taxonomy( + 'ct', + 'post', + array( + 'show_in_rest' => true, + 'rest_base' => 'ct', + 'rest_namespace' => 'wordpress/v1', + ) + ); + + $this->assertSame( '/wordpress/v1/ct', rest_get_route_for_taxonomy_items( 'ct' ) ); + unregister_post_type( 'ct' ); + } + /** * @ticket 50300 * diff --git a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php index 357e1c7e17..17be251c1d 100644 --- a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php +++ b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php @@ -219,7 +219,7 @@ class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcas $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 10, $properties ); + $this->assertCount( 11, $properties ); $this->assertArrayHasKey( 'capabilities', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'hierarchical', $properties ); @@ -230,6 +230,7 @@ class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcas $this->assertArrayHasKey( 'types', $properties ); $this->assertArrayHasKey( 'visibility', $properties ); $this->assertArrayHasKey( 'rest_base', $properties ); + $this->assertArrayHasKey( 'rest_namespace', $properties ); } /** @@ -252,6 +253,7 @@ class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcas $this->assertSame( $tax_obj->description, $data['description'] ); $this->assertSame( $tax_obj->hierarchical, $data['hierarchical'] ); $this->assertSame( $tax_obj->rest_base, $data['rest_base'] ); + $this->assertSame( $tax_obj->rest_namespace, $data['rest_namespace'] ); $this->assertSame( rest_url( 'wp/v2/taxonomies' ), $links['collection'][0]['href'] ); $this->assertArrayHasKey( 'https://api.w.org/items', $links ); if ( 'edit' === $context ) { diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 63b70dd2c8..7852fd34af 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -8589,6 +8589,7 @@ mockedApiResponse.TaxonomiesCollection = { ], "hierarchical": true, "rest_base": "categories", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8618,6 +8619,7 @@ mockedApiResponse.TaxonomiesCollection = { ], "hierarchical": false, "rest_base": "tags", + "rest_namespace": "wp/v2", "_links": { "collection": [ { @@ -8648,7 +8650,8 @@ mockedApiResponse.TaxonomyModel = { "post" ], "hierarchical": true, - "rest_base": "categories" + "rest_base": "categories", + "rest_namespace": "wp/v2" }; mockedApiResponse.CategoriesCollection = [