ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-settings-controller.php (10395B)


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