|
1 <?php |
|
2 /** |
|
3 * REST API: WP_REST_Meta_Fields class |
|
4 * |
|
5 * @package WordPress |
|
6 * @subpackage REST_API |
|
7 * @since 4.7.0 |
|
8 */ |
|
9 |
|
10 /** |
|
11 * Core class to manage meta values for an object via the REST API. |
|
12 * |
|
13 * @since 4.7.0 |
|
14 */ |
|
15 abstract class WP_REST_Meta_Fields { |
|
16 |
|
17 /** |
|
18 * Retrieves the object meta type. |
|
19 * |
|
20 * @since 4.7.0 |
|
21 * |
|
22 * @return string One of 'post', 'comment', 'term', 'user', or anything |
|
23 * else supported by `_get_meta_table()`. |
|
24 */ |
|
25 abstract protected function get_meta_type(); |
|
26 |
|
27 /** |
|
28 * Retrieves the object meta subtype. |
|
29 * |
|
30 * @since 4.9.8 |
|
31 * |
|
32 * @return string Subtype for the meta type, or empty string if no specific subtype. |
|
33 */ |
|
34 protected function get_meta_subtype() { |
|
35 return ''; |
|
36 } |
|
37 |
|
38 /** |
|
39 * Retrieves the object type for register_rest_field(). |
|
40 * |
|
41 * @since 4.7.0 |
|
42 * |
|
43 * @return string The REST field type, such as post type name, taxonomy name, 'comment', or `user`. |
|
44 */ |
|
45 abstract protected function get_rest_field_type(); |
|
46 |
|
47 /** |
|
48 * Registers the meta field. |
|
49 * |
|
50 * @since 4.7.0 |
|
51 * |
|
52 * @see register_rest_field() |
|
53 */ |
|
54 public function register_field() { |
|
55 register_rest_field( $this->get_rest_field_type(), 'meta', array( |
|
56 'get_callback' => array( $this, 'get_value' ), |
|
57 'update_callback' => array( $this, 'update_value' ), |
|
58 'schema' => $this->get_field_schema(), |
|
59 )); |
|
60 } |
|
61 |
|
62 /** |
|
63 * Retrieves the meta field value. |
|
64 * |
|
65 * @since 4.7.0 |
|
66 * |
|
67 * @param int $object_id Object ID to fetch meta for. |
|
68 * @param WP_REST_Request $request Full details about the request. |
|
69 * @return WP_Error|object Object containing the meta values by name, otherwise WP_Error object. |
|
70 */ |
|
71 public function get_value( $object_id, $request ) { |
|
72 $fields = $this->get_registered_fields(); |
|
73 $response = array(); |
|
74 |
|
75 foreach ( $fields as $meta_key => $args ) { |
|
76 $name = $args['name']; |
|
77 $all_values = get_metadata( $this->get_meta_type(), $object_id, $meta_key, false ); |
|
78 if ( $args['single'] ) { |
|
79 if ( empty( $all_values ) ) { |
|
80 $value = $args['schema']['default']; |
|
81 } else { |
|
82 $value = $all_values[0]; |
|
83 } |
|
84 $value = $this->prepare_value_for_response( $value, $request, $args ); |
|
85 } else { |
|
86 $value = array(); |
|
87 foreach ( $all_values as $row ) { |
|
88 $value[] = $this->prepare_value_for_response( $row, $request, $args ); |
|
89 } |
|
90 } |
|
91 |
|
92 $response[ $name ] = $value; |
|
93 } |
|
94 |
|
95 return $response; |
|
96 } |
|
97 |
|
98 /** |
|
99 * Prepares a meta value for a response. |
|
100 * |
|
101 * This is required because some native types cannot be stored correctly |
|
102 * in the database, such as booleans. We need to cast back to the relevant |
|
103 * type before passing back to JSON. |
|
104 * |
|
105 * @since 4.7.0 |
|
106 * |
|
107 * @param mixed $value Meta value to prepare. |
|
108 * @param WP_REST_Request $request Current request object. |
|
109 * @param array $args Options for the field. |
|
110 * @return mixed Prepared value. |
|
111 */ |
|
112 protected function prepare_value_for_response( $value, $request, $args ) { |
|
113 if ( ! empty( $args['prepare_callback'] ) ) { |
|
114 $value = call_user_func( $args['prepare_callback'], $value, $request, $args ); |
|
115 } |
|
116 |
|
117 return $value; |
|
118 } |
|
119 |
|
120 /** |
|
121 * Updates meta values. |
|
122 * |
|
123 * @since 4.7.0 |
|
124 * |
|
125 * @param array $meta Array of meta parsed from the request. |
|
126 * @param int $object_id Object ID to fetch meta for. |
|
127 * @return WP_Error|null WP_Error if one occurs, null on success. |
|
128 */ |
|
129 public function update_value( $meta, $object_id ) { |
|
130 $fields = $this->get_registered_fields(); |
|
131 foreach ( $fields as $meta_key => $args ) { |
|
132 $name = $args['name']; |
|
133 if ( ! array_key_exists( $name, $meta ) ) { |
|
134 continue; |
|
135 } |
|
136 |
|
137 /* |
|
138 * A null value means reset the field, which is essentially deleting it |
|
139 * from the database and then relying on the default value. |
|
140 */ |
|
141 if ( is_null( $meta[ $name ] ) ) { |
|
142 $result = $this->delete_meta_value( $object_id, $meta_key, $name ); |
|
143 if ( is_wp_error( $result ) ) { |
|
144 return $result; |
|
145 } |
|
146 continue; |
|
147 } |
|
148 |
|
149 $is_valid = rest_validate_value_from_schema( $meta[ $name ], $args['schema'], 'meta.' . $name ); |
|
150 if ( is_wp_error( $is_valid ) ) { |
|
151 $is_valid->add_data( array( 'status' => 400 ) ); |
|
152 return $is_valid; |
|
153 } |
|
154 |
|
155 $value = rest_sanitize_value_from_schema( $meta[ $name ], $args['schema'] ); |
|
156 |
|
157 if ( $args['single'] ) { |
|
158 $result = $this->update_meta_value( $object_id, $meta_key, $name, $value ); |
|
159 } else { |
|
160 $result = $this->update_multi_meta_value( $object_id, $meta_key, $name, $value ); |
|
161 } |
|
162 |
|
163 if ( is_wp_error( $result ) ) { |
|
164 return $result; |
|
165 } |
|
166 } |
|
167 |
|
168 return null; |
|
169 } |
|
170 |
|
171 /** |
|
172 * Deletes a meta value for an object. |
|
173 * |
|
174 * @since 4.7.0 |
|
175 * |
|
176 * @param int $object_id Object ID the field belongs to. |
|
177 * @param string $meta_key Key for the field. |
|
178 * @param string $name Name for the field that is exposed in the REST API. |
|
179 * @return bool|WP_Error True if meta field is deleted, WP_Error otherwise. |
|
180 */ |
|
181 protected function delete_meta_value( $object_id, $meta_key, $name ) { |
|
182 $meta_type = $this->get_meta_type(); |
|
183 if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $meta_key ) ) { |
|
184 return new WP_Error( |
|
185 'rest_cannot_delete', |
|
186 /* translators: %s: custom field key */ |
|
187 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), |
|
188 array( 'key' => $name, 'status' => rest_authorization_required_code() ) |
|
189 ); |
|
190 } |
|
191 |
|
192 if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ) ) ) { |
|
193 return new WP_Error( |
|
194 'rest_meta_database_error', |
|
195 __( 'Could not delete meta value from database.' ), |
|
196 array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR ) |
|
197 ); |
|
198 } |
|
199 |
|
200 return true; |
|
201 } |
|
202 |
|
203 /** |
|
204 * Updates multiple meta values for an object. |
|
205 * |
|
206 * Alters the list of values in the database to match the list of provided values. |
|
207 * |
|
208 * @since 4.7.0 |
|
209 * |
|
210 * @param int $object_id Object ID to update. |
|
211 * @param string $meta_key Key for the custom field. |
|
212 * @param string $name Name for the field that is exposed in the REST API. |
|
213 * @param array $values List of values to update to. |
|
214 * @return bool|WP_Error True if meta fields are updated, WP_Error otherwise. |
|
215 */ |
|
216 protected function update_multi_meta_value( $object_id, $meta_key, $name, $values ) { |
|
217 $meta_type = $this->get_meta_type(); |
|
218 if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) { |
|
219 return new WP_Error( |
|
220 'rest_cannot_update', |
|
221 /* translators: %s: custom field key */ |
|
222 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), |
|
223 array( 'key' => $name, 'status' => rest_authorization_required_code() ) |
|
224 ); |
|
225 } |
|
226 |
|
227 $current = get_metadata( $meta_type, $object_id, $meta_key, false ); |
|
228 |
|
229 $to_remove = $current; |
|
230 $to_add = $values; |
|
231 |
|
232 foreach ( $to_add as $add_key => $value ) { |
|
233 $remove_keys = array_keys( $to_remove, $value, true ); |
|
234 |
|
235 if ( empty( $remove_keys ) ) { |
|
236 continue; |
|
237 } |
|
238 |
|
239 if ( count( $remove_keys ) > 1 ) { |
|
240 // To remove, we need to remove first, then add, so don't touch. |
|
241 continue; |
|
242 } |
|
243 |
|
244 $remove_key = $remove_keys[0]; |
|
245 |
|
246 unset( $to_remove[ $remove_key ] ); |
|
247 unset( $to_add[ $add_key ] ); |
|
248 } |
|
249 |
|
250 // `delete_metadata` removes _all_ instances of the value, so only call once. |
|
251 $to_remove = array_unique( $to_remove ); |
|
252 |
|
253 foreach ( $to_remove as $value ) { |
|
254 if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { |
|
255 return new WP_Error( |
|
256 'rest_meta_database_error', |
|
257 __( 'Could not update meta value in database.' ), |
|
258 array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR ) |
|
259 ); |
|
260 } |
|
261 } |
|
262 |
|
263 foreach ( $to_add as $value ) { |
|
264 if ( ! add_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { |
|
265 return new WP_Error( |
|
266 'rest_meta_database_error', |
|
267 __( 'Could not update meta value in database.' ), |
|
268 array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR ) |
|
269 ); |
|
270 } |
|
271 } |
|
272 |
|
273 return true; |
|
274 } |
|
275 |
|
276 /** |
|
277 * Updates a meta value for an object. |
|
278 * |
|
279 * @since 4.7.0 |
|
280 * |
|
281 * @param int $object_id Object ID to update. |
|
282 * @param string $meta_key Key for the custom field. |
|
283 * @param string $name Name for the field that is exposed in the REST API. |
|
284 * @param mixed $value Updated value. |
|
285 * @return bool|WP_Error True if the meta field was updated, WP_Error otherwise. |
|
286 */ |
|
287 protected function update_meta_value( $object_id, $meta_key, $name, $value ) { |
|
288 $meta_type = $this->get_meta_type(); |
|
289 if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) { |
|
290 return new WP_Error( |
|
291 'rest_cannot_update', |
|
292 /* translators: %s: custom field key */ |
|
293 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), |
|
294 array( 'key' => $name, 'status' => rest_authorization_required_code() ) |
|
295 ); |
|
296 } |
|
297 |
|
298 $meta_key = wp_slash( $meta_key ); |
|
299 $meta_value = wp_slash( $value ); |
|
300 |
|
301 // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false. |
|
302 $old_value = get_metadata( $meta_type, $object_id, $meta_key ); |
|
303 |
|
304 if ( 1 === count( $old_value ) ) { |
|
305 if ( $old_value[0] === $meta_value ) { |
|
306 return true; |
|
307 } |
|
308 } |
|
309 |
|
310 if ( ! update_metadata( $meta_type, $object_id, $meta_key, $meta_value ) ) { |
|
311 return new WP_Error( |
|
312 'rest_meta_database_error', |
|
313 __( 'Could not update meta value in database.' ), |
|
314 array( 'key' => $name, 'status' => WP_Http::INTERNAL_SERVER_ERROR ) |
|
315 ); |
|
316 } |
|
317 |
|
318 return true; |
|
319 } |
|
320 |
|
321 /** |
|
322 * Retrieves all the registered meta fields. |
|
323 * |
|
324 * @since 4.7.0 |
|
325 * |
|
326 * @return array Registered fields. |
|
327 */ |
|
328 protected function get_registered_fields() { |
|
329 $registered = array(); |
|
330 |
|
331 $meta_type = $this->get_meta_type(); |
|
332 $meta_subtype = $this->get_meta_subtype(); |
|
333 |
|
334 $meta_keys = get_registered_meta_keys( $meta_type ); |
|
335 if ( ! empty( $meta_subtype ) ) { |
|
336 $meta_keys = array_merge( $meta_keys, get_registered_meta_keys( $meta_type, $meta_subtype ) ); |
|
337 } |
|
338 |
|
339 foreach ( $meta_keys as $name => $args ) { |
|
340 if ( empty( $args['show_in_rest'] ) ) { |
|
341 continue; |
|
342 } |
|
343 |
|
344 $rest_args = array(); |
|
345 |
|
346 if ( is_array( $args['show_in_rest'] ) ) { |
|
347 $rest_args = $args['show_in_rest']; |
|
348 } |
|
349 |
|
350 $default_args = array( |
|
351 'name' => $name, |
|
352 'single' => $args['single'], |
|
353 'type' => ! empty( $args['type'] ) ? $args['type'] : null, |
|
354 'schema' => array(), |
|
355 'prepare_callback' => array( $this, 'prepare_value' ), |
|
356 ); |
|
357 |
|
358 $default_schema = array( |
|
359 'type' => $default_args['type'], |
|
360 'description' => empty( $args['description'] ) ? '' : $args['description'], |
|
361 'default' => isset( $args['default'] ) ? $args['default'] : null, |
|
362 ); |
|
363 |
|
364 $rest_args = array_merge( $default_args, $rest_args ); |
|
365 $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); |
|
366 |
|
367 $type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null; |
|
368 $type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type; |
|
369 |
|
370 if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number' ) ) ) { |
|
371 continue; |
|
372 } |
|
373 |
|
374 if ( empty( $rest_args['single'] ) ) { |
|
375 $rest_args['schema']['items'] = array( |
|
376 'type' => $rest_args['type'], |
|
377 ); |
|
378 $rest_args['schema']['type'] = 'array'; |
|
379 } |
|
380 |
|
381 $registered[ $name ] = $rest_args; |
|
382 } |
|
383 |
|
384 return $registered; |
|
385 } |
|
386 |
|
387 /** |
|
388 * Retrieves the object's meta schema, conforming to JSON Schema. |
|
389 * |
|
390 * @since 4.7.0 |
|
391 * |
|
392 * @return array Field schema data. |
|
393 */ |
|
394 public function get_field_schema() { |
|
395 $fields = $this->get_registered_fields(); |
|
396 |
|
397 $schema = array( |
|
398 'description' => __( 'Meta fields.' ), |
|
399 'type' => 'object', |
|
400 'context' => array( 'view', 'edit' ), |
|
401 'properties' => array(), |
|
402 'arg_options' => array( |
|
403 'sanitize_callback' => null, |
|
404 'validate_callback' => array( $this, 'check_meta_is_array' ), |
|
405 ), |
|
406 ); |
|
407 |
|
408 foreach ( $fields as $args ) { |
|
409 $schema['properties'][ $args['name'] ] = $args['schema']; |
|
410 } |
|
411 |
|
412 return $schema; |
|
413 } |
|
414 |
|
415 /** |
|
416 * Prepares a meta value for output. |
|
417 * |
|
418 * Default preparation for meta fields. Override by passing the |
|
419 * `prepare_callback` in your `show_in_rest` options. |
|
420 * |
|
421 * @since 4.7.0 |
|
422 * |
|
423 * @param mixed $value Meta value from the database. |
|
424 * @param WP_REST_Request $request Request object. |
|
425 * @param array $args REST-specific options for the meta key. |
|
426 * @return mixed Value prepared for output. If a non-JsonSerializable object, null. |
|
427 */ |
|
428 public static function prepare_value( $value, $request, $args ) { |
|
429 $type = $args['schema']['type']; |
|
430 |
|
431 // For multi-value fields, check the item type instead. |
|
432 if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) { |
|
433 $type = $args['schema']['items']['type']; |
|
434 } |
|
435 |
|
436 switch ( $type ) { |
|
437 case 'string': |
|
438 $value = (string) $value; |
|
439 break; |
|
440 case 'integer': |
|
441 $value = (int) $value; |
|
442 break; |
|
443 case 'number': |
|
444 $value = (float) $value; |
|
445 break; |
|
446 case 'boolean': |
|
447 $value = (bool) $value; |
|
448 break; |
|
449 } |
|
450 |
|
451 // Don't allow objects to be output. |
|
452 if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) { |
|
453 return null; |
|
454 } |
|
455 |
|
456 return $value; |
|
457 } |
|
458 |
|
459 /** |
|
460 * Check the 'meta' value of a request is an associative array. |
|
461 * |
|
462 * @since 4.7.0 |
|
463 * |
|
464 * @param mixed $value The meta value submitted in the request. |
|
465 * @param WP_REST_Request $request Full details about the request. |
|
466 * @param string $param The parameter name. |
|
467 * @return WP_Error|string The meta array, if valid, otherwise an error. |
|
468 */ |
|
469 public function check_meta_is_array( $value, $request, $param ) { |
|
470 if ( ! is_array( $value ) ) { |
|
471 return false; |
|
472 } |
|
473 |
|
474 return $value; |
|
475 } |
|
476 } |