class-wp-rest-meta-fields.php 17 KB

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