/includes/class-wc-webhook.php

  1. <?php 
  2.  
  3. if ( ! defined( 'ABSPATH' ) ) { 
  4. exit; // Exit if accessed directly 
  5.  
  6. /** 
  7. * WooCommerce Webhook class. 
  8. * 
  9. * This class handles storing and retrieving webhook data from the associated. 
  10. * `shop_webhook` custom post type, as well as delivery logs from the `webhook_delivery`. 
  11. * comment type. 
  12. * 
  13. * Webhooks are enqueued to their associated actions, delivered, and logged. 
  14. * 
  15. * @author WooThemes 
  16. * @category Webhooks 
  17. * @package WooCommerce/Webhooks 
  18. * @since 2.2 
  19. */ 
  20. class WC_Webhook { 
  21.  
  22. /** @var int webhook ID (post ID) */ 
  23. public $id; 
  24.  
  25. /** 
  26. * Setup webhook & load post data. 
  27. * 
  28. * @since 2.2 
  29. * @param string|int $id 
  30. */ 
  31. public function __construct( $id ) { 
  32.  
  33. $id = absint( $id ); 
  34.  
  35. if ( ! $id ) { 
  36. return; 
  37.  
  38. $this->id = $id; 
  39. $this->post_data = get_post( $id ); 
  40.  
  41.  
  42. /** 
  43. * Magic isset as a wrapper around metadata_exists(). 
  44. * 
  45. * @since 2.2 
  46. * @param string $key 
  47. * @return bool true if $key isset, false otherwise 
  48. */ 
  49. public function __isset( $key ) { 
  50. if ( ! $this->id ) { 
  51. return false; 
  52. return metadata_exists( 'post', $this->id, '_' . $key ); 
  53.  
  54.  
  55. /** 
  56. * Magic get, wraps get_post_meta() for all keys except $status. 
  57. * 
  58. * @since 2.2 
  59. * @param string $key 
  60. * @return mixed value 
  61. */ 
  62. public function __get( $key ) { 
  63.  
  64. if ( 'status' === $key ) { 
  65. $value = $this->get_status(); 
  66. } else { 
  67. $value = get_post_meta( $this->id, '_' . $key, true ); 
  68.  
  69. return $value; 
  70.  
  71.  
  72. /** 
  73. * Enqueue the hooks associated with the webhook. 
  74. * 
  75. * @since 2.2 
  76. */ 
  77. public function enqueue() { 
  78. $hooks = $this->get_hooks(); 
  79. $url = $this->get_delivery_url(); 
  80.  
  81. if ( is_array( $hooks ) && ! empty( $url ) ) { 
  82. foreach ( $hooks as $hook ) { 
  83. add_action( $hook, array( $this, 'process' ) ); 
  84.  
  85.  
  86. /** 
  87. * Process the webhook for delivery by verifying that it should be delivered. 
  88. * and scheduling the delivery (in the background by default, or immediately). 
  89. * 
  90. * @since 2.2 
  91. * @param mixed $arg the first argument provided from the associated hooks 
  92. */ 
  93. public function process( $arg ) { 
  94.  
  95. // verify that webhook should be processed for delivery 
  96. if ( ! $this->should_deliver( $arg ) ) { 
  97. return; 
  98.  
  99. // webhooks are processed in the background by default 
  100. // so as to avoid delays or failures in delivery from affecting the 
  101. // user who triggered it 
  102. if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $this, $arg ) ) { 
  103.  
  104. // deliver in background 
  105. wp_schedule_single_event( time(), 'woocommerce_deliver_webhook_async', array( $this->id, $arg ) ); 
  106.  
  107. } else { 
  108.  
  109. // deliver immediately 
  110. $this->deliver( $arg ); 
  111.  
  112. /** 
  113. * Helper to check if the webhook should be delivered, as some hooks. 
  114. * (like `wp_trash_post`) will fire for every post type, not just ours. 
  115. * 
  116. * @since 2.2 
  117. * @param mixed $arg first hook argument 
  118. * @return bool true if webhook should be delivered, false otherwise 
  119. */ 
  120. private function should_deliver( $arg ) { 
  121. $should_deliver = true; 
  122. $current_action = current_action(); 
  123.  
  124. // only active webhooks can be delivered 
  125. if ( 'active' != $this->get_status() ) { 
  126. $should_deliver = false; 
  127. } elseif ( in_array( $current_action, array( 'delete_post', 'wp_trash_post' ), true ) ) { 
  128. // Only deliver deleted event for coupons, orders, and products. 
  129. if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ) ) ) { 
  130. $should_deliver = false; 
  131.  
  132. // Check if is delivering for the correct resource. 
  133. if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { 
  134. $should_deliver = false; 
  135. } elseif ( 'delete_user' == $current_action ) { 
  136. $user = get_userdata( absint( $arg ) ); 
  137.  
  138. // only deliver deleted customer event for users with customer role 
  139. if ( ! $user || ! in_array( 'customer', (array) $user->roles ) ) { 
  140. $should_deliver = false; 
  141.  
  142. // check if the custom order type has chosen to exclude order webhooks from triggering along with its own webhooks. 
  143. } elseif ( 'order' == $this->get_resource() && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ) ) ) { 
  144. $should_deliver = false; 
  145.  
  146. } elseif ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { 
  147. // the `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks 
  148. // fire for create and update of products and orders, so check the post 
  149. // creation date to determine the actual event 
  150. $resource = get_post( absint( $arg ) ); 
  151.  
  152. // Drafts don't have post_date_gmt so calculate it here 
  153. $gmt_date = get_gmt_from_date( $resource->post_date ); 
  154.  
  155. // a resource is considered created when the hook is executed within 10 seconds of the post creation date 
  156. $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); 
  157.  
  158. if ( 'created' == $this->get_event() && ! $resource_created ) { 
  159. $should_deliver = false; 
  160. } elseif ( 'updated' == $this->get_event() && $resource_created ) { 
  161. $should_deliver = false; 
  162.  
  163. /** 
  164. * Let other plugins intercept deliver for some messages queue like rabbit/zeromq 
  165. */ 
  166. return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg ); 
  167.  
  168.  
  169. /** 
  170. * Deliver the webhook payload using wp_safe_remote_request(). 
  171. * 
  172. * @since 2.2 
  173. * @param mixed $arg First hook argument. 
  174. */ 
  175. public function deliver( $arg ) { 
  176. $payload = $this->build_payload( $arg ); 
  177.  
  178. // Setup request args. 
  179. $http_args = array( 
  180. 'method' => 'POST',  
  181. 'timeout' => MINUTE_IN_SECONDS,  
  182. 'redirection' => 0,  
  183. 'httpversion' => '1.0',  
  184. 'blocking' => true,  
  185. 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),  
  186. 'body' => trim( json_encode( $payload ) ),  
  187. 'headers' => array( 'Content-Type' => 'application/json' ),  
  188. 'cookies' => array(),  
  189. ); 
  190.  
  191. $http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->id ); 
  192.  
  193. // Add custom headers. 
  194. $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0. 
  195. $http_args['headers']['X-WC-Webhook-Topic'] = $this->get_topic(); 
  196. $http_args['headers']['X-WC-Webhook-Resource'] = $this->get_resource(); 
  197. $http_args['headers']['X-WC-Webhook-Event'] = $this->get_event(); 
  198. $http_args['headers']['X-WC-Webhook-Signature'] = $this->generate_signature( $http_args['body'] ); 
  199. $http_args['headers']['X-WC-Webhook-ID'] = $this->id; 
  200. $http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id = $this->get_new_delivery_id(); 
  201.  
  202. $start_time = microtime( true ); 
  203.  
  204. // Webhook away! 
  205. $response = wp_safe_remote_request( $this->get_delivery_url(), $http_args ); 
  206.  
  207. $duration = round( microtime( true ) - $start_time, 5 ); 
  208.  
  209. $this->log_delivery( $delivery_id, $http_args, $response, $duration ); 
  210.  
  211. do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->id ); 
  212.  
  213. /** 
  214. * Get Legacy API payload. 
  215. * 
  216. * @since 3.0.0 
  217. * @param string $resource Resource type. 
  218. * @param int $resource_id Resource ID. 
  219. * @param string $event Event type. 
  220. * @return array 
  221. */ 
  222. private function get_legacy_api_payload( $resource, $resource_id, $event ) { 
  223. // Include & load API classes. 
  224. WC()->api->includes(); 
  225. WC()->api->register_resources( new WC_API_Server( '/' ) ); 
  226.  
  227. switch ( $resource ) { 
  228. case 'coupon' : 
  229. $payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id ); 
  230. break; 
  231.  
  232. case 'customer' : 
  233. $payload = WC()->api->WC_API_Customers->get_customer( $resource_id ); 
  234. break; 
  235.  
  236. case 'order' : 
  237. $payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) ); 
  238. break; 
  239.  
  240. case 'product' : 
  241. // Bulk and quick edit action hooks return a product object instead of an ID. 
  242. if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { 
  243. $resource_id = $resource_id->get_id(); 
  244. $payload = WC()->api->WC_API_Products->get_product( $resource_id ); 
  245. break; 
  246.  
  247. // Custom topics include the first hook argument. 
  248. case 'action' : 
  249. $payload = array( 
  250. 'action' => current( $this->get_hooks() ),  
  251. 'arg' => $resource_id,  
  252. ); 
  253. break; 
  254.  
  255. default : 
  256. $payload = array(); 
  257. break; 
  258.  
  259. return $payload; 
  260.  
  261. /** 
  262. * Get WP API integration payload. 
  263. * 
  264. * @since 3.0.0 
  265. * @param string $resource Resource type. 
  266. * @param int $resource_id Resource ID. 
  267. * @param string $event Event type. 
  268. * @return array 
  269. */ 
  270. private function get_wp_api_payload( $resource, $resource_id, $event ) { 
  271. $version_suffix = 'wp_api_v1' === $this->get_api_version() ? '_V1' : ''; 
  272.  
  273. switch ( $resource ) { 
  274. case 'coupon' : 
  275. case 'customer' : 
  276. case 'order' : 
  277. case 'product' : 
  278. $class = 'WC_REST_' . ucfirst( $resource ) . 's' . $version_suffix . '_Controller'; 
  279. $request = new WP_REST_Request( 'GET' ); 
  280. $controller = new $class; 
  281.  
  282. // Bulk and quick edit action hooks return a product object instead of an ID. 
  283. if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) { 
  284. $resource_id = $resource_id->get_id(); 
  285.  
  286. $request->set_param( 'id', $resource_id ); 
  287. $result = $controller->get_item( $request ); 
  288. $payload = isset( $result->data ) ? $result->data : array(); 
  289. break; 
  290.  
  291. // Custom topics include the first hook argument. 
  292. case 'action' : 
  293. $payload = array( 
  294. 'action' => current( $this->get_hooks() ),  
  295. 'arg' => $resource_id,  
  296. ); 
  297. break; 
  298.  
  299. default : 
  300. $payload = array(); 
  301. break; 
  302.  
  303. return $payload; 
  304.  
  305. /** 
  306. * Build the payload data for the webhook. 
  307. * 
  308. * @since 2.2 
  309. * @param mixed $resource_id first hook argument, typically the resource ID 
  310. * @return mixed payload data 
  311. */ 
  312. private function build_payload( $resource_id ) { 
  313. // build the payload with the same user context as the user who created 
  314. // the webhook -- this avoids permission errors as background processing 
  315. // runs with no user context 
  316. $current_user = get_current_user_id(); 
  317. wp_set_current_user( $this->get_user_id() ); 
  318.  
  319. $resource = $this->get_resource(); 
  320. $event = $this->get_event(); 
  321.  
  322. // If a resource has been deleted, just include the ID. 
  323. if ( 'deleted' == $event ) { 
  324. $payload = array( 
  325. 'id' => $resource_id,  
  326. ); 
  327. } else { 
  328. if ( in_array( $this->get_api_version(), array( 'wp_api_v1', 'wp_api_v2' ), true ) ) { 
  329. $payload = $this->get_wp_api_payload( $resource, $resource_id, $event ); 
  330. } else { 
  331. $payload = $this->get_legacy_api_payload( $resource, $resource_id, $event ); 
  332.  
  333. // Restore the current user. 
  334. wp_set_current_user( $current_user ); 
  335.  
  336. return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->id ); 
  337.  
  338.  
  339. /** 
  340. * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the. 
  341. * recipient can verify the authenticity of the webhook. Note that the signature. 
  342. * is calculated after the body has already been encoded (JSON by default). 
  343. * 
  344. * @since 2.2 
  345. * @param string $payload payload data to hash 
  346. * @return string hash 
  347. */ 
  348. public function generate_signature( $payload ) { 
  349.  
  350. $hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->id ); 
  351.  
  352. return base64_encode( hash_hmac( $hash_algo, $payload, $this->get_secret(), true ) ); 
  353.  
  354.  
  355. /** 
  356. * Create a new comment for log the delivery request/response and. 
  357. * return the ID for inclusion in the webhook request. 
  358. * 
  359. * @since 2.2 
  360. * @return int delivery (comment) ID 
  361. */ 
  362. public function get_new_delivery_id() { 
  363.  
  364. $comment_data = apply_filters( 'woocommerce_new_webhook_delivery_data', array( 
  365. 'comment_author' => __( 'WooCommerce', 'woocommerce' ),  
  366. 'comment_author_email' => sanitize_email( sprintf( '%s@%s', strtolower( __( 'WooCommerce', 'woocommerce' ) ), isset( $_SERVER['HTTP_HOST'] ) ? str_replace( 'www.', '', $_SERVER['HTTP_HOST'] ) : 'noreply.com' ) ),  
  367. 'comment_post_ID' => $this->id,  
  368. 'comment_agent' => 'WooCommerce Hookshot',  
  369. 'comment_type' => 'webhook_delivery',  
  370. 'comment_parent' => 0,  
  371. 'comment_approved' => 1,  
  372. ), $this->id ); 
  373.  
  374. $comment_id = wp_insert_comment( $comment_data ); 
  375.  
  376. return $comment_id; 
  377.  
  378.  
  379. /** 
  380. * Log the delivery request/response. 
  381. * 
  382. * @since 2.2 
  383. * @param int $delivery_id previously created comment ID 
  384. * @param array $request request data 
  385. * @param array|WP_Error $response response data 
  386. * @param float $duration request duration 
  387. */ 
  388. public function log_delivery( $delivery_id, $request, $response, $duration ) { 
  389.  
  390. // save request data 
  391. add_comment_meta( $delivery_id, '_request_method', $request['method'] ); 
  392. add_comment_meta( $delivery_id, '_request_headers', array_merge( array( 'User-Agent' => $request['user-agent'] ), $request['headers'] ) ); 
  393. add_comment_meta( $delivery_id, '_request_body', $request['body'] ); 
  394.  
  395. // parse response 
  396. if ( is_wp_error( $response ) ) { 
  397. $response_code = $response->get_error_code(); 
  398. $response_message = $response->get_error_message(); 
  399. $response_headers = array(); 
  400. $response_body = ''; 
  401.  
  402. } else { 
  403. $response_code = wp_remote_retrieve_response_code( $response ); 
  404. $response_message = wp_remote_retrieve_response_message( $response ); 
  405. $response_headers = wp_remote_retrieve_headers( $response ); 
  406. $response_body = wp_remote_retrieve_body( $response ); 
  407.  
  408. // save response data 
  409. add_comment_meta( $delivery_id, '_response_code', $response_code ); 
  410. add_comment_meta( $delivery_id, '_response_message', $response_message ); 
  411. add_comment_meta( $delivery_id, '_response_headers', $response_headers ); 
  412. add_comment_meta( $delivery_id, '_response_body', $response_body ); 
  413.  
  414. // save duration 
  415. add_comment_meta( $delivery_id, '_duration', $duration ); 
  416.  
  417. // set a summary for quick display 
  418. $args = array( 
  419. 'comment_ID' => $delivery_id,  
  420. 'comment_content' => sprintf( 'HTTP %s %s: %s', $response_code, $response_message, $response_body ),  
  421. ); 
  422.  
  423. wp_update_comment( $args ); 
  424.  
  425. // track failures 
  426. if ( intval( $response_code ) >= 200 && intval( $response_code ) < 300 ) { 
  427. delete_post_meta( $this->id, '_failure_count' ); 
  428. } else { 
  429. $this->failed_delivery(); 
  430.  
  431. // keep the 25 most recent delivery logs 
  432. $log = wp_count_comments( $this->id ); 
  433. if ( $log->total_comments > apply_filters( 'woocommerce_max_webhook_delivery_logs', 25 ) ) { 
  434. global $wpdb; 
  435.  
  436. $comment_id = $wpdb->get_var( $wpdb->prepare( "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_post_ID = %d ORDER BY comment_date_gmt ASC LIMIT 1", $this->id ) ); 
  437.  
  438. if ( $comment_id ) { 
  439. wp_delete_comment( $comment_id, true ); 
  440.  
  441. /** 
  442. * Track consecutive delivery failures and automatically disable the webhook. 
  443. * if more than 5 consecutive failures occur. A failure is defined as a. 
  444. * non-2xx response. 
  445. * 
  446. * @since 2.2 
  447. */ 
  448. private function failed_delivery() { 
  449.  
  450. $failures = $this->get_failure_count(); 
  451.  
  452. if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) { 
  453.  
  454. $this->update_status( 'disabled' ); 
  455.  
  456. } else { 
  457.  
  458. update_post_meta( $this->id, '_failure_count', ++$failures ); 
  459.  
  460.  
  461. /** 
  462. * Get the delivery logs for this webhook. 
  463. * 
  464. * @since 2.2 
  465. * @return array 
  466. */ 
  467. public function get_delivery_logs() { 
  468.  
  469. $args = array( 
  470. 'post_id' => $this->id,  
  471. 'status' => 'approve',  
  472. 'type' => 'webhook_delivery',  
  473. ); 
  474.  
  475. remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_webhook_comments' ), 10, 1 ); 
  476.  
  477. $logs = get_comments( $args ); 
  478.  
  479. add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_webhook_comments' ), 10, 1 ); 
  480.  
  481. $delivery_logs = array(); 
  482.  
  483. foreach ( $logs as $log ) { 
  484.  
  485. $log = $this->get_delivery_log( $log->comment_ID ); 
  486.  
  487. $delivery_logs[] = ( ! empty( $log ) ? $log : array() ); 
  488.  
  489. return $delivery_logs; 
  490.  
  491.  
  492. /** 
  493. * Get the delivery log specified by the ID. The delivery log includes: 
  494. * 
  495. * + duration 
  496. * + summary 
  497. * + request method/url 
  498. * + request headers/body 
  499. * + response code/message/headers/body 
  500. * 
  501. * @since 2.2 
  502. * @param int $delivery_id 
  503. * @return bool|array false if invalid delivery ID, array of log data otherwise 
  504. */ 
  505. public function get_delivery_log( $delivery_id ) { 
  506.  
  507. $log = get_comment( $delivery_id ); 
  508.  
  509. // valid comment and ensure delivery log belongs to this webhook 
  510. if ( is_null( $log ) || $log->comment_post_ID != $this->id ) { 
  511. return false; 
  512.  
  513. $delivery_log = array( 
  514. 'id' => intval( $delivery_id ),  
  515. 'duration' => get_comment_meta( $delivery_id, '_duration', true ),  
  516. 'summary' => $log->comment_content,  
  517. 'request_method' => get_comment_meta( $delivery_id, '_request_method', true ),  
  518. 'request_url' => $this->get_delivery_url(),  
  519. 'request_headers' => get_comment_meta( $delivery_id, '_request_headers', true ),  
  520. 'request_body' => get_comment_meta( $delivery_id, '_request_body', true ),  
  521. 'response_code' => get_comment_meta( $delivery_id, '_response_code', true ),  
  522. 'response_message' => get_comment_meta( $delivery_id, '_response_message', true ),  
  523. 'response_headers' => get_comment_meta( $delivery_id, '_response_headers', true ),  
  524. 'response_body' => get_comment_meta( $delivery_id, '_response_body', true ),  
  525. 'comment' => $log,  
  526. ); 
  527.  
  528. return apply_filters( 'woocommerce_webhook_delivery_log', $delivery_log, $delivery_id, $this->id ); 
  529.  
  530. /** 
  531. * Set the webhook topic and associated hooks. The topic resource & event. 
  532. * are also saved separately. 
  533. * 
  534. * @since 2.2 
  535. * @param string $topic 
  536. */ 
  537. public function set_topic( $topic ) { 
  538.  
  539. $topic = strtolower( $topic ); 
  540.  
  541. list( $resource, $event ) = explode( '.', $topic ); 
  542.  
  543. update_post_meta( $this->id, '_topic', $topic ); 
  544. update_post_meta( $this->id, '_resource', $resource ); 
  545. update_post_meta( $this->id, '_event', $event ); 
  546.  
  547. // custom topics are mapped to a single hook 
  548. if ( 'action' === $resource ) { 
  549.  
  550. update_post_meta( $this->id, '_hooks', array( $event ) ); 
  551.  
  552. } else { 
  553.  
  554. // API topics have multiple hooks 
  555. update_post_meta( $this->id, '_hooks', $this->get_topic_hooks( $topic ) ); 
  556.  
  557. /** 
  558. * Get the associated hook names for a topic. 
  559. * 
  560. * @since 2.2 
  561. * @param string $topic 
  562. * @return array hook names 
  563. */ 
  564. private function get_topic_hooks( $topic ) { 
  565.  
  566. $topic_hooks = array( 
  567. 'coupon.created' => array( 
  568. 'woocommerce_process_shop_coupon_meta',  
  569. 'woocommerce_api_create_coupon',  
  570. ),  
  571. 'coupon.updated' => array( 
  572. 'woocommerce_process_shop_coupon_meta',  
  573. 'woocommerce_api_edit_coupon',  
  574. ),  
  575. 'coupon.deleted' => array( 
  576. 'wp_trash_post',  
  577. ),  
  578. 'customer.created' => array( 
  579. 'user_register',  
  580. 'woocommerce_created_customer',  
  581. 'woocommerce_api_create_customer',  
  582. ),  
  583. 'customer.updated' => array( 
  584. 'profile_update',  
  585. 'woocommerce_api_edit_customer',  
  586. 'woocommerce_customer_save_address',  
  587. ),  
  588. 'customer.deleted' => array( 
  589. 'delete_user',  
  590. ),  
  591. 'order.created' => array( 
  592. 'woocommerce_checkout_order_processed',  
  593. 'woocommerce_process_shop_order_meta',  
  594. 'woocommerce_api_create_order',  
  595. ),  
  596. 'order.updated' => array( 
  597. 'woocommerce_process_shop_order_meta',  
  598. 'woocommerce_api_edit_order',  
  599. 'woocommerce_order_edit_status',  
  600. 'woocommerce_order_status_changed',  
  601. ),  
  602. 'order.deleted' => array( 
  603. 'wp_trash_post',  
  604. ),  
  605. 'product.created' => array( 
  606. 'woocommerce_process_product_meta',  
  607. 'woocommerce_api_create_product',  
  608. ),  
  609. 'product.updated' => array( 
  610. 'woocommerce_process_product_meta',  
  611. 'woocommerce_api_edit_product',  
  612. 'woocommerce_product_quick_edit_save',  
  613. 'woocommerce_product_bulk_edit_save',  
  614. ),  
  615. 'product.deleted' => array( 
  616. 'wp_trash_post',  
  617. ),  
  618. ); 
  619.  
  620. $topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this ); 
  621.  
  622. return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array(); 
  623.  
  624. /** 
  625. * Send a test ping to the delivery URL, sent when the webhook is first created. 
  626. * 
  627. * @since 2.2 
  628. * @return bool|WP_Error 
  629. */ 
  630. public function deliver_ping() { 
  631. $args = array( 
  632. 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),  
  633. 'body' => "webhook_id={$this->id}",  
  634. ); 
  635.  
  636. $test = wp_safe_remote_post( $this->get_delivery_url(), $args ); 
  637. $response_code = wp_remote_retrieve_response_code( $test ); 
  638.  
  639. if ( is_wp_error( $test ) ) { 
  640. return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) ); 
  641.  
  642. if ( 200 !== $response_code ) { 
  643. return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) ); 
  644.  
  645. return true; 
  646.  
  647. /** 
  648. * Get the webhook status: 
  649. * 
  650. * + `active` - delivers payload. 
  651. * + `paused` - does not deliver payload, paused by admin. 
  652. * + `disabled` - does not delivery payload, paused automatically due to. 
  653. * consecutive failures. 
  654. * 
  655. * @since 2.2 
  656. * @return string status 
  657. */ 
  658. public function get_status() { 
  659.  
  660. switch ( $this->get_post_data()->post_status ) { 
  661.  
  662. case 'publish': 
  663. $status = 'active'; 
  664. break; 
  665.  
  666. case 'draft': 
  667. $status = 'paused'; 
  668. break; 
  669.  
  670. case 'pending': 
  671. $status = 'disabled'; 
  672. break; 
  673.  
  674. default: 
  675. $status = 'paused'; 
  676.  
  677. return apply_filters( 'woocommerce_webhook_status', $status, $this->id ); 
  678.  
  679. /** 
  680. * Get the webhook i18n status. 
  681. * 
  682. * @return string 
  683. */ 
  684. public function get_i18n_status() { 
  685. $status = $this->get_status(); 
  686. $statuses = wc_get_webhook_statuses(); 
  687.  
  688. return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status; 
  689.  
  690. /** 
  691. * Update the webhook status, see get_status() for valid statuses. 
  692. * 
  693. * @since 2.2 
  694. * @param $status 
  695. */ 
  696. public function update_status( $status ) { 
  697. global $wpdb; 
  698.  
  699. switch ( $status ) { 
  700.  
  701. case 'active' : 
  702. $post_status = 'publish'; 
  703. break; 
  704.  
  705. case 'paused' : 
  706. $post_status = 'draft'; 
  707. break; 
  708.  
  709. case 'disabled' : 
  710. $post_status = 'pending'; 
  711. break; 
  712.  
  713. default : 
  714. $post_status = 'draft'; 
  715. break; 
  716.  
  717. $wpdb->update( $wpdb->posts, array( 'post_status' => $post_status ), array( 'ID' => $this->id ) ); 
  718. clean_post_cache( $this->id ); 
  719.  
  720. /** 
  721. * Set the delivery URL. 
  722. * 
  723. * @since 2.2 
  724. * @param string $url 
  725. */ 
  726. public function set_delivery_url( $url ) { 
  727. if ( update_post_meta( $this->id, '_delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) ) ) { 
  728. update_post_meta( $this->id, '_webhook_pending_delivery', true ); 
  729.  
  730. /** 
  731. * Get the delivery URL. 
  732. * 
  733. * @since 2.2 
  734. * @return string 
  735. */ 
  736. public function get_delivery_url() { 
  737.  
  738. return apply_filters( 'woocommerce_webhook_delivery_url', $this->delivery_url, $this->id ); 
  739.  
  740. /** 
  741. * Set the secret used for generating the HMAC-SHA256 signature. 
  742. * 
  743. * @since 2.2 
  744. * @param string $secret 
  745. */ 
  746. public function set_secret( $secret ) { 
  747.  
  748. update_post_meta( $this->id, '_secret', $secret ); 
  749.  
  750. /** 
  751. * Get the secret used for generating the HMAC-SHA256 signature. 
  752. * 
  753. * @since 2.2 
  754. * @return string 
  755. */ 
  756. public function get_secret() { 
  757. return apply_filters( 'woocommerce_webhook_secret', $this->secret, $this->id ); 
  758.  
  759. /** 
  760. * Get the friendly name for the webhook. 
  761. * 
  762. * @since 2.2 
  763. * @return string 
  764. */ 
  765. public function get_name() { 
  766. return apply_filters( 'woocommerce_webhook_name', $this->get_post_data()->post_title, $this->id ); 
  767.  
  768. /** 
  769. * Get the webhook topic, e.g. `order.created`. 
  770. * 
  771. * @since 2.2 
  772. * @return string 
  773. */ 
  774. public function get_topic() { 
  775. return apply_filters( 'woocommerce_webhook_topic', $this->topic, $this->id ); 
  776.  
  777. /** 
  778. * Get the hook names for the webhook. 
  779. * 
  780. * @since 2.2 
  781. * @return array hook names 
  782. */ 
  783. public function get_hooks() { 
  784. return apply_filters( 'woocommerce_webhook_hooks', $this->hooks, $this->id ); 
  785.  
  786. /** 
  787. * Get the resource for the webhook, e.g. `order`. 
  788. * 
  789. * @since 2.2 
  790. * @return string 
  791. */ 
  792. public function get_resource() { 
  793. return apply_filters( 'woocommerce_webhook_resource', $this->resource, $this->id ); 
  794.  
  795. /** 
  796. * Get the event for the webhook, e.g. `created`. 
  797. * 
  798. * @since 2.2 
  799. * @return string 
  800. */ 
  801. public function get_event() { 
  802. return apply_filters( 'woocommerce_webhook_event', $this->event, $this->id ); 
  803.  
  804. /** 
  805. * Get the failure count. 
  806. * 
  807. * @since 2.2 
  808. * @return int 
  809. */ 
  810. public function get_failure_count() { 
  811. return intval( $this->failure_count ); 
  812.  
  813. /** 
  814. * Get the user ID for this webhook. 
  815. * 
  816. * @since 2.2 
  817. * @return int|string user ID 
  818. */ 
  819. public function get_user_id() { 
  820. return $this->get_post_data()->post_author; 
  821.  
  822. /** 
  823. * Get the post data for the webhook. 
  824. * 
  825. * @since 2.2 
  826. * @return null|WP_Post 
  827. */ 
  828. public function get_post_data() { 
  829. return $this->post_data; 
  830.  
  831. /** 
  832. * Set API version. 
  833. * 
  834. * @since 3.0.0 
  835. * @param string $version REST API version. 
  836. */ 
  837. public function set_api_version( $version ) { 
  838. $versions = array( 
  839. 'wp_api_v2',  
  840. 'wp_api_v1',  
  841. 'legacy_v3',  
  842. ); 
  843.  
  844. if ( ! in_array( $version, $versions, true ) ) { 
  845. $version = 'wp_api_v2'; 
  846.  
  847. update_post_meta( $this->id, '_api_version', $version ); 
  848.  
  849. /** 
  850. * API version. 
  851. * 
  852. * @since 3.0.0 
  853. * @return string 
  854. */ 
  855. public function get_api_version() { 
  856. return $this->api_version ? $this->api_version : 'legacy_v3'; 
.