[ Index ] |
PHP Cross Reference of WordPress |
[Summary view] [Print] [Text view]
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 * @deprecated 5.6.0 52 * 53 * @see register_rest_field() 54 */ 55 public function register_field() { 56 _deprecated_function( __METHOD__, '5.6.0' ); 57 58 register_rest_field( 59 $this->get_rest_field_type(), 60 'meta', 61 array( 62 'get_callback' => array( $this, 'get_value' ), 63 'update_callback' => array( $this, 'update_value' ), 64 'schema' => $this->get_field_schema(), 65 ) 66 ); 67 } 68 69 /** 70 * Retrieves the meta field value. 71 * 72 * @since 4.7.0 73 * 74 * @param int $object_id Object ID to fetch meta for. 75 * @param WP_REST_Request $request Full details about the request. 76 * @return array Array containing the meta values keyed by name. 77 */ 78 public function get_value( $object_id, $request ) { 79 $fields = $this->get_registered_fields(); 80 $response = array(); 81 82 foreach ( $fields as $meta_key => $args ) { 83 $name = $args['name']; 84 $all_values = get_metadata( $this->get_meta_type(), $object_id, $meta_key, false ); 85 86 if ( $args['single'] ) { 87 if ( empty( $all_values ) ) { 88 $value = $args['schema']['default']; 89 } else { 90 $value = $all_values[0]; 91 } 92 93 $value = $this->prepare_value_for_response( $value, $request, $args ); 94 } else { 95 $value = array(); 96 97 if ( is_array( $all_values ) ) { 98 foreach ( $all_values as $row ) { 99 $value[] = $this->prepare_value_for_response( $row, $request, $args ); 100 } 101 } 102 } 103 104 $response[ $name ] = $value; 105 } 106 107 return $response; 108 } 109 110 /** 111 * Prepares a meta value for a response. 112 * 113 * This is required because some native types cannot be stored correctly 114 * in the database, such as booleans. We need to cast back to the relevant 115 * type before passing back to JSON. 116 * 117 * @since 4.7.0 118 * 119 * @param mixed $value Meta value to prepare. 120 * @param WP_REST_Request $request Current request object. 121 * @param array $args Options for the field. 122 * @return mixed Prepared value. 123 */ 124 protected function prepare_value_for_response( $value, $request, $args ) { 125 if ( ! empty( $args['prepare_callback'] ) ) { 126 $value = call_user_func( $args['prepare_callback'], $value, $request, $args ); 127 } 128 129 return $value; 130 } 131 132 /** 133 * Updates meta values. 134 * 135 * @since 4.7.0 136 * 137 * @param array $meta Array of meta parsed from the request. 138 * @param int $object_id Object ID to fetch meta for. 139 * @return null|WP_Error Null on success, WP_Error object on failure. 140 */ 141 public function update_value( $meta, $object_id ) { 142 $fields = $this->get_registered_fields(); 143 144 foreach ( $fields as $meta_key => $args ) { 145 $name = $args['name']; 146 if ( ! array_key_exists( $name, $meta ) ) { 147 continue; 148 } 149 150 $value = $meta[ $name ]; 151 152 /* 153 * A null value means reset the field, which is essentially deleting it 154 * from the database and then relying on the default value. 155 * 156 * Non-single meta can also be removed by passing an empty array. 157 */ 158 if ( is_null( $value ) || ( array() === $value && ! $args['single'] ) ) { 159 $args = $this->get_registered_fields()[ $meta_key ]; 160 161 if ( $args['single'] ) { 162 $current = get_metadata( $this->get_meta_type(), $object_id, $meta_key, true ); 163 164 if ( is_wp_error( rest_validate_value_from_schema( $current, $args['schema'] ) ) ) { 165 return new WP_Error( 166 'rest_invalid_stored_value', 167 /* translators: %s: Custom field key. */ 168 sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), 169 array( 'status' => 500 ) 170 ); 171 } 172 } 173 174 $result = $this->delete_meta_value( $object_id, $meta_key, $name ); 175 if ( is_wp_error( $result ) ) { 176 return $result; 177 } 178 continue; 179 } 180 181 if ( ! $args['single'] && is_array( $value ) && count( array_filter( $value, 'is_null' ) ) ) { 182 return new WP_Error( 183 'rest_invalid_stored_value', 184 /* translators: %s: Custom field key. */ 185 sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), 186 array( 'status' => 500 ) 187 ); 188 } 189 190 $is_valid = rest_validate_value_from_schema( $value, $args['schema'], 'meta.' . $name ); 191 if ( is_wp_error( $is_valid ) ) { 192 $is_valid->add_data( array( 'status' => 400 ) ); 193 return $is_valid; 194 } 195 196 $value = rest_sanitize_value_from_schema( $value, $args['schema'] ); 197 198 if ( $args['single'] ) { 199 $result = $this->update_meta_value( $object_id, $meta_key, $name, $value ); 200 } else { 201 $result = $this->update_multi_meta_value( $object_id, $meta_key, $name, $value ); 202 } 203 204 if ( is_wp_error( $result ) ) { 205 return $result; 206 } 207 } 208 209 return null; 210 } 211 212 /** 213 * Deletes a meta value for an object. 214 * 215 * @since 4.7.0 216 * 217 * @param int $object_id Object ID the field belongs to. 218 * @param string $meta_key Key for the field. 219 * @param string $name Name for the field that is exposed in the REST API. 220 * @return true|WP_Error True if meta field is deleted, WP_Error otherwise. 221 */ 222 protected function delete_meta_value( $object_id, $meta_key, $name ) { 223 $meta_type = $this->get_meta_type(); 224 225 if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $meta_key ) ) { 226 return new WP_Error( 227 'rest_cannot_delete', 228 /* translators: %s: Custom field key. */ 229 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), 230 array( 231 'key' => $name, 232 'status' => rest_authorization_required_code(), 233 ) 234 ); 235 } 236 237 if ( null === get_metadata_raw( $meta_type, $object_id, wp_slash( $meta_key ) ) ) { 238 return true; 239 } 240 241 if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ) ) ) { 242 return new WP_Error( 243 'rest_meta_database_error', 244 __( 'Could not delete meta value from database.' ), 245 array( 246 'key' => $name, 247 'status' => WP_Http::INTERNAL_SERVER_ERROR, 248 ) 249 ); 250 } 251 252 return true; 253 } 254 255 /** 256 * Updates multiple meta values for an object. 257 * 258 * Alters the list of values in the database to match the list of provided values. 259 * 260 * @since 4.7.0 261 * 262 * @param int $object_id Object ID to update. 263 * @param string $meta_key Key for the custom field. 264 * @param string $name Name for the field that is exposed in the REST API. 265 * @param array $values List of values to update to. 266 * @return true|WP_Error True if meta fields are updated, WP_Error otherwise. 267 */ 268 protected function update_multi_meta_value( $object_id, $meta_key, $name, $values ) { 269 $meta_type = $this->get_meta_type(); 270 271 if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) { 272 return new WP_Error( 273 'rest_cannot_update', 274 /* translators: %s: Custom field key. */ 275 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), 276 array( 277 'key' => $name, 278 'status' => rest_authorization_required_code(), 279 ) 280 ); 281 } 282 283 $current_values = get_metadata( $meta_type, $object_id, $meta_key, false ); 284 $subtype = get_object_subtype( $meta_type, $object_id ); 285 286 if ( ! is_array( $current_values ) ) { 287 $current_values = array(); 288 } 289 290 $to_remove = $current_values; 291 $to_add = $values; 292 293 foreach ( $to_add as $add_key => $value ) { 294 $remove_keys = array_keys( 295 array_filter( 296 $current_values, 297 function ( $stored_value ) use ( $meta_key, $subtype, $value ) { 298 return $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $value ); 299 } 300 ) 301 ); 302 303 if ( empty( $remove_keys ) ) { 304 continue; 305 } 306 307 if ( count( $remove_keys ) > 1 ) { 308 // To remove, we need to remove first, then add, so don't touch. 309 continue; 310 } 311 312 $remove_key = $remove_keys[0]; 313 314 unset( $to_remove[ $remove_key ] ); 315 unset( $to_add[ $add_key ] ); 316 } 317 318 /* 319 * `delete_metadata` removes _all_ instances of the value, so only call once. Otherwise, 320 * `delete_metadata` will return false for subsequent calls of the same value. 321 * Use serialization to produce a predictable string that can be used by array_unique. 322 */ 323 $to_remove = array_map( 'maybe_unserialize', array_unique( array_map( 'maybe_serialize', $to_remove ) ) ); 324 325 foreach ( $to_remove as $value ) { 326 if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { 327 return new WP_Error( 328 'rest_meta_database_error', 329 /* translators: %s: Custom field key. */ 330 sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ), 331 array( 332 'key' => $name, 333 'status' => WP_Http::INTERNAL_SERVER_ERROR, 334 ) 335 ); 336 } 337 } 338 339 foreach ( $to_add as $value ) { 340 if ( ! add_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { 341 return new WP_Error( 342 'rest_meta_database_error', 343 /* translators: %s: Custom field key. */ 344 sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ), 345 array( 346 'key' => $name, 347 'status' => WP_Http::INTERNAL_SERVER_ERROR, 348 ) 349 ); 350 } 351 } 352 353 return true; 354 } 355 356 /** 357 * Updates a meta value for an object. 358 * 359 * @since 4.7.0 360 * 361 * @param int $object_id Object ID to update. 362 * @param string $meta_key Key for the custom field. 363 * @param string $name Name for the field that is exposed in the REST API. 364 * @param mixed $value Updated value. 365 * @return true|WP_Error True if the meta field was updated, WP_Error otherwise. 366 */ 367 protected function update_meta_value( $object_id, $meta_key, $name, $value ) { 368 $meta_type = $this->get_meta_type(); 369 370 if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) { 371 return new WP_Error( 372 'rest_cannot_update', 373 /* translators: %s: Custom field key. */ 374 sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ), 375 array( 376 'key' => $name, 377 'status' => rest_authorization_required_code(), 378 ) 379 ); 380 } 381 382 // Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false. 383 $old_value = get_metadata( $meta_type, $object_id, $meta_key ); 384 $subtype = get_object_subtype( $meta_type, $object_id ); 385 386 if ( is_array( $old_value ) && 1 === count( $old_value ) 387 && $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $old_value[0], $value ) 388 ) { 389 return true; 390 } 391 392 if ( ! update_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) { 393 return new WP_Error( 394 'rest_meta_database_error', 395 /* translators: %s: Custom field key. */ 396 sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ), 397 array( 398 'key' => $name, 399 'status' => WP_Http::INTERNAL_SERVER_ERROR, 400 ) 401 ); 402 } 403 404 return true; 405 } 406 407 /** 408 * Checks if the user provided value is equivalent to a stored value for the given meta key. 409 * 410 * @since 5.5.0 411 * 412 * @param string $meta_key The meta key being checked. 413 * @param string $subtype The object subtype. 414 * @param mixed $stored_value The currently stored value retrieved from get_metadata(). 415 * @param mixed $user_value The value provided by the user. 416 * @return bool 417 */ 418 protected function is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $user_value ) { 419 $args = $this->get_registered_fields()[ $meta_key ]; 420 $sanitized = sanitize_meta( $meta_key, $user_value, $this->get_meta_type(), $subtype ); 421 422 if ( in_array( $args['type'], array( 'string', 'number', 'integer', 'boolean' ), true ) ) { 423 // The return value of get_metadata will always be a string for scalar types. 424 $sanitized = (string) $sanitized; 425 } 426 427 return $sanitized === $stored_value; 428 } 429 430 /** 431 * Retrieves all the registered meta fields. 432 * 433 * @since 4.7.0 434 * 435 * @return array Registered fields. 436 */ 437 protected function get_registered_fields() { 438 $registered = array(); 439 440 $meta_type = $this->get_meta_type(); 441 $meta_subtype = $this->get_meta_subtype(); 442 443 $meta_keys = get_registered_meta_keys( $meta_type ); 444 if ( ! empty( $meta_subtype ) ) { 445 $meta_keys = array_merge( $meta_keys, get_registered_meta_keys( $meta_type, $meta_subtype ) ); 446 } 447 448 foreach ( $meta_keys as $name => $args ) { 449 if ( empty( $args['show_in_rest'] ) ) { 450 continue; 451 } 452 453 $rest_args = array(); 454 455 if ( is_array( $args['show_in_rest'] ) ) { 456 $rest_args = $args['show_in_rest']; 457 } 458 459 $default_args = array( 460 'name' => $name, 461 'single' => $args['single'], 462 'type' => ! empty( $args['type'] ) ? $args['type'] : null, 463 'schema' => array(), 464 'prepare_callback' => array( $this, 'prepare_value' ), 465 ); 466 467 $default_schema = array( 468 'type' => $default_args['type'], 469 'description' => empty( $args['description'] ) ? '' : $args['description'], 470 'default' => isset( $args['default'] ) ? $args['default'] : null, 471 ); 472 473 $rest_args = array_merge( $default_args, $rest_args ); 474 $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] ); 475 476 $type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null; 477 $type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type; 478 479 if ( null === $rest_args['schema']['default'] ) { 480 $rest_args['schema']['default'] = static::get_empty_value_for_type( $type ); 481 } 482 483 $rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] ); 484 485 if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ), true ) ) { 486 continue; 487 } 488 489 if ( empty( $rest_args['single'] ) ) { 490 $rest_args['schema'] = array( 491 'type' => 'array', 492 'items' => $rest_args['schema'], 493 ); 494 } 495 496 $registered[ $name ] = $rest_args; 497 } 498 499 return $registered; 500 } 501 502 /** 503 * Retrieves the object's meta schema, conforming to JSON Schema. 504 * 505 * @since 4.7.0 506 * 507 * @return array Field schema data. 508 */ 509 public function get_field_schema() { 510 $fields = $this->get_registered_fields(); 511 512 $schema = array( 513 'description' => __( 'Meta fields.' ), 514 'type' => 'object', 515 'context' => array( 'view', 'edit' ), 516 'properties' => array(), 517 'arg_options' => array( 518 'sanitize_callback' => null, 519 'validate_callback' => array( $this, 'check_meta_is_array' ), 520 ), 521 ); 522 523 foreach ( $fields as $args ) { 524 $schema['properties'][ $args['name'] ] = $args['schema']; 525 } 526 527 return $schema; 528 } 529 530 /** 531 * Prepares a meta value for output. 532 * 533 * Default preparation for meta fields. Override by passing the 534 * `prepare_callback` in your `show_in_rest` options. 535 * 536 * @since 4.7.0 537 * 538 * @param mixed $value Meta value from the database. 539 * @param WP_REST_Request $request Request object. 540 * @param array $args REST-specific options for the meta key. 541 * @return mixed Value prepared for output. If a non-JsonSerializable object, null. 542 */ 543 public static function prepare_value( $value, $request, $args ) { 544 if ( $args['single'] ) { 545 $schema = $args['schema']; 546 } else { 547 $schema = $args['schema']['items']; 548 } 549 550 if ( '' === $value && in_array( $schema['type'], array( 'boolean', 'integer', 'number' ), true ) ) { 551 $value = static::get_empty_value_for_type( $schema['type'] ); 552 } 553 554 if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { 555 return null; 556 } 557 558 return rest_sanitize_value_from_schema( $value, $schema ); 559 } 560 561 /** 562 * Check the 'meta' value of a request is an associative array. 563 * 564 * @since 4.7.0 565 * 566 * @param mixed $value The meta value submitted in the request. 567 * @param WP_REST_Request $request Full details about the request. 568 * @param string $param The parameter name. 569 * @return array|false The meta array, if valid, false otherwise. 570 */ 571 public function check_meta_is_array( $value, $request, $param ) { 572 if ( ! is_array( $value ) ) { 573 return false; 574 } 575 576 return $value; 577 } 578 579 /** 580 * Recursively add additionalProperties = false to all objects in a schema if no additionalProperties setting 581 * is specified. 582 * 583 * This is needed to restrict properties of objects in meta values to only 584 * registered items, as the REST API will allow additional properties by 585 * default. 586 * 587 * @since 5.3.0 588 * @deprecated 5.6.0 Use rest_default_additional_properties_to_false() instead. 589 * 590 * @param array $schema The schema array. 591 * @return array 592 */ 593 protected function default_additional_properties_to_false( $schema ) { 594 _deprecated_function( __METHOD__, '5.6.0', 'rest_default_additional_properties_to_false()' ); 595 596 return rest_default_additional_properties_to_false( $schema ); 597 } 598 599 /** 600 * Gets the empty value for a schema type. 601 * 602 * @since 5.3.0 603 * 604 * @param string $type The schema type. 605 * @return mixed 606 */ 607 protected static function get_empty_value_for_type( $type ) { 608 switch ( $type ) { 609 case 'string': 610 return ''; 611 case 'boolean': 612 return false; 613 case 'integer': 614 return 0; 615 case 'number': 616 return 0.0; 617 case 'array': 618 case 'object': 619 return array(); 620 default: 621 return null; 622 } 623 } 624 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Nov 21 01:00:03 2024 | Cross-referenced by PHPXref 0.7.1 |