Make WordPress Core

Changeset 59032


Ignore:
Timestamp:
09/17/2024 09:50:38 PM (23 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.