/wp-content/plugins/rest-api/lib/fields/class-wp-rest-meta-fields.php

https://gitlab.com/code26/selah · PHP · 350 lines · 211 code · 45 blank · 94 comment · 29 complexity · ab145afb832ba27d3ab35a2d5035965d MD5 · raw file

  1. <?php
  2. /**
  3. * Manage meta values for an object.
  4. */
  5. abstract class WP_REST_Meta_Fields {
  6. /**
  7. * Get the object type for meta.
  8. *
  9. * @return string One of 'post', 'comment', 'term', 'user', or anything else supported by `_get_meta_table()`
  10. */
  11. abstract protected function get_meta_type();
  12. /**
  13. * Get the object type for `register_rest_field`.
  14. *
  15. * @return string Custom post type, 'taxonomy', 'comment', or `user`
  16. */
  17. abstract protected function get_rest_field_type();
  18. /**
  19. * Register the meta field.
  20. */
  21. public function register_field() {
  22. register_rest_field( $this->get_rest_field_type(), 'meta', array(
  23. 'get_callback' => array( $this, 'get_value' ),
  24. 'update_callback' => array( $this, 'update_value' ),
  25. 'schema' => $this->get_field_schema(),
  26. ));
  27. }
  28. /**
  29. * Get the `meta` field value.
  30. *
  31. * @param int $id Object ID to fetch meta for.
  32. * @param WP_REST_Request $request Full details about the request.
  33. * @return WP_Error|array
  34. */
  35. public function get_value( $id, $request ) {
  36. $fields = $this->get_registered_fields();
  37. $response = array();
  38. foreach ( $fields as $name => $args ) {
  39. $all_values = get_metadata( $this->get_meta_type(), $id, $name, false );
  40. if ( $args['single'] ) {
  41. if ( empty( $all_values ) ) {
  42. $value = $args['schema']['default'];
  43. } else {
  44. $value = $all_values[0];
  45. }
  46. $value = $this->prepare_value_for_response( $value, $request, $args );
  47. } else {
  48. $value = array();
  49. foreach ( $all_values as $row ) {
  50. $value[] = $this->prepare_value_for_response( $row, $request, $args );
  51. }
  52. }
  53. $response[ $name ] = $value;
  54. }
  55. return (object) $response;
  56. }
  57. /**
  58. * Prepare value for response.
  59. *
  60. * This is required because some native types cannot be stored correctly in
  61. * the database, such as booleans. We need to cast back to the relevant type
  62. * before passing back to JSON.
  63. *
  64. * @param mixed $value Value to prepare.
  65. * @param WP_REST_Request $request Current request object.
  66. * @param array $args Options for the field.
  67. * @return mixed Prepared value.
  68. */
  69. protected function prepare_value_for_response( $value, $request, $args ) {
  70. if ( ! empty( $args['prepare_callback'] ) ) {
  71. $value = call_user_func( $args['prepare_callback'], $value, $request, $args );
  72. }
  73. return $value;
  74. }
  75. /**
  76. * Update meta values.
  77. *
  78. * @param WP_REST_Request $request Full detail about the request.
  79. * @return WP_Error|null Error if one occurs, null on success.
  80. */
  81. public function update_value( $params, $id ) {
  82. $fields = $this->get_registered_fields();
  83. foreach ( $fields as $name => $args ) {
  84. if ( ! array_key_exists( $name, $params ) ) {
  85. continue;
  86. }
  87. // A null value means reset the field, which is essentially deleting it
  88. // from the database and then relying on the default value.
  89. if ( is_null( $params[ $name ] ) ) {
  90. $result = $this->delete_meta_value( $id, $name );
  91. } elseif ( $args['single'] ) {
  92. $result = $this->update_meta_value( $id, $name, $params[ $name ] );
  93. } else {
  94. $result = $this->update_multi_meta_value( $id, $name, $params[ $name ] );
  95. }
  96. if ( is_wp_error( $result ) ) {
  97. return $result;
  98. }
  99. }
  100. return null;
  101. }
  102. /**
  103. * Delete meta value for an object.
  104. *
  105. * @param int $object Object ID the field belongs to.
  106. * @param string $name Key for the field.
  107. * @return bool|WP_Error True if meta field is deleted, error otherwise.
  108. */
  109. protected function delete_meta_value( $object, $name ) {
  110. if ( ! current_user_can( 'delete_post_meta', $object, $name ) ) {
  111. return new WP_Error(
  112. 'rest_cannot_delete',
  113. sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
  114. array( 'key' => $name, 'status' => rest_authorization_required_code() )
  115. );
  116. }
  117. if ( ! delete_metadata( $this->get_meta_type(), $object, wp_slash( $name ) ) ) {
  118. return new WP_Error(
  119. 'rest_meta_database_error',
  120. __( 'Could not delete meta value from database.' ),
  121. array( 'key' => $name, 'status' => WP_HTTP::INTERNAL_SERVER_ERROR )
  122. );
  123. }
  124. return true;
  125. }
  126. /**
  127. * Update multiple meta values for an object.
  128. *
  129. * Alters the list of values in the database to match the list of provided values.
  130. *
  131. * @param int $object Object ID.
  132. * @param string $name Key for the custom field.
  133. * @param array $values List of values to update to.
  134. * @return bool|WP_Error True if meta fields are updated, error otherwise.
  135. */
  136. protected function update_multi_meta_value( $object, $name, $values ) {
  137. if ( ! current_user_can( 'edit_post_meta', $object, $name ) ) {
  138. return new WP_Error(
  139. 'rest_cannot_update',
  140. sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
  141. array( 'key' => $name, 'status' => rest_authorization_required_code() )
  142. );
  143. }
  144. $current = get_metadata( $this->get_meta_type(), $object, $name, false );
  145. $to_remove = $current;
  146. $to_add = $values;
  147. foreach ( $to_add as $add_key => $value ) {
  148. $remove_keys = array_keys( $to_remove, $value, true );
  149. if ( empty( $remove_keys ) ) {
  150. continue;
  151. }
  152. if ( count( $remove_keys ) > 1 ) {
  153. // To remove, we need to remove first, then add, so don't touch.
  154. continue;
  155. }
  156. $remove_key = $remove_keys[0];
  157. unset( $to_remove[ $remove_key ] );
  158. unset( $to_add[ $add_key ] );
  159. }
  160. // `delete_metadata` removes _all_ instances of the value, so only call
  161. // once.
  162. $to_remove = array_unique( $to_remove );
  163. foreach ( $to_remove as $value ) {
  164. if ( ! delete_metadata( $this->get_meta_type(), $object, wp_slash( $name ), wp_slash( $value ) ) ) {
  165. return new WP_Error(
  166. 'rest_meta_database_error',
  167. __( 'Could not update meta value in database.' ),
  168. array( 'key' => $name, 'status' => WP_HTTP::INTERNAL_SERVER_ERROR )
  169. );
  170. }
  171. }
  172. foreach ( $to_add as $value ) {
  173. if ( ! add_metadata( $this->get_meta_type(), $object, wp_slash( $name ), wp_slash( $value ) ) ) {
  174. return new WP_Error(
  175. 'rest_meta_database_error',
  176. __( 'Could not update meta value in database.' ),
  177. array( 'key' => $name, 'status' => WP_HTTP::INTERNAL_SERVER_ERROR )
  178. );
  179. }
  180. }
  181. return true;
  182. }
  183. /**
  184. * Update meta value for an object.
  185. *
  186. * @param int $object Object ID.
  187. * @param string $name Key for the custom field.
  188. * @return bool|WP_Error True if meta field is deleted, error otherwise.
  189. */
  190. protected function update_meta_value( $object, $name, $value ) {
  191. if ( ! current_user_can( 'edit_post_meta', $object, $name ) ) {
  192. return new WP_Error(
  193. 'rest_cannot_update',
  194. sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
  195. array( 'key' => $name, 'status' => rest_authorization_required_code() )
  196. );
  197. }
  198. if ( ! update_metadata( $this->get_meta_type(), $object, wp_slash( $name ), wp_slash( $value ) ) ) {
  199. return new WP_Error(
  200. 'rest_meta_database_error',
  201. __( 'Could not update meta value in database.' ),
  202. array( 'key' => $name, 'status' => WP_HTTP::INTERNAL_SERVER_ERROR )
  203. );
  204. }
  205. return true;
  206. }
  207. /**
  208. * Get all the registered meta fields.
  209. *
  210. * @return array
  211. */
  212. protected function get_registered_fields() {
  213. $registered = array();
  214. foreach ( get_registered_meta_keys( $this->get_meta_type() ) as $name => $args ) {
  215. if ( empty( $args['show_in_rest'] ) ) {
  216. continue;
  217. }
  218. $rest_args = array();
  219. if ( is_array( $args['show_in_rest'] ) ) {
  220. $rest_args = $args['show_in_rest'];
  221. }
  222. $default_args = array(
  223. 'name' => $name,
  224. 'single' => $args['single'],
  225. 'schema' => array(),
  226. 'prepare_callback' => array( $this, 'prepare_value' ),
  227. );
  228. $default_schema = array(
  229. 'type' => null,
  230. 'description' => empty( $args['description'] ) ? '' : $args['description'],
  231. 'default' => isset( $args['default'] ) ? $args['default'] : null,
  232. );
  233. $rest_args = array_merge( $default_args, $rest_args );
  234. $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
  235. if ( empty( $rest_args['schema']['type'] ) ) {
  236. // Skip over meta fields that don't have a defined type
  237. if ( empty( $args['type'] ) ) {
  238. continue;
  239. }
  240. if ( $rest_args['single'] ) {
  241. $rest_args['schema']['type'] = $args['type'];
  242. } else {
  243. $rest_args['schema']['type'] = 'array';
  244. $rest_args['schema']['items'] = array(
  245. 'type' => $args['type'],
  246. );
  247. }
  248. }
  249. $registered[ $rest_args['name'] ] = $rest_args;
  250. }
  251. return $registered;
  252. }
  253. /**
  254. * Get the object's `meta` schema, conforming to JSON Schema.
  255. *
  256. * @return array
  257. */
  258. public function get_field_schema() {
  259. $fields = $this->get_registered_fields();
  260. $schema = array(
  261. 'description' => __( 'Meta fields.' ),
  262. 'type' => 'object',
  263. 'context' => array( 'view', 'edit' ),
  264. 'properties' => array(),
  265. );
  266. foreach ( $fields as $key => $args ) {
  267. $schema['properties'][ $key ] = $args['schema'];
  268. }
  269. return $schema;
  270. }
  271. /**
  272. * Prepare a meta value for output.
  273. *
  274. * Default preparation for meta fields. Override by passing the
  275. * `prepare_callback` in your `show_in_rest` options.
  276. *
  277. * @param mixed $value Meta value from the database.
  278. * @param WP_REST_Request $request Request object.
  279. * @param array $args REST-specific options for the meta key.
  280. * @return mixed Value prepared for output.
  281. */
  282. public static function prepare_value( $value, $request, $args ) {
  283. $type = $args['schema']['type'];
  284. // For multi-value fields, check the item type instead.
  285. if ( 'array' === $type && ! empty( $args['schema']['items']['type'] ) ) {
  286. $type = $args['schema']['items']['type'];
  287. }
  288. switch ( $type ) {
  289. case 'string':
  290. $value = strval( $value );
  291. break;
  292. case 'number':
  293. $value = floatval( $value );
  294. break;
  295. case 'boolean':
  296. $value = (bool) $value;
  297. break;
  298. }
  299. // Don't allow objects to be output.
  300. if ( is_object( $value ) && ! ( $value instanceof JsonSerializable ) ) {
  301. return null;
  302. }
  303. return $value;
  304. }
  305. }