Make WordPress Core

Ticket #42094: 42094.diff

File 42094.diff, 18.3 KB (added by dlh, 5 years ago)
  • src/wp-includes/rest-api.php

    diff --git src/wp-includes/rest-api.php src/wp-includes/rest-api.php
    index 4b49ebfdcb..68f13c2ac1 100644
    function rest_send_allow_header( $response, $server, $request ) { 
    696696        return $response;
    697697}
    698698
     699/**
     700 * Recursively computes the intersection of arrays using keys for comparison.
     701 *
     702 * @param  array $array1 The array with master keys to check.
     703 * @param  array $array2 An array to compare keys against.
     704 *
     705 * @return array An associative array containing all the entries of array1 which have keys that are present in all arguments.
     706 */
     707function _rest_array_intersect_key_recursive( $array1, $array2 ) {
     708        $array1 = array_intersect_key( $array1, $array2 );
     709        foreach ( $array1 as $key => $value ) {
     710                if ( is_array( $value ) && is_array( $array2[ $key ] ) ) {
     711                        $array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] );
     712                }
     713        }
     714        return $array1;
     715}
     716
    699717/**
    700718 * Filter the API response to include only a white-listed set of response object fields.
    701719 *
    function rest_filter_response_fields( $response, $server, $request ) { 
    723741        // Trim off outside whitespace from the comma delimited list.
    724742        $fields = array_map( 'trim', $fields );
    725743
    726         $fields_as_keyed = array_combine( $fields, array_fill( 0, count( $fields ), true ) );
     744        // Create nested array of accepted field hierarchy.
     745        $fields_as_keyed = array();
     746        foreach ( $fields as $field ) {
     747                $parts = explode( '.', $field );
     748                $ref   = &$fields_as_keyed;
     749                while ( count( $parts ) > 1 ) {
     750                        $next         = array_shift( $parts );
     751                        $ref[ $next ] = array();
     752                        $ref          = &$ref[ $next ];
     753                }
     754                $last         = array_shift( $parts );
     755                $ref[ $last ] = true;
     756        }
    727757
    728758        if ( wp_is_numeric_array( $data ) ) {
    729759                $new_data = array();
    730760                foreach ( $data as $item ) {
    731                         $new_data[] = array_intersect_key( $item, $fields_as_keyed );
     761                        $new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
    732762                }
    733763        } else {
    734                 $new_data = array_intersect_key( $data, $fields_as_keyed );
     764                $new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );
    735765        }
    736766
    737767        $response->set_data( $new_data );
    function rest_filter_response_fields( $response, $server, $request ) { 
    739769        return $response;
    740770}
    741771
     772/**
     773 * Given an array of fields to include in a response, some of which may be
     774 * `nested.fields`, determine whether the provided field should be included
     775 * in the response body.
     776 *
     777 * If a parent field is passed in, the presence of any nested field within
     778 * that parent will cause the method to return `true`. For example "title"
     779 * will return true if any of `title`, `title.raw` or `title.rendered` is
     780 * provided.
     781 *
     782 * @since 5.3.0
     783 *
     784 * @param string $field  A field to test for inclusion in the response body.
     785 * @param array  $fields An array of string fields supported by the endpoint.
     786 * @return bool Whether to include the field or not.
     787 */
     788function rest_is_field_included( $field, $fields ) {
     789        if ( in_array( $field, $fields, true ) ) {
     790                return true;
     791        }
     792        foreach ( $fields as $accepted_field ) {
     793                // Check to see if $field is the parent of any item in $fields.
     794                // A field "parent" should be accepted if "parent.child" is accepted.
     795                if ( strpos( $accepted_field, "$field." ) === 0 ) {
     796                        return true;
     797                }
     798                // Conversely, if "parent" is accepted, all "parent.child" fields should
     799                // also be accepted.
     800                if ( strpos( $field, "$accepted_field." ) === 0 ) {
     801                        return true;
     802                }
     803        }
     804        return false;
     805}
     806
    742807/**
    743808 * Adds the REST API URL to the WP RSD endpoint.
    744809 *
  • src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php

    diff --git src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
    index 7adc5ed616..1bc38124e9 100644
    abstract class WP_REST_Controller { 
    562562                if ( in_array( 'id', $fields, true ) ) {
    563563                        $requested_fields[] = 'id';
    564564                }
    565                 return array_intersect( $fields, $requested_fields );
     565                // Return the list of all requested fields which appear in the schema.
     566                return array_reduce(
     567                        $requested_fields,
     568                        function( $response_fields, $field ) use ( $fields ) {
     569                                if ( in_array( $field, $fields, true ) ) {
     570                                        $response_fields[] = $field;
     571                                        return $response_fields;
     572                                }
     573                                // Check for nested fields if $field is not a direct match.
     574                                $nested_fields = explode( '.', $field );
     575                                // A nested field is included so long as its top-level property is
     576                                // present in the schema.
     577                                if ( in_array( $nested_fields[0], $fields, true ) ) {
     578                                        $response_fields[] = $field;
     579                                }
     580                                return $response_fields;
     581                        },
     582                        array()
     583                );
    566584        }
    567585
    568586        /**
  • src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    diff --git src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
    index e53ff4b910..883e9b31fe 100644
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    14391439                // Base fields for every post.
    14401440                $data = array();
    14411441
    1442                 if ( in_array( 'id', $fields, true ) ) {
     1442                if ( rest_is_field_included( 'id', $fields ) ) {
    14431443                        $data['id'] = $post->ID;
    14441444                }
    14451445
    1446                 if ( in_array( 'date', $fields, true ) ) {
     1446                if ( rest_is_field_included( 'date', $fields ) ) {
    14471447                        $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
    14481448                }
    14491449
    1450                 if ( in_array( 'date_gmt', $fields, true ) ) {
     1450                if ( rest_is_field_included( 'date_gmt', $fields ) ) {
    14511451                        // For drafts, `post_date_gmt` may not be set, indicating that the
    14521452                        // date of the draft should be updated each time it is saved (see
    14531453                        // #38883).  In this case, shim the value based on the `post_date`
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    14601460                        $data['date_gmt'] = $this->prepare_date_response( $post_date_gmt );
    14611461                }
    14621462
    1463                 if ( in_array( 'guid', $fields, true ) ) {
     1463                if ( rest_is_field_included( 'guid', $fields ) ) {
    14641464                        $data['guid'] = array(
    14651465                                /** This filter is documented in wp-includes/post-template.php */
    14661466                                'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    14681468                        );
    14691469                }
    14701470
    1471                 if ( in_array( 'modified', $fields, true ) ) {
     1471                if ( rest_is_field_included( 'modified', $fields ) ) {
    14721472                        $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
    14731473                }
    14741474
    1475                 if ( in_array( 'modified_gmt', $fields, true ) ) {
     1475                if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
    14761476                        // For drafts, `post_modified_gmt` may not be set (see
    14771477                        // `post_date_gmt` comments above).  In this case, shim the value
    14781478                        // based on the `post_modified` field with the site's timezone
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    14851485                        $data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt );
    14861486                }
    14871487
    1488                 if ( in_array( 'password', $fields, true ) ) {
     1488                if ( rest_is_field_included( 'password', $fields ) ) {
    14891489                        $data['password'] = $post->post_password;
    14901490                }
    14911491
    1492                 if ( in_array( 'slug', $fields, true ) ) {
     1492                if ( rest_is_field_included( 'slug', $fields ) ) {
    14931493                        $data['slug'] = $post->post_name;
    14941494                }
    14951495
    1496                 if ( in_array( 'status', $fields, true ) ) {
     1496                if ( rest_is_field_included( 'status', $fields ) ) {
    14971497                        $data['status'] = $post->post_status;
    14981498                }
    14991499
    1500                 if ( in_array( 'type', $fields, true ) ) {
     1500                if ( rest_is_field_included( 'type', $fields ) ) {
    15011501                        $data['type'] = $post->post_type;
    15021502                }
    15031503
    1504                 if ( in_array( 'link', $fields, true ) ) {
     1504                if ( rest_is_field_included( 'link', $fields ) ) {
    15051505                        $data['link'] = get_permalink( $post->ID );
    15061506                }
    15071507
    1508                 if ( in_array( 'title', $fields, true ) ) {
     1508                if ( rest_is_field_included( 'title', $fields ) ) {
     1509                        $data['title'] = array();
     1510                }
     1511                if ( rest_is_field_included( 'title.raw', $fields ) ) {
     1512                        $data['title']['raw'] = $post->post_title;
     1513                }
     1514                if ( rest_is_field_included( 'title.rendered', $fields ) ) {
    15091515                        add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
    15101516
    1511                         $data['title'] = array(
    1512                                 'raw'      => $post->post_title,
    1513                                 'rendered' => get_the_title( $post->ID ),
    1514                         );
     1517                        $data['title']['rendered'] = get_the_title( $post->ID );
    15151518
    15161519                        remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
    15171520                }
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    15251528                        $has_password_filter = true;
    15261529                }
    15271530
    1528                 if ( in_array( 'content', $fields, true ) ) {
    1529                         $data['content'] = array(
    1530                                 'raw'           => $post->post_content,
    1531                                 /** This filter is documented in wp-includes/post-template.php */
    1532                                 'rendered'      => post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content ),
    1533                                 'protected'     => (bool) $post->post_password,
    1534                                 'block_version' => block_version( $post->post_content ),
    1535                         );
     1531                if ( rest_is_field_included( 'content', $fields ) ) {
     1532                        $data['content'] = array();
     1533                }
     1534                if ( rest_is_field_included( 'content.raw', $fields ) ) {
     1535                        $data['content']['raw'] = $post->post_content;
     1536                }
     1537                if ( rest_is_field_included( 'content.rendered', $fields ) ) {
     1538                        /** This filter is documented in wp-includes/post-template.php */
     1539                        $data['content']['rendered'] = post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content );
     1540                }
     1541                if ( rest_is_field_included( 'content.protected', $fields ) ) {
     1542                        $data['content']['protected'] = (bool) $post->post_password;
     1543                }
     1544                if ( rest_is_field_included( 'content.block_version', $fields ) ) {
     1545                        $data['content']['block_version'] = block_version( $post->post_content );
    15361546                }
    15371547
    1538                 if ( in_array( 'excerpt', $fields, true ) ) {
     1548                if ( rest_is_field_included( 'excerpt', $fields ) ) {
    15391549                        /** This filter is documented in wp-includes/post-template.php */
    15401550                        $excerpt         = apply_filters( 'the_excerpt', apply_filters( 'get_the_excerpt', $post->post_excerpt, $post ) );
    15411551                        $data['excerpt'] = array(
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    15501560                        remove_filter( 'post_password_required', '__return_false' );
    15511561                }
    15521562
    1553                 if ( in_array( 'author', $fields, true ) ) {
     1563                if ( rest_is_field_included( 'author', $fields ) ) {
    15541564                        $data['author'] = (int) $post->post_author;
    15551565                }
    15561566
    1557                 if ( in_array( 'featured_media', $fields, true ) ) {
     1567                if ( rest_is_field_included( 'featured_media', $fields ) ) {
    15581568                        $data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
    15591569                }
    15601570
    1561                 if ( in_array( 'parent', $fields, true ) ) {
     1571                if ( rest_is_field_included( 'parent', $fields ) ) {
    15621572                        $data['parent'] = (int) $post->post_parent;
    15631573                }
    15641574
    1565                 if ( in_array( 'menu_order', $fields, true ) ) {
     1575                if ( rest_is_field_included( 'menu_order', $fields ) ) {
    15661576                        $data['menu_order'] = (int) $post->menu_order;
    15671577                }
    15681578
    1569                 if ( in_array( 'comment_status', $fields, true ) ) {
     1579                if ( rest_is_field_included( 'comment_status', $fields ) ) {
    15701580                        $data['comment_status'] = $post->comment_status;
    15711581                }
    15721582
    1573                 if ( in_array( 'ping_status', $fields, true ) ) {
     1583                if ( rest_is_field_included( 'ping_status', $fields ) ) {
    15741584                        $data['ping_status'] = $post->ping_status;
    15751585                }
    15761586
    1577                 if ( in_array( 'sticky', $fields, true ) ) {
     1587                if ( rest_is_field_included( 'sticky', $fields ) ) {
    15781588                        $data['sticky'] = is_sticky( $post->ID );
    15791589                }
    15801590
    1581                 if ( in_array( 'template', $fields, true ) ) {
     1591                if ( rest_is_field_included( 'template', $fields ) ) {
    15821592                        $template = get_page_template_slug( $post->ID );
    15831593                        if ( $template ) {
    15841594                                $data['template'] = $template;
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    15871597                        }
    15881598                }
    15891599
    1590                 if ( in_array( 'format', $fields, true ) ) {
     1600                if ( rest_is_field_included( 'format', $fields ) ) {
    15911601                        $data['format'] = get_post_format( $post->ID );
    15921602
    15931603                        // Fill in blank post format.
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    15961606                        }
    15971607                }
    15981608
    1599                 if ( in_array( 'meta', $fields, true ) ) {
     1609                if ( rest_is_field_included( 'meta', $fields ) ) {
    16001610                        $data['meta'] = $this->meta->get_value( $post->ID, $request );
    16011611                }
    16021612
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    16051615                foreach ( $taxonomies as $taxonomy ) {
    16061616                        $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
    16071617
    1608                         if ( in_array( $base, $fields, true ) ) {
     1618                        if ( rest_is_field_included( $base, $fields ) ) {
    16091619                                $terms         = get_the_terms( $post, $taxonomy->name );
    16101620                                $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
    16111621                        }
    class WP_REST_Posts_Controller extends WP_REST_Controller { 
    16131623
    16141624                $post_type_obj = get_post_type_object( $post->post_type );
    16151625                if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
    1616                         $permalink_template_requested = in_array( 'permalink_template', $fields, true );
    1617                         $generated_slug_requested     = in_array( 'generated_slug', $fields, true );
     1626                        $permalink_template_requested = rest_is_field_included( 'permalink_template', $fields );
     1627                        $generated_slug_requested     = rest_is_field_included( 'generated_slug', $fields );
    16181628
    16191629                        if ( $permalink_template_requested || $generated_slug_requested ) {
    16201630                                if ( ! function_exists( 'get_sample_permalink' ) ) {
  • tests/phpunit/tests/rest-api.php

    diff --git tests/phpunit/tests/rest-api.php tests/phpunit/tests/rest-api.php
    index c81efd67b6..512aa61b6d 100644
    class Tests_REST_API extends WP_UnitTestCase { 
    519519                );
    520520        }
    521521
     522        /**
     523         * Ensure that nested fields may be whitelisted with request['_fields'].
     524         *
     525         * @ticket 42094
     526         */
     527        public function test_rest_filter_response_fields_nested_field_filter() {
     528                $response = new WP_REST_Response();
     529
     530                $response->set_data(
     531                        array(
     532                                'a' => 0,
     533                                'b' => array(
     534                                        '1' => 1,
     535                                        '2' => 2,
     536                                ),
     537                                'c' => 3,
     538                                'd' => array(
     539                                        '4' => 4,
     540                                        '5' => 5,
     541                                ),
     542                        )
     543                );
     544                $request = array(
     545                        '_fields' => 'b.1,c,d.5',
     546                );
     547
     548                $response = rest_filter_response_fields( $response, null, $request );
     549                $this->assertEquals(
     550                        array(
     551                                'b' => array(
     552                                        '1' => 1,
     553                                ),
     554                                'c' => 3,
     555                                'd' => array(
     556                                        '5' => 5,
     557                                ),
     558                        ),
     559                        $response->get_data()
     560                );
     561        }
     562
     563        /**
     564         * @ticket 42094
     565         */
     566        public function test_rest_is_field_included() {
     567                $fields = array(
     568                        'id',
     569                        'title',
     570                        'content.raw',
     571                        'custom.property',
     572                );
     573
     574                $this->assertTrue( rest_is_field_included( 'id', $fields ) );
     575                $this->assertTrue( rest_is_field_included( 'title', $fields ) );
     576                $this->assertTrue( rest_is_field_included( 'title.raw', $fields ) );
     577                $this->assertTrue( rest_is_field_included( 'title.rendered', $fields ) );
     578                $this->assertTrue( rest_is_field_included( 'content', $fields ) );
     579                $this->assertTrue( rest_is_field_included( 'content.raw', $fields ) );
     580                $this->assertTrue( rest_is_field_included( 'custom.property', $fields ) );
     581                $this->assertFalse( rest_is_field_included( 'content.rendered', $fields ) );
     582                $this->assertFalse( rest_is_field_included( 'type', $fields ) );
     583                $this->assertFalse( rest_is_field_included( 'meta', $fields ) );
     584                $this->assertFalse( rest_is_field_included( 'meta.value', $fields ) );
     585        }
     586
    522587        /**
    523588         * The get_rest_url function should return a URL consistently terminated with a "/",
    524589         * whether the blog is configured with pretty permalink support or not.
  • tests/phpunit/tests/rest-api/rest-controller.php

    diff --git tests/phpunit/tests/rest-api/rest-controller.php tests/phpunit/tests/rest-api/rest-controller.php
    index f130abc62d..91e1d0c5b7 100644
    class WP_Test_REST_Controller extends WP_Test_REST_TestCase { 
    232232        public function data_get_fields_for_response() {
    233233                return array(
    234234                        array(
    235                                 'somestring,someinteger',
     235                                'somestring,someinteger,someinvalidkey',
    236236                                array(
    237237                                        'somestring',
    238238                                        'someinteger',
  • tests/phpunit/tests/rest-api/rest-posts-controller.php

    diff --git tests/phpunit/tests/rest-api/rest-posts-controller.php tests/phpunit/tests/rest-api/rest-posts-controller.php
    index ba174c448e..95e8ee11c2 100644
    class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te 
    16891689                );
    16901690        }
    16911691
     1692        /**
     1693         * @ticket 42094
     1694         */
     1695        public function test_prepare_item_filters_content_when_needed() {
     1696                $filter_count   = 0;
     1697                $filter_content = function() use ( &$filter_count ) {
     1698                        $filter_count++;
     1699                        return '<p>Filtered content.</p>';
     1700                };
     1701                add_filter( 'the_content', $filter_content );
     1702
     1703                wp_set_current_user( self::$editor_id );
     1704                $endpoint = new WP_REST_Posts_Controller( 'post' );
     1705                $request  = new WP_REST_REQUEST( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     1706
     1707                $request->set_param( 'context', 'edit' );
     1708                $request->set_param( '_fields', 'content.rendered' );
     1709
     1710                $post     = get_post( self::$post_id );
     1711                $response = $endpoint->prepare_item_for_response( $post, $request );
     1712
     1713                remove_filter( 'the_content', $filter_content );
     1714
     1715                $this->assertEquals(
     1716                        array(
     1717                                'id'      => self::$post_id,
     1718                                'content' => array(
     1719                                        'rendered' => '<p>Filtered content.</p>',
     1720                                ),
     1721                        ),
     1722                        $response->get_data()
     1723                );
     1724                $this->assertSame( 1, $filter_count );
     1725        }
     1726
     1727        /**
     1728         * @ticket 42094
     1729         */
     1730        public function test_prepare_item_skips_content_filter_if_not_needed() {
     1731                $filter_count   = 0;
     1732                $filter_content = function() use ( &$filter_count ) {
     1733                        $filter_count++;
     1734                        return '<p>Filtered content.</p>';
     1735                };
     1736                add_filter( 'the_content', $filter_content );
     1737
     1738                wp_set_current_user( self::$editor_id );
     1739                $endpoint = new WP_REST_Posts_Controller( 'post' );
     1740                $request  = new WP_REST_REQUEST( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
     1741
     1742                $request->set_param( 'context', 'edit' );
     1743                $request->set_param( '_fields', 'content.raw' );
     1744
     1745                $post     = get_post( self::$post_id );
     1746                $response = $endpoint->prepare_item_for_response( $post, $request );
     1747
     1748                remove_filter( 'the_content', $filter_content );
     1749
     1750                $this->assertEquals(
     1751                        array(
     1752                                'id'      => $post->ID,
     1753                                'content' => array(
     1754                                        'raw' => $post->post_content,
     1755                                ),
     1756                        ),
     1757                        $response->get_data()
     1758                );
     1759                $this->assertSame( 0, $filter_count );
     1760        }
     1761
    16921762        public function test_create_item() {
    16931763                wp_set_current_user( self::$editor_id );
    16941764