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


Generated: Sun Jul 12 01:00:03 2020 Cross-referenced by PHPXref 0.7.1