WordPress/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php
Rachel Baker 4a37a04d0f REST API: Return WP_Error when a client is attempting to update an option with a non-scalar value to null.
A `null` value is returned in the response for any option that has a non-scalar value.

To protect clients from accidentally including the `null` values from a response object in a request, we do not allow options with non-scalar values to be updated to `null`. Without this added protection a client could mistakenly delete all options that have non-scalar values from the database.

Props joehoyle, rachelbaker.
Fixes #38527.
Built from https://develop.svn.wordpress.org/trunk@38982


git-svn-id: http://core.svn.wordpress.org/trunk@38925 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2016-10-27 16:08:45 +00:00

254 lines
7.4 KiB
PHP

<?php
/**
* Manage a WordPress site's settings.
*/
class WP_REST_Settings_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 4.7.0
* @access public
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'settings';
}
/**
* Register the routes for the objects of the controller.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'args' => array(),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check if a given request has access to read and manage settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return boolean
*/
public function get_item_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
/**
* Get the settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|array
*/
public function get_item( $request ) {
$options = $this->get_registered_options();
$response = array();
foreach ( $options as $name => $args ) {
/**
* Filters the value of a setting recognized by the REST API.
*
* Allow hijacking the setting value and overriding the built-in behavior by returning a
* non-null value. The returned value will be presented as the setting value instead.
*
* @since 4.7.0
*
* @param mixed $result Value to use for the requested setting. Can be a scalar
* matching the registered schema for the setting, or null to
* follow the default `get_option` behavior.
* @param string $name Setting name (as shown in REST API responses).
* @param array $args Arguments passed to `register_setting()` for this setting.
*/
$response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
if ( is_null( $response[ $name ] ) ) {
// Default to a null value as "null" in the response means "not set".
$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
}
// Because get_option() is lossy, we have to
// cast values to the type they are registered with.
$response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
}
return $response;
}
/**
* Prepare a value for output based off a schema array.
*
* @param mixed $value
* @param array $schema
* @return mixed
*/
protected function prepare_value( $value, $schema ) {
// If the value is not a scalar, it's not possible to cast it to
// anything.
if ( ! is_scalar( $value ) ) {
return null;
}
switch ( $schema['type'] ) {
case 'string':
return (string) $value;
case 'number':
return (float) $value;
case 'boolean':
return (bool) $value;
default:
return null;
}
}
/**
* Update settings for the settings object.
*
* @param WP_REST_Request $request Full detail about the request.
* @return WP_Error|array
*/
public function update_item( $request ) {
$options = $this->get_registered_options();
$params = $request->get_params();
foreach ( $options as $name => $args ) {
if ( ! array_key_exists( $name, $params ) ) {
continue;
}
/**
* Filters whether to preempt a setting value update.
*
* Allow hijacking the setting update logic and overriding the built-in behavior by
* returning true.
*
* @since 4.7.0
*
* @param boolean $result Whether to override the default behavior for updating the
* value of a setting.
* @param string $name Setting name (as shown in REST API responses).
* @param mixed $value Updated setting value.
* @param array $args Arguments passed to `register_setting()` for this setting.
*/
$updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
if ( $updated ) {
continue;
}
/**
* A `null` value for an option would have the same effect as
* deleting the option from the database, and relying on the
* default value.
*/
if ( is_null( $request[ $name ] ) ) {
/**
* A `null` value is returned in the response for any option
* that has a non-scalar value.
*
* To protect clients from accidentally including the `null`
* values from a response object in a request, we do not allow
* options with non-scalar values to be updated to `null`.
* Without this added protection a client could mistakenly
* delete all options that have non-scalar values from the
* database.
*/
if ( ! is_scalar( get_option( $args['option_name'], false ) ) ) {
return new WP_Error(
'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 )
);
}
delete_option( $args['option_name'] );
} else {
update_option( $args['option_name'], $request[ $name ] );
}
}
return $this->get_item( $request );
}
/**
* Get all the registered options for the Settings API
*
* @return array
*/
protected function get_registered_options() {
$rest_options = array();
foreach ( get_registered_settings() as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$defaults = array(
'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
'schema' => array(),
);
$rest_args = array_merge( $defaults, $rest_args );
$default_schema = array(
'type' => empty( $args['type'] ) ? null : $args['type'],
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
$rest_args['option_name'] = $name;
// Skip over settings that don't have a defined type in the schema.
if ( empty( $rest_args['schema']['type'] ) ) {
continue;
}
// Whitelist the supported types for settings, as we don't want invalid types
// to be updated with arbitrary values that we can't do decent sanitizing for.
if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'string', 'boolean' ), true ) ) {
continue;
}
$rest_options[ $rest_args['name'] ] = $rest_args;
}
return $rest_options;
}
/**
* Get the site setting schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$options = $this->get_registered_options();
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'settings',
'type' => 'object',
'properties' => array(),
);
foreach ( $options as $option_name => $option ) {
$schema['properties'][ $option_name ] = $option['schema'];
}
return $this->add_additional_fields_schema( $schema );
}
}