Нет описания

class-wc-rest-controller.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. <?php
  2. /**
  3. * REST Controller
  4. *
  5. * This class extend `WP_REST_Controller` in order to include /batch endpoint
  6. * for almost all endpoints in WooCommerce REST API.
  7. *
  8. * It's required to follow "Controller Classes" guide before extending this class:
  9. * <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/>
  10. *
  11. * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS.
  12. * If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`.
  13. *
  14. * @class WC_REST_Controller
  15. * @package WooCommerce\RestApi
  16. * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/
  17. */
  18. if ( ! defined( 'ABSPATH' ) ) {
  19. exit;
  20. }
  21. /**
  22. * Abstract Rest Controller Class
  23. *
  24. * @package WooCommerce\RestApi
  25. * @extends WP_REST_Controller
  26. * @version 2.6.0
  27. */
  28. abstract class WC_REST_Controller extends WP_REST_Controller {
  29. /**
  30. * Endpoint namespace.
  31. *
  32. * @var string
  33. */
  34. protected $namespace = 'wc/v1';
  35. /**
  36. * Route base.
  37. *
  38. * @var string
  39. */
  40. protected $rest_base = '';
  41. /**
  42. * Used to cache computed return fields.
  43. *
  44. * @var null|array
  45. */
  46. private $_fields = null;
  47. /**
  48. * Used to verify if cached fields are for correct request object.
  49. *
  50. * @var null|WP_REST_Request
  51. */
  52. private $_request = null;
  53. /**
  54. * Add the schema from additional fields to an schema array.
  55. *
  56. * The type of object is inferred from the passed schema.
  57. *
  58. * @param array $schema Schema array.
  59. *
  60. * @return array
  61. */
  62. protected function add_additional_fields_schema( $schema ) {
  63. if ( empty( $schema['title'] ) ) {
  64. return $schema;
  65. }
  66. /**
  67. * Can't use $this->get_object_type otherwise we cause an inf loop.
  68. */
  69. $object_type = $schema['title'];
  70. $additional_fields = $this->get_additional_fields( $object_type );
  71. foreach ( $additional_fields as $field_name => $field_options ) {
  72. if ( ! $field_options['schema'] ) {
  73. continue;
  74. }
  75. $schema['properties'][ $field_name ] = $field_options['schema'];
  76. }
  77. $schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] );
  78. return $schema;
  79. }
  80. /**
  81. * Compatibility functions for WP 5.5, since custom types are not supported anymore.
  82. * See @link https://core.trac.wordpress.org/changeset/48306
  83. *
  84. * @param string $method Optional. HTTP method of the request.
  85. *
  86. * @return array Endpoint arguments.
  87. */
  88. public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
  89. $endpoint_args = parent::get_endpoint_args_for_item_schema( $method );
  90. if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) {
  91. return $endpoint_args;
  92. }
  93. $endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args );
  94. return $endpoint_args;
  95. }
  96. /**
  97. * Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore.
  98. *
  99. * See @link https://core.trac.wordpress.org/changeset/48306
  100. *
  101. * We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema.
  102. *
  103. * @param array $endpoint_args Schema with datatypes to convert.
  104. * @return mixed Schema with converted datatype.
  105. */
  106. protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) {
  107. if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) {
  108. return $endpoint_args;
  109. }
  110. foreach ( $endpoint_args as $field_id => $params ) {
  111. if ( ! isset( $params['type'] ) ) {
  112. continue;
  113. }
  114. /**
  115. * Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'.
  116. */
  117. if ( 'date-time' === $params['type'] ) {
  118. $params['type'] = array( 'null', 'string' );
  119. }
  120. /**
  121. * WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods.
  122. */
  123. if ( 'mixed' === $params['type'] ) {
  124. $params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' );
  125. }
  126. if ( isset( $params['properties'] ) ) {
  127. $params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] );
  128. }
  129. if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) {
  130. $params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] );
  131. }
  132. $endpoint_args[ $field_id ] = $params;
  133. }
  134. return $endpoint_args;
  135. }
  136. /**
  137. * Get normalized rest base.
  138. *
  139. * @return string
  140. */
  141. protected function get_normalized_rest_base() {
  142. return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
  143. }
  144. /**
  145. * Check batch limit.
  146. *
  147. * @param array $items Request items.
  148. * @return bool|WP_Error
  149. */
  150. protected function check_batch_limit( $items ) {
  151. $limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
  152. $total = 0;
  153. if ( ! empty( $items['create'] ) ) {
  154. $total += count( $items['create'] );
  155. }
  156. if ( ! empty( $items['update'] ) ) {
  157. $total += count( $items['update'] );
  158. }
  159. if ( ! empty( $items['delete'] ) ) {
  160. $total += count( $items['delete'] );
  161. }
  162. if ( $total > $limit ) {
  163. /* translators: %s: items limit */
  164. return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) );
  165. }
  166. return true;
  167. }
  168. /**
  169. * Bulk create, update and delete items.
  170. *
  171. * @param WP_REST_Request $request Full details about the request.
  172. * @return array Of WP_Error or WP_REST_Response.
  173. */
  174. public function batch_items( $request ) {
  175. /**
  176. * REST Server
  177. *
  178. * @var WP_REST_Server $wp_rest_server
  179. */
  180. global $wp_rest_server;
  181. // Get the request params.
  182. $items = array_filter( $request->get_params() );
  183. $query = $request->get_query_params();
  184. $response = array();
  185. // Check batch limit.
  186. $limit = $this->check_batch_limit( $items );
  187. if ( is_wp_error( $limit ) ) {
  188. return $limit;
  189. }
  190. if ( ! empty( $items['create'] ) ) {
  191. foreach ( $items['create'] as $item ) {
  192. $_item = new WP_REST_Request( 'POST' );
  193. // Default parameters.
  194. $defaults = array();
  195. $schema = $this->get_public_item_schema();
  196. foreach ( $schema['properties'] as $arg => $options ) {
  197. if ( isset( $options['default'] ) ) {
  198. $defaults[ $arg ] = $options['default'];
  199. }
  200. }
  201. $_item->set_default_params( $defaults );
  202. // Set request parameters.
  203. $_item->set_body_params( $item );
  204. // Set query (GET) parameters.
  205. $_item->set_query_params( $query );
  206. $_response = $this->create_item( $_item );
  207. if ( is_wp_error( $_response ) ) {
  208. $response['create'][] = array(
  209. 'id' => 0,
  210. 'error' => array(
  211. 'code' => $_response->get_error_code(),
  212. 'message' => $_response->get_error_message(),
  213. 'data' => $_response->get_error_data(),
  214. ),
  215. );
  216. } else {
  217. $response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
  218. }
  219. }
  220. }
  221. if ( ! empty( $items['update'] ) ) {
  222. foreach ( $items['update'] as $item ) {
  223. $_item = new WP_REST_Request( 'PUT' );
  224. $_item->set_body_params( $item );
  225. $_response = $this->update_item( $_item );
  226. if ( is_wp_error( $_response ) ) {
  227. $response['update'][] = array(
  228. 'id' => $item['id'],
  229. 'error' => array(
  230. 'code' => $_response->get_error_code(),
  231. 'message' => $_response->get_error_message(),
  232. 'data' => $_response->get_error_data(),
  233. ),
  234. );
  235. } else {
  236. $response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
  237. }
  238. }
  239. }
  240. if ( ! empty( $items['delete'] ) ) {
  241. foreach ( $items['delete'] as $id ) {
  242. $id = (int) $id;
  243. if ( 0 === $id ) {
  244. continue;
  245. }
  246. $_item = new WP_REST_Request( 'DELETE' );
  247. $_item->set_query_params(
  248. array(
  249. 'id' => $id,
  250. 'force' => true,
  251. )
  252. );
  253. $_response = $this->delete_item( $_item );
  254. if ( is_wp_error( $_response ) ) {
  255. $response['delete'][] = array(
  256. 'id' => $id,
  257. 'error' => array(
  258. 'code' => $_response->get_error_code(),
  259. 'message' => $_response->get_error_message(),
  260. 'data' => $_response->get_error_data(),
  261. ),
  262. );
  263. } else {
  264. $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
  265. }
  266. }
  267. }
  268. return $response;
  269. }
  270. /**
  271. * Validate a text value for a text based setting.
  272. *
  273. * @since 3.0.0
  274. * @param string $value Value.
  275. * @param array $setting Setting.
  276. * @return string
  277. */
  278. public function validate_setting_text_field( $value, $setting ) {
  279. $value = is_null( $value ) ? '' : $value;
  280. return wp_kses_post( trim( stripslashes( $value ) ) );
  281. }
  282. /**
  283. * Validate select based settings.
  284. *
  285. * @since 3.0.0
  286. * @param string $value Value.
  287. * @param array $setting Setting.
  288. * @return string|WP_Error
  289. */
  290. public function validate_setting_select_field( $value, $setting ) {
  291. if ( array_key_exists( $value, $setting['options'] ) ) {
  292. return $value;
  293. } else {
  294. return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
  295. }
  296. }
  297. /**
  298. * Validate multiselect based settings.
  299. *
  300. * @since 3.0.0
  301. * @param array $values Values.
  302. * @param array $setting Setting.
  303. * @return array|WP_Error
  304. */
  305. public function validate_setting_multiselect_field( $values, $setting ) {
  306. if ( empty( $values ) ) {
  307. return array();
  308. }
  309. if ( ! is_array( $values ) ) {
  310. return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
  311. }
  312. $final_values = array();
  313. foreach ( $values as $value ) {
  314. if ( array_key_exists( $value, $setting['options'] ) ) {
  315. $final_values[] = $value;
  316. }
  317. }
  318. return $final_values;
  319. }
  320. /**
  321. * Validate image_width based settings.
  322. *
  323. * @since 3.0.0
  324. * @param array $values Values.
  325. * @param array $setting Setting.
  326. * @return string|WP_Error
  327. */
  328. public function validate_setting_image_width_field( $values, $setting ) {
  329. if ( ! is_array( $values ) ) {
  330. return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
  331. }
  332. $current = $setting['value'];
  333. if ( isset( $values['width'] ) ) {
  334. $current['width'] = intval( $values['width'] );
  335. }
  336. if ( isset( $values['height'] ) ) {
  337. $current['height'] = intval( $values['height'] );
  338. }
  339. if ( isset( $values['crop'] ) ) {
  340. $current['crop'] = (bool) $values['crop'];
  341. }
  342. return $current;
  343. }
  344. /**
  345. * Validate radio based settings.
  346. *
  347. * @since 3.0.0
  348. * @param string $value Value.
  349. * @param array $setting Setting.
  350. * @return string|WP_Error
  351. */
  352. public function validate_setting_radio_field( $value, $setting ) {
  353. return $this->validate_setting_select_field( $value, $setting );
  354. }
  355. /**
  356. * Validate checkbox based settings.
  357. *
  358. * @since 3.0.0
  359. * @param string $value Value.
  360. * @param array $setting Setting.
  361. * @return string|WP_Error
  362. */
  363. public function validate_setting_checkbox_field( $value, $setting ) {
  364. if ( in_array( $value, array( 'yes', 'no' ) ) ) {
  365. return $value;
  366. } elseif ( empty( $value ) ) {
  367. $value = isset( $setting['default'] ) ? $setting['default'] : 'no';
  368. return $value;
  369. } else {
  370. return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
  371. }
  372. }
  373. /**
  374. * Validate textarea based settings.
  375. *
  376. * @since 3.0.0
  377. * @param string $value Value.
  378. * @param array $setting Setting.
  379. * @return string
  380. */
  381. public function validate_setting_textarea_field( $value, $setting ) {
  382. $value = is_null( $value ) ? '' : $value;
  383. return wp_kses(
  384. trim( stripslashes( $value ) ),
  385. array_merge(
  386. array(
  387. 'iframe' => array(
  388. 'src' => true,
  389. 'style' => true,
  390. 'id' => true,
  391. 'class' => true,
  392. ),
  393. ),
  394. wp_kses_allowed_html( 'post' )
  395. )
  396. );
  397. }
  398. /**
  399. * Add meta query.
  400. *
  401. * @since 3.0.0
  402. * @param array $args Query args.
  403. * @param array $meta_query Meta query.
  404. * @return array
  405. */
  406. protected function add_meta_query( $args, $meta_query ) {
  407. if ( empty( $args['meta_query'] ) ) {
  408. $args['meta_query'] = array();
  409. }
  410. $args['meta_query'][] = $meta_query;
  411. return $args['meta_query'];
  412. }
  413. /**
  414. * Get the batch schema, conforming to JSON Schema.
  415. *
  416. * @return array
  417. */
  418. public function get_public_batch_schema() {
  419. $schema = array(
  420. '$schema' => 'http://json-schema.org/draft-04/schema#',
  421. 'title' => 'batch',
  422. 'type' => 'object',
  423. 'properties' => array(
  424. 'create' => array(
  425. 'description' => __( 'List of created resources.', 'woocommerce' ),
  426. 'type' => 'array',
  427. 'context' => array( 'view', 'edit' ),
  428. 'items' => array(
  429. 'type' => 'object',
  430. ),
  431. ),
  432. 'update' => array(
  433. 'description' => __( 'List of updated resources.', 'woocommerce' ),
  434. 'type' => 'array',
  435. 'context' => array( 'view', 'edit' ),
  436. 'items' => array(
  437. 'type' => 'object',
  438. ),
  439. ),
  440. 'delete' => array(
  441. 'description' => __( 'List of delete resources.', 'woocommerce' ),
  442. 'type' => 'array',
  443. 'context' => array( 'view', 'edit' ),
  444. 'items' => array(
  445. 'type' => 'integer',
  446. ),
  447. ),
  448. ),
  449. );
  450. return $schema;
  451. }
  452. /**
  453. * Gets an array of fields to be included on the response.
  454. *
  455. * Included fields are based on item schema and `_fields=` request argument.
  456. * Updated from WordPress 5.3, included into this class to support old versions.
  457. *
  458. * @since 3.5.0
  459. * @param WP_REST_Request $request Full details about the request.
  460. * @return array Fields to be included in the response.
  461. */
  462. public function get_fields_for_response( $request ) {
  463. // From xdebug profiling, this method could take upto 25% of request time in index calls.
  464. // Cache it and make sure _fields was cached on current request object!
  465. // TODO: Submit this caching behavior in core.
  466. if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) {
  467. return $this->_fields;
  468. }
  469. $this->_request = $request;
  470. $schema = $this->get_item_schema();
  471. $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
  472. $additional_fields = $this->get_additional_fields();
  473. foreach ( $additional_fields as $field_name => $field_options ) {
  474. // For back-compat, include any field with an empty schema
  475. // because it won't be present in $this->get_item_schema().
  476. if ( is_null( $field_options['schema'] ) ) {
  477. $properties[ $field_name ] = $field_options;
  478. }
  479. }
  480. // Exclude fields that specify a different context than the request context.
  481. $context = $request['context'];
  482. if ( $context ) {
  483. foreach ( $properties as $name => $options ) {
  484. if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
  485. unset( $properties[ $name ] );
  486. }
  487. }
  488. }
  489. $fields = array_keys( $properties );
  490. if ( ! isset( $request['_fields'] ) ) {
  491. $this->_fields = $fields;
  492. return $fields;
  493. }
  494. $requested_fields = wp_parse_list( $request['_fields'] );
  495. if ( 0 === count( $requested_fields ) ) {
  496. $this->_fields = $fields;
  497. return $fields;
  498. }
  499. // Trim off outside whitespace from the comma delimited list.
  500. $requested_fields = array_map( 'trim', $requested_fields );
  501. // Always persist 'id', because it can be needed for add_additional_fields_to_object().
  502. if ( in_array( 'id', $fields, true ) ) {
  503. $requested_fields[] = 'id';
  504. }
  505. // Return the list of all requested fields which appear in the schema.
  506. $this->_fields = array_reduce(
  507. $requested_fields,
  508. function( $response_fields, $field ) use ( $fields ) {
  509. if ( in_array( $field, $fields, true ) ) {
  510. $response_fields[] = $field;
  511. return $response_fields;
  512. }
  513. // Check for nested fields if $field is not a direct match.
  514. $nested_fields = explode( '.', $field );
  515. // A nested field is included so long as its top-level property
  516. // is present in the schema.
  517. if ( in_array( $nested_fields[0], $fields, true ) ) {
  518. $response_fields[] = $field;
  519. }
  520. return $response_fields;
  521. },
  522. array()
  523. );
  524. return $this->_fields;
  525. }
  526. }