Make WordPress Core


Ignore:
Timestamp:
04/08/2026 03:47:24 PM (5 months ago)
Author:
jorgefilipecosta
Message:

Abilities: Strip internal schema keywords from abilities REST responses.

Remove WordPress-internal properties (sanitize_callback, validate_callback, arg_options) from ability input_schema and output_schema fields in REST responses. These properties are used server-side but are not valid JSON Schema keywords and cause client-side validators to fail.

Props jorgefilipecosta, ocean90, gziolo.
Fixes #65035.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php

    r61999 r62221  
    777777                $this->assertEmpty( $data, 'Should return empty array for non-existent category' );
    778778        }
     779
     780        /**
     781         * Test that WordPress-internal schema keywords are stripped from ability schemas in REST response.
     782         *
     783         * @ticket 65035
     784         */
     785        public function test_internal_schema_keywords_stripped_from_response(): void {
     786                $this->register_test_ability(
     787                        'test/with-internal-keywords',
     788                        array(
     789                                'label'               => 'Test Internal Keywords',
     790                                'description'         => 'Tests stripping of internal schema keywords',
     791                                'category'            => 'general',
     792                                'input_schema'        => array(
     793                                        'type'       => 'object',
     794                                        'properties' => array(
     795                                                'content' => array(
     796                                                        'type'              => 'string',
     797                                                        'description'       => 'The content value.',
     798                                                        'sanitize_callback' => 'sanitize_text_field',
     799                                                        'validate_callback' => 'is_string',
     800                                                        'arg_options'       => array( 'sanitize_callback' => 'wp_kses_post' ),
     801                                                ),
     802                                        ),
     803                                ),
     804                                'output_schema'       => array(
     805                                        'type'              => 'string',
     806                                        'sanitize_callback' => 'sanitize_text_field',
     807                                ),
     808                                'execute_callback'    => static function ( $input ) {
     809                                        return $input['content'];
     810                                },
     811                                'permission_callback' => '__return_true',
     812                                'meta'                => array( 'show_in_rest' => true ),
     813                        )
     814                );
     815
     816                $request  = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/with-internal-keywords' );
     817                $response = $this->server->dispatch( $request );
     818
     819                $this->assertSame( 200, $response->get_status() );
     820
     821                $data = $response->get_data();
     822                $this->assertArrayHasKey( 'input_schema', $data );
     823                $this->assertArrayHasKey( 'properties', $data['input_schema'] );
     824                $this->assertArrayHasKey( 'content', $data['input_schema']['properties'] );
     825                $this->assertArrayHasKey( 'output_schema', $data );
     826
     827                // Verify internal keywords are stripped from input_schema properties.
     828                $content_schema = $data['input_schema']['properties']['content'];
     829                $this->assertArrayNotHasKey( 'sanitize_callback', $content_schema );
     830                $this->assertArrayNotHasKey( 'validate_callback', $content_schema );
     831                $this->assertArrayNotHasKey( 'arg_options', $content_schema );
     832
     833                // Verify valid JSON Schema keywords are preserved.
     834                $this->assertSame( 'string', $content_schema['type'] );
     835                $this->assertSame( 'The content value.', $content_schema['description'] );
     836
     837                // Verify internal keywords are stripped from output_schema.
     838                $this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema'] );
     839                $this->assertSame( 'string', $data['output_schema']['type'] );
     840        }
     841
     842        /**
     843         * Test that internal schema keywords are stripped from nested sub-schema locations.
     844         *
     845         * @ticket 64098
     846         */
     847        public function test_internal_schema_keywords_stripped_from_nested_sub_schemas(): void {
     848                $this->register_test_ability(
     849                        'test/nested-internal-keywords',
     850                        array(
     851                                'label'               => 'Test Nested Keywords',
     852                                'description'         => 'Tests stripping from all sub-schema locations',
     853                                'category'            => 'general',
     854                                'input_schema'        => array(
     855                                        'type'                 => 'object',
     856                                        'anyOf'                => array(
     857                                                array(
     858                                                        'type'              => 'object',
     859                                                        'sanitize_callback' => 'sanitize_text_field',
     860                                                        'properties'        => array(
     861                                                                'value' => array(
     862                                                                        'type'              => 'string',
     863                                                                        'validate_callback' => 'is_string',
     864                                                                ),
     865                                                        ),
     866                                                ),
     867                                                array(
     868                                                        'type'        => 'number',
     869                                                        'arg_options' => array( 'sanitize_callback' => 'absint' ),
     870                                                ),
     871                                        ),
     872                                        'oneOf'                => array(
     873                                                array(
     874                                                        'type'              => 'string',
     875                                                        'sanitize_callback' => 'sanitize_text_field',
     876                                                ),
     877                                        ),
     878                                        'allOf'                => array(
     879                                                array(
     880                                                        'type'              => 'object',
     881                                                        'validate_callback' => 'rest_validate_request_arg',
     882                                                ),
     883                                        ),
     884                                        'not'                  => array(
     885                                                'type'        => 'null',
     886                                                'arg_options' => array( 'sanitize_callback' => 'absint' ),
     887                                        ),
     888                                        'patternProperties'    => array(
     889                                                '^S_' => array(
     890                                                        'type'              => 'string',
     891                                                        'sanitize_callback' => 'sanitize_text_field',
     892                                                ),
     893                                        ),
     894                                        'definitions'          => array(
     895                                                'address' => array(
     896                                                        'type'              => 'object',
     897                                                        'validate_callback' => 'rest_validate_request_arg',
     898                                                        'properties'        => array(
     899                                                                'street' => array(
     900                                                                        'type'              => 'string',
     901                                                                        'sanitize_callback' => 'sanitize_text_field',
     902                                                                ),
     903                                                        ),
     904                                                ),
     905                                        ),
     906                                        'dependencies'         => array(
     907                                                'bar' => array(
     908                                                        'type'              => 'object',
     909                                                        'validate_callback' => 'rest_validate_request_arg',
     910                                                        'properties'        => array(
     911                                                                'baz' => array(
     912                                                                        'type'              => 'string',
     913                                                                        'sanitize_callback' => 'sanitize_text_field',
     914                                                                ),
     915                                                        ),
     916                                                ),
     917                                                'qux' => array( 'bar' ),
     918                                        ),
     919                                        'additionalProperties' => array(
     920                                                'type'              => 'string',
     921                                                'sanitize_callback' => 'sanitize_text_field',
     922                                        ),
     923                                ),
     924                                'output_schema'       => array(
     925                                        'type'            => 'array',
     926                                        'items'           => array(
     927                                                array(
     928                                                        'type'              => 'string',
     929                                                        'validate_callback' => 'is_string',
     930                                                ),
     931                                                array(
     932                                                        'type'        => 'number',
     933                                                        'arg_options' => array( 'sanitize_callback' => 'absint' ),
     934                                                ),
     935                                        ),
     936                                        'additionalItems' => array(
     937                                                'type'              => 'boolean',
     938                                                'sanitize_callback' => 'rest_sanitize_boolean',
     939                                        ),
     940                                ),
     941                                'execute_callback'    => static function ( $input ) {
     942                                        return array();
     943                                },
     944                                'permission_callback' => '__return_true',
     945                                'meta'                => array( 'show_in_rest' => true ),
     946                        )
     947                );
     948
     949                $request  = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/nested-internal-keywords' );
     950                $response = $this->server->dispatch( $request );
     951
     952                $this->assertSame( 200, $response->get_status() );
     953
     954                $data = $response->get_data();
     955
     956                // Verify internal keywords are stripped from anyOf sub-schemas.
     957                $this->assertArrayHasKey( 'anyOf', $data['input_schema'] );
     958                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['anyOf'][0] );
     959                $this->assertSame( 'object', $data['input_schema']['anyOf'][0]['type'] );
     960                $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['anyOf'][0]['properties']['value'] );
     961                $this->assertSame( 'string', $data['input_schema']['anyOf'][0]['properties']['value']['type'] );
     962                $this->assertArrayNotHasKey( 'arg_options', $data['input_schema']['anyOf'][1] );
     963                $this->assertSame( 'number', $data['input_schema']['anyOf'][1]['type'] );
     964
     965                // Verify internal keywords are stripped from oneOf sub-schemas.
     966                $this->assertArrayHasKey( 'oneOf', $data['input_schema'] );
     967                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['oneOf'][0] );
     968                $this->assertSame( 'string', $data['input_schema']['oneOf'][0]['type'] );
     969
     970                // Verify internal keywords are stripped from allOf sub-schemas.
     971                $this->assertArrayHasKey( 'allOf', $data['input_schema'] );
     972                $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['allOf'][0] );
     973                $this->assertSame( 'object', $data['input_schema']['allOf'][0]['type'] );
     974
     975                // Verify internal keywords are stripped from not sub-schema.
     976                $this->assertArrayHasKey( 'not', $data['input_schema'] );
     977                $this->assertArrayNotHasKey( 'arg_options', $data['input_schema']['not'] );
     978                $this->assertSame( 'null', $data['input_schema']['not']['type'] );
     979
     980                // Verify internal keywords are stripped from patternProperties sub-schemas.
     981                $this->assertArrayHasKey( 'patternProperties', $data['input_schema'] );
     982                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['patternProperties']['^S_'] );
     983                $this->assertSame( 'string', $data['input_schema']['patternProperties']['^S_']['type'] );
     984
     985                // Verify internal keywords are stripped from dependencies schema values.
     986                $this->assertArrayHasKey( 'dependencies', $data['input_schema'] );
     987                $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['dependencies']['bar'] );
     988                $this->assertSame( 'object', $data['input_schema']['dependencies']['bar']['type'] );
     989                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['dependencies']['bar']['properties']['baz'] );
     990                $this->assertSame( 'string', $data['input_schema']['dependencies']['bar']['properties']['baz']['type'] );
     991                // Property dependencies (numeric arrays) should pass through unchanged.
     992                $this->assertSame( array( 'bar' ), $data['input_schema']['dependencies']['qux'] );
     993
     994                // Verify internal keywords are stripped from definitions sub-schemas.
     995                $this->assertArrayHasKey( 'definitions', $data['input_schema'] );
     996                $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['definitions']['address'] );
     997                $this->assertSame( 'object', $data['input_schema']['definitions']['address']['type'] );
     998                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['definitions']['address']['properties']['street'] );
     999                $this->assertSame( 'string', $data['input_schema']['definitions']['address']['properties']['street']['type'] );
     1000
     1001                // Verify internal keywords are stripped from additionalProperties sub-schema.
     1002                $this->assertArrayHasKey( 'additionalProperties', $data['input_schema'] );
     1003                $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['additionalProperties'] );
     1004                $this->assertSame( 'string', $data['input_schema']['additionalProperties']['type'] );
     1005
     1006                // Verify internal keywords are stripped from tuple-style items sub-schemas.
     1007                $this->assertArrayHasKey( 'items', $data['output_schema'] );
     1008                $this->assertCount( 2, $data['output_schema']['items'] );
     1009                $this->assertArrayNotHasKey( 'validate_callback', $data['output_schema']['items'][0] );
     1010                $this->assertSame( 'string', $data['output_schema']['items'][0]['type'] );
     1011                $this->assertArrayNotHasKey( 'arg_options', $data['output_schema']['items'][1] );
     1012                $this->assertSame( 'number', $data['output_schema']['items'][1]['type'] );
     1013
     1014                // Verify internal keywords are stripped from additionalItems sub-schema.
     1015                $this->assertArrayHasKey( 'additionalItems', $data['output_schema'] );
     1016                $this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema']['additionalItems'] );
     1017                $this->assertSame( 'boolean', $data['output_schema']['additionalItems']['type'] );
     1018        }
    7791019}
Note: See TracChangeset for help on using the changeset viewer.