wp/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 <?php
       
     2 /**
       
     3  * REST API: WP_REST_Settings_Controller class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage REST_API
       
     7  * @since 4.7.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Core class used to manage a site's settings via the REST API.
       
    12  *
       
    13  * @since 4.7.0
       
    14  *
       
    15  * @see WP_REST_Controller
       
    16  */
       
    17 class WP_REST_Settings_Controller extends WP_REST_Controller {
       
    18 
       
    19 	/**
       
    20 	 * Constructor.
       
    21 	 *
       
    22 	 * @since 4.7.0
       
    23 	 */
       
    24 	public function __construct() {
       
    25 		$this->namespace = 'wp/v2';
       
    26 		$this->rest_base = 'settings';
       
    27 	}
       
    28 
       
    29 	/**
       
    30 	 * Registers the routes for the objects of the controller.
       
    31 	 *
       
    32 	 * @since 4.7.0
       
    33 	 *
       
    34 	 * @see register_rest_route()
       
    35 	 */
       
    36 	public function register_routes() {
       
    37 
       
    38 		register_rest_route( $this->namespace, '/' . $this->rest_base, array(
       
    39 			array(
       
    40 				'methods'             => WP_REST_Server::READABLE,
       
    41 				'callback'            => array( $this, 'get_item' ),
       
    42 				'args'                => array(),
       
    43 				'permission_callback' => array( $this, 'get_item_permissions_check' ),
       
    44 			),
       
    45 			array(
       
    46 				'methods'             => WP_REST_Server::EDITABLE,
       
    47 				'callback'            => array( $this, 'update_item' ),
       
    48 				'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
       
    49 				'permission_callback' => array( $this, 'get_item_permissions_check' ),
       
    50 			),
       
    51 			'schema' => array( $this, 'get_public_item_schema' ),
       
    52 		) );
       
    53 
       
    54 	}
       
    55 
       
    56 	/**
       
    57 	 * Checks if a given request has access to read and manage settings.
       
    58 	 *
       
    59 	 * @since 4.7.0
       
    60 	 *
       
    61 	 * @param WP_REST_Request $request Full details about the request.
       
    62 	 * @return bool True if the request has read access for the item, otherwise false.
       
    63 	 */
       
    64 	public function get_item_permissions_check( $request ) {
       
    65 		return current_user_can( 'manage_options' );
       
    66 	}
       
    67 
       
    68 	/**
       
    69 	 * Retrieves the settings.
       
    70 	 *
       
    71 	 * @since 4.7.0
       
    72 	 *
       
    73 	 * @param WP_REST_Request $request Full details about the request.
       
    74 	 * @return array|WP_Error Array on success, or WP_Error object on failure.
       
    75 	 */
       
    76 	public function get_item( $request ) {
       
    77 		$options  = $this->get_registered_options();
       
    78 		$response = array();
       
    79 
       
    80 		foreach ( $options as $name => $args ) {
       
    81 			/**
       
    82 			 * Filters the value of a setting recognized by the REST API.
       
    83 			 *
       
    84 			 * Allow hijacking the setting value and overriding the built-in behavior by returning a
       
    85 			 * non-null value.  The returned value will be presented as the setting value instead.
       
    86 			 *
       
    87 			 * @since 4.7.0
       
    88 			 *
       
    89 			 * @param mixed  $result Value to use for the requested setting. Can be a scalar
       
    90 			 *                       matching the registered schema for the setting, or null to
       
    91 			 *                       follow the default get_option() behavior.
       
    92 			 * @param string $name   Setting name (as shown in REST API responses).
       
    93 			 * @param array  $args   Arguments passed to register_setting() for this setting.
       
    94 			 */
       
    95 			$response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
       
    96 
       
    97 			if ( is_null( $response[ $name ] ) ) {
       
    98 				// Default to a null value as "null" in the response means "not set".
       
    99 				$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
       
   100 			}
       
   101 
       
   102 			/*
       
   103 			 * Because get_option() is lossy, we have to
       
   104 			 * cast values to the type they are registered with.
       
   105 			 */
       
   106 			$response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
       
   107 		}
       
   108 
       
   109 		return $response;
       
   110 	}
       
   111 
       
   112 	/**
       
   113 	 * Prepares a value for output based off a schema array.
       
   114 	 *
       
   115 	 * @since 4.7.0
       
   116 	 *
       
   117 	 * @param mixed $value  Value to prepare.
       
   118 	 * @param array $schema Schema to match.
       
   119 	 * @return mixed The prepared value.
       
   120 	 */
       
   121 	protected function prepare_value( $value, $schema ) {
       
   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 ) ) ) {
       
   126 			return null;
       
   127 		}
       
   128 		return rest_sanitize_value_from_schema( $value, $schema );
       
   129 	}
       
   130 
       
   131 	/**
       
   132 	 * Updates settings for the settings object.
       
   133 	 *
       
   134 	 * @since 4.7.0
       
   135 	 *
       
   136 	 * @param WP_REST_Request $request Full details about the request.
       
   137 	 * @return array|WP_Error Array on success, or error object on failure.
       
   138 	 */
       
   139 	public function update_item( $request ) {
       
   140 		$options = $this->get_registered_options();
       
   141 
       
   142 		$params  = $request->get_params();
       
   143 
       
   144 		foreach ( $options as $name => $args ) {
       
   145 			if ( ! array_key_exists( $name, $params ) ) {
       
   146 				continue;
       
   147 			}
       
   148 
       
   149 			/**
       
   150 			 * Filters whether to preempt a setting value update.
       
   151 			 *
       
   152 			 * Allows hijacking the setting update logic and overriding the built-in behavior by
       
   153 			 * returning true.
       
   154 			 *
       
   155 			 * @since 4.7.0
       
   156 			 *
       
   157 			 * @param bool   $result Whether to override the default behavior for updating the
       
   158 			 *                       value of a setting.
       
   159 			 * @param string $name   Setting name (as shown in REST API responses).
       
   160 			 * @param mixed  $value  Updated setting value.
       
   161 			 * @param array  $args   Arguments passed to register_setting() for this setting.
       
   162 			 */
       
   163 			$updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
       
   164 
       
   165 			if ( $updated ) {
       
   166 				continue;
       
   167 			}
       
   168 
       
   169 			/*
       
   170 			 * A null value for an option would have the same effect as
       
   171 			 * deleting the option from the database, and relying on the
       
   172 			 * default value.
       
   173 			 */
       
   174 			if ( is_null( $request[ $name ] ) ) {
       
   175 				/*
       
   176 				 * A null value is returned in the response for any option
       
   177 				 * that has a non-scalar value.
       
   178 				 *
       
   179 				 * To protect clients from accidentally including the null
       
   180 				 * values from a response object in a request, we do not allow
       
   181 				 * options with values that don't pass validation to be updated to null.
       
   182 				 * Without this added protection a client could mistakenly
       
   183 				 * delete all options that have invalid values from the
       
   184 				 * database.
       
   185 				 */
       
   186 				if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
       
   187 					return new WP_Error(
       
   188 						'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 )
       
   189 					);
       
   190 				}
       
   191 
       
   192 				delete_option( $args['option_name'] );
       
   193 			} else {
       
   194 				update_option( $args['option_name'], $request[ $name ] );
       
   195 			}
       
   196 		}
       
   197 
       
   198 		return $this->get_item( $request );
       
   199 	}
       
   200 
       
   201 	/**
       
   202 	 * Retrieves all of the registered options for the Settings API.
       
   203 	 *
       
   204 	 * @since 4.7.0
       
   205 	 *
       
   206 	 * @return array Array of registered options.
       
   207 	 */
       
   208 	protected function get_registered_options() {
       
   209 		$rest_options = array();
       
   210 
       
   211 		foreach ( get_registered_settings() as $name => $args ) {
       
   212 			if ( empty( $args['show_in_rest'] ) ) {
       
   213 				continue;
       
   214 			}
       
   215 
       
   216 			$rest_args = array();
       
   217 
       
   218 			if ( is_array( $args['show_in_rest'] ) ) {
       
   219 				$rest_args = $args['show_in_rest'];
       
   220 			}
       
   221 
       
   222 			$defaults = array(
       
   223 				'name'   => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
       
   224 				'schema' => array(),
       
   225 			);
       
   226 
       
   227 			$rest_args = array_merge( $defaults, $rest_args );
       
   228 
       
   229 			$default_schema = array(
       
   230 				'type'        => empty( $args['type'] ) ? null : $args['type'],
       
   231 				'description' => empty( $args['description'] ) ? '' : $args['description'],
       
   232 				'default'     => isset( $args['default'] ) ? $args['default'] : null,
       
   233 			);
       
   234 
       
   235 			$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
       
   236 			$rest_args['option_name'] = $name;
       
   237 
       
   238 			// Skip over settings that don't have a defined type in the schema.
       
   239 			if ( empty( $rest_args['schema']['type'] ) ) {
       
   240 				continue;
       
   241 			}
       
   242 
       
   243 			/*
       
   244 			 * Whitelist the supported types for settings, as we don't want invalid types
       
   245 			 * to be updated with arbitrary values that we can't do decent sanitizing for.
       
   246 			 */
       
   247 			if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
       
   248 				continue;
       
   249 			}
       
   250 
       
   251 			$rest_args['schema'] = $this->set_additional_properties_to_false( $rest_args['schema'] );
       
   252 
       
   253 			$rest_options[ $rest_args['name'] ] = $rest_args;
       
   254 		}
       
   255 
       
   256 		return $rest_options;
       
   257 	}
       
   258 
       
   259 	/**
       
   260 	 * Retrieves the site setting schema, conforming to JSON Schema.
       
   261 	 *
       
   262 	 * @since 4.7.0
       
   263 	 *
       
   264 	 * @return array Item schema data.
       
   265 	 */
       
   266 	public function get_item_schema() {
       
   267 		$options = $this->get_registered_options();
       
   268 
       
   269 		$schema = array(
       
   270 			'$schema'    => 'http://json-schema.org/draft-04/schema#',
       
   271 			'title'      => 'settings',
       
   272 			'type'       => 'object',
       
   273 			'properties' => array(),
       
   274 		);
       
   275 
       
   276 		foreach ( $options as $option_name => $option ) {
       
   277 			$schema['properties'][ $option_name ] = $option['schema'];
       
   278 			$schema['properties'][ $option_name ]['arg_options'] = array(
       
   279 				'sanitize_callback' => array( $this, 'sanitize_callback' ),
       
   280 			);
       
   281 		}
       
   282 
       
   283 		return $this->add_additional_fields_schema( $schema );
       
   284 	}
       
   285 
       
   286 	/**
       
   287 	 * Custom sanitize callback used for all options to allow the use of 'null'.
       
   288 	 *
       
   289 	 * By default, the schema of settings will throw an error if a value is set to
       
   290 	 * `null` as it's not a valid value for something like "type => string". We
       
   291 	 * provide a wrapper sanitizer to whitelist the use of `null`.
       
   292 	 *
       
   293 	 * @since 4.7.0
       
   294 	 *
       
   295 	 * @param  mixed           $value   The value for the setting.
       
   296 	 * @param  WP_REST_Request $request The request object.
       
   297 	 * @param  string          $param   The parameter name.
       
   298 	 * @return mixed|WP_Error
       
   299 	 */
       
   300 	public function sanitize_callback( $value, $request, $param ) {
       
   301 		if ( is_null( $value ) ) {
       
   302 			return $value;
       
   303 		}
       
   304 		return rest_parse_request_arg( $value, $request, $param );
       
   305 	}
       
   306 
       
   307 	/**
       
   308 	 * Recursively add additionalProperties = false to all objects in a schema.
       
   309 	 *
       
   310 	 * This is need to restrict properties of objects in settings values to only
       
   311 	 * registered items, as the REST API will allow additional properties by
       
   312 	 * default.
       
   313 	 *
       
   314 	 * @since 4.9.0
       
   315 	 *
       
   316 	 * @param array $schema The schema array.
       
   317 	 * @return array
       
   318 	 */
       
   319 	protected function set_additional_properties_to_false( $schema ) {
       
   320 		switch ( $schema['type'] ) {
       
   321 			case 'object':
       
   322 				foreach ( $schema['properties'] as $key => $child_schema ) {
       
   323 					$schema['properties'][ $key ] = $this->set_additional_properties_to_false( $child_schema );
       
   324 				}
       
   325 				$schema['additionalProperties'] = false;
       
   326 				break;
       
   327 			case 'array':
       
   328 				$schema['items'] = $this->set_additional_properties_to_false( $schema['items'] );
       
   329 				break;
       
   330 		}
       
   331 
       
   332 		return $schema;
       
   333 	}
       
   334 }