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