PageRenderTime 121ms CodeModel.GetById 34ms RepoModel.GetById 1ms app.codeStats 0ms

/wordpress/wp-content/plugins/woocommerce/includes/cli/class-wc-cli-rest-command.php

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