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


Generated: Tue Oct 27 01:00:08 2020 Cross-referenced by PHPXref 0.7.1