balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-rest-meta-fields.php (18078B)


      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 }