Bez popisu

class-wc-webhook.php 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  1. <?php
  2. /**
  3. * Webhook
  4. *
  5. * This class handles storing and retrieving webhook data from the associated.
  6. *
  7. * Webhooks are enqueued to their associated actions, delivered, and logged.
  8. *
  9. * @version 3.2.0
  10. * @package WooCommerce\Webhooks
  11. * @since 2.2.0
  12. */
  13. use Automattic\Jetpack\Constants;
  14. use Automattic\WooCommerce\Utilities\NumberUtil;
  15. defined( 'ABSPATH' ) || exit;
  16. require_once __DIR__ . '/legacy/class-wc-legacy-webhook.php';
  17. /**
  18. * Webhook class.
  19. */
  20. class WC_Webhook extends WC_Legacy_Webhook {
  21. /**
  22. * Store which object IDs this webhook has processed (ie scheduled to be delivered)
  23. * within the current page request.
  24. *
  25. * @var array
  26. */
  27. protected $processed = array();
  28. /**
  29. * Stores webhook data.
  30. *
  31. * @var array
  32. */
  33. protected $data = array(
  34. 'date_created' => null,
  35. 'date_modified' => null,
  36. 'status' => 'disabled',
  37. 'delivery_url' => '',
  38. 'secret' => '',
  39. 'name' => '',
  40. 'topic' => '',
  41. 'hooks' => '',
  42. 'resource' => '',
  43. 'event' => '',
  44. 'failure_count' => 0,
  45. 'user_id' => 0,
  46. 'api_version' => 3,
  47. 'pending_delivery' => false,
  48. );
  49. /**
  50. * Load webhook data based on how WC_Webhook is called.
  51. *
  52. * @param WC_Webhook|int $data Webhook ID or data.
  53. * @throws Exception If webhook cannot be read/found and $data is set.
  54. */
  55. public function __construct( $data = 0 ) {
  56. parent::__construct( $data );
  57. if ( $data instanceof WC_Webhook ) {
  58. $this->set_id( absint( $data->get_id() ) );
  59. } elseif ( is_numeric( $data ) ) {
  60. $this->set_id( $data );
  61. }
  62. $this->data_store = WC_Data_Store::load( 'webhook' );
  63. // If we have an ID, load the webhook from the DB.
  64. if ( $this->get_id() ) {
  65. try {
  66. $this->data_store->read( $this );
  67. } catch ( Exception $e ) {
  68. $this->set_id( 0 );
  69. $this->set_object_read( true );
  70. }
  71. } else {
  72. $this->set_object_read( true );
  73. }
  74. }
  75. /**
  76. * Enqueue the hooks associated with the webhook.
  77. *
  78. * @since 2.2.0
  79. */
  80. public function enqueue() {
  81. $hooks = $this->get_hooks();
  82. $url = $this->get_delivery_url();
  83. if ( is_array( $hooks ) && ! empty( $url ) ) {
  84. foreach ( $hooks as $hook ) {
  85. add_action( $hook, array( $this, 'process' ) );
  86. }
  87. }
  88. }
  89. /**
  90. * Process the webhook for delivery by verifying that it should be delivered.
  91. * and scheduling the delivery (in the background by default, or immediately).
  92. *
  93. * @since 2.2.0
  94. * @param mixed $arg The first argument provided from the associated hooks.
  95. * @return mixed $arg Returns the argument in case the webhook was hooked into a filter.
  96. */
  97. public function process( $arg ) {
  98. // Verify that webhook should be processed for delivery.
  99. if ( ! $this->should_deliver( $arg ) ) {
  100. return;
  101. }
  102. // Mark this $arg as processed to ensure it doesn't get processed again within the current request.
  103. $this->processed[] = $arg;
  104. /**
  105. * Process webhook delivery.
  106. *
  107. * @since 3.3.0
  108. * @hooked wc_webhook_process_delivery - 10
  109. */
  110. do_action( 'woocommerce_webhook_process_delivery', $this, $arg );
  111. return $arg;
  112. }
  113. /**
  114. * Helper to check if the webhook should be delivered, as some hooks.
  115. * (like `wp_trash_post`) will fire for every post type, not just ours.
  116. *
  117. * @since 2.2.0
  118. * @param mixed $arg First hook argument.
  119. * @return bool True if webhook should be delivered, false otherwise.
  120. */
  121. private function should_deliver( $arg ) {
  122. $should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg ) && ! $this->is_already_processed( $arg );
  123. /**
  124. * Let other plugins intercept deliver for some messages queue like rabbit/zeromq.
  125. *
  126. * @param bool $should_deliver True if the webhook should be sent, or false to not send it.
  127. * @param WC_Webhook $this The current webhook class.
  128. * @param mixed $arg First hook argument.
  129. */
  130. return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg );
  131. }
  132. /**
  133. * Returns if webhook is active.
  134. *
  135. * @since 3.6.0
  136. * @return bool True if validation passes.
  137. */
  138. private function is_active() {
  139. return 'active' === $this->get_status();
  140. }
  141. /**
  142. * Returns if topic is valid.
  143. *
  144. * @since 3.6.0
  145. * @return bool True if validation passes.
  146. */
  147. private function is_valid_topic() {
  148. return wc_is_webhook_valid_topic( $this->get_topic() );
  149. }
  150. /**
  151. * Validates the criteria for certain actions.
  152. *
  153. * @since 3.6.0
  154. * @param mixed $arg First hook argument.
  155. * @return bool True if validation passes.
  156. */
  157. private function is_valid_action( $arg ) {
  158. $current_action = current_action();
  159. $return = true;
  160. switch ( $current_action ) {
  161. case 'delete_post':
  162. case 'wp_trash_post':
  163. case 'untrashed_post':
  164. $return = $this->is_valid_post_action( $arg );
  165. break;
  166. case 'delete_user':
  167. $return = $this->is_valid_user_action( $arg );
  168. break;
  169. }
  170. if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) {
  171. $return = $this->is_valid_processing_action( $arg );
  172. }
  173. return $return;
  174. }
  175. /**
  176. * Validates post actions.
  177. *
  178. * @since 3.6.0
  179. * @param mixed $arg First hook argument.
  180. * @return bool True if validation passes.
  181. */
  182. private function is_valid_post_action( $arg ) {
  183. // Only deliver deleted/restored event for coupons, orders, and products.
  184. if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) {
  185. return false;
  186. }
  187. // Check if is delivering for the correct resource.
  188. if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) {
  189. return false;
  190. }
  191. return true;
  192. }
  193. /**
  194. * Validates user actions.
  195. *
  196. * @since 3.6.0
  197. * @param mixed $arg First hook argument.
  198. * @return bool True if validation passes.
  199. */
  200. private function is_valid_user_action( $arg ) {
  201. $user = get_userdata( absint( $arg ) );
  202. // Only deliver deleted customer event for users with customer role.
  203. if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) {
  204. return false;
  205. }
  206. return true;
  207. }
  208. /**
  209. * Validates WC processing actions.
  210. *
  211. * @since 3.6.0
  212. * @param mixed $arg First hook argument.
  213. * @return bool True if validation passes.
  214. */
  215. private function is_valid_processing_action( $arg ) {
  216. // The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks
  217. // fire for create and update of products and orders, so check the post
  218. // creation date to determine the actual event.
  219. $resource = get_post( absint( $arg ) );
  220. // Drafts don't have post_date_gmt so calculate it here.
  221. $gmt_date = get_gmt_from_date( $resource->post_date );
  222. // A resource is considered created when the hook is executed within 10 seconds of the post creation date.
  223. $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) );
  224. if ( 'created' === $this->get_event() && ! $resource_created ) {
  225. return false;
  226. } elseif ( 'updated' === $this->get_event() && $resource_created ) {
  227. return false;
  228. }
  229. return true;
  230. }
  231. /**
  232. * Checks the resource for this webhook is valid e.g. valid post status.
  233. *
  234. * @since 3.6.0
  235. * @param mixed $arg First hook argument.
  236. * @return bool True if validation passes.
  237. */
  238. private function is_valid_resource( $arg ) {
  239. $resource = $this->get_resource();
  240. if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) {
  241. $status = get_post_status( absint( $arg ) );
  242. // Ignore auto drafts for all resources.
  243. if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) {
  244. return false;
  245. }
  246. // Ignore standard drafts for orders.
  247. if ( 'order' === $resource && 'draft' === $status ) {
  248. return false;
  249. }
  250. // Check registered order types for order types args.
  251. if ( 'order' === $resource && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) {
  252. return false;
  253. }
  254. }
  255. return true;
  256. }
  257. /**
  258. * Checks if the specified resource has already been queued for delivery within the current request.
  259. *
  260. * Helps avoid duplication of data being sent for topics that have more than one hook defined.
  261. *
  262. * @param mixed $arg First hook argument.
  263. *
  264. * @return bool
  265. */
  266. protected function is_already_processed( $arg ) {
  267. return false !== array_search( $arg, $this->processed, true );
  268. }
  269. /**
  270. * Deliver the webhook payload using wp_safe_remote_request().
  271. *
  272. * @since 2.2.0
  273. * @param mixed $arg First hook argument.
  274. */
  275. public function deliver( $arg ) {
  276. $start_time = microtime( true );
  277. $payload = $this->build_payload( $arg );
  278. // Setup request args.
  279. $http_args = array(
  280. 'method' => 'POST',
  281. 'timeout' => MINUTE_IN_SECONDS,
  282. 'redirection' => 0,
  283. 'httpversion' => '1.0',
  284. 'blocking' => true,
  285. 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ),
  286. 'body' => trim( wp_json_encode( $payload ) ),
  287. 'headers' => array(
  288. 'Content-Type' => 'application/json',
  289. ),
  290. 'cookies' => array(),
  291. );
  292. $http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() );
  293. // Add custom headers.
  294. $delivery_id = $this->get_new_delivery_id();
  295. $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0.
  296. $http_args['headers']['X-WC-Webhook-Topic'] = $this->get_topic();
  297. $http_args['headers']['X-WC-Webhook-Resource'] = $this->get_resource();
  298. $http_args['headers']['X-WC-Webhook-Event'] = $this->get_event();
  299. $http_args['headers']['X-WC-Webhook-Signature'] = $this->generate_signature( $http_args['body'] );
  300. $http_args['headers']['X-WC-Webhook-ID'] = $this->get_id();
  301. $http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id;
  302. // Webhook away!
  303. $response = wp_safe_remote_request( $this->get_delivery_url(), $http_args );
  304. $duration = NumberUtil::round( microtime( true ) - $start_time, 5 );
  305. $this->log_delivery( $delivery_id, $http_args, $response, $duration );
  306. do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
  307. }
  308. /**
  309. * Get Legacy API payload.
  310. *
  311. * @since 3.0.0
  312. * @param string $resource Resource type.
  313. * @param int $resource_id Resource ID.
  314. * @param string $event Event type.
  315. * @return array
  316. */
  317. private function get_legacy_api_payload( $resource, $resource_id, $event ) {
  318. // Include & load API classes.
  319. WC()->api->includes();
  320. WC()->api->register_resources( new WC_API_Server( '/' ) );
  321. switch ( $resource ) {
  322. case 'coupon':
  323. $payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id );
  324. break;
  325. case 'customer':
  326. $payload = WC()->api->WC_API_Customers->get_customer( $resource_id );
  327. break;
  328. case 'order':
  329. $payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) );
  330. break;
  331. case 'product':
  332. // Bulk and quick edit action hooks return a product object instead of an ID.
  333. if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
  334. $resource_id = $resource_id->get_id();
  335. }
  336. $payload = WC()->api->WC_API_Products->get_product( $resource_id );
  337. break;
  338. // Custom topics include the first hook argument.
  339. case 'action':
  340. $payload = array(
  341. 'action' => current( $this->get_hooks() ),
  342. 'arg' => $resource_id,
  343. );
  344. break;
  345. default:
  346. $payload = array();
  347. break;
  348. }
  349. return $payload;
  350. }
  351. /**
  352. * Get WP API integration payload.
  353. *
  354. * @since 3.0.0
  355. * @param string $resource Resource type.
  356. * @param int $resource_id Resource ID.
  357. * @param string $event Event type.
  358. * @return array
  359. */
  360. private function get_wp_api_payload( $resource, $resource_id, $event ) {
  361. switch ( $resource ) {
  362. case 'coupon':
  363. case 'customer':
  364. case 'order':
  365. case 'product':
  366. // Bulk and quick edit action hooks return a product object instead of an ID.
  367. if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
  368. $resource_id = $resource_id->get_id();
  369. }
  370. $version = str_replace( 'wp_api_', '', $this->get_api_version() );
  371. $payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" );
  372. break;
  373. // Custom topics include the first hook argument.
  374. case 'action':
  375. $payload = array(
  376. 'action' => current( $this->get_hooks() ),
  377. 'arg' => $resource_id,
  378. );
  379. break;
  380. default:
  381. $payload = array();
  382. break;
  383. }
  384. return $payload;
  385. }
  386. /**
  387. * Build the payload data for the webhook.
  388. *
  389. * @since 2.2.0
  390. * @param mixed $resource_id First hook argument, typically the resource ID.
  391. * @return mixed Payload data.
  392. */
  393. public function build_payload( $resource_id ) {
  394. // Build the payload with the same user context as the user who created
  395. // the webhook -- this avoids permission errors as background processing
  396. // runs with no user context.
  397. $current_user = get_current_user_id();
  398. wp_set_current_user( $this->get_user_id() );
  399. $resource = $this->get_resource();
  400. $event = $this->get_event();
  401. // If a resource has been deleted, just include the ID.
  402. if ( 'deleted' === $event ) {
  403. $payload = array(
  404. 'id' => $resource_id,
  405. );
  406. } else {
  407. if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
  408. $payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
  409. } else {
  410. $payload = $this->get_legacy_api_payload( $resource, $resource_id, $event );
  411. }
  412. }
  413. // Restore the current user.
  414. wp_set_current_user( $current_user );
  415. return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->get_id() );
  416. }
  417. /**
  418. * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the
  419. * recipient can verify the authenticity of the webhook. Note that the signature
  420. * is calculated after the body has already been encoded (JSON by default).
  421. *
  422. * @since 2.2.0
  423. * @param string $payload Payload data to hash.
  424. * @return string
  425. */
  426. public function generate_signature( $payload ) {
  427. $hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );
  428. // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
  429. return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) );
  430. }
  431. /**
  432. * Generate a new unique hash as a delivery id based on current time and wehbook id.
  433. * Return the hash for inclusion in the webhook request.
  434. *
  435. * @since 2.2.0
  436. * @return string
  437. */
  438. public function get_new_delivery_id() {
  439. // Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID.
  440. return wp_hash( $this->get_id() . strtotime( 'now' ) );
  441. }
  442. /**
  443. * Log the delivery request/response.
  444. *
  445. * @since 2.2.0
  446. * @param string $delivery_id Previously created hash.
  447. * @param array $request Request data.
  448. * @param array|WP_Error $response Response data.
  449. * @param float $duration Request duration.
  450. */
  451. public function log_delivery( $delivery_id, $request, $response, $duration ) {
  452. $logger = wc_get_logger();
  453. $message = array(
  454. 'Webhook Delivery' => array(
  455. 'Delivery ID' => $delivery_id,
  456. 'Date' => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ),
  457. 'URL' => $this->get_delivery_url(),
  458. 'Duration' => $duration,
  459. 'Request' => array(
  460. 'Method' => $request['method'],
  461. 'Headers' => array_merge(
  462. array(
  463. 'User-Agent' => $request['user-agent'],
  464. ),
  465. $request['headers']
  466. ),
  467. ),
  468. 'Body' => wp_slash( $request['body'] ),
  469. ),
  470. );
  471. // Parse response.
  472. if ( is_wp_error( $response ) ) {
  473. $response_code = $response->get_error_code();
  474. $response_message = $response->get_error_message();
  475. $response_headers = array();
  476. $response_body = '';
  477. } else {
  478. $response_code = wp_remote_retrieve_response_code( $response );
  479. $response_message = wp_remote_retrieve_response_message( $response );
  480. $response_headers = wp_remote_retrieve_headers( $response );
  481. $response_body = wp_remote_retrieve_body( $response );
  482. }
  483. $message['Webhook Delivery']['Response'] = array(
  484. 'Code' => $response_code,
  485. 'Message' => $response_message,
  486. 'Headers' => $response_headers,
  487. 'Body' => $response_body,
  488. );
  489. if ( ! Constants::is_true( 'WP_DEBUG' ) ) {
  490. $message['Webhook Delivery']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.';
  491. $message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.';
  492. }
  493. $logger->info(
  494. wc_print_r( $message, true ),
  495. array(
  496. 'source' => 'webhooks-delivery',
  497. )
  498. );
  499. // Track failures.
  500. // Check for a success, which is a 2xx, 301 or 302 Response Code.
  501. if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) {
  502. $this->set_failure_count( 0 );
  503. $this->save();
  504. } else {
  505. $this->failed_delivery();
  506. }
  507. }
  508. /**
  509. * Track consecutive delivery failures and automatically disable the webhook.
  510. * if more than 5 consecutive failures occur. A failure is defined as a.
  511. * non-2xx response.
  512. *
  513. * @since 2.2.0
  514. */
  515. private function failed_delivery() {
  516. $failures = $this->get_failure_count();
  517. if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) {
  518. $this->set_status( 'disabled' );
  519. do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() );
  520. } else {
  521. $this->set_failure_count( ++$failures );
  522. }
  523. $this->save();
  524. }
  525. /**
  526. * Get the delivery logs for this webhook.
  527. *
  528. * @since 3.3.0
  529. * @return string
  530. */
  531. public function get_delivery_logs() {
  532. return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
  533. }
  534. /**
  535. * Get the delivery log specified by the ID. The delivery log includes:
  536. *
  537. * + duration
  538. * + summary
  539. * + request method/url
  540. * + request headers/body
  541. * + response code/message/headers/body
  542. *
  543. * @since 2.2
  544. * @deprecated 3.3.0
  545. * @param int $delivery_id Delivery ID.
  546. * @return void
  547. */
  548. public function get_delivery_log( $delivery_id ) {
  549. wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' );
  550. }
  551. /**
  552. * Send a test ping to the delivery URL, sent when the webhook is first created.
  553. *
  554. * @since 2.2.0
  555. * @return bool|WP_Error
  556. */
  557. public function deliver_ping() {
  558. $args = array(
  559. 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ),
  560. 'body' => 'webhook_id=' . $this->get_id(),
  561. );
  562. $test = wp_safe_remote_post( $this->get_delivery_url(), $args );
  563. $response_code = wp_remote_retrieve_response_code( $test );
  564. if ( is_wp_error( $test ) ) {
  565. /* translators: error message */
  566. return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) );
  567. }
  568. if ( 200 !== $response_code ) {
  569. /* translators: error message */
  570. return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) );
  571. }
  572. $this->set_pending_delivery( false );
  573. $this->save();
  574. return true;
  575. }
  576. /*
  577. |--------------------------------------------------------------------------
  578. | Getters
  579. |--------------------------------------------------------------------------
  580. */
  581. /**
  582. * Get the friendly name for the webhook.
  583. *
  584. * @since 2.2.0
  585. * @param string $context What the value is for.
  586. * Valid values are 'view' and 'edit'.
  587. * @return string
  588. */
  589. public function get_name( $context = 'view' ) {
  590. return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() );
  591. }
  592. /**
  593. * Get the webhook status.
  594. *
  595. * - 'active' - delivers payload.
  596. * - 'paused' - does not deliver payload, paused by admin.
  597. * - 'disabled' - does not delivery payload, paused automatically due to consecutive failures.
  598. *
  599. * @since 2.2.0
  600. * @param string $context What the value is for.
  601. * Valid values are 'view' and 'edit'.
  602. * @return string status
  603. */
  604. public function get_status( $context = 'view' ) {
  605. return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() );
  606. }
  607. /**
  608. * Get webhook created date.
  609. *
  610. * @since 3.2.0
  611. * @param string $context What the value is for.
  612. * Valid values are 'view' and 'edit'.
  613. * @return WC_DateTime|null Object if the date is set or null if there is no date.
  614. */
  615. public function get_date_created( $context = 'view' ) {
  616. return $this->get_prop( 'date_created', $context );
  617. }
  618. /**
  619. * Get webhook modified date.
  620. *
  621. * @since 3.2.0
  622. * @param string $context What the value is for.
  623. * Valid values are 'view' and 'edit'.
  624. * @return WC_DateTime|null Object if the date is set or null if there is no date.
  625. */
  626. public function get_date_modified( $context = 'view' ) {
  627. return $this->get_prop( 'date_modified', $context );
  628. }
  629. /**
  630. * Get the secret used for generating the HMAC-SHA256 signature.
  631. *
  632. * @since 2.2.0
  633. * @param string $context What the value is for.
  634. * Valid values are 'view' and 'edit'.
  635. * @return string
  636. */
  637. public function get_secret( $context = 'view' ) {
  638. return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() );
  639. }
  640. /**
  641. * Get the webhook topic, e.g. `order.created`.
  642. *
  643. * @since 2.2.0
  644. * @param string $context What the value is for.
  645. * Valid values are 'view' and 'edit'.
  646. * @return string
  647. */
  648. public function get_topic( $context = 'view' ) {
  649. return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() );
  650. }
  651. /**
  652. * Get the delivery URL.
  653. *
  654. * @since 2.2.0
  655. * @param string $context What the value is for.
  656. * Valid values are 'view' and 'edit'.
  657. * @return string
  658. */
  659. public function get_delivery_url( $context = 'view' ) {
  660. return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() );
  661. }
  662. /**
  663. * Get the user ID for this webhook.
  664. *
  665. * @since 2.2.0
  666. * @param string $context What the value is for.
  667. * Valid values are 'view' and 'edit'.
  668. * @return int
  669. */
  670. public function get_user_id( $context = 'view' ) {
  671. return $this->get_prop( 'user_id', $context );
  672. }
  673. /**
  674. * API version.
  675. *
  676. * @since 3.0.0
  677. * @param string $context What the value is for.
  678. * Valid values are 'view' and 'edit'.
  679. * @return string
  680. */
  681. public function get_api_version( $context = 'view' ) {
  682. $version = $this->get_prop( 'api_version', $context );
  683. return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3';
  684. }
  685. /**
  686. * Get the failure count.
  687. *
  688. * @since 2.2.0
  689. * @param string $context What the value is for.
  690. * Valid values are 'view' and 'edit'.
  691. * @return int
  692. */
  693. public function get_failure_count( $context = 'view' ) {
  694. return $this->get_prop( 'failure_count', $context );
  695. }
  696. /**
  697. * Get pending delivery.
  698. *
  699. * @since 3.2.0
  700. * @param string $context What the value is for.
  701. * Valid values are 'view' and 'edit'.
  702. * @return bool
  703. */
  704. public function get_pending_delivery( $context = 'view' ) {
  705. return $this->get_prop( 'pending_delivery', $context );
  706. }
  707. /*
  708. |--------------------------------------------------------------------------
  709. | Setters
  710. |--------------------------------------------------------------------------
  711. */
  712. /**
  713. * Set webhook name.
  714. *
  715. * @since 3.2.0
  716. * @param string $name Webhook name.
  717. */
  718. public function set_name( $name ) {
  719. $this->set_prop( 'name', $name );
  720. }
  721. /**
  722. * Set webhook created date.
  723. *
  724. * @since 3.2.0
  725. * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
  726. * If the DateTime string has no timezone or offset,
  727. * WordPress site timezone will be assumed.
  728. * Null if their is no date.
  729. */
  730. public function set_date_created( $date = null ) {
  731. $this->set_date_prop( 'date_created', $date );
  732. }
  733. /**
  734. * Set webhook modified date.
  735. *
  736. * @since 3.2.0
  737. * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
  738. * If the DateTime string has no timezone or offset,
  739. * WordPress site timezone will be assumed.
  740. * Null if their is no date.
  741. */
  742. public function set_date_modified( $date = null ) {
  743. $this->set_date_prop( 'date_modified', $date );
  744. }
  745. /**
  746. * Set status.
  747. *
  748. * @since 3.2.0
  749. * @param string $status Status.
  750. */
  751. public function set_status( $status ) {
  752. if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
  753. $status = 'disabled';
  754. }
  755. $this->set_prop( 'status', $status );
  756. }
  757. /**
  758. * Set the secret used for generating the HMAC-SHA256 signature.
  759. *
  760. * @since 2.2.0
  761. * @param string $secret Secret.
  762. */
  763. public function set_secret( $secret ) {
  764. $this->set_prop( 'secret', $secret );
  765. }
  766. /**
  767. * Set the webhook topic and associated hooks.
  768. * The topic resource & event are also saved separately.
  769. *
  770. * @since 2.2.0
  771. * @param string $topic Webhook topic.
  772. */
  773. public function set_topic( $topic ) {
  774. $topic = wc_clean( $topic );
  775. if ( ! wc_is_webhook_valid_topic( $topic ) ) {
  776. $topic = '';
  777. }
  778. $this->set_prop( 'topic', $topic );
  779. }
  780. /**
  781. * Set the delivery URL.
  782. *
  783. * @since 2.2.0
  784. * @param string $url Delivery URL.
  785. */
  786. public function set_delivery_url( $url ) {
  787. $this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) );
  788. }
  789. /**
  790. * Set user ID.
  791. *
  792. * @since 3.2.0
  793. * @param int $user_id User ID.
  794. */
  795. public function set_user_id( $user_id ) {
  796. $this->set_prop( 'user_id', (int) $user_id );
  797. }
  798. /**
  799. * Set API version.
  800. *
  801. * @since 3.0.0
  802. * @param int|string $version REST API version.
  803. */
  804. public function set_api_version( $version ) {
  805. if ( ! is_numeric( $version ) ) {
  806. $version = $this->data_store->get_api_version_number( $version );
  807. }
  808. $this->set_prop( 'api_version', (int) $version );
  809. }
  810. /**
  811. * Set pending delivery.
  812. *
  813. * @since 3.2.0
  814. * @param bool $pending_delivery Set true if is pending for delivery.
  815. */
  816. public function set_pending_delivery( $pending_delivery ) {
  817. $this->set_prop( 'pending_delivery', (bool) $pending_delivery );
  818. }
  819. /**
  820. * Set failure count.
  821. *
  822. * @since 3.2.0
  823. * @param bool $failure_count Total of failures.
  824. */
  825. public function set_failure_count( $failure_count ) {
  826. $this->set_prop( 'failure_count', intval( $failure_count ) );
  827. }
  828. /*
  829. |--------------------------------------------------------------------------
  830. | Non-CRUD Getters
  831. |--------------------------------------------------------------------------
  832. */
  833. /**
  834. * Get the associated hook names for a topic.
  835. *
  836. * @since 2.2.0
  837. * @param string $topic Topic name.
  838. * @return array
  839. */
  840. private function get_topic_hooks( $topic ) {
  841. $topic_hooks = array(
  842. 'coupon.created' => array(
  843. 'woocommerce_process_shop_coupon_meta',
  844. 'woocommerce_new_coupon',
  845. ),
  846. 'coupon.updated' => array(
  847. 'woocommerce_process_shop_coupon_meta',
  848. 'woocommerce_update_coupon',
  849. ),
  850. 'coupon.deleted' => array(
  851. 'wp_trash_post',
  852. ),
  853. 'coupon.restored' => array(
  854. 'untrashed_post',
  855. ),
  856. 'customer.created' => array(
  857. 'user_register',
  858. 'woocommerce_created_customer',
  859. 'woocommerce_new_customer',
  860. ),
  861. 'customer.updated' => array(
  862. 'profile_update',
  863. 'woocommerce_update_customer',
  864. ),
  865. 'customer.deleted' => array(
  866. 'delete_user',
  867. ),
  868. 'order.created' => array(
  869. 'woocommerce_new_order',
  870. ),
  871. 'order.updated' => array(
  872. 'woocommerce_update_order',
  873. 'woocommerce_order_refunded',
  874. ),
  875. 'order.deleted' => array(
  876. 'wp_trash_post',
  877. ),
  878. 'order.restored' => array(
  879. 'untrashed_post',
  880. ),
  881. 'product.created' => array(
  882. 'woocommerce_process_product_meta',
  883. 'woocommerce_new_product',
  884. 'woocommerce_new_product_variation',
  885. ),
  886. 'product.updated' => array(
  887. 'woocommerce_process_product_meta',
  888. 'woocommerce_update_product',
  889. 'woocommerce_update_product_variation',
  890. ),
  891. 'product.deleted' => array(
  892. 'wp_trash_post',
  893. ),
  894. 'product.restored' => array(
  895. 'untrashed_post',
  896. ),
  897. );
  898. $topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );
  899. return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array();
  900. }
  901. /**
  902. * Get the hook names for the webhook.
  903. *
  904. * @since 2.2.0
  905. * @return array
  906. */
  907. public function get_hooks() {
  908. if ( 'action' === $this->get_resource() ) {
  909. $hooks = array( $this->get_event() );
  910. } else {
  911. $hooks = $this->get_topic_hooks( $this->get_topic() );
  912. }
  913. return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() );
  914. }
  915. /**
  916. * Get the resource for the webhook, e.g. `order`.
  917. *
  918. * @since 2.2.0
  919. * @return string
  920. */
  921. public function get_resource() {
  922. $topic = explode( '.', $this->get_topic() );
  923. return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() );
  924. }
  925. /**
  926. * Get the event for the webhook, e.g. `created`.
  927. *
  928. * @since 2.2.0
  929. * @return string
  930. */
  931. public function get_event() {
  932. $topic = explode( '.', $this->get_topic() );
  933. return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_id() );
  934. }
  935. /**
  936. * Get the webhook i18n status.
  937. *
  938. * @return string
  939. */
  940. public function get_i18n_status() {
  941. $status = $this->get_status();
  942. $statuses = wc_get_webhook_statuses();
  943. return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status;
  944. }
  945. }