class-wp-rest-settings-controller.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <?php
  2. /**
  3. * REST API: WP_REST_Settings_Controller class
  4. *
  5. * @package WordPress
  6. * @subpackage REST_API
  7. * @since 4.7.0
  8. */
  9. /**
  10. * Core class used to manage a site's settings via the REST API.
  11. *
  12. * @since 4.7.0
  13. *
  14. * @see WP_REST_Controller
  15. */
  16. class WP_REST_Settings_Controller extends WP_REST_Controller {
  17. /**
  18. * Constructor.
  19. *
  20. * @since 4.7.0
  21. */
  22. public function __construct() {
  23. $this->namespace = 'wp/v2';
  24. $this->rest_base = 'settings';
  25. }
  26. /**
  27. * Registers the routes for the objects of the controller.
  28. *
  29. * @since 4.7.0
  30. *
  31. * @see register_rest_route()
  32. */
  33. public function register_routes() {
  34. register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  35. array(
  36. 'methods' => WP_REST_Server::READABLE,
  37. 'callback' => array( $this, 'get_item' ),
  38. 'args' => array(),
  39. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  40. ),
  41. array(
  42. 'methods' => WP_REST_Server::EDITABLE,
  43. 'callback' => array( $this, 'update_item' ),
  44. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  45. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  46. ),
  47. 'schema' => array( $this, 'get_public_item_schema' ),
  48. ) );
  49. }
  50. /**
  51. * Checks if a given request has access to read and manage settings.
  52. *
  53. * @since 4.7.0
  54. *
  55. * @param WP_REST_Request $request Full details about the request.
  56. * @return bool True if the request has read access for the item, otherwise false.
  57. */
  58. public function get_item_permissions_check( $request ) {
  59. return current_user_can( 'manage_options' );
  60. }
  61. /**
  62. * Retrieves the settings.
  63. *
  64. * @since 4.7.0
  65. *
  66. * @param WP_REST_Request $request Full details about the request.
  67. * @return array|WP_Error Array on success, or WP_Error object on failure.
  68. */
  69. public function get_item( $request ) {
  70. $options = $this->get_registered_options();
  71. $response = array();
  72. foreach ( $options as $name => $args ) {
  73. /**
  74. * Filters the value of a setting recognized by the REST API.
  75. *
  76. * Allow hijacking the setting value and overriding the built-in behavior by returning a
  77. * non-null value. The returned value will be presented as the setting value instead.
  78. *
  79. * @since 4.7.0
  80. *
  81. * @param mixed $result Value to use for the requested setting. Can be a scalar
  82. * matching the registered schema for the setting, or null to
  83. * follow the default get_option() behavior.
  84. * @param string $name Setting name (as shown in REST API responses).
  85. * @param array $args Arguments passed to register_setting() for this setting.
  86. */
  87. $response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
  88. if ( is_null( $response[ $name ] ) ) {
  89. // Default to a null value as "null" in the response means "not set".
  90. $response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
  91. }
  92. /*
  93. * Because get_option() is lossy, we have to
  94. * cast values to the type they are registered with.
  95. */
  96. $response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
  97. }
  98. return $response;
  99. }
  100. /**
  101. * Prepares a value for output based off a schema array.
  102. *
  103. * @since 4.7.0
  104. *
  105. * @param mixed $value Value to prepare.
  106. * @param array $schema Schema to match.
  107. * @return mixed The prepared value.
  108. */
  109. protected function prepare_value( $value, $schema ) {
  110. // If the value is not valid by the schema, set the value to null. Null
  111. // values are specifcally non-destructive so this will not cause overwriting
  112. // the current invalid value to null.
  113. if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
  114. return null;
  115. }
  116. return rest_sanitize_value_from_schema( $value, $schema );
  117. }
  118. /**
  119. * Updates settings for the settings object.
  120. *
  121. * @since 4.7.0
  122. *
  123. * @param WP_REST_Request $request Full details about the request.
  124. * @return array|WP_Error Array on success, or error object on failure.
  125. */
  126. public function update_item( $request ) {
  127. $options = $this->get_registered_options();
  128. $params = $request->get_params();
  129. foreach ( $options as $name => $args ) {
  130. if ( ! array_key_exists( $name, $params ) ) {
  131. continue;
  132. }
  133. /**
  134. * Filters whether to preempt a setting value update.
  135. *
  136. * Allows hijacking the setting update logic and overriding the built-in behavior by
  137. * returning true.
  138. *
  139. * @since 4.7.0
  140. *
  141. * @param bool $result Whether to override the default behavior for updating the
  142. * value of a setting.
  143. * @param string $name Setting name (as shown in REST API responses).
  144. * @param mixed $value Updated setting value.
  145. * @param array $args Arguments passed to register_setting() for this setting.
  146. */
  147. $updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
  148. if ( $updated ) {
  149. continue;
  150. }
  151. /*
  152. * A null value for an option would have the same effect as
  153. * deleting the option from the database, and relying on the
  154. * default value.
  155. */
  156. if ( is_null( $request[ $name ] ) ) {
  157. /*
  158. * A null value is returned in the response for any option
  159. * that has a non-scalar value.
  160. *
  161. * To protect clients from accidentally including the null
  162. * values from a response object in a request, we do not allow
  163. * options with values that don't pass validation to be updated to null.
  164. * Without this added protection a client could mistakenly
  165. * delete all options that have invalid values from the
  166. * database.
  167. */
  168. if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
  169. return new WP_Error(
  170. 'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 )
  171. );
  172. }
  173. delete_option( $args['option_name'] );
  174. } else {
  175. update_option( $args['option_name'], $request[ $name ] );
  176. }
  177. }
  178. return $this->get_item( $request );
  179. }
  180. /**
  181. * Retrieves all of the registered options for the Settings API.
  182. *
  183. * @since 4.7.0
  184. *
  185. * @return array Array of registered options.
  186. */
  187. protected function get_registered_options() {
  188. $rest_options = array();
  189. foreach ( get_registered_settings() as $name => $args ) {
  190. if ( empty( $args['show_in_rest'] ) ) {
  191. continue;
  192. }
  193. $rest_args = array();
  194. if ( is_array( $args['show_in_rest'] ) ) {
  195. $rest_args = $args['show_in_rest'];
  196. }
  197. $defaults = array(
  198. 'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
  199. 'schema' => array(),
  200. );
  201. $rest_args = array_merge( $defaults, $rest_args );
  202. $default_schema = array(
  203. 'type' => empty( $args['type'] ) ? null : $args['type'],
  204. 'description' => empty( $args['description'] ) ? '' : $args['description'],
  205. 'default' => isset( $args['default'] ) ? $args['default'] : null,
  206. );
  207. $rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
  208. $rest_args['option_name'] = $name;
  209. // Skip over settings that don't have a defined type in the schema.
  210. if ( empty( $rest_args['schema']['type'] ) ) {
  211. continue;
  212. }
  213. /*
  214. * Whitelist the supported types for settings, as we don't want invalid types
  215. * to be updated with arbitrary values that we can't do decent sanitizing for.
  216. */
  217. if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
  218. continue;
  219. }
  220. $rest_args['schema'] = $this->set_additional_properties_to_false( $rest_args['schema'] );
  221. $rest_options[ $rest_args['name'] ] = $rest_args;
  222. }
  223. return $rest_options;
  224. }
  225. /**
  226. * Retrieves the site setting schema, conforming to JSON Schema.
  227. *
  228. * @since 4.7.0
  229. *
  230. * @return array Item schema data.
  231. */
  232. public function get_item_schema() {
  233. $options = $this->get_registered_options();
  234. $schema = array(
  235. '$schema' => 'http://json-schema.org/draft-04/schema#',
  236. 'title' => 'settings',
  237. 'type' => 'object',
  238. 'properties' => array(),
  239. );
  240. foreach ( $options as $option_name => $option ) {
  241. $schema['properties'][ $option_name ] = $option['schema'];
  242. $schema['properties'][ $option_name ]['arg_options'] = array(
  243. 'sanitize_callback' => array( $this, 'sanitize_callback' ),
  244. );
  245. }
  246. return $this->add_additional_fields_schema( $schema );
  247. }
  248. /**
  249. * Custom sanitize callback used for all options to allow the use of 'null'.
  250. *
  251. * By default, the schema of settings will throw an error if a value is set to
  252. * `null` as it's not a valid value for something like "type => string". We
  253. * provide a wrapper sanitizer to whitelist the use of `null`.
  254. *
  255. * @since 4.7.0
  256. *
  257. * @param mixed $value The value for the setting.
  258. * @param WP_REST_Request $request The request object.
  259. * @param string $param The parameter name.
  260. * @return mixed|WP_Error
  261. */
  262. public function sanitize_callback( $value, $request, $param ) {
  263. if ( is_null( $value ) ) {
  264. return $value;
  265. }
  266. return rest_parse_request_arg( $value, $request, $param );
  267. }
  268. /**
  269. * Recursively add additionalProperties = false to all objects in a schema.
  270. *
  271. * This is need to restrict properties of objects in settings values to only
  272. * registered items, as the REST API will allow additional properties by
  273. * default.
  274. *
  275. * @since 4.9.0
  276. *
  277. * @param array $schema The schema array.
  278. * @return array
  279. */
  280. protected function set_additional_properties_to_false( $schema ) {
  281. switch ( $schema['type'] ) {
  282. case 'object':
  283. foreach ( $schema['properties'] as $key => $child_schema ) {
  284. $schema['properties'][ $key ] = $this->set_additional_properties_to_false( $child_schema );
  285. }
  286. $schema['additionalProperties'] = false;
  287. break;
  288. case 'array':
  289. $schema['items'] = $this->set_additional_properties_to_false( $schema['items'] );
  290. break;
  291. }
  292. return $schema;
  293. }
  294. }