class-wc-webhook.php 27 KB

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