Make WordPress Core

Changeset 41758


Ignore:
Timestamp:
10/05/2017 12:18:44 AM (7 years ago)
Author:
kadamwhite
Message:

REST API: Support objects in settings schema.

Enables register_setting to accept an object as its schema value, allowing settings to accept non-scalar values through the REST API.
This whitelists the added type in the settings controller, and passes properties from argument registration into the validation functions.

Props joehoyle.
See #38583.

Location:
trunk
Files:
3 edited

Legend:

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

    r41591 r41758  
    546546            }
    547547
    548             foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) {
     548            foreach ( array( 'type', 'format', 'enum', 'items', 'properties' ) as $schema_prop ) {
    549549                if ( isset( $params[ $schema_prop ] ) ) {
    550550                    $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php

    r41731 r41758  
    120120     */
    121121    protected function prepare_value( $value, $schema ) {
    122         // If the value is not a scalar, it's not possible to cast it to anything.
    123         if ( ! is_scalar( $value ) ) {
     122        // If the value is not valid by the schema, set the value to null. Null
     123        // values are specifcally non-destructive so this will not cause overwriting
     124        // the current invalid value to null.
     125        if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
    124126            return null;
    125127        }
    126 
    127         switch ( $schema['type'] ) {
    128             case 'string':
    129                 return (string) $value;
    130             case 'integer':
    131                 return (int) $value;
    132             case 'number':
    133                 return (float) $value;
    134             case 'boolean':
    135                 return (bool) $value;
    136             default:
    137                 return null;
    138         }
     128        return rest_sanitize_value_from_schema( $value, $schema );
    139129    }
    140130
     
    149139    public function update_item( $request ) {
    150140        $options = $this->get_registered_options();
     141
    151142        $params  = $request->get_params();
    152143
     
    188179                 * To protect clients from accidentally including the null
    189180                 * values from a response object in a request, we do not allow
    190                  * options with non-scalar values to be updated to null.
     181                 * options with values that don't pass validation to be updated to null.
    191182                 * Without this added protection a client could mistakenly
    192                  * delete all options that have non-scalar values from the
     183                 * delete all options that have invalid values from the
    193184                 * database.
    194185                 */
    195                 if ( ! is_scalar( get_option( $args['option_name'], false ) ) ) {
     186                if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
    196187                    return new WP_Error(
    197188                        'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 )
     
    254245             * to be updated with arbitrary values that we can't do decent sanitizing for.
    255246             */
    256             if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean' ), true ) ) {
     247            if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
    257248                continue;
    258249            }
  • trunk/tests/phpunit/tests/rest-api/rest-settings-controller.php

    r41228 r41758  
    128128    }
    129129
     130    public function test_get_item_with_custom_array_setting() {
     131        wp_set_current_user( self::$administrator );
     132
     133        register_setting( 'somegroup', 'mycustomsetting', array(
     134            'show_in_rest' => array(
     135                'schema' => array(
     136                    'type'    => 'array',
     137                    'items'   => array(
     138                        'type' => 'integer',
     139                    ),
     140                ),
     141            ),
     142            'type'         => 'array',
     143        ) );
     144
     145        // Array is cast to correct types.
     146        update_option( 'mycustomsetting', array( '1', '2' ) );
     147        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     148        $response = $this->server->dispatch( $request );
     149        $data = $response->get_data();
     150        $this->assertEquals( array( 1, 2 ), $data['mycustomsetting'] );
     151
     152        // Empty array works as expected.
     153        update_option( 'mycustomsetting', array() );
     154        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     155        $response = $this->server->dispatch( $request );
     156        $data = $response->get_data();
     157        $this->assertEquals( array(), $data['mycustomsetting'] );
     158
     159        // Invalid value
     160        update_option( 'mycustomsetting', array( array( 1 ) ) );
     161        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     162        $response = $this->server->dispatch( $request );
     163        $data = $response->get_data();
     164        $this->assertEquals( null, $data['mycustomsetting'] );
     165
     166        // No option value
     167        delete_option( 'mycustomsetting' );
     168        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     169        $response = $this->server->dispatch( $request );
     170        $data = $response->get_data();
     171        $this->assertEquals( null, $data['mycustomsetting'] );
     172
     173        unregister_setting( 'somegroup', 'mycustomsetting' );
     174    }
     175
     176    public function test_get_item_with_custom_object_setting() {
     177        wp_set_current_user( self::$administrator );
     178
     179        register_setting( 'somegroup', 'mycustomsetting', array(
     180            'show_in_rest' => array(
     181                'schema' => array(
     182                    'type'    => 'object',
     183                    'properties' => array(
     184                        'a' => array(
     185                            'type' => 'integer',
     186                        ),
     187                    ),
     188                ),
     189            ),
     190            'type'         => 'object',
     191        ) );
     192
     193        // Object is cast to correct types.
     194        update_option( 'mycustomsetting', array( 'a' => '1' ) );
     195        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     196        $response = $this->server->dispatch( $request );
     197        $data = $response->get_data();
     198        $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] );
     199
     200        // Empty array works as expected.
     201        update_option( 'mycustomsetting', array() );
     202        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     203        $response = $this->server->dispatch( $request );
     204        $data = $response->get_data();
     205        $this->assertEquals( array(), $data['mycustomsetting'] );
     206
     207        // Invalid value
     208        update_option( 'mycustomsetting', array( 'a' => 1, 'b' => 2 ) );
     209        $request = new WP_REST_Request( 'GET', '/wp/v2/settings' );
     210        $response = $this->server->dispatch( $request );
     211        $data = $response->get_data();
     212        $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] );
     213
     214        unregister_setting( 'somegroup', 'mycustomsetting' );
     215    }
     216
    130217    public function get_setting_custom_callback( $result, $name, $args ) {
    131218        switch ( $name ) {
     
    216303        $data = $response->get_data();
    217304        $this->assertEquals( null, $data['mycustomsettinginrest'] );
     305        unregister_setting( 'somegroup', 'mycustomsetting' );
    218306    }
    219307
     
    243331    }
    244332
     333    public function test_update_item_with_array() {
     334        register_setting( 'somegroup', 'mycustomsetting', array(
     335            'show_in_rest' => array(
     336                'schema' => array(
     337                    'type'  => 'array',
     338                    'items' => array(
     339                        'type' => 'integer',
     340                    ),
     341                ),
     342            ),
     343            'type'         => 'array',
     344        ) );
     345
     346        // We have to re-register the route, as the args changes based off registered settings.
     347        $this->server->override_by_default = true;
     348        $this->endpoint->register_routes();
     349        wp_set_current_user( self::$administrator );
     350
     351        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     352        $request->set_param( 'mycustomsetting', array( '1', '2' ) );
     353        $response = $this->server->dispatch( $request );
     354        $data = $response->get_data();
     355        $this->assertEquals( array( 1, 2 ), $data['mycustomsetting'] );
     356        $this->assertEquals( array( 1, 2 ), get_option( 'mycustomsetting' ) );
     357
     358        // Setting an empty array.
     359        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     360        $request->set_param( 'mycustomsetting', array() );
     361        $response = $this->server->dispatch( $request );
     362        $data = $response->get_data();
     363        $this->assertEquals( array(), $data['mycustomsetting'] );
     364        $this->assertEquals( array(), get_option( 'mycustomsetting' ) );
     365
     366        // Setting an invalid array.
     367        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     368        $request->set_param( 'mycustomsetting', array( 'invalid' ) );
     369        $response = $this->server->dispatch( $request );
     370
     371        $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     372        unregister_setting( 'somegroup', 'mycustomsetting' );
     373    }
     374
     375    public function test_update_item_with_object() {
     376        register_setting( 'somegroup', 'mycustomsetting', array(
     377            'show_in_rest' => array(
     378                'schema' => array(
     379                    'type'       => 'object',
     380                    'properties' => array(
     381                        'a' => array(
     382                            'type' => 'integer',
     383                        ),
     384                    ),
     385                ),
     386            ),
     387            'type'         => 'object',
     388        ) );
     389
     390        // We have to re-register the route, as the args changes based off registered settings.
     391        $this->server->override_by_default = true;
     392        $this->endpoint->register_routes();
     393        wp_set_current_user( self::$administrator );
     394
     395        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     396        $request->set_param( 'mycustomsetting', array( 'a' => 1 ) );
     397        $response = $this->server->dispatch( $request );
     398        $data = $response->get_data();
     399        $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] );
     400        $this->assertEquals( array( 'a' => 1 ), get_option( 'mycustomsetting' ) );
     401
     402        // Setting an empty object.
     403        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     404        $request->set_param( 'mycustomsetting', array() );
     405        $response = $this->server->dispatch( $request );
     406        $data = $response->get_data();
     407        $this->assertEquals( array(), $data['mycustomsetting'] );
     408        $this->assertEquals( array(), get_option( 'mycustomsetting' ) );
     409
     410        // Setting an invalid object.
     411        $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' );
     412        $request->set_param( 'mycustomsetting', array( 'a' => 'invalid' ) );
     413        $response = $this->server->dispatch( $request );
     414        $this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
     415        unregister_setting( 'somegroup', 'mycustomsetting' );
     416    }
     417
    245418    public function test_update_item_with_filter() {
    246419        wp_set_current_user( self::$administrator );
Note: See TracChangeset for help on using the changeset viewer.