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