Issues (942)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/class-wc-webhook.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
14
defined( 'ABSPATH' ) || exit;
15
16 1
require_once 'legacy/class-wc-legacy-webhook.php';
17
18
/**
19
 * Webhook class.
20
 */
21
class WC_Webhook extends WC_Legacy_Webhook {
22
23
	/**
24
	 * Stores webhook data.
25
	 *
26
	 * @var array
27
	 */
28
	protected $data = array(
29
		'date_created'     => null,
30
		'date_modified'    => null,
31
		'status'           => 'disabled',
32
		'delivery_url'     => '',
33
		'secret'           => '',
34
		'name'             => '',
35
		'topic'            => '',
36
		'hooks'            => '',
37
		'resource'         => '',
38
		'event'            => '',
39
		'failure_count'    => 0,
40
		'user_id'          => 0,
41
		'api_version'      => 3,
42
		'pending_delivery' => false,
43
	);
44
45
	/**
46
	 * Load webhook data based on how WC_Webhook is called.
47
	 *
48
	 * @param WC_Webhook|int $data Webhook ID or data.
49
	 * @throws Exception If webhook cannot be read/found and $data is set.
50
	 */
51 26
	public function __construct( $data = 0 ) {
52 26
		parent::__construct( $data );
53
54 26 View Code Duplication
		if ( $data instanceof WC_Webhook ) {
55
			$this->set_id( absint( $data->get_id() ) );
56 26
		} elseif ( is_numeric( $data ) ) {
57 26
			$this->set_id( $data );
58
		}
59
60 26
		$this->data_store = WC_Data_Store::load( 'webhook' );
61
62
		// If we have an ID, load the webhook from the DB.
63 26 View Code Duplication
		if ( $this->get_id() ) {
64
			try {
65 8
				$this->data_store->read( $this );
66
			} catch ( Exception $e ) {
67
				$this->set_id( 0 );
68 8
				$this->set_object_read( true );
69
			}
70
		} else {
71 26
			$this->set_object_read( true );
72
		}
73
	}
74
75
	/**
76
	 * Enqueue the hooks associated with the webhook.
77
	 *
78
	 * @since 2.2.0
79
	 */
80 8
	public function enqueue() {
81 8
		$hooks = $this->get_hooks();
82 8
		$url   = $this->get_delivery_url();
83
84 8
		if ( is_array( $hooks ) && ! empty( $url ) ) {
85 8
			foreach ( $hooks as $hook ) {
86 8
				add_action( $hook, array( $this, 'process' ) );
87
			}
88
		}
89
	}
90
91
	/**
92
	 * Process the webhook for delivery by verifying that it should be delivered.
93
	 * and scheduling the delivery (in the background by default, or immediately).
94
	 *
95
	 * @since  2.2.0
96
	 * @param  mixed $arg The first argument provided from the associated hooks.
97
	 * @return mixed $arg Returns the argument in case the webhook was hooked into a filter.
98
	 */
99
	public function process( $arg ) {
100
101
		// Verify that webhook should be processed for delivery.
102
		if ( ! $this->should_deliver( $arg ) ) {
103
			return;
104
		}
105
106
		/**
107
		 * Process webhook delivery.
108
		 *
109
		 * @since 3.3.0
110
		 * @hooked wc_webhook_process_delivery - 10
111
		 */
112
		do_action( 'woocommerce_webhook_process_delivery', $this, $arg );
113
114
		return $arg;
115
	}
116
117
	/**
118
	 * Helper to check if the webhook should be delivered, as some hooks.
119
	 * (like `wp_trash_post`) will fire for every post type, not just ours.
120
	 *
121
	 * @since  2.2.0
122
	 * @param  mixed $arg First hook argument.
123
	 * @return bool       True if webhook should be delivered, false otherwise.
124
	 */
125
	private function should_deliver( $arg ) {
126
		$should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg );
127
128
		/**
129
		 * Let other plugins intercept deliver for some messages queue like rabbit/zeromq.
130
		 *
131
		 * @param bool       $should_deliver True if the webhook should be sent, or false to not send it.
132
		 * @param WC_Webhook $this The current webhook class.
133
		 * @param mixed      $arg First hook argument.
134
		 */
135
		return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg );
136
	}
137
138
	/**
139
	 * Returns if webhook is active.
140
	 *
141
	 * @since  3.6.0
142
	 * @return bool  True if validation passes.
143
	 */
144
	private function is_active() {
145
		return 'active' === $this->get_status();
146
	}
147
148
	/**
149
	 * Returns if topic is valid.
150
	 *
151
	 * @since  3.6.0
152
	 * @return bool  True if validation passes.
153
	 */
154
	private function is_valid_topic() {
155
		return wc_is_webhook_valid_topic( $this->get_topic() );
156
	}
157
158
	/**
159
	 * Validates the criteria for certain actions.
160
	 *
161
	 * @since  3.6.0
162
	 * @param  mixed $arg First hook argument.
163
	 * @return bool       True if validation passes.
164
	 */
165
	private function is_valid_action( $arg ) {
166
		$current_action = current_action();
167
		$return         = true;
168
169
		switch ( $current_action ) {
170
			case 'delete_post':
171
			case 'wp_trash_post':
172
			case 'untrashed_post':
173
				$return = $this->is_valid_post_action( $arg );
174
				break;
175
			case 'delete_user':
176
				$return = $this->is_valid_user_action( $arg );
177
				break;
178
		}
179
180
		if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) {
181
			$return = $this->is_valid_processing_action( $arg );
182
		}
183
184
		return $return;
185
	}
186
187
	/**
188
	 * Validates post actions.
189
	 *
190
	 * @since  3.6.0
191
	 * @param  mixed $arg First hook argument.
192
	 * @return bool       True if validation passes.
193
	 */
194
	private function is_valid_post_action( $arg ) {
195
		// Only deliver deleted/restored event for coupons, orders, and products.
196
		if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) {
197
			return false;
198
		}
199
200
		// Check if is delivering for the correct resource.
201
		if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) {
202
			return false;
203
		}
204
		return true;
205
	}
206
207
	/**
208
	 * Validates user actions.
209
	 *
210
	 * @since  3.6.0
211
	 * @param  mixed $arg First hook argument.
212
	 * @return bool       True if validation passes.
213
	 */
214
	private function is_valid_user_action( $arg ) {
215
		$user = get_userdata( absint( $arg ) );
216
217
		// Only deliver deleted customer event for users with customer role.
218
		if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) {
219
			return false;
220
		}
221
222
		return true;
223
	}
224
225
	/**
226
	 * Validates WC processing actions.
227
	 *
228
	 * @since  3.6.0
229
	 * @param  mixed $arg First hook argument.
230
	 * @return bool       True if validation passes.
231
	 */
232
	private function is_valid_processing_action( $arg ) {
233
		// The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks
234
		// fire for create and update of products and orders, so check the post
235
		// creation date to determine the actual event.
236
		$resource = get_post( absint( $arg ) );
237
238
		// Drafts don't have post_date_gmt so calculate it here.
239
		$gmt_date = get_gmt_from_date( $resource->post_date );
240
241
		// A resource is considered created when the hook is executed within 10 seconds of the post creation date.
242
		$resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) );
243
244
		if ( 'created' === $this->get_event() && ! $resource_created ) {
245
			return false;
246
		} elseif ( 'updated' === $this->get_event() && $resource_created ) {
247
			return false;
248
		}
249
		return true;
250
	}
251
252
	/**
253
	 * Checks the resource for this webhook is valid e.g. valid post status.
254
	 *
255
	 * @since  3.6.0
256
	 * @param  mixed $arg First hook argument.
257
	 * @return bool       True if validation passes.
258
	 */
259
	private function is_valid_resource( $arg ) {
260
		$resource = $this->get_resource();
261
262
		if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) {
263
			$status = get_post_status( absint( $arg ) );
264
265
			// Ignore auto drafts for all resources.
266
			if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) {
267
				return false;
268
			}
269
270
			// Ignore standard drafts for orders.
271
			if ( 'order' === $resource && 'draft' === $status ) {
272
				return false;
273
			}
274
275
			// Check registered order types for order types args.
276
			if ( 'order' === $resource && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) {
277
				return false;
278
			}
279
		}
280
		return true;
281
	}
282
283
	/**
284
	 * Deliver the webhook payload using wp_safe_remote_request().
285
	 *
286
	 * @since 2.2.0
287
	 * @param mixed $arg First hook argument.
288
	 */
289
	public function deliver( $arg ) {
290
		$start_time = microtime( true );
291
		$payload    = $this->build_payload( $arg );
292
293
		// Setup request args.
294
		$http_args = array(
295
			'method'      => 'POST',
296
			'timeout'     => MINUTE_IN_SECONDS,
297
			'redirection' => 0,
298
			'httpversion' => '1.0',
299
			'blocking'    => true,
300
			'user-agent'  => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
301
			'body'        => trim( wp_json_encode( $payload ) ),
302
			'headers'     => array(
303
				'Content-Type' => 'application/json',
304
			),
305
			'cookies'     => array(),
306
		);
307
308
		$http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() );
309
310
		// Add custom headers.
311
		$delivery_id                                      = $this->get_new_delivery_id();
312
		$http_args['headers']['X-WC-Webhook-Source']      = home_url( '/' ); // Since 2.6.0.
313
		$http_args['headers']['X-WC-Webhook-Topic']       = $this->get_topic();
314
		$http_args['headers']['X-WC-Webhook-Resource']    = $this->get_resource();
315
		$http_args['headers']['X-WC-Webhook-Event']       = $this->get_event();
316
		$http_args['headers']['X-WC-Webhook-Signature']   = $this->generate_signature( $http_args['body'] );
317
		$http_args['headers']['X-WC-Webhook-ID']          = $this->get_id();
318
		$http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id;
319
320
		// Webhook away!
321
		$response = wp_safe_remote_request( $this->get_delivery_url(), $http_args );
322
323
		$duration = round( microtime( true ) - $start_time, 5 );
324
325
		$this->log_delivery( $delivery_id, $http_args, $response, $duration );
326
327
		do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
328
	}
329
330
	/**
331
	 * Get Legacy API payload.
332
	 *
333
	 * @since  3.0.0
334
	 * @param  string $resource    Resource type.
335
	 * @param  int    $resource_id Resource ID.
336
	 * @param  string $event       Event type.
337
	 * @return array
338
	 */
339
	private function get_legacy_api_payload( $resource, $resource_id, $event ) {
340
		// Include & load API classes.
341
		WC()->api->includes();
342
		WC()->api->register_resources( new WC_API_Server( '/' ) );
343
344
		switch ( $resource ) {
345
			case 'coupon':
346
				$payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id );
347
				break;
348
349
			case 'customer':
350
				$payload = WC()->api->WC_API_Customers->get_customer( $resource_id );
351
				break;
352
353
			case 'order':
354
				$payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) );
355
				break;
356
357
			case 'product':
358
				// Bulk and quick edit action hooks return a product object instead of an ID.
359
				if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
360
					$resource_id = $resource_id->get_id();
361
				}
362
				$payload = WC()->api->WC_API_Products->get_product( $resource_id );
363
				break;
364
365
			// Custom topics include the first hook argument.
366
			case 'action':
367
				$payload = array(
368
					'action' => current( $this->get_hooks() ),
369
					'arg'    => $resource_id,
370
				);
371
				break;
372
373
			default:
374
				$payload = array();
375
				break;
376
		}
377
378
		return $payload;
379
	}
380
381
	/**
382
	 * Get WP API integration payload.
383
	 *
384
	 * @since  3.0.0
385
	 * @param  string $resource    Resource type.
386
	 * @param  int    $resource_id Resource ID.
387
	 * @param  string $event       Event type.
388
	 * @return array
389
	 */
390
	private function get_wp_api_payload( $resource, $resource_id, $event ) {
391
		switch ( $resource ) {
392
			case 'coupon':
393
			case 'customer':
394
			case 'order':
395
			case 'product':
396
				// Bulk and quick edit action hooks return a product object instead of an ID.
397
				if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
398
					$resource_id = $resource_id->get_id();
399
				}
400
401
				$version = str_replace( 'wp_api_', '', $this->get_api_version() );
402
				$payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$resource_id}" );
403
				break;
404
405
			// Custom topics include the first hook argument.
406
			case 'action':
407
				$payload = array(
408
					'action' => current( $this->get_hooks() ),
409
					'arg'    => $resource_id,
410
				);
411
				break;
412
413
			default:
414
				$payload = array();
415
				break;
416
		}
417
418
		return $payload;
419
	}
420
421
	/**
422
	 * Build the payload data for the webhook.
423
	 *
424
	 * @since  2.2.0
425
	 * @param  mixed $resource_id First hook argument, typically the resource ID.
426
	 * @return mixed              Payload data.
427
	 */
428
	public function build_payload( $resource_id ) {
429
		// Build the payload with the same user context as the user who created
430
		// the webhook -- this avoids permission errors as background processing
431
		// runs with no user context.
432
		$current_user = get_current_user_id();
433
		wp_set_current_user( $this->get_user_id() );
434
435
		$resource = $this->get_resource();
436
		$event    = $this->get_event();
437
438
		// If a resource has been deleted, just include the ID.
439
		if ( 'deleted' === $event ) {
440
			$payload = array(
441
				'id' => $resource_id,
442
			);
443
		} else {
444
			if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
445
				$payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
446
			} else {
447
				$payload = $this->get_legacy_api_payload( $resource, $resource_id, $event );
448
			}
449
		}
450
451
		// Restore the current user.
452
		wp_set_current_user( $current_user );
453
454
		return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->get_id() );
455
	}
456
457
	/**
458
	 * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the.
459
	 * recipient can verify the authenticity of the webhook. Note that the signature.
460
	 * is calculated after the body has already been encoded (JSON by default).
461
	 *
462
	 * @since  2.2.0
463
	 * @param  string $payload Payload data to hash.
464
	 * @return string
465
	 */
466 1
	public function generate_signature( $payload ) {
467 1
		$hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );
468
469 1
		return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) );
470
	}
471
472
	/**
473
	 * Generate a new unique hash as a delivery id based on current time and wehbook id.
474
	 * Return the hash for inclusion in the webhook request.
475
	 *
476
	 * @since  2.2.0
477
	 * @return string
478
	 */
479
	public function get_new_delivery_id() {
480
		// Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID.
481
		return wp_hash( $this->get_id() . strtotime( 'now' ) );
482
	}
483
484
	/**
485
	 * Log the delivery request/response.
486
	 *
487
	 * @since 2.2.0
488
	 * @param string         $delivery_id Previously created hash.
489
	 * @param array          $request     Request data.
490
	 * @param array|WP_Error $response    Response data.
491
	 * @param float          $duration    Request duration.
492
	 */
493
	public function log_delivery( $delivery_id, $request, $response, $duration ) {
494
		$logger  = wc_get_logger();
495
		$message = array(
496
			'Webhook Delivery' => array(
497
				'Delivery ID' => $delivery_id,
498
				'Date'        => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ),
499
				'URL'         => $this->get_delivery_url(),
500
				'Duration'    => $duration,
501
				'Request'     => array(
502
					'Method'  => $request['method'],
503
					'Headers' => array_merge(
504
						array(
505
							'User-Agent' => $request['user-agent'],
506
						),
507
						$request['headers']
508
					),
509
				),
510
				'Body'        => wp_slash( $request['body'] ),
511
			),
512
		);
513
514
		// Parse response.
515
		if ( is_wp_error( $response ) ) {
516
			$response_code    = $response->get_error_code();
517
			$response_message = $response->get_error_message();
518
			$response_headers = array();
519
			$response_body    = '';
520
		} else {
521
			$response_code    = wp_remote_retrieve_response_code( $response );
522
			$response_message = wp_remote_retrieve_response_message( $response );
523
			$response_headers = wp_remote_retrieve_headers( $response );
524
			$response_body    = wp_remote_retrieve_body( $response );
525
		}
526
527
		$message['Webhook Delivery']['Response'] = array(
528
			'Code'    => $response_code,
529
			'Message' => $response_message,
530
			'Headers' => $response_headers,
531
			'Body'    => $response_body,
532
		);
533
534
		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
535
			$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.';
536
			$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.';
537
		}
538
539
		$logger->info(
540
			wc_print_r( $message, true ),
0 ignored issues
show
It seems like wc_print_r($message, true) targeting wc_print_r() can also be of type boolean; however, WC_Logger::info() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
541
			array(
542
				'source' => 'webhooks-delivery',
543
			)
544
		);
545
546
		// Track failures.
547
		// Check for a success, which is a 2xx, 301 or 302 Response Code.
548
		if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) {
549
			$this->set_failure_count( 0 );
550
			$this->save();
551
		} else {
552
			$this->failed_delivery();
553
		}
554
	}
555
556
	/**
557
	 * Track consecutive delivery failures and automatically disable the webhook.
558
	 * if more than 5 consecutive failures occur. A failure is defined as a.
559
	 * non-2xx response.
560
	 *
561
	 * @since 2.2.0
562
	 */
563
	private function failed_delivery() {
564
		$failures = $this->get_failure_count();
565
566
		if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) {
567
			$this->set_status( 'disabled' );
568
569
			do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() );
570
		} else {
571
			$this->set_failure_count( ++$failures );
572
		}
573
574
		$this->save();
575
	}
576
577
	/**
578
	 * Get the delivery logs for this webhook.
579
	 *
580
	 * @since  3.3.0
581
	 * @return string
582
	 */
583
	public function get_delivery_logs() {
584
		return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
585
	}
586
587
	/**
588
	 * Get the delivery log specified by the ID. The delivery log includes:
589
	 *
590
	 * + duration
591
	 * + summary
592
	 * + request method/url
593
	 * + request headers/body
594
	 * + response code/message/headers/body
595
	 *
596
	 * @since 2.2
597
	 * @deprecated 3.3.0
598
	 * @param int $delivery_id Delivery ID.
599
	 * @return void
600
	 */
601
	public function get_delivery_log( $delivery_id ) {
602
		wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' );
603
	}
604
605
	/**
606
	 * Send a test ping to the delivery URL, sent when the webhook is first created.
607
	 *
608
	 * @since  2.2.0
609
	 * @return bool|WP_Error
610
	 */
611
	public function deliver_ping() {
612
		$args = array(
613
			'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
614
			'body'       => 'webhook_id=' . $this->get_id(),
615
		);
616
617
		$test          = wp_safe_remote_post( $this->get_delivery_url(), $args );
618
		$response_code = wp_remote_retrieve_response_code( $test );
619
620
		if ( is_wp_error( $test ) ) {
621
			/* translators: error message */
622
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) );
623
		}
624
625
		if ( 200 !== $response_code ) {
626
			/* translators: error message */
627
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) );
628
		}
629
630
		$this->set_pending_delivery( false );
631
		$this->save();
632
633
		return true;
634
	}
635
636
	/*
637
	|--------------------------------------------------------------------------
638
	| Getters
639
	|--------------------------------------------------------------------------
640
	*/
641
642
	/**
643
	 * Get the friendly name for the webhook.
644
	 *
645
	 * @since  2.2.0
646
	 * @param  string $context What the value is for.
647
	 *                         Valid values are 'view' and 'edit'.
648
	 * @return string
649
	 */
650 10
	public function get_name( $context = 'view' ) {
651 10
		return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() );
652
	}
653
654
	/**
655
	 * Get the webhook status.
656
	 *
657
	 * - 'active' - delivers payload.
658
	 * - 'paused' - does not deliver payload, paused by admin.
659
	 * - 'disabled' - does not delivery payload, paused automatically due to consecutive failures.
660
	 *
661
	 * @since  2.2.0
662
	 * @param  string $context What the value is for.
663
	 *                         Valid values are 'view' and 'edit'.
664
	 * @return string status
665
	 */
666 11
	public function get_status( $context = 'view' ) {
667 11
		return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() );
668
	}
669
670
	/**
671
	 * Get webhook created date.
672
	 *
673
	 * @since  3.2.0
674
	 * @param  string $context  What the value is for.
675
	 *                          Valid values are 'view' and 'edit'.
676
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
677
	 */
678 1
	public function get_date_created( $context = 'view' ) {
679 1
		return $this->get_prop( 'date_created', $context );
680
	}
681
682
	/**
683
	 * Get webhook modified date.
684
	 *
685
	 * @since  3.2.0
686
	 * @param  string $context  What the value is for.
687
	 *                          Valid values are 'view' and 'edit'.
688
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
689
	 */
690 1
	public function get_date_modified( $context = 'view' ) {
691 1
		return $this->get_prop( 'date_modified', $context );
692
	}
693
694
	/**
695
	 * Get the secret used for generating the HMAC-SHA256 signature.
696
	 *
697
	 * @since  2.2.0
698
	 * @param  string $context What the value is for.
699
	 *                         Valid values are 'view' and 'edit'.
700
	 * @return string
701
	 */
702 11
	public function get_secret( $context = 'view' ) {
703 11
		return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() );
704
	}
705
706
	/**
707
	 * Get the webhook topic, e.g. `order.created`.
708
	 *
709
	 * @since  2.2.0
710
	 * @param  string $context What the value is for.
711
	 *                         Valid values are 'view' and 'edit'.
712
	 * @return string
713
	 */
714 13
	public function get_topic( $context = 'view' ) {
715 13
		return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() );
716
	}
717
718
	/**
719
	 * Get the delivery URL.
720
	 *
721
	 * @since  2.2.0
722
	 * @param  string $context What the value is for.
723
	 *                         Valid values are 'view' and 'edit'.
724
	 * @return string
725
	 */
726 10
	public function get_delivery_url( $context = 'view' ) {
727 10
		return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() );
728
	}
729
730
	/**
731
	 * Get the user ID for this webhook.
732
	 *
733
	 * @since  2.2.0
734
	 * @param  string $context What the value is for.
735
	 *                         Valid values are 'view' and 'edit'.
736
	 * @return int
737
	 */
738 10
	public function get_user_id( $context = 'view' ) {
739 10
		return $this->get_prop( 'user_id', $context );
740
	}
741
742
	/**
743
	 * API version.
744
	 *
745
	 * @since  3.0.0
746
	 * @param  string $context What the value is for.
747
	 *                         Valid values are 'view' and 'edit'.
748
	 * @return string
749
	 */
750 10
	public function get_api_version( $context = 'view' ) {
751 10
		$version = $this->get_prop( 'api_version', $context );
752
753 10
		return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3';
754
	}
755
756
	/**
757
	 * Get the failure count.
758
	 *
759
	 * @since  2.2.0
760
	 * @param  string $context What the value is for.
761
	 *                         Valid values are 'view' and 'edit'.
762
	 * @return int
763
	 */
764 10
	public function get_failure_count( $context = 'view' ) {
765 10
		return $this->get_prop( 'failure_count', $context );
766
	}
767
768
	/**
769
	 * Get pending delivery.
770
	 *
771
	 * @since  3.2.0
772
	 * @param  string $context What the value is for.
773
	 *                         Valid values are 'view' and 'edit'.
774
	 * @return bool
775
	 */
776 10
	public function get_pending_delivery( $context = 'view' ) {
777 10
		return $this->get_prop( 'pending_delivery', $context );
778
	}
779
780
	/*
781
	|--------------------------------------------------------------------------
782
	| Setters
783
	|--------------------------------------------------------------------------
784
	 */
785
786
	/**
787
	 * Set webhook name.
788
	 *
789
	 * @since 3.2.0
790
	 * @param string $name Webhook name.
791
	 */
792 9
	public function set_name( $name ) {
793 9
		$this->set_prop( 'name', $name );
794
	}
795
796
	/**
797
	 * Set webhook created date.
798
	 *
799
	 * @since 3.2.0
800
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
801
	 *                                  If the DateTime string has no timezone or offset,
802
	 *                                  WordPress site timezone will be assumed.
803
	 *                                  Null if their is no date.
804
	 */
805 10
	public function set_date_created( $date = null ) {
806 10
		$this->set_date_prop( 'date_created', $date );
807
	}
808
809
	/**
810
	 * Set webhook modified date.
811
	 *
812
	 * @since 3.2.0
813
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
814
	 *                                  If the DateTime string has no timezone or offset,
815
	 *                                  WordPress site timezone will be assumed.
816
	 *                                  Null if their is no date.
817
	 */
818 1
	public function set_date_modified( $date = null ) {
819 1
		$this->set_date_prop( 'date_modified', $date );
820
	}
821
822
	/**
823
	 * Set status.
824
	 *
825
	 * @since 3.2.0
826
	 * @param string $status Status.
827
	 */
828 10
	public function set_status( $status ) {
829 10
		if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
830
			$status = 'disabled';
831
		}
832
833 10
		$this->set_prop( 'status', $status );
834
	}
835
836
	/**
837
	 * Set the secret used for generating the HMAC-SHA256 signature.
838
	 *
839
	 * @since 2.2.0
840
	 * @param string $secret Secret.
841
	 */
842 9
	public function set_secret( $secret ) {
843 9
		$this->set_prop( 'secret', $secret );
844
	}
845
846
	/**
847
	 * Set the webhook topic and associated hooks.
848
	 * The topic resource & event are also saved separately.
849
	 *
850
	 * @since 2.2.0
851
	 * @param string $topic Webhook topic.
852
	 */
853 12
	public function set_topic( $topic ) {
854 12
		$topic = wc_clean( $topic );
855
856 12
		if ( ! wc_is_webhook_valid_topic( $topic ) ) {
857
			$topic = '';
858
		}
859
860 12
		$this->set_prop( 'topic', $topic );
861
	}
862
863
	/**
864
	 * Set the delivery URL.
865
	 *
866
	 * @since 2.2.0
867
	 * @param string $url Delivery URL.
868
	 */
869 9
	public function set_delivery_url( $url ) {
870 9
		$this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) );
871
	}
872
873
	/**
874
	 * Set user ID.
875
	 *
876
	 * @since 3.2.0
877
	 * @param int $user_id User ID.
878
	 */
879 9
	public function set_user_id( $user_id ) {
880 9
		$this->set_prop( 'user_id', (int) $user_id );
881
	}
882
883
	/**
884
	 * Set API version.
885
	 *
886
	 * @since 3.0.0
887
	 * @param int|string $version REST API version.
888
	 */
889 9
	public function set_api_version( $version ) {
890 9
		if ( ! is_numeric( $version ) ) {
891 1
			$version = $this->data_store->get_api_version_number( $version );
892
		}
893
894 9
		$this->set_prop( 'api_version', (int) $version );
895
	}
896
897
	/**
898
	 * Set pending delivery.
899
	 *
900
	 * @since 3.2.0
901
	 * @param bool $pending_delivery Set true if is pending for delivery.
902
	 */
903 10
	public function set_pending_delivery( $pending_delivery ) {
904 10
		$this->set_prop( 'pending_delivery', (bool) $pending_delivery );
905
	}
906
907
	/**
908
	 * Set failure count.
909
	 *
910
	 * @since 3.2.0
911
	 * @param bool $failure_count Total of failures.
912
	 */
913 9
	public function set_failure_count( $failure_count ) {
914 9
		$this->set_prop( 'failure_count', intval( $failure_count ) );
915
	}
916
917
	/*
918
	|--------------------------------------------------------------------------
919
	| Non-CRUD Getters
920
	|--------------------------------------------------------------------------
921
	*/
922
923
	/**
924
	 * Get the associated hook names for a topic.
925
	 *
926
	 * @since  2.2.0
927
	 * @param  string $topic Topic name.
928
	 * @return array
929
	 */
930 1
	private function get_topic_hooks( $topic ) {
931
		$topic_hooks = array(
932
			'coupon.created'   => array(
933 1
				'woocommerce_process_shop_coupon_meta',
934
				'woocommerce_new_coupon',
935
			),
936
			'coupon.updated'   => array(
937
				'woocommerce_process_shop_coupon_meta',
938
				'woocommerce_update_coupon',
939
			),
940
			'coupon.deleted'   => array(
941
				'wp_trash_post',
942
			),
943
			'coupon.restored'  => array(
944
				'untrashed_post',
945
			),
946
			'customer.created' => array(
947
				'user_register',
948
				'woocommerce_created_customer',
949
				'woocommerce_new_customer',
950
			),
951
			'customer.updated' => array(
952
				'profile_update',
953
				'woocommerce_update_customer',
954
			),
955
			'customer.deleted' => array(
956
				'delete_user',
957
			),
958
			'order.created'    => array(
959
				'woocommerce_new_order',
960
			),
961
			'order.updated'    => array(
962
				'woocommerce_update_order',
963
				'woocommerce_order_refunded',
964
			),
965
			'order.deleted'    => array(
966
				'wp_trash_post',
967
			),
968
			'order.restored'   => array(
969
				'untrashed_post',
970
			),
971
			'product.created'  => array(
972
				'woocommerce_process_product_meta',
973
				'woocommerce_new_product',
974
				'woocommerce_new_product_variation',
975
			),
976
			'product.updated'  => array(
977
				'woocommerce_process_product_meta',
978
				'woocommerce_update_product',
979
				'woocommerce_update_product_variation',
980
			),
981
			'product.deleted'  => array(
982
				'wp_trash_post',
983
			),
984
			'product.restored' => array(
985
				'untrashed_post',
986
			),
987
		);
988
989 1
		$topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );
990
991 1
		return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array();
992
	}
993
994
	/**
995
	 * Get the hook names for the webhook.
996
	 *
997
	 * @since  2.2.0
998
	 * @return array
999
	 */
1000 9
	public function get_hooks() {
1001 9
		if ( 'action' === $this->get_resource() ) {
1002 8
			$hooks = array( $this->get_event() );
1003
		} else {
1004 1
			$hooks = $this->get_topic_hooks( $this->get_topic() );
1005
		}
1006
1007 9
		return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() );
1008
	}
1009
1010
	/**
1011
	 * Get the resource for the webhook, e.g. `order`.
1012
	 *
1013
	 * @since  2.2.0
1014
	 * @return string
1015
	 */
1016 10
	public function get_resource() {
1017 10
		$topic = explode( '.', $this->get_topic() );
1018
1019 10
		return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() );
1020
	}
1021
1022
	/**
1023
	 * Get the event for the webhook, e.g. `created`.
1024
	 *
1025
	 * @since  2.2.0
1026
	 * @return string
1027
	 */
1028 9
	public function get_event() {
1029 9
		$topic = explode( '.', $this->get_topic() );
1030
1031 9
		return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_id() );
1032
	}
1033
1034
	/**
1035
	 * Get the webhook i18n status.
1036
	 *
1037
	 * @return string
1038
	 */
1039 1
	public function get_i18n_status() {
1040 1
		$status   = $this->get_status();
1041 1
		$statuses = wc_get_webhook_statuses();
1042
1043 1
		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status;
1044
	}
1045
}
1046