Make WordPress Core

Changeset 59032


Ignore:
Timestamp:
09/17/2024 09:50:38 PM (3 months ago)
Author:
TimothyBlynJacobs
Message:

REST API: Automatically populate targetHints for the Allow header.

The REST API uses the "Allow" header to communicate what methods a user is authorized to perform on a resource. This works great when operating on a single item route, but can break down when needing to determine authorization over a collection of items.

This commit uses the "targetHints" property of JSON Hyper Schema to provide access to the "allow" header for "self" links. This alleviates needing to make a separate network request for each item in a collection.

Props mamaduka, noisysocks, peterwilsoncc, spacedmonkey, swissspidy, timothyblynjacobs, tyxla, youknowriad.
Fixes #61739.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r58688 r59032  
    637637                $attributes         = $item['attributes'];
    638638                $attributes['href'] = $item['href'];
    639                 $data[ $rel ][]     = $attributes;
     639
     640                if ( 'self' !== $rel ) {
     641                    $data[ $rel ][] = $attributes;
     642                    continue;
     643                }
     644
     645                $target_hints = self::get_target_hints_for_link( $attributes );
     646                if ( $target_hints ) {
     647                    $attributes['targetHints'] = $target_hints;
     648                }
     649
     650                $data[ $rel ][] = $attributes;
    640651            }
    641652        }
    642653
    643654        return $data;
     655    }
     656
     657    /**
     658     * Gets the target links for a REST API Link.
     659     *
     660     * @since 6.7.0
     661     *
     662     * @param array $link
     663     *
     664     * @return array|null
     665     */
     666    protected static function get_target_hints_for_link( $link ) {
     667        // Prefer targetHints that were specifically designated by the developer.
     668        if ( isset( $link['targetHints']['allow'] ) ) {
     669            return null;
     670        }
     671
     672        $request = WP_REST_Request::from_url( $link['href'] );
     673        if ( ! $request ) {
     674            return null;
     675        }
     676
     677        $server = rest_get_server();
     678        $match  = $server->match_request_to_handler( $request );
     679
     680        if ( is_wp_error( $match ) ) {
     681            return null;
     682        }
     683
     684        if ( is_wp_error( $request->has_valid_params() ) ) {
     685            return null;
     686        }
     687
     688        if ( is_wp_error( $request->sanitize_params() ) ) {
     689            return null;
     690        }
     691
     692        $target_hints = array();
     693
     694        $response = new WP_REST_Response();
     695        $response->set_matched_route( $match[0] );
     696        $response->set_matched_handler( $match[1] );
     697        $headers = rest_send_allow_header( $response, $server, $request )->get_headers();
     698
     699        foreach ( $headers as $name => $value ) {
     700            $name = WP_REST_Request::canonicalize_header_name( $name );
     701
     702            $target_hints[ $name ] = array_map( 'trim', explode( ',', $value ) );
     703        }
     704
     705        return $target_hints;
    644706    }
    645707
  • trunk/tests/phpunit/tests/rest-api/rest-server.php

    r57987 r59032  
    1010class Tests_REST_Server extends WP_Test_REST_TestCase {
    1111    protected static $icon_id;
     12    protected static $admin_id;
     13    protected static $post_id;
    1214
    1315    /**
     
    2224
    2325    public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
    24         $filename      = DIR_TESTDATA . '/images/test-image-large.jpg';
    25         self::$icon_id = $factory->attachment->create_upload_object( $filename );
     26        $filename       = DIR_TESTDATA . '/images/test-image-large.jpg';
     27        self::$icon_id  = $factory->attachment->create_upload_object( $filename );
     28        self::$admin_id = $factory->user->create(
     29            array(
     30                'role' => 'administrator',
     31            )
     32        );
     33        self::$post_id  = $factory->post->create();
    2634    }
    2735
    2836    public static function tear_down_after_class() {
    2937        wp_delete_attachment( self::$icon_id, true );
     38        self::delete_user( self::$admin_id );
     39        wp_delete_post( self::$post_id );
    3040
    3141        parent::tear_down_after_class();
     
    24322442    }
    24332443
     2444    /**
     2445     * @ticket 61739
     2446     */
     2447    public function test_validates_request_when_building_target_hints() {
     2448        register_rest_route(
     2449            'test-ns/v1',
     2450            '/test/(?P<id>\d+)',
     2451            array(
     2452                array(
     2453                    'methods'             => \WP_REST_Server::READABLE,
     2454                    'callback'            => static function () {
     2455                        return new \WP_REST_Response();
     2456                    },
     2457                    'permission_callback' => '__return_true',
     2458                    'args'                => array(
     2459                        'id' => array(
     2460                            'type' => 'integer',
     2461                        ),
     2462                    ),
     2463                ),
     2464            )
     2465        );
     2466
     2467        $response = new WP_REST_Response();
     2468        $response->add_link( 'self', rest_url( 'test-ns/v1/test/garbage' ) );
     2469
     2470        $links = rest_get_server()::get_response_links( $response );
     2471
     2472        $this->assertArrayHasKey( 'self', $links );
     2473        $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] );
     2474    }
     2475
     2476    /**
     2477     * @ticket 61739
     2478     */
     2479    public function test_sanitizes_request_when_building_target_hints() {
     2480        $validated_param = null;
     2481        register_rest_route(
     2482            'test-ns/v1',
     2483            '/test/(?P<id>\d+)',
     2484            array(
     2485                array(
     2486                    'methods'             => \WP_REST_Server::READABLE,
     2487                    'callback'            => static function () {
     2488                        return new \WP_REST_Response();
     2489                    },
     2490                    'permission_callback' => function ( WP_REST_Request $request ) use ( &$validated_param ) {
     2491                        $validated_param = $request['id'];
     2492
     2493                        return true;
     2494                    },
     2495                    'args'                => array(
     2496                        'id' => array(
     2497                            'type' => 'integer',
     2498                        ),
     2499                    ),
     2500                ),
     2501            )
     2502        );
     2503
     2504        $response = new WP_REST_Response();
     2505        $response->add_link( 'self', rest_url( 'test-ns/v1/test/5' ) );
     2506
     2507        $links = rest_get_server()::get_response_links( $response );
     2508
     2509        $this->assertArrayHasKey( 'self', $links );
     2510        $this->assertArrayHasKey( 'targetHints', $links['self'][0] );
     2511        $this->assertIsInt( $validated_param );
     2512    }
     2513
     2514    /**
     2515     * @ticket 61739
     2516     */
     2517    public function test_populates_target_hints_for_administrator() {
     2518        wp_set_current_user( self::$admin_id );
     2519        $response = rest_do_request( '/wp/v2/posts' );
     2520        $post     = $response->get_data()[0];
     2521
     2522        $link = $post['_links']['self'][0];
     2523        $this->assertArrayHasKey( 'targetHints', $link );
     2524        $this->assertArrayHasKey( 'allow', $link['targetHints'] );
     2525        $this->assertSame( array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), $link['targetHints']['allow'] );
     2526    }
     2527
     2528    /**
     2529     * @ticket 61739
     2530     */
     2531    public function test_populates_target_hints_for_logged_out_user() {
     2532        $response = rest_do_request( '/wp/v2/posts' );
     2533        $post     = $response->get_data()[0];
     2534
     2535        $link = $post['_links']['self'][0];
     2536        $this->assertArrayHasKey( 'targetHints', $link );
     2537        $this->assertArrayHasKey( 'allow', $link['targetHints'] );
     2538        $this->assertSame( array( 'GET' ), $link['targetHints']['allow'] );
     2539    }
     2540
     2541    /**
     2542     * @ticket 61739
     2543     */
     2544    public function test_does_not_error_on_invalid_urls() {
     2545        $response = new WP_REST_Response();
     2546        $response->add_link( 'self', 'this is not a real URL' );
     2547
     2548        $links = rest_get_server()::get_response_links( $response );
     2549        $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] );
     2550    }
     2551
     2552    /**
     2553     * @ticket 61739
     2554     */
     2555    public function test_does_not_error_on_bad_rest_api_routes() {
     2556        $response = new WP_REST_Response();
     2557        $response->add_link( 'self', rest_url( '/this/is/not/a/real/route' ) );
     2558
     2559        $links = rest_get_server()::get_response_links( $response );
     2560        $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] );
     2561    }
     2562
     2563    /**
     2564     * @ticket 61739
     2565     */
     2566    public function test_prefers_developer_defined_target_hints() {
     2567        $response = new WP_REST_Response();
     2568        $response->add_link(
     2569            'self',
     2570            '/wp/v2/posts/' . self::$post_id,
     2571            array(
     2572                'targetHints' => array(
     2573                    'allow' => array( 'GET', 'PUT' ),
     2574                ),
     2575            )
     2576        );
     2577
     2578        $links = rest_get_server()::get_response_links( $response );
     2579        $link  = $links['self'][0];
     2580        $this->assertArrayHasKey( 'targetHints', $link );
     2581        $this->assertArrayHasKey( 'allow', $link['targetHints'] );
     2582        $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] );
     2583    }
     2584
    24342585    public function _validate_as_integer_123( $value, $request, $key ) {
    24352586        if ( ! is_int( $value ) ) {
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r58822 r59032  
    1233512335            "self": [
    1233612336                {
    12337                     "href": "http://example.org/index.php?rest_route=/wp/v2/posts/4"
     12337                    "href": "http://example.org/index.php?rest_route=/wp/v2/posts/4",
     12338                    "targetHints": {
     12339                        "allow": [
     12340                            "GET",
     12341                            "POST",
     12342                            "PUT",
     12343                            "PATCH",
     12344                            "DELETE"
     12345                        ]
     12346                    }
    1233812347                }
    1233912348            ],
     
    1264212651            "self": [
    1264312652                {
    12644                     "href": "http://example.org/index.php?rest_route=/wp/v2/pages/7"
     12653                    "href": "http://example.org/index.php?rest_route=/wp/v2/pages/7",
     12654                    "targetHints": {
     12655                        "allow": [
     12656                            "GET",
     12657                            "POST",
     12658                            "PUT",
     12659                            "PATCH",
     12660                            "DELETE"
     12661                        ]
     12662                    }
    1264512663                }
    1264612664            ],
     
    1293312951            "self": [
    1293412952                {
    12935                     "href": "http://example.org/index.php?rest_route=/wp/v2/media/10"
     12953                    "href": "http://example.org/index.php?rest_route=/wp/v2/media/10",
     12954                    "targetHints": {
     12955                        "allow": [
     12956                            "GET",
     12957                            "POST",
     12958                            "PUT",
     12959                            "PATCH",
     12960                            "DELETE"
     12961                        ]
     12962                    }
    1293612963                }
    1293712964            ],
     
    1363013657            "self": [
    1363113658                {
    13632                     "href": "http://example.org/index.php?rest_route=/wp/v2/categories/1"
     13659                    "href": "http://example.org/index.php?rest_route=/wp/v2/categories/1",
     13660                    "targetHints": {
     13661                        "allow": [
     13662                            "GET",
     13663                            "POST",
     13664                            "PUT",
     13665                            "PATCH"
     13666                        ]
     13667                    }
    1363313668                }
    1363413669            ],
     
    1369513730            "self": [
    1369613731                {
    13697                     "href": "http://example.org/index.php?rest_route=/wp/v2/tags/2"
     13732                    "href": "http://example.org/index.php?rest_route=/wp/v2/tags/2",
     13733                    "targetHints": {
     13734                        "allow": [
     13735                            "GET",
     13736                            "POST",
     13737                            "PUT",
     13738                            "PATCH",
     13739                            "DELETE"
     13740                        ]
     13741                    }
    1369813742                }
    1369913743            ],
     
    1375913803            "self": [
    1376013804                {
    13761                     "href": "http://example.org/index.php?rest_route=/wp/v2/users/1"
     13805                    "href": "http://example.org/index.php?rest_route=/wp/v2/users/1",
     13806                    "targetHints": {
     13807                        "allow": [
     13808                            "GET",
     13809                            "POST",
     13810                            "PUT",
     13811                            "PATCH",
     13812                            "DELETE"
     13813                        ]
     13814                    }
    1376213815                }
    1376313816            ],
     
    1378713840            "self": [
    1378813841                {
    13789                     "href": "http://example.org/index.php?rest_route=/wp/v2/users/2"
     13842                    "href": "http://example.org/index.php?rest_route=/wp/v2/users/2",
     13843                    "targetHints": {
     13844                        "allow": [
     13845                            "GET",
     13846                            "POST",
     13847                            "PUT",
     13848                            "PATCH",
     13849                            "DELETE"
     13850                        ]
     13851                    }
    1379013852                }
    1379113853            ],
     
    1386013922            "self": [
    1386113923                {
    13862                     "href": "http://example.org/index.php?rest_route=/wp/v2/comments/2"
     13924                    "href": "http://example.org/index.php?rest_route=/wp/v2/comments/2",
     13925                    "targetHints": {
     13926                        "allow": [
     13927                            "GET",
     13928                            "POST",
     13929                            "PUT",
     13930                            "PATCH",
     13931                            "DELETE"
     13932                        ]
     13933                    }
    1386313934                }
    1386413935            ],
Note: See TracChangeset for help on using the changeset viewer.