Make WordPress Core

Changeset 48273


Ignore:
Timestamp:
07/02/2020 05:55:04 AM (4 years ago)
Author:
TimothyBlynJacobs
Message:

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.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-taxonomy.php

    r47808 r48273  
    209209     */
    210210    public $rest_controller_class;
     211
     212    /**
     213     * The controller instance for this taxonomy's REST API endpoints.
     214     *
     215     * Lazily computed. Should be accessed using {@see WP_Taxonomy::get_rest_controller()}.
     216     *
     217     * @since 5.5.0
     218     * @var WP_REST_Controller $rest_controller
     219     */
     220    public $rest_controller;
    211221
    212222    /**
     
    453463        remove_filter( 'wp_ajax_add-' . $this->name, '_wp_ajax_add_hierarchical_term' );
    454464    }
     465
     466    /**
     467     * Gets the REST API controller for this taxonomy.
     468     *
     469     * Will only instantiate the controller class once per request.
     470     *
     471     * @since 5.5.0
     472     *
     473     * @return WP_REST_Controller|null The controller instance, or null if the taxonomy
     474     *                                 is set not to show in rest.
     475     */
     476    public function get_rest_controller() {
     477        if ( ! $this->show_in_rest ) {
     478            return null;
     479        }
     480
     481        $class = $this->rest_controller_class ? $this->rest_controller_class : WP_REST_Terms_Controller::class;
     482
     483        if ( ! class_exists( $class ) ) {
     484            return null;
     485        }
     486
     487        if ( ! is_subclass_of( $class, WP_REST_Controller::class ) ) {
     488            return null;
     489        }
     490
     491        if ( ! $this->rest_controller ) {
     492            $this->rest_controller = new $class( $this->name );
     493        }
     494
     495        if ( ! ( $this->rest_controller instanceof $class ) ) {
     496            return null;
     497        }
     498
     499        return $this->rest_controller;
     500    }
    455501}
  • trunk/src/wp-includes/rest-api.php

    r48242 r48273  
    235235    // Terms.
    236236    foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
    237         $class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
    238 
    239         if ( ! class_exists( $class ) ) {
    240             continue;
    241         }
    242         $controller = new $class( $taxonomy->name );
    243         if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
     237        $controller = $taxonomy->get_rest_controller();
     238
     239        if ( ! $controller ) {
    244240            continue;
    245241        }
     
    874870    }
    875871
    876     echo "<link rel='https://api.w.org/' href='" . esc_url( $api_root ) . "' />\n";
     872    printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) );
     873
     874    $resource = rest_get_queried_resource_route();
     875
     876    if ( $resource ) {
     877        printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) );
     878    }
    877879}
    878880
     
    893895    }
    894896
    895     header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false );
     897    header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false );
     898
     899    $resource = rest_get_queried_resource_route();
     900
     901    if ( $resource ) {
     902        header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false );
     903    }
    896904}
    897905
     
    18241832    return $schema;
    18251833}
     1834
     1835/**
     1836 * Gets the REST API route for a post.
     1837 *
     1838 * @since 5.5.0
     1839 *
     1840 * @param int|WP_Post $post Post ID or post object.
     1841 * @return string The route path with a leading slash for the given post, or an empty string if there is not a route.
     1842 */
     1843function rest_get_route_for_post( $post ) {
     1844    $post = get_post( $post );
     1845
     1846    if ( ! $post instanceof WP_Post ) {
     1847        return '';
     1848    }
     1849
     1850    $post_type = get_post_type_object( $post->post_type );
     1851    if ( ! $post_type ) {
     1852        return '';
     1853    }
     1854
     1855    $controller = $post_type->get_rest_controller();
     1856    if ( ! $controller ) {
     1857        return '';
     1858    }
     1859
     1860    $route = '';
     1861
     1862    // The only two controllers that we can detect are the Attachments and Posts controllers.
     1863    if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
     1864        $namespace = 'wp/v2';
     1865        $rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
     1866        $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
     1867    }
     1868
     1869    /**
     1870     * Filters the REST API route for a post.
     1871     *
     1872     * @since 5.5.0
     1873     *
     1874     * @param string  $route The route path.
     1875     * @param WP_Post $post  The post object.
     1876     */
     1877    return apply_filters( 'rest_route_for_post', $route, $post );
     1878}
     1879
     1880/**
     1881 * Gets the REST API route for a term.
     1882 *
     1883 * @since 5.5.0
     1884 *
     1885 * @param int|WP_Term $term Term ID or term object.
     1886 * @return string The route path with a leading slash for the given term, or an empty string if there is not a route.
     1887 */
     1888function rest_get_route_for_term( $term ) {
     1889    $term = get_term( $term );
     1890
     1891    if ( ! $term instanceof WP_Term ) {
     1892        return '';
     1893    }
     1894
     1895    $taxonomy = get_taxonomy( $term->taxonomy );
     1896    if ( ! $taxonomy ) {
     1897        return '';
     1898    }
     1899
     1900    $controller = $taxonomy->get_rest_controller();
     1901    if ( ! $controller ) {
     1902        return '';
     1903    }
     1904
     1905    $route = '';
     1906
     1907    // The only controller that works is the Terms controller.
     1908    if ( 'WP_REST_Terms_Controller' === get_class( $controller ) ) {
     1909        $namespace = 'wp/v2';
     1910        $rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
     1911        $route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id );
     1912    }
     1913
     1914    /**
     1915     * Filters the REST API route for a term.
     1916     *
     1917     * @since 5.5.0
     1918     *
     1919     * @param string  $route The route path.
     1920     * @param WP_Term $term  The term object.
     1921     */
     1922    return apply_filters( 'rest_route_for_term', $route, $term );
     1923}
     1924
     1925/**
     1926 * Gets the REST route for the currently queried object.
     1927 *
     1928 * @since 5.5.0
     1929 *
     1930 * @return string The REST route of the resource, or an empty string if no resource identified.
     1931 */
     1932function rest_get_queried_resource_route() {
     1933    if ( is_singular() ) {
     1934        $route = rest_get_route_for_post( get_queried_object() );
     1935    } elseif ( is_category() || is_tag() || is_tax() ) {
     1936        $route = rest_get_route_for_term( get_queried_object() );
     1937    } elseif ( is_author() ) {
     1938        $route = '/wp/v2/users/' . get_queried_object_id();
     1939    } else {
     1940        $route = '';
     1941    }
     1942
     1943    /**
     1944     * Filters the REST route for the currently queried object.
     1945     *
     1946     * @since 5.5.0
     1947     *
     1948     * @param string $link The route with a leading slash, or an empty string.
     1949     */
     1950    return apply_filters( 'rest_queried_resource_route', $route );
     1951}
  • trunk/tests/phpunit/tests/rest-api.php

    r48121 r48273  
    13501350        );
    13511351    }
     1352
     1353    /**
     1354     * @ticket 49116
     1355     */
     1356    public function test_rest_get_route_for_post_non_post() {
     1357        $this->assertEquals( '', rest_get_route_for_post( 'garbage' ) );
     1358    }
     1359
     1360    /**
     1361     * @ticket 49116
     1362     */
     1363    public function test_rest_get_route_for_post_invalid_post_type() {
     1364        register_post_type( 'invalid' );
     1365        $post = self::factory()->post->create_and_get( array( 'post_type' => 'invalid' ) );
     1366        unregister_post_type( 'invalid' );
     1367
     1368        $this->assertEquals( '', rest_get_route_for_post( $post ) );
     1369    }
     1370
     1371    /**
     1372     * @ticket 49116
     1373     */
     1374    public function test_rest_get_route_for_post_non_rest() {
     1375        $post = self::factory()->post->create_and_get( array( 'post_type' => 'custom_css' ) );
     1376        $this->assertEquals( '', rest_get_route_for_post( $post ) );
     1377    }
     1378
     1379    /**
     1380     * @ticket 49116
     1381     */
     1382    public function test_rest_get_route_for_post_custom_controller() {
     1383        $post = self::factory()->post->create_and_get( array( 'post_type' => 'wp_block' ) );
     1384        $this->assertEquals( '', rest_get_route_for_post( $post ) );
     1385    }
     1386
     1387    /**
     1388     * @ticket 49116
     1389     */
     1390    public function test_rest_get_route_for_post() {
     1391        $post = self::factory()->post->create_and_get();
     1392        $this->assertEquals( '/wp/v2/posts/' . $post->ID, rest_get_route_for_post( $post ) );
     1393    }
     1394
     1395    /**
     1396     * @ticket 49116
     1397     */
     1398    public function test_rest_get_route_for_media() {
     1399        $post = self::factory()->attachment->create_and_get();
     1400        $this->assertEquals( '/wp/v2/media/' . $post->ID, rest_get_route_for_post( $post ) );
     1401    }
     1402
     1403    /**
     1404     * @ticket 49116
     1405     */
     1406    public function test_rest_get_route_for_post_id() {
     1407        $post = self::factory()->post->create_and_get();
     1408        $this->assertEquals( '/wp/v2/posts/' . $post->ID, rest_get_route_for_post( $post->ID ) );
     1409    }
     1410
     1411    /**
     1412     * @ticket 49116
     1413     */
     1414    public function test_rest_get_route_for_term_non_term() {
     1415        $this->assertEquals( '', rest_get_route_for_term( 'garbage' ) );
     1416    }
     1417
     1418    /**
     1419     * @ticket 49116
     1420     */
     1421    public function test_rest_get_route_for_term_invalid_term_type() {
     1422        register_taxonomy( 'invalid', 'post' );
     1423        $term = self::factory()->term->create_and_get( array( 'taxonomy' => 'invalid' ) );
     1424        unregister_taxonomy( 'invalid' );
     1425
     1426        $this->assertEquals( '', rest_get_route_for_term( $term ) );
     1427    }
     1428
     1429    /**
     1430     * @ticket 49116
     1431     */
     1432    public function test_rest_get_route_for_term_non_rest() {
     1433        $term = self::factory()->term->create_and_get( array( 'taxonomy' => 'post_format' ) );
     1434        $this->assertEquals( '', rest_get_route_for_term( $term ) );
     1435    }
     1436
     1437    /**
     1438     * @ticket 49116
     1439     */
     1440    public function test_rest_get_route_for_term() {
     1441        $term = self::factory()->term->create_and_get();
     1442        $this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term ) );
     1443    }
     1444
     1445    /**
     1446     * @ticket 49116
     1447     */
     1448    public function test_rest_get_route_for_category() {
     1449        $term = self::factory()->category->create_and_get();
     1450        $this->assertEquals( '/wp/v2/categories/' . $term->term_id, rest_get_route_for_term( $term ) );
     1451    }
     1452
     1453    /**
     1454     * @ticket 49116
     1455     */
     1456    public function test_rest_get_route_for_term_id() {
     1457        $term = self::factory()->term->create_and_get();
     1458        $this->assertEquals( '/wp/v2/tags/' . $term->term_id, rest_get_route_for_term( $term->term_id ) );
     1459    }
    13521460}
  • trunk/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php

    r47122 r48273  
    292292    }
    293293
     294    /**
     295     * @ticket 49116
     296     */
     297    public function test_get_for_taxonomy_reuses_same_instance() {
     298        $this->assertSame(
     299            get_taxonomy( 'category' )->get_rest_controller(),
     300            get_taxonomy( 'category' )->get_rest_controller()
     301        );
     302    }
     303
     304    /**
     305     * @ticket 49116
     306     */
     307    public function test_get_for_taxonomy_returns_terms_controller_if_custom_class_not_specified() {
     308        register_taxonomy(
     309            'test',
     310            'post',
     311            array(
     312                'show_in_rest' => true,
     313            )
     314        );
     315
     316        $this->assertInstanceOf(
     317            WP_REST_Terms_Controller::class,
     318            get_taxonomy( 'test' )->get_rest_controller()
     319        );
     320    }
     321
    294322}
Note: See TracChangeset for help on using the changeset viewer.