[ Index ]

PHP Cross Reference of WordPress

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  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  }


Generated: Thu Mar 28 01:00:02 2024 Cross-referenced by PHPXref 0.7.1