[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/fields/ -> class-wp-rest-meta-fields.php (source)

   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  }


Generated : Thu Oct 1 08:20:30 2026 Cross-referenced by PHPXref