REST API: Link to the REST route for the currently queried resource.

This allows for programatically determining the REST version of the current page. The links also aid human discovery of the REST API in general.

Props dshanske, tfrommen, TimothyBlynJacobs.
Fixes #49116.


git-svn-id: https://develop.svn.wordpress.org/trunk@48273 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Timothy Jacobs 2020-07-02 05:55:04 +00:00
parent 284b92b122
commit 6cc0063ba6
4 changed files with 316 additions and 8 deletions

View File

@ -209,6 +209,16 @@ final class WP_Taxonomy {
*/
public $rest_controller_class;
/**
* The controller instance for this taxonomy's REST API endpoints.
*
* Lazily computed. Should be accessed using {@see WP_Taxonomy::get_rest_controller()}.
*
* @since 5.5.0
* @var WP_REST_Controller $rest_controller
*/
public $rest_controller;
/**
* Whether it is a built-in taxonomy.
*
@ -452,4 +462,40 @@ final class WP_Taxonomy {
public function remove_hooks() {
remove_filter( 'wp_ajax_add-' . $this->name, '_wp_ajax_add_hierarchical_term' );
}
/**
* Gets the REST API controller for this taxonomy.
*
* Will only instantiate the controller class once per request.
*
* @since 5.5.0
*
* @return WP_REST_Controller|null The controller instance, or null if the taxonomy
* is set not to show in rest.
*/
public function get_rest_controller() {
if ( ! $this->show_in_rest ) {
return null;
}
$class = $this->rest_controller_class ? $this->rest_controller_class : WP_REST_Terms_Controller::class;
if ( ! class_exists( $class ) ) {
return null;
}
if ( ! is_subclass_of( $class, WP_REST_Controller::class ) ) {
return null;
}
if ( ! $this->rest_controller ) {
$this->rest_controller = new $class( $this->name );
}
if ( ! ( $this->rest_controller instanceof $class ) ) {
return null;
}
return $this->rest_controller;
}
}

View File

@ -234,13 +234,9 @@ function create_initial_rest_routes() {
// Terms.
foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
$class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
$controller = $taxonomy->get_rest_controller();
if ( ! class_exists( $class ) ) {
continue;
}
$controller = new $class( $taxonomy->name );
if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
if ( ! $controller ) {
continue;
}
@ -873,7 +869,13 @@ function rest_output_link_wp_head() {
return;
}
echo "<link rel='https://api.w.org/' href='" . esc_url( $api_root ) . "' />\n";
printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) );
$resource = rest_get_queried_resource_route();
if ( $resource ) {
printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) );
}
}
/**
@ -892,7 +894,13 @@ function rest_output_link_header() {
return;
}
header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false );
header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false );
$resource = rest_get_queried_resource_route();
if ( $resource ) {
header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false );
}
}
/**
@ -1823,3 +1831,121 @@ function rest_default_additional_properties_to_false( $schema ) {
return $schema;
}
/**
* Gets the REST API route for a post.
*
* @since 5.5.0
*
* @param int|WP_Post $post Post ID or post object.
* @return string The route path with a leading slash for the given post, or an empty string if there is not a route.
*/
function rest_get_route_for_post( $post ) {
$post = get_post( $post );
if ( ! $post instanceof WP_Post ) {
return '';
}
$post_type = get_post_type_object( $post->post_type );
if ( ! $post_type ) {
return '';
}
$controller = $post_type->get_rest_controller();
if ( ! $controller ) {
return '';
}
$route = '';
// The only two controllers that we can detect are the Attachments and Posts controllers.
if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
$namespace = 'wp/v2';
$rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
$route = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
}
/**
* Filters the REST API route for a post.
*
* @since 5.5.0
*
* @param string $route The route path.
* @param WP_Post $post The post object.
*/
return apply_filters( 'rest_route_for_post', $route, $post );
}
/**
* Gets the REST API route for a term.
*
* @since 5.5.0
*
* @param int|WP_Term $term Term ID or term object.
* @return string The route path with a leading slash for the given term, or an empty string if there is not a route.
*/
function rest_get_route_for_term( $term ) {
$term = get_term( $term );
if ( ! $term instanceof WP_Term ) {
return '';
}
$taxonomy = get_taxonomy( $term->taxonomy );
if ( ! $taxonomy ) {
return '';
}
$controller = $taxonomy->get_rest_controller();
if ( ! $controller ) {
return '';
}
$route = '';
// The only controller that works is the Terms controller.
if ( 'WP_REST_Terms_Controller' === get_class( $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 );
}
/**
* Filters the REST API route for a term.
*
* @since 5.5.0
*
* @param string $route The route path.
* @param WP_Term $term The term object.
*/
return apply_filters( 'rest_route_for_term', $route, $term );
}
/**
* Gets the REST route for the currently queried object.
*
* @since 5.5.0
*
* @return string The REST route of the resource, or an empty string if no resource identified.
*/
function rest_get_queried_resource_route() {
if ( is_singular() ) {
$route = rest_get_route_for_post( get_queried_object() );
} elseif ( is_category() || is_tag() || is_tax() ) {
$route = rest_get_route_for_term( get_queried_object() );
} elseif ( is_author() ) {
$route = '/wp/v2/users/' . get_queried_object_id();
} else {
$route = '';
}
/**
* Filters the REST route for the currently queried object.
*
* @since 5.5.0
*
* @param string $link The route with a leading slash, or an empty string.
*/
return apply_filters( 'rest_queried_resource_route', $route );
}

View File

@ -1349,4 +1349,112 @@ class Tests_REST_API extends WP_UnitTestCase {
array( new WP_REST_Response( 'rest' ), 'rest' ),
);
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post_non_post() {
$this->assertEquals( '', rest_get_route_for_post( 'garbage' ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post_invalid_post_type() {
register_post_type( 'invalid' );
$post = self::factory()->post->create_and_get( array( 'post_type' => 'invalid' ) );
unregister_post_type( 'invalid' );
$this->assertEquals( '', rest_get_route_for_post( $post ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post_non_rest() {
$post = self::factory()->post->create_and_get( array( 'post_type' => 'custom_css' ) );
$this->assertEquals( '', rest_get_route_for_post( $post ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post_custom_controller() {
$post = self::factory()->post->create_and_get( array( 'post_type' => 'wp_block' ) );
$this->assertEquals( '', rest_get_route_for_post( $post ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post() {
$post = self::factory()->post->create_and_get();
$this->assertEquals( '/wp/v2/posts/' . $post->ID, rest_get_route_for_post( $post ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_media() {
$post = self::factory()->attachment->create_and_get();
$this->assertEquals( '/wp/v2/media/' . $post->ID, rest_get_route_for_post( $post ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_post_id() {
$post = self::factory()->post->create_and_get();
$this->assertEquals( '/wp/v2/posts/' . $post->ID, rest_get_route_for_post( $post->ID ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_term_non_term() {
$this->assertEquals( '', rest_get_route_for_term( 'garbage' ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_term_invalid_term_type() {
register_taxonomy( 'invalid', 'post' );
$term = self::factory()->term->create_and_get( array( 'taxonomy' => 'invalid' ) );
unregister_taxonomy( 'invalid' );
$this->assertEquals( '', rest_get_route_for_term( $term ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_term_non_rest() {
$term = self::factory()->term->create_and_get( array( 'taxonomy' => 'post_format' ) );
$this->assertEquals( '', rest_get_route_for_term( $term ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_term() {
$term = self::factory()->term->create_and_get();
$this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_category() {
$term = self::factory()->category->create_and_get();
$this->assertEquals( '/wp/v2/categories/' . $term->term_id, rest_get_route_for_term( $term ) );
}
/**
* @ticket 49116
*/
public function test_rest_get_route_for_term_id() {
$term = self::factory()->term->create_and_get();
$this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) );
}
}

View File

@ -291,4 +291,32 @@ class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcas
$this->assertEquals( count( $taxonomies ), count( $data ) );
}
/**
* @ticket 49116
*/
public function test_get_for_taxonomy_reuses_same_instance() {
$this->assertSame(
get_taxonomy( 'category' )->get_rest_controller(),
get_taxonomy( 'category' )->get_rest_controller()
);
}
/**
* @ticket 49116
*/
public function test_get_for_taxonomy_returns_terms_controller_if_custom_class_not_specified() {
register_taxonomy(
'test',
'post',
array(
'show_in_rest' => true,
)
);
$this->assertInstanceOf(
WP_REST_Terms_Controller::class,
get_taxonomy( 'test' )->get_rest_controller()
);
}
}