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

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