|
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 } |