Нет описания

class-wc-cli-rest-command.php 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. /**
  3. * WP_CLI_Rest_Command class file.
  4. *
  5. * @package WooCommerce\CLI
  6. */
  7. use Automattic\Jetpack\Constants;
  8. use Automattic\WooCommerce\Utilities\NumberUtil;
  9. if ( ! defined( 'ABSPATH' ) ) {
  10. exit;
  11. }
  12. /**
  13. * Main Command for WooCommere CLI.
  14. *
  15. * Since a lot of WC operations can be handled via the REST API, we base our CLI
  16. * off of Restful to generate commands for each WooCommerce REST API endpoint
  17. * so most of the logic is shared.
  18. *
  19. * Forked from wp-cli/restful (by Daniel Bachhuber, released under the MIT license https://opensource.org/licenses/MIT).
  20. * https://github.com/wp-cli/restful
  21. *
  22. * @version 3.0.0
  23. * @package WooCommerce
  24. */
  25. class WC_CLI_REST_Command {
  26. /**
  27. * Endpoints that have a parent ID.
  28. * Ex: Product reviews, which has a product ID and a review ID.
  29. *
  30. * @var array
  31. */
  32. protected $routes_with_parent_id = array(
  33. 'customer_download',
  34. 'product_review',
  35. 'order_note',
  36. 'shop_order_refund',
  37. );
  38. /**
  39. * Name of command/endpoint object.
  40. *
  41. * @var string
  42. */
  43. private $name;
  44. /**
  45. * Endpoint route.
  46. *
  47. * @var string
  48. */
  49. private $route;
  50. /**
  51. * Main resource ID.
  52. *
  53. * @var int
  54. */
  55. private $resource_identifier;
  56. /**
  57. * Schema for command.
  58. *
  59. * @var array
  60. */
  61. private $schema;
  62. /**
  63. * List of supported IDs and their description (name => desc).
  64. *
  65. * @var array
  66. */
  67. private $supported_ids = array();
  68. /**
  69. * Sets up REST Command.
  70. *
  71. * @param string $name Name of endpoint object (comes from schema).
  72. * @param string $route Path to route of this endpoint.
  73. * @param array $schema Schema object.
  74. */
  75. public function __construct( $name, $route, $schema ) {
  76. $this->name = $name;
  77. preg_match_all( '#\([^\)]+\)#', $route, $matches );
  78. $first_match = $matches[0];
  79. $resource_id = ! empty( $matches[0] ) ? array_pop( $matches[0] ) : null;
  80. $this->route = rtrim( $route );
  81. $this->schema = $schema;
  82. $this->resource_identifier = $resource_id;
  83. if ( in_array( $name, $this->routes_with_parent_id, true ) ) {
  84. $is_singular = substr( $this->route, - strlen( $resource_id ) ) === $resource_id;
  85. if ( ! $is_singular ) {
  86. $this->resource_identifier = $first_match[0];
  87. }
  88. }
  89. }
  90. /**
  91. * Passes supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
  92. *
  93. * @param array $supported_ids List of supported IDs.
  94. */
  95. public function set_supported_ids( $supported_ids = array() ) {
  96. $this->supported_ids = $supported_ids;
  97. }
  98. /**
  99. * Returns an ID of supported ID arguments (things like product_id, order_id, etc) that we should look for in addition to id.
  100. *
  101. * @return array
  102. */
  103. public function get_supported_ids() {
  104. return $this->supported_ids;
  105. }
  106. /**
  107. * Create a new item.
  108. *
  109. * @subcommand create
  110. *
  111. * @param array $args WP-CLI positional arguments.
  112. * @param array $assoc_args WP-CLI associative arguments.
  113. */
  114. public function create_item( $args, $assoc_args ) {
  115. $assoc_args = self::decode_json( $assoc_args );
  116. list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
  117. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
  118. WP_CLI::line( $body['id'] );
  119. } else {
  120. WP_CLI::success( "Created {$this->name} {$body['id']}." );
  121. }
  122. }
  123. /**
  124. * Delete an existing item.
  125. *
  126. * @subcommand delete
  127. *
  128. * @param array $args WP-CLI positional arguments.
  129. * @param array $assoc_args WP-CLI associative arguments.
  130. */
  131. public function delete_item( $args, $assoc_args ) {
  132. list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args );
  133. $object_id = isset( $body['id'] ) ? $body['id'] : '';
  134. if ( ! $object_id && isset( $body['slug'] ) ) {
  135. $object_id = $body['slug'];
  136. }
  137. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
  138. WP_CLI::line( $object_id );
  139. } else {
  140. if ( empty( $assoc_args['force'] ) ) {
  141. WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$object_id}" );
  142. } else {
  143. WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$object_id}." );
  144. }
  145. }
  146. }
  147. /**
  148. * Get a single item.
  149. *
  150. * @subcommand get
  151. *
  152. * @param array $args WP-CLI positional arguments.
  153. * @param array $assoc_args WP-CLI associative arguments.
  154. */
  155. public function get_item( $args, $assoc_args ) {
  156. $route = $this->get_filled_route( $args );
  157. list( $status, $body, $headers ) = $this->do_request( 'GET', $route, $assoc_args );
  158. if ( ! empty( $assoc_args['fields'] ) ) {
  159. $body = self::limit_item_to_fields( $body, $assoc_args['fields'] );
  160. }
  161. if ( empty( $assoc_args['format'] ) ) {
  162. $assoc_args['format'] = 'table';
  163. }
  164. if ( 'headers' === $assoc_args['format'] ) {
  165. echo wp_json_encode( $headers );
  166. } elseif ( 'body' === $assoc_args['format'] ) {
  167. echo wp_json_encode( $body );
  168. } elseif ( 'envelope' === $assoc_args['format'] ) {
  169. echo wp_json_encode(
  170. array(
  171. 'body' => $body,
  172. 'headers' => $headers,
  173. 'status' => $status,
  174. )
  175. );
  176. } else {
  177. $formatter = $this->get_formatter( $assoc_args );
  178. $formatter->display_item( $body );
  179. }
  180. }
  181. /**
  182. * List all items.
  183. *
  184. * @subcommand list
  185. *
  186. * @param array $args WP-CLI positional arguments.
  187. * @param array $assoc_args WP-CLI associative arguments.
  188. */
  189. public function list_items( $args, $assoc_args ) {
  190. if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
  191. $method = 'HEAD';
  192. } else {
  193. $method = 'GET';
  194. }
  195. if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) {
  196. $assoc_args['per_page'] = '100';
  197. }
  198. list( $status, $body, $headers ) = $this->do_request( $method, $this->get_filled_route( $args ), $assoc_args );
  199. if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
  200. $items = array_column( $body, 'id' );
  201. } else {
  202. $items = $body;
  203. }
  204. if ( ! empty( $assoc_args['fields'] ) ) {
  205. foreach ( $items as $key => $item ) {
  206. $items[ $key ] = self::limit_item_to_fields( $item, $assoc_args['fields'] );
  207. }
  208. }
  209. if ( empty( $assoc_args['format'] ) ) {
  210. $assoc_args['format'] = 'table';
  211. }
  212. if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) {
  213. echo (int) $headers['X-WP-Total'];
  214. } elseif ( 'headers' === $assoc_args['format'] ) {
  215. echo wp_json_encode( $headers );
  216. } elseif ( 'body' === $assoc_args['format'] ) {
  217. echo wp_json_encode( $body );
  218. } elseif ( 'envelope' === $assoc_args['format'] ) {
  219. echo wp_json_encode(
  220. array(
  221. 'body' => $body,
  222. 'headers' => $headers,
  223. 'status' => $status,
  224. 'api_url' => $this->api_url,
  225. )
  226. );
  227. } else {
  228. $formatter = $this->get_formatter( $assoc_args );
  229. $formatter->display_items( $items );
  230. }
  231. }
  232. /**
  233. * Update an existing item.
  234. *
  235. * @subcommand update
  236. *
  237. * @param array $args WP-CLI positional arguments.
  238. * @param array $assoc_args WP-CLI associative arguments.
  239. */
  240. public function update_item( $args, $assoc_args ) {
  241. $assoc_args = self::decode_json( $assoc_args );
  242. list( $status, $body ) = $this->do_request( 'POST', $this->get_filled_route( $args ), $assoc_args );
  243. if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
  244. WP_CLI::line( $body['id'] );
  245. } else {
  246. WP_CLI::success( __( 'Updated', 'woocommerce' ) . " {$this->name} {$body['id']}." );
  247. }
  248. }
  249. /**
  250. * Do a REST Request
  251. *
  252. * @param string $method Request method. Examples: 'POST', 'PUT', 'DELETE' or 'GET'.
  253. * @param string $route Resource route.
  254. * @param array $assoc_args Associative arguments passed to the originating WP-CLI command.
  255. *
  256. * @return array
  257. */
  258. private function do_request( $method, $route, $assoc_args ) {
  259. wc_maybe_define_constant( 'REST_REQUEST', true );
  260. $request = new WP_REST_Request( $method, $route );
  261. if ( in_array( $method, array( 'POST', 'PUT' ), true ) ) {
  262. $request->set_body_params( $assoc_args );
  263. } else {
  264. foreach ( $assoc_args as $key => $value ) {
  265. $request->set_param( $key, $value );
  266. }
  267. }
  268. if ( Constants::is_true( 'SAVEQUERIES' ) ) {
  269. $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array();
  270. }
  271. $response = rest_do_request( $request );
  272. if ( Constants::is_true( 'SAVEQUERIES' ) ) {
  273. $performed_queries = array();
  274. foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) {
  275. if ( in_array( $key, $original_queries, true ) ) {
  276. continue;
  277. }
  278. $performed_queries[] = $query;
  279. }
  280. usort(
  281. $performed_queries,
  282. function( $a, $b ) {
  283. if ( $a[1] === $b[1] ) {
  284. return 0;
  285. }
  286. return ( $a[1] > $b[1] ) ? -1 : 1;
  287. }
  288. );
  289. $query_count = count( $performed_queries );
  290. $query_total_time = 0;
  291. foreach ( $performed_queries as $query ) {
  292. $query_total_time += $query[1];
  293. }
  294. $slow_query_message = '';
  295. if ( $performed_queries && 'wc' === WP_CLI::get_config( 'debug' ) ) {
  296. $slow_query_message .= '. Ordered by slowness, the queries are:' . PHP_EOL;
  297. foreach ( $performed_queries as $i => $query ) {
  298. $i++;
  299. $bits = explode( ', ', $query[2] );
  300. $backtrace = implode( ', ', array_slice( $bits, 13 ) );
  301. $seconds = NumberUtil::round( $query[1], 6 );
  302. $slow_query_message .= <<<EOT
  303. {$i}:
  304. - {$seconds} seconds
  305. - {$backtrace}
  306. - {$query[0]}
  307. EOT;
  308. $slow_query_message .= PHP_EOL;
  309. }
  310. } elseif ( 'wc' !== WP_CLI::get_config( 'debug' ) ) {
  311. $slow_query_message = '. Use --debug=wc to see all queries.';
  312. }
  313. $query_total_time = NumberUtil::round( $query_total_time, 6 );
  314. WP_CLI::debug( "wc command executed {$query_count} queries in {$query_total_time} seconds{$slow_query_message}", 'wc' );
  315. }
  316. $error = $response->as_error();
  317. if ( $error ) {
  318. // For authentication errors (status 401), include a reminder to set the --user flag.
  319. // WP_CLI::error will only return the first message from WP_Error, so we will pass a string containing both instead.
  320. if ( 401 === $response->get_status() ) {
  321. $errors = $error->get_error_messages();
  322. $errors[] = __( 'Make sure to include the --user flag with an account that has permissions for this action.', 'woocommerce' ) . ' {"status":401}';
  323. $error = implode( "\n", $errors );
  324. }
  325. WP_CLI::error( $error );
  326. }
  327. return array( $response->get_status(), $response->get_data(), $response->get_headers() );
  328. }
  329. /**
  330. * Get Formatter object based on supplied parameters.
  331. *
  332. * @param array $assoc_args Parameters passed to command. Determines formatting.
  333. * @return \WP_CLI\Formatter
  334. */
  335. protected function get_formatter( &$assoc_args ) {
  336. if ( ! empty( $assoc_args['fields'] ) ) {
  337. if ( is_string( $assoc_args['fields'] ) ) {
  338. $fields = explode( ',', $assoc_args['fields'] );
  339. } else {
  340. $fields = $assoc_args['fields'];
  341. }
  342. } else {
  343. if ( ! empty( $assoc_args['context'] ) ) {
  344. $fields = $this->get_context_fields( $assoc_args['context'] );
  345. } else {
  346. $fields = $this->get_context_fields( 'view' );
  347. }
  348. }
  349. return new \WP_CLI\Formatter( $assoc_args, $fields );
  350. }
  351. /**
  352. * Get a list of fields present in a given context
  353. *
  354. * @param string $context Scope under which the request is made. Determines fields present in response.
  355. * @return array
  356. */
  357. private function get_context_fields( $context ) {
  358. $fields = array();
  359. foreach ( $this->schema['properties'] as $key => $args ) {
  360. if ( empty( $args['context'] ) || in_array( $context, $args['context'], true ) ) {
  361. $fields[] = $key;
  362. }
  363. }
  364. return $fields;
  365. }
  366. /**
  367. * Get the route for this resource
  368. *
  369. * @param array $args Positional arguments passed to the originating WP-CLI command.
  370. * @return string
  371. */
  372. private function get_filled_route( $args = array() ) {
  373. $supported_id_matched = false;
  374. $route = $this->route;
  375. foreach ( $this->get_supported_ids() as $id_name => $id_desc ) {
  376. if ( 'id' !== $id_name && strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) {
  377. $route = str_replace( array( '(?P<' . $id_name . '>[\d]+)', '(?P<' . $id_name . '>\w[\w\s\-]*)' ), $args[0], $route );
  378. $supported_id_matched = true;
  379. }
  380. }
  381. if ( ! empty( $args ) ) {
  382. $id_replacement = $supported_id_matched && ! empty( $args[1] ) ? $args[1] : $args[0];
  383. $route = str_replace( array( '(?P<id>[\d]+)', '(?P<id>[\w-]+)' ), $id_replacement, $route );
  384. }
  385. return rtrim( $route );
  386. }
  387. /**
  388. * Reduce an item to specific fields.
  389. *
  390. * @param array $item Item to reduce.
  391. * @param array $fields Fields to keep.
  392. * @return array
  393. */
  394. private static function limit_item_to_fields( $item, $fields ) {
  395. if ( empty( $fields ) ) {
  396. return $item;
  397. }
  398. if ( is_string( $fields ) ) {
  399. $fields = explode( ',', $fields );
  400. }
  401. foreach ( $item as $i => $field ) {
  402. if ( ! in_array( $i, $fields, true ) ) {
  403. unset( $item[ $i ] );
  404. }
  405. }
  406. return $item;
  407. }
  408. /**
  409. * JSON can be passed in some more complicated objects, like the payment gateway settings array.
  410. * This function decodes the json (if present) and tries to get it's value.
  411. *
  412. * @param array $arr Array that will be scanned for JSON encoded values.
  413. *
  414. * @return array
  415. */
  416. protected function decode_json( $arr ) {
  417. foreach ( $arr as $key => $value ) {
  418. if ( '[' === substr( $value, 0, 1 ) || '{' === substr( $value, 0, 1 ) ) {
  419. $arr[ $key ] = json_decode( $value, true );
  420. } else {
  421. continue;
  422. }
  423. }
  424. return $arr;
  425. }
  426. }