class-wc-api-webhooks.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <?php
  2. /**
  3. * WooCommerce API Webhooks class
  4. *
  5. * Handles requests to the /webhooks endpoint
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 2.2
  11. */
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit; // Exit if accessed directly
  14. }
  15. class WC_API_Webhooks extends WC_API_Resource {
  16. /** @var string $base the route base */
  17. protected $base = '/webhooks';
  18. /**
  19. * Register the routes for this class
  20. *
  21. * @since 2.2
  22. * @param array $routes
  23. * @return array
  24. */
  25. public function register_routes( $routes ) {
  26. # GET|POST /webhooks
  27. $routes[ $this->base ] = array(
  28. array( array( $this, 'get_webhooks' ), WC_API_Server::READABLE ),
  29. array( array( $this, 'create_webhook' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ),
  30. );
  31. # GET /webhooks/count
  32. $routes[ $this->base . '/count' ] = array(
  33. array( array( $this, 'get_webhooks_count' ), WC_API_Server::READABLE ),
  34. );
  35. # GET|PUT|DELETE /webhooks/<id>
  36. $routes[ $this->base . '/(?P<id>\d+)' ] = array(
  37. array( array( $this, 'get_webhook' ), WC_API_Server::READABLE ),
  38. array( array( $this, 'edit_webhook' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ),
  39. array( array( $this, 'delete_webhook' ), WC_API_Server::DELETABLE ),
  40. );
  41. # GET /webhooks/<id>/deliveries
  42. $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries' ] = array(
  43. array( array( $this, 'get_webhook_deliveries' ), WC_API_Server::READABLE ),
  44. );
  45. # GET /webhooks/<webhook_id>/deliveries/<id>
  46. $routes[ $this->base . '/(?P<webhook_id>\d+)/deliveries/(?P<id>\d+)' ] = array(
  47. array( array( $this, 'get_webhook_delivery' ), WC_API_Server::READABLE ),
  48. );
  49. return $routes;
  50. }
  51. /**
  52. * Get all webhooks
  53. *
  54. * @since 2.2
  55. *
  56. * @param array $fields
  57. * @param array $filter
  58. * @param string $status
  59. * @param int $page
  60. *
  61. * @return array
  62. */
  63. public function get_webhooks( $fields = null, $filter = array(), $status = null, $page = 1 ) {
  64. if ( ! empty( $status ) ) {
  65. $filter['status'] = $status;
  66. }
  67. $filter['page'] = $page;
  68. $query = $this->query_webhooks( $filter );
  69. $webhooks = array();
  70. foreach ( $query['results'] as $webhook_id ) {
  71. $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) );
  72. }
  73. $this->server->add_pagination_headers( $query['headers'] );
  74. return array( 'webhooks' => $webhooks );
  75. }
  76. /**
  77. * Get the webhook for the given ID
  78. *
  79. * @since 2.2
  80. * @param int $id webhook ID
  81. * @param array $fields
  82. * @return array|WP_Error
  83. */
  84. public function get_webhook( $id, $fields = null ) {
  85. // ensure webhook ID is valid & user has permission to read
  86. $id = $this->validate_request( $id, 'shop_webhook', 'read' );
  87. if ( is_wp_error( $id ) ) {
  88. return $id;
  89. }
  90. $webhook = wc_get_webhook( $id );
  91. $webhook_data = array(
  92. 'id' => $webhook->get_id(),
  93. 'name' => $webhook->get_name(),
  94. 'status' => $webhook->get_status(),
  95. 'topic' => $webhook->get_topic(),
  96. 'resource' => $webhook->get_resource(),
  97. 'event' => $webhook->get_event(),
  98. 'hooks' => $webhook->get_hooks(),
  99. 'delivery_url' => $webhook->get_delivery_url(),
  100. 'created_at' => $this->server->format_datetime( $webhook->get_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times.
  101. 'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times.
  102. );
  103. return array( 'webhook' => apply_filters( 'woocommerce_api_webhook_response', $webhook_data, $webhook, $fields, $this ) );
  104. }
  105. /**
  106. * Get the total number of webhooks
  107. *
  108. * @since 2.2
  109. *
  110. * @param string $status
  111. * @param array $filter
  112. *
  113. * @return array|WP_Error
  114. */
  115. public function get_webhooks_count( $status = null, $filter = array() ) {
  116. try {
  117. if ( ! current_user_can( 'manage_woocommerce' ) ) {
  118. throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_webhooks_count', __( 'You do not have permission to read the webhooks count', 'woocommerce' ), 401 );
  119. }
  120. if ( ! empty( $status ) ) {
  121. $filter['status'] = $status;
  122. }
  123. $query = $this->query_webhooks( $filter );
  124. return array( 'count' => $query['headers']->total );
  125. } catch ( WC_API_Exception $e ) {
  126. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  127. }
  128. }
  129. /**
  130. * Create an webhook
  131. *
  132. * @since 2.2
  133. *
  134. * @param array $data parsed webhook data
  135. *
  136. * @return array|WP_Error
  137. */
  138. public function create_webhook( $data ) {
  139. try {
  140. if ( ! isset( $data['webhook'] ) ) {
  141. throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'webhook' ), 400 );
  142. }
  143. $data = $data['webhook'];
  144. // permission check
  145. if ( ! current_user_can( 'manage_woocommerce' ) ) {
  146. throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_webhooks', __( 'You do not have permission to create webhooks.', 'woocommerce' ), 401 );
  147. }
  148. $data = apply_filters( 'woocommerce_api_create_webhook_data', $data, $this );
  149. // validate topic
  150. if ( empty( $data['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
  151. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic is required and must be valid.', 'woocommerce' ), 400 );
  152. }
  153. // validate delivery URL
  154. if ( empty( $data['delivery_url'] ) || ! wc_is_valid_url( $data['delivery_url'] ) ) {
  155. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
  156. }
  157. $webhook_data = apply_filters( 'woocommerce_new_webhook_data', array(
  158. 'post_type' => 'shop_webhook',
  159. 'post_status' => 'publish',
  160. 'ping_status' => 'closed',
  161. 'post_author' => get_current_user_id(),
  162. 'post_password' => strlen( ( $password = uniqid( 'webhook_' ) ) ) > 20 ? substr( $password, 0, 20 ) : $password,
  163. 'post_title' => ! empty( $data['name'] ) ? $data['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ),
  164. ), $data, $this );
  165. $webhook = new WC_Webhook();
  166. $webhook->set_name( $webhook_data['post_title'] );
  167. $webhook->set_user_id( $webhook_data['post_author'] );
  168. $webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' );
  169. $webhook->set_topic( $data['topic'] );
  170. $webhook->set_delivery_url( $data['delivery_url'] );
  171. $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) );
  172. $webhook->set_api_version( 'legacy_v3' );
  173. $webhook->save();
  174. $webhook->deliver_ping();
  175. // HTTP 201 Created
  176. $this->server->send_status( 201 );
  177. do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this );
  178. return $this->get_webhook( $webhook->get_id() );
  179. } catch ( WC_API_Exception $e ) {
  180. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  181. }
  182. }
  183. /**
  184. * Edit a webhook
  185. *
  186. * @since 2.2
  187. *
  188. * @param int $id webhook ID
  189. * @param array $data parsed webhook data
  190. *
  191. * @return array|WP_Error
  192. */
  193. public function edit_webhook( $id, $data ) {
  194. try {
  195. if ( ! isset( $data['webhook'] ) ) {
  196. throw new WC_API_Exception( 'woocommerce_api_missing_webhook_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'webhook' ), 400 );
  197. }
  198. $data = $data['webhook'];
  199. $id = $this->validate_request( $id, 'shop_webhook', 'edit' );
  200. if ( is_wp_error( $id ) ) {
  201. return $id;
  202. }
  203. $data = apply_filters( 'woocommerce_api_edit_webhook_data', $data, $id, $this );
  204. $webhook = wc_get_webhook( $id );
  205. // update topic
  206. if ( ! empty( $data['topic'] ) ) {
  207. if ( wc_is_webhook_valid_topic( strtolower( $data['topic'] ) ) ) {
  208. $webhook->set_topic( $data['topic'] );
  209. } else {
  210. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_topic', __( 'Webhook topic must be valid.', 'woocommerce' ), 400 );
  211. }
  212. }
  213. // update delivery URL
  214. if ( ! empty( $data['delivery_url'] ) ) {
  215. if ( wc_is_valid_url( $data['delivery_url'] ) ) {
  216. $webhook->set_delivery_url( $data['delivery_url'] );
  217. } else {
  218. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_url', __( 'Webhook delivery URL must be a valid URL starting with http:// or https://', 'woocommerce' ), 400 );
  219. }
  220. }
  221. // update secret
  222. if ( ! empty( $data['secret'] ) ) {
  223. $webhook->set_secret( $data['secret'] );
  224. }
  225. // update status
  226. if ( ! empty( $data['status'] ) ) {
  227. $webhook->set_status( $data['status'] );
  228. }
  229. // update name
  230. if ( ! empty( $data['name'] ) ) {
  231. $webhook->set_name( $data['name'] );
  232. }
  233. $webhook->save();
  234. do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this );
  235. return $this->get_webhook( $webhook->get_id() );
  236. } catch ( WC_API_Exception $e ) {
  237. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  238. }
  239. }
  240. /**
  241. * Delete a webhook
  242. *
  243. * @since 2.2
  244. * @param int $id webhook ID
  245. * @return array|WP_Error
  246. */
  247. public function delete_webhook( $id ) {
  248. $id = $this->validate_request( $id, 'shop_webhook', 'delete' );
  249. if ( is_wp_error( $id ) ) {
  250. return $id;
  251. }
  252. do_action( 'woocommerce_api_delete_webhook', $id, $this );
  253. $webhook = wc_get_webhook( $id );
  254. return $webhook->delete( true );
  255. }
  256. /**
  257. * Get webhooks total results
  258. *
  259. * @since 3.3.0
  260. * @param array $args Request arguments for filtering query.
  261. * @return array
  262. */
  263. private function get_webhooks_total_results( $args = array() ) {
  264. $data_store = WC_Data_Store::load( 'webhook' );
  265. $args['limit'] = -1;
  266. $args['offset'] = 0;
  267. return count( $data_store->search_webhooks( $args ) );
  268. }
  269. /**
  270. * Helper method to get webhook post objects
  271. *
  272. * @since 2.2
  273. * @param array $args Request arguments for filtering query.
  274. * @return array
  275. */
  276. private function query_webhooks( $args ) {
  277. $args = $this->merge_query_args( array(), $args );
  278. $args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) );
  279. if ( empty( $args['offset'] ) ) {
  280. $args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0;
  281. }
  282. $page = $args['paged'];
  283. unset( $args['paged'], $args['posts_per_page'] );
  284. if ( isset( $args['s'] ) ) {
  285. $args['search'] = $args['s'];
  286. unset( $args['s'] );
  287. }
  288. // Post type to webhook status.
  289. if ( ! empty( $args['post_status'] ) ) {
  290. $args['status'] = $args['post_status'];
  291. unset( $args['post_status'] );
  292. }
  293. if ( ! empty( $args['post__in'] ) ) {
  294. $args['include'] = $args['post__in'];
  295. unset( $args['post__in'] );
  296. }
  297. if ( ! empty( $args['date_query'] ) ) {
  298. foreach ( $args['date_query'] as $date_query ) {
  299. if ( 'post_date_gmt' === $date_query['column'] ) {
  300. $args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
  301. $args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
  302. } elseif ( 'post_modified_gmt' === $date_query['column'] ) {
  303. $args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null;
  304. $args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null;
  305. }
  306. }
  307. unset( $args['date_query'] );
  308. }
  309. // Get the webhooks.
  310. $data_store = WC_Data_Store::load( 'webhook' );
  311. $results = $data_store->search_webhooks( $args );
  312. // Get total items.
  313. $headers = new stdClass;
  314. $headers->page = $page;
  315. $headers->total = $this->get_webhooks_total_results( $args );
  316. $headers->is_single = $args['limit'] > $headers->total;
  317. $headers->total_pages = ceil( $headers->total / $args['limit'] );
  318. return array(
  319. 'results' => $results,
  320. 'headers' => $headers,
  321. );
  322. }
  323. /**
  324. * Get deliveries for a webhook
  325. *
  326. * @since 2.2
  327. * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
  328. * @param string $webhook_id webhook ID
  329. * @param string|null $fields fields to include in response
  330. * @return array|WP_Error
  331. */
  332. public function get_webhook_deliveries( $webhook_id, $fields = null ) {
  333. // Ensure ID is valid webhook ID
  334. $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
  335. if ( is_wp_error( $webhook_id ) ) {
  336. return $webhook_id;
  337. }
  338. return array( 'webhook_deliveries' => array() );
  339. }
  340. /**
  341. * Get the delivery log for the given webhook ID and delivery ID
  342. *
  343. * @since 2.2
  344. * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
  345. * @param string $webhook_id webhook ID
  346. * @param string $id delivery log ID
  347. * @param string|null $fields fields to limit response to
  348. *
  349. * @return array|WP_Error
  350. */
  351. public function get_webhook_delivery( $webhook_id, $id, $fields = null ) {
  352. try {
  353. // Validate webhook ID
  354. $webhook_id = $this->validate_request( $webhook_id, 'shop_webhook', 'read' );
  355. if ( is_wp_error( $webhook_id ) ) {
  356. return $webhook_id;
  357. }
  358. $id = absint( $id );
  359. if ( empty( $id ) ) {
  360. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery ID.', 'woocommerce' ), 404 );
  361. }
  362. $webhook = new WC_Webhook( $webhook_id );
  363. $log = 0;
  364. if ( ! $log ) {
  365. throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 );
  366. }
  367. return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) );
  368. } catch ( WC_API_Exception $e ) {
  369. return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
  370. }
  371. }
  372. /**
  373. * Validate the request by checking:
  374. *
  375. * 1) the ID is a valid integer.
  376. * 2) the ID returns a valid post object and matches the provided post type.
  377. * 3) the current user has the proper permissions to read/edit/delete the post.
  378. *
  379. * @since 3.3.0
  380. * @param string|int $id The post ID
  381. * @param string $type The post type, either `shop_order`, `shop_coupon`, or `product`.
  382. * @param string $context The context of the request, either `read`, `edit` or `delete`.
  383. * @return int|WP_Error Valid post ID or WP_Error if any of the checks fails.
  384. */
  385. protected function validate_request( $id, $type, $context ) {
  386. $id = absint( $id );
  387. // Validate ID.
  388. if ( empty( $id ) ) {
  389. return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
  390. }
  391. $webhook = wc_get_webhook( $id );
  392. if ( null === $webhook ) {
  393. return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) );
  394. }
  395. // Validate permissions.
  396. switch ( $context ) {
  397. case 'read':
  398. if ( ! current_user_can( 'manage_woocommerce' ) ) {
  399. return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
  400. }
  401. break;
  402. case 'edit':
  403. if ( ! current_user_can( 'manage_woocommerce' ) ) {
  404. return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
  405. }
  406. break;
  407. case 'delete':
  408. if ( ! current_user_can( 'manage_woocommerce' ) ) {
  409. return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) );
  410. }
  411. break;
  412. }
  413. return $id;
  414. }
  415. }