class-wc-rest-setting-options-controller.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. <?php
  2. /**
  3. * REST API Setting Options controller
  4. *
  5. * Handles requests to the /settings/$group/$setting endpoints.
  6. *
  7. * @package WooCommerce/API
  8. * @since 3.0.0
  9. */
  10. defined( 'ABSPATH' ) || exit;
  11. /**
  12. * REST API Setting Options controller class.
  13. *
  14. * @package WooCommerce/API
  15. * @extends WC_REST_Controller
  16. */
  17. class WC_REST_Setting_Options_Controller extends WC_REST_Controller {
  18. /**
  19. * WP REST API namespace/version.
  20. *
  21. * @var string
  22. */
  23. protected $namespace = 'wc/v2';
  24. /**
  25. * Route base.
  26. *
  27. * @var string
  28. */
  29. protected $rest_base = 'settings/(?P<group_id>[\w-]+)';
  30. /**
  31. * Register routes.
  32. *
  33. * @since 3.0.0
  34. */
  35. public function register_routes() {
  36. register_rest_route(
  37. $this->namespace, '/' . $this->rest_base, array(
  38. 'args' => array(
  39. 'group' => array(
  40. 'description' => __( 'Settings group ID.', 'woocommerce' ),
  41. 'type' => 'string',
  42. ),
  43. ),
  44. array(
  45. 'methods' => WP_REST_Server::READABLE,
  46. 'callback' => array( $this, 'get_items' ),
  47. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  48. ),
  49. 'schema' => array( $this, 'get_public_item_schema' ),
  50. )
  51. );
  52. register_rest_route(
  53. $this->namespace, '/' . $this->rest_base . '/batch', array(
  54. 'args' => array(
  55. 'group' => array(
  56. 'description' => __( 'Settings group ID.', 'woocommerce' ),
  57. 'type' => 'string',
  58. ),
  59. ),
  60. array(
  61. 'methods' => WP_REST_Server::EDITABLE,
  62. 'callback' => array( $this, 'batch_items' ),
  63. 'permission_callback' => array( $this, 'update_items_permissions_check' ),
  64. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  65. ),
  66. 'schema' => array( $this, 'get_public_batch_schema' ),
  67. )
  68. );
  69. register_rest_route(
  70. $this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
  71. 'args' => array(
  72. 'group' => array(
  73. 'description' => __( 'Settings group ID.', 'woocommerce' ),
  74. 'type' => 'string',
  75. ),
  76. 'id' => array(
  77. 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
  78. 'type' => 'string',
  79. ),
  80. ),
  81. array(
  82. 'methods' => WP_REST_Server::READABLE,
  83. 'callback' => array( $this, 'get_item' ),
  84. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  85. ),
  86. array(
  87. 'methods' => WP_REST_Server::EDITABLE,
  88. 'callback' => array( $this, 'update_item' ),
  89. 'permission_callback' => array( $this, 'update_items_permissions_check' ),
  90. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  91. ),
  92. 'schema' => array( $this, 'get_public_item_schema' ),
  93. )
  94. );
  95. }
  96. /**
  97. * Return a single setting.
  98. *
  99. * @since 3.0.0
  100. * @param WP_REST_Request $request Request data.
  101. * @return WP_Error|WP_REST_Response
  102. */
  103. public function get_item( $request ) {
  104. $setting = $this->get_setting( $request['group_id'], $request['id'] );
  105. if ( is_wp_error( $setting ) ) {
  106. return $setting;
  107. }
  108. $response = $this->prepare_item_for_response( $setting, $request );
  109. return rest_ensure_response( $response );
  110. }
  111. /**
  112. * Return all settings in a group.
  113. *
  114. * @since 3.0.0
  115. * @param WP_REST_Request $request Request data.
  116. * @return WP_Error|WP_REST_Response
  117. */
  118. public function get_items( $request ) {
  119. $settings = $this->get_group_settings( $request['group_id'] );
  120. if ( is_wp_error( $settings ) ) {
  121. return $settings;
  122. }
  123. $data = array();
  124. foreach ( $settings as $setting_obj ) {
  125. $setting = $this->prepare_item_for_response( $setting_obj, $request );
  126. $setting = $this->prepare_response_for_collection( $setting );
  127. if ( $this->is_setting_type_valid( $setting['type'] ) ) {
  128. $data[] = $setting;
  129. }
  130. }
  131. return rest_ensure_response( $data );
  132. }
  133. /**
  134. * Get all settings in a group.
  135. *
  136. * @since 3.0.0
  137. * @param string $group_id Group ID.
  138. * @return array|WP_Error
  139. */
  140. public function get_group_settings( $group_id ) {
  141. if ( empty( $group_id ) ) {
  142. return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
  143. }
  144. $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() );
  145. if ( empty( $settings ) ) {
  146. return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
  147. }
  148. $filtered_settings = array();
  149. foreach ( $settings as $setting ) {
  150. $option_key = $setting['option_key'];
  151. $setting = $this->filter_setting( $setting );
  152. $default = isset( $setting['default'] ) ? $setting['default'] : '';
  153. // Get the option value.
  154. if ( is_array( $option_key ) ) {
  155. $option = get_option( $option_key[0] );
  156. $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
  157. } else {
  158. $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default );
  159. $setting['value'] = $admin_setting_value;
  160. }
  161. if ( 'multi_select_countries' === $setting['type'] ) {
  162. $setting['options'] = WC()->countries->get_countries();
  163. $setting['type'] = 'multiselect';
  164. } elseif ( 'single_select_country' === $setting['type'] ) {
  165. $setting['type'] = 'select';
  166. $setting['options'] = $this->get_countries_and_states();
  167. }
  168. $filtered_settings[] = $setting;
  169. }
  170. return $filtered_settings;
  171. }
  172. /**
  173. * Returns a list of countries and states for use in the base location setting.
  174. *
  175. * @since 3.0.7
  176. * @return array Array of states and countries.
  177. */
  178. private function get_countries_and_states() {
  179. $countries = WC()->countries->get_countries();
  180. if ( ! $countries ) {
  181. return array();
  182. }
  183. $output = array();
  184. foreach ( $countries as $key => $value ) {
  185. $states = WC()->countries->get_states( $key );
  186. if ( $states ) {
  187. foreach ( $states as $state_key => $state_value ) {
  188. $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
  189. }
  190. } else {
  191. $output[ $key ] = $value;
  192. }
  193. }
  194. return $output;
  195. }
  196. /**
  197. * Get setting data.
  198. *
  199. * @since 3.0.0
  200. * @param string $group_id Group ID.
  201. * @param string $setting_id Setting ID.
  202. * @return stdClass|WP_Error
  203. */
  204. public function get_setting( $group_id, $setting_id ) {
  205. if ( empty( $setting_id ) ) {
  206. return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
  207. }
  208. $settings = $this->get_group_settings( $group_id );
  209. if ( is_wp_error( $settings ) ) {
  210. return $settings;
  211. }
  212. $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id );
  213. if ( empty( $array_key ) ) {
  214. return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
  215. }
  216. $setting = $settings[ $array_key[0] ];
  217. if ( ! $this->is_setting_type_valid( $setting['type'] ) ) {
  218. return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
  219. }
  220. return $setting;
  221. }
  222. /**
  223. * Bulk create, update and delete items.
  224. *
  225. * @since 3.0.0
  226. * @param WP_REST_Request $request Full details about the request.
  227. * @return array Of WP_Error or WP_REST_Response.
  228. */
  229. public function batch_items( $request ) {
  230. // Get the request params.
  231. $items = array_filter( $request->get_params() );
  232. /*
  233. * Since our batch settings update is group-specific and matches based on the route,
  234. * we inject the URL parameters (containing group) into the batch items
  235. */
  236. if ( ! empty( $items['update'] ) ) {
  237. $to_update = array();
  238. foreach ( $items['update'] as $item ) {
  239. $to_update[] = array_merge( $request->get_url_params(), $item );
  240. }
  241. $request = new WP_REST_Request( $request->get_method() );
  242. $request->set_body_params( array( 'update' => $to_update ) );
  243. }
  244. return parent::batch_items( $request );
  245. }
  246. /**
  247. * Update a single setting in a group.
  248. *
  249. * @since 3.0.0
  250. * @param WP_REST_Request $request Request data.
  251. * @return WP_Error|WP_REST_Response
  252. */
  253. public function update_item( $request ) {
  254. $setting = $this->get_setting( $request['group_id'], $request['id'] );
  255. if ( is_wp_error( $setting ) ) {
  256. return $setting;
  257. }
  258. if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) {
  259. $value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting );
  260. } else {
  261. $value = $this->validate_setting_text_field( $request['value'], $setting );
  262. }
  263. if ( is_wp_error( $value ) ) {
  264. return $value;
  265. }
  266. if ( is_array( $setting['option_key'] ) ) {
  267. $setting['value'] = $value;
  268. $option_key = $setting['option_key'];
  269. $prev = get_option( $option_key[0] );
  270. $prev[ $option_key[1] ] = $request['value'];
  271. update_option( $option_key[0], $prev );
  272. } else {
  273. $update_data = array();
  274. $update_data[ $setting['option_key'] ] = $value;
  275. $setting['value'] = $value;
  276. WC_Admin_Settings::save_fields( array( $setting ), $update_data );
  277. }
  278. $response = $this->prepare_item_for_response( $setting, $request );
  279. return rest_ensure_response( $response );
  280. }
  281. /**
  282. * Prepare a single setting object for response.
  283. *
  284. * @since 3.0.0
  285. * @param object $item Setting object.
  286. * @param WP_REST_Request $request Request object.
  287. * @return WP_REST_Response $response Response data.
  288. */
  289. public function prepare_item_for_response( $item, $request ) {
  290. unset( $item['option_key'] );
  291. $data = $this->filter_setting( $item );
  292. $data = $this->add_additional_fields_to_object( $data, $request );
  293. $data = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] );
  294. $response = rest_ensure_response( $data );
  295. $response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) );
  296. return $response;
  297. }
  298. /**
  299. * Prepare links for the request.
  300. *
  301. * @since 3.0.0
  302. * @param string $setting_id Setting ID.
  303. * @param string $group_id Group ID.
  304. * @return array Links for the given setting.
  305. */
  306. protected function prepare_links( $setting_id, $group_id ) {
  307. $base = str_replace( '(?P<group_id>[\w-]+)', $group_id, $this->rest_base );
  308. $links = array(
  309. 'self' => array(
  310. 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ),
  311. ),
  312. 'collection' => array(
  313. 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
  314. ),
  315. );
  316. return $links;
  317. }
  318. /**
  319. * Makes sure the current user has access to READ the settings APIs.
  320. *
  321. * @since 3.0.0
  322. * @param WP_REST_Request $request Full data about the request.
  323. * @return WP_Error|boolean
  324. */
  325. public function get_items_permissions_check( $request ) {
  326. if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
  327. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  328. }
  329. return true;
  330. }
  331. /**
  332. * Makes sure the current user has access to WRITE the settings APIs.
  333. *
  334. * @since 3.0.0
  335. * @param WP_REST_Request $request Full data about the request.
  336. * @return WP_Error|boolean
  337. */
  338. public function update_items_permissions_check( $request ) {
  339. if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
  340. return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  341. }
  342. return true;
  343. }
  344. /**
  345. * Filters out bad values from the settings array/filter so we
  346. * only return known values via the API.
  347. *
  348. * @since 3.0.0
  349. * @param array $setting Settings.
  350. * @return array
  351. */
  352. public function filter_setting( $setting ) {
  353. $setting = array_intersect_key(
  354. $setting,
  355. array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) )
  356. );
  357. if ( empty( $setting['options'] ) ) {
  358. unset( $setting['options'] );
  359. }
  360. if ( 'image_width' === $setting['type'] ) {
  361. $setting = $this->cast_image_width( $setting );
  362. }
  363. return $setting;
  364. }
  365. /**
  366. * For image_width, Crop can return "0" instead of false -- so we want
  367. * to make sure we return these consistently the same we accept them.
  368. *
  369. * @todo remove in 4.0
  370. * @since 3.0.0
  371. * @param array $setting Settings.
  372. * @return array
  373. */
  374. public function cast_image_width( $setting ) {
  375. foreach ( array( 'default', 'value' ) as $key ) {
  376. if ( isset( $setting[ $key ] ) ) {
  377. $setting[ $key ]['width'] = intval( $setting[ $key ]['width'] );
  378. $setting[ $key ]['height'] = intval( $setting[ $key ]['height'] );
  379. $setting[ $key ]['crop'] = (bool) $setting[ $key ]['crop'];
  380. }
  381. }
  382. return $setting;
  383. }
  384. /**
  385. * Callback for allowed keys for each setting response.
  386. *
  387. * @since 3.0.0
  388. * @param string $key Key to check.
  389. * @return boolean
  390. */
  391. public function allowed_setting_keys( $key ) {
  392. return in_array(
  393. $key, array(
  394. 'id',
  395. 'label',
  396. 'description',
  397. 'default',
  398. 'tip',
  399. 'placeholder',
  400. 'type',
  401. 'options',
  402. 'value',
  403. 'option_key',
  404. )
  405. );
  406. }
  407. /**
  408. * Boolean for if a setting type is a valid supported setting type.
  409. *
  410. * @since 3.0.0
  411. * @param string $type Type.
  412. * @return bool
  413. */
  414. public function is_setting_type_valid( $type ) {
  415. return in_array(
  416. $type, array(
  417. 'text', // Validates with validate_setting_text_field.
  418. 'email', // Validates with validate_setting_text_field.
  419. 'number', // Validates with validate_setting_text_field.
  420. 'color', // Validates with validate_setting_text_field.
  421. 'password', // Validates with validate_setting_text_field.
  422. 'textarea', // Validates with validate_setting_textarea_field.
  423. 'select', // Validates with validate_setting_select_field.
  424. 'multiselect', // Validates with validate_setting_multiselect_field.
  425. 'radio', // Validates with validate_setting_radio_field (-> validate_setting_select_field).
  426. 'checkbox', // Validates with validate_setting_checkbox_field.
  427. 'image_width', // Validates with validate_setting_image_width_field.
  428. 'thumbnail_cropping', // Validates with validate_setting_text_field.
  429. )
  430. );
  431. }
  432. /**
  433. * Get the settings schema, conforming to JSON Schema.
  434. *
  435. * @since 3.0.0
  436. * @return array
  437. */
  438. public function get_item_schema() {
  439. $schema = array(
  440. '$schema' => 'http://json-schema.org/draft-04/schema#',
  441. 'title' => 'setting',
  442. 'type' => 'object',
  443. 'properties' => array(
  444. 'id' => array(
  445. 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
  446. 'type' => 'string',
  447. 'arg_options' => array(
  448. 'sanitize_callback' => 'sanitize_title',
  449. ),
  450. 'context' => array( 'view', 'edit' ),
  451. 'readonly' => true,
  452. ),
  453. 'label' => array(
  454. 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
  455. 'type' => 'string',
  456. 'arg_options' => array(
  457. 'sanitize_callback' => 'sanitize_text_field',
  458. ),
  459. 'context' => array( 'view', 'edit' ),
  460. 'readonly' => true,
  461. ),
  462. 'description' => array(
  463. 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
  464. 'type' => 'string',
  465. 'arg_options' => array(
  466. 'sanitize_callback' => 'sanitize_text_field',
  467. ),
  468. 'context' => array( 'view', 'edit' ),
  469. 'readonly' => true,
  470. ),
  471. 'value' => array(
  472. 'description' => __( 'Setting value.', 'woocommerce' ),
  473. 'type' => 'mixed',
  474. 'context' => array( 'view', 'edit' ),
  475. ),
  476. 'default' => array(
  477. 'description' => __( 'Default value for the setting.', 'woocommerce' ),
  478. 'type' => 'mixed',
  479. 'context' => array( 'view', 'edit' ),
  480. 'readonly' => true,
  481. ),
  482. 'tip' => array(
  483. 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
  484. 'type' => 'string',
  485. 'arg_options' => array(
  486. 'sanitize_callback' => 'sanitize_text_field',
  487. ),
  488. 'context' => array( 'view', 'edit' ),
  489. 'readonly' => true,
  490. ),
  491. 'placeholder' => array(
  492. 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
  493. 'type' => 'string',
  494. 'arg_options' => array(
  495. 'sanitize_callback' => 'sanitize_text_field',
  496. ),
  497. 'context' => array( 'view', 'edit' ),
  498. 'readonly' => true,
  499. ),
  500. 'type' => array(
  501. 'description' => __( 'Type of setting.', 'woocommerce' ),
  502. 'type' => 'string',
  503. 'arg_options' => array(
  504. 'sanitize_callback' => 'sanitize_text_field',
  505. ),
  506. 'context' => array( 'view', 'edit' ),
  507. 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox', 'thumbnail_cropping' ),
  508. 'readonly' => true,
  509. ),
  510. 'options' => array(
  511. 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
  512. 'type' => 'object',
  513. 'context' => array( 'view', 'edit' ),
  514. 'readonly' => true,
  515. ),
  516. ),
  517. );
  518. return $this->add_additional_fields_schema( $schema );
  519. }
  520. }