Completed
Push — master ( 07eadf...5397e4 )
by Mike
20:38 queued 11s
created

WC_Webhook::should_deliver()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 12
ccs 0
cts 6
cp 0
crap 20
rs 9.8666
c 0
b 0
f 0
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'      => 2,
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 19
	public function __construct( $data = 0 ) {
52 19
		parent::__construct( $data );
53
54 19 View Code Duplication
		if ( $data instanceof WC_Webhook ) {
55
			$this->set_id( absint( $data->get_id() ) );
56 19
		} elseif ( is_numeric( $data ) ) {
57 19
			$this->set_id( $data );
58
		}
59
60 19
		$this->data_store = WC_Data_Store::load( 'webhook' );
61
62
		// If we have an ID, load the webhook from the DB.
63 19 View Code Duplication
		if ( $this->get_id() ) {
64
			try {
65 1
				$this->data_store->read( $this );
66
			} catch ( Exception $e ) {
67
				$this->set_id( 0 );
68 1
				$this->set_object_read( true );
69
			}
70
		} else {
71 19
			$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 1
	public function enqueue() {
81 1
		$hooks = $this->get_hooks();
82 1
		$url   = $this->get_delivery_url();
83
84 1
		if ( is_array( $hooks ) && ! empty( $url ) ) {
85 1
			foreach ( $hooks as $hook ) {
86 1
				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
		return true;
276
	}
277
278
	/**
279
	 * Deliver the webhook payload using wp_safe_remote_request().
280
	 *
281
	 * @since 2.2.0
282
	 * @param mixed $arg First hook argument.
283
	 */
284
	public function deliver( $arg ) {
285
		$start_time = microtime( true );
286
		$payload    = $this->build_payload( $arg );
287
288
		// Setup request args.
289
		$http_args = array(
290
			'method'      => 'POST',
291
			'timeout'     => MINUTE_IN_SECONDS,
292
			'redirection' => 0,
293
			'httpversion' => '1.0',
294
			'blocking'    => true,
295
			'user-agent'  => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
296
			'body'        => trim( wp_json_encode( $payload ) ),
297
			'headers'     => array(
298
				'Content-Type' => 'application/json',
299
			),
300
			'cookies'     => array(),
301
		);
302
303
		$http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() );
304
305
		// Add custom headers.
306
		$delivery_id                                      = $this->get_new_delivery_id();
307
		$http_args['headers']['X-WC-Webhook-Source']      = home_url( '/' ); // Since 2.6.0.
308
		$http_args['headers']['X-WC-Webhook-Topic']       = $this->get_topic();
309
		$http_args['headers']['X-WC-Webhook-Resource']    = $this->get_resource();
310
		$http_args['headers']['X-WC-Webhook-Event']       = $this->get_event();
311
		$http_args['headers']['X-WC-Webhook-Signature']   = $this->generate_signature( $http_args['body'] );
312
		$http_args['headers']['X-WC-Webhook-ID']          = $this->get_id();
313
		$http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id;
314
315
		// Webhook away!
316
		$response = wp_safe_remote_request( $this->get_delivery_url(), $http_args );
317
318
		$duration = round( microtime( true ) - $start_time, 5 );
319
320
		$this->log_delivery( $delivery_id, $http_args, $response, $duration );
321
322
		do_action( 'woocommerce_webhook_delivery', $http_args, $response, $duration, $arg, $this->get_id() );
323
	}
324
325
	/**
326
	 * Get Legacy API payload.
327
	 *
328
	 * @since  3.0.0
329
	 * @param  string $resource    Resource type.
330
	 * @param  int    $resource_id Resource ID.
331
	 * @param  string $event       Event type.
332
	 * @return array
333
	 */
334
	private function get_legacy_api_payload( $resource, $resource_id, $event ) {
335
		// Include & load API classes.
336
		WC()->api->includes();
337
		WC()->api->register_resources( new WC_API_Server( '/' ) );
338
339
		switch ( $resource ) {
340
			case 'coupon':
341
				$payload = WC()->api->WC_API_Coupons->get_coupon( $resource_id );
342
				break;
343
344
			case 'customer':
345
				$payload = WC()->api->WC_API_Customers->get_customer( $resource_id );
346
				break;
347
348
			case 'order':
349
				$payload = WC()->api->WC_API_Orders->get_order( $resource_id, null, apply_filters( 'woocommerce_webhook_order_payload_filters', array() ) );
350
				break;
351
352
			case 'product':
353
				// Bulk and quick edit action hooks return a product object instead of an ID.
354
				if ( 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
355
					$resource_id = $resource_id->get_id();
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resource_id (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
356
				}
357
				$payload = WC()->api->WC_API_Products->get_product( $resource_id );
358
				break;
359
360
			// Custom topics include the first hook argument.
361
			case 'action':
362
				$payload = array(
363
					'action' => current( $this->get_hooks() ),
364
					'arg'    => $resource_id,
365
				);
366
				break;
367
368
			default:
369
				$payload = array();
370 1
				break;
371 1
		}
372
373 1
		return $payload;
374
	}
375
376
	/**
377
	 * Get WP API integration payload.
378
	 *
379
	 * @since  3.0.0
380
	 * @param  string $resource    Resource type.
381
	 * @param  int    $resource_id Resource ID.
382
	 * @param  string $event       Event type.
383
	 * @return array
384
	 */
385
	private function get_wp_api_payload( $resource, $resource_id, $event ) {
386
		$rest_api_versions = wc_get_webhook_rest_api_versions();
387
		$version_suffix    = end( $rest_api_versions ) !== $this->get_api_version() ? strtoupper( str_replace( 'wp_api', '', $this->get_api_version() ) ) : '';
388
389
		// Load REST API endpoints to generate payload.
390
		if ( ! did_action( 'rest_api_init' ) ) {
391
			WC()->api->rest_api_includes();
392
		}
393
394
		switch ( $resource ) {
395
			case 'coupon':
396
			case 'customer':
397
			case 'order':
398
			case 'product':
399
				$class      = 'WC_REST_' . ucfirst( $resource ) . 's' . $version_suffix . '_Controller';
400
				$request    = new WP_REST_Request( 'GET' );
401
				$controller = new $class();
402
403
				// Bulk and quick edit action hooks return a product object instead of an ID.
404
				if ( 'product' === $resource && 'updated' === $event && is_a( $resource_id, 'WC_Product' ) ) {
405
					$resource_id = $resource_id->get_id();
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resource_id (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
406
				}
407
408
				$request->set_param( 'id', $resource_id );
409
				$result  = $controller->get_item( $request );
410
				$payload = isset( $result->data ) ? $result->data : array();
411
				break;
412
413
			// Custom topics include the first hook argument.
414
			case 'action':
415
				$payload = array(
416
					'action' => current( $this->get_hooks() ),
417
					'arg'    => $resource_id,
418
				);
419
				break;
420
421
			default:
422
				$payload = array();
423
				break;
424
		}
425
426
		return $payload;
427
	}
428
429
	/**
430
	 * Build the payload data for the webhook.
431
	 *
432
	 * @since  2.2.0
433
	 * @param  mixed $resource_id First hook argument, typically the resource ID.
434
	 * @return mixed              Payload data.
435
	 */
436
	public function build_payload( $resource_id ) {
437
		// Build the payload with the same user context as the user who created
438
		// the webhook -- this avoids permission errors as background processing
439
		// runs with no user context.
440
		$current_user = get_current_user_id();
441
		wp_set_current_user( $this->get_user_id() );
442
443
		$resource = $this->get_resource();
444
		$event    = $this->get_event();
445
446
		// If a resource has been deleted, just include the ID.
447
		if ( 'deleted' === $event ) {
448
			$payload = array(
449
				'id' => $resource_id,
450
			);
451
		} else {
452
			if ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
453
				$payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
454
			} else {
455
				$payload = $this->get_legacy_api_payload( $resource, $resource_id, $event );
456
			}
457
		}
458
459
		// Restore the current user.
460
		wp_set_current_user( $current_user );
461
462
		return apply_filters( 'woocommerce_webhook_payload', $payload, $resource, $resource_id, $this->get_id() );
463
	}
464
465
	/**
466
	 * Generate a base64-encoded HMAC-SHA256 signature of the payload body so the.
467
	 * recipient can verify the authenticity of the webhook. Note that the signature.
468
	 * is calculated after the body has already been encoded (JSON by default).
469
	 *
470
	 * @since  2.2.0
471
	 * @param  string $payload Payload data to hash.
472
	 * @return string
473
	 */
474
	public function generate_signature( $payload ) {
475
		$hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );
476
477
		return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) );
478
	}
479
480
	/**
481
	 * Generate a new unique hash as a delivery id based on current time and wehbook id.
482
	 * Return the hash for inclusion in the webhook request.
483
	 *
484
	 * @since  2.2.0
485
	 * @return string
486
	 */
487
	public function get_new_delivery_id() {
488
		// Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID.
489
		return wp_hash( $this->get_id() . strtotime( 'now' ) );
490
	}
491
492
	/**
493
	 * Log the delivery request/response.
494
	 *
495
	 * @since 2.2.0
496
	 * @param string         $delivery_id Previously created hash.
497
	 * @param array          $request     Request data.
498
	 * @param array|WP_Error $response    Response data.
499
	 * @param float          $duration    Request duration.
500
	 */
501
	public function log_delivery( $delivery_id, $request, $response, $duration ) {
502
		$logger  = wc_get_logger();
503
		$message = array(
504
			'Webhook Delivery' => array(
505
				'Delivery ID' => $delivery_id,
506
				'Date'        => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ),
507
				'URL'         => $this->get_delivery_url(),
508
				'Duration'    => $duration,
509
				'Request'     => array(
510
					'Method'  => $request['method'],
511
					'Headers' => array_merge(
512
						array(
513
							'User-Agent' => $request['user-agent'],
514
						),
515
						$request['headers']
516
					),
517
				),
518
				'Body'        => wp_slash( $request['body'] ),
519
			),
520
		);
521
522
		// Parse response.
523
		if ( is_wp_error( $response ) ) {
524
			$response_code    = $response->get_error_code();
525
			$response_message = $response->get_error_message();
526
			$response_headers = array();
527
			$response_body    = '';
528
		} else {
529
			$response_code    = wp_remote_retrieve_response_code( $response );
530
			$response_message = wp_remote_retrieve_response_message( $response );
531
			$response_headers = wp_remote_retrieve_headers( $response );
532
			$response_body    = wp_remote_retrieve_body( $response );
533
		}
534
535
		$message['Webhook Delivery']['Response'] = array(
536
			'Code'    => $response_code,
537
			'Message' => $response_message,
538
			'Headers' => $response_headers,
539
			'Body'    => $response_body,
540
		);
541
542
		if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
543
			$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.';
544
			$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.';
545
		}
546
547
		$logger->info(
548
			wc_print_r( $message, true ),
0 ignored issues
show
Bug introduced by
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...
549
			array(
550
				'source' => 'webhooks-delivery',
551
			)
552
		);
553 3
554 3
		// Track failures.
555
		// Check for a success, which is a 2xx, 301 or 302 Response Code.
556
		if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) {
557
			$this->set_failure_count( 0 );
558
			$this->save();
559
		} else {
560
			$this->failed_delivery();
561
		}
562
	}
563
564
	/**
565
	 * Track consecutive delivery failures and automatically disable the webhook.
566
	 * if more than 5 consecutive failures occur. A failure is defined as a.
567
	 * non-2xx response.
568
	 *
569 4
	 * @since 2.2.0
570 4
	 */
571
	private function failed_delivery() {
572
		$failures = $this->get_failure_count();
573
574
		if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) {
575
			$this->set_status( 'disabled' );
576
577
			do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() );
578
		} else {
579
			$this->set_failure_count( ++$failures );
580
		}
581 1
582 1
		$this->save();
583
	}
584
585
	/**
586
	 * Get the delivery logs for this webhook.
587
	 *
588
	 * @since  3.3.0
589
	 * @return string
590
	 */
591
	public function get_delivery_logs() {
592
		return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=logs' ) ) );
593 1
	}
594 1
595
	/**
596
	 * Get the delivery log specified by the ID. The delivery log includes:
597
	 *
598
	 * + duration
599
	 * + summary
600
	 * + request method/url
601
	 * + request headers/body
602
	 * + response code/message/headers/body
603
	 *
604
	 * @since 2.2
605 4
	 * @deprecated 3.3.0
606 4
	 * @param int $delivery_id Delivery ID.
607
	 * @return void
608
	 */
609
	public function get_delivery_log( $delivery_id ) {
610
		wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' );
611
	}
612
613
	/**
614
	 * Send a test ping to the delivery URL, sent when the webhook is first created.
615
	 *
616
	 * @since  2.2.0
617 6
	 * @return bool|WP_Error
618 6
	 */
619
	public function deliver_ping() {
620
		$args = array(
621
			'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
622
			'body'       => 'webhook_id=' . $this->get_id(),
623
		);
624
625
		$test          = wp_safe_remote_post( $this->get_delivery_url(), $args );
626
		$response_code = wp_remote_retrieve_response_code( $test );
627
628
		if ( is_wp_error( $test ) ) {
629 3
			/* translators: error message */
630 3
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) );
631
		}
632
633
		if ( 200 !== $response_code ) {
634
			/* translators: error message */
635
			return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) );
636
		}
637
638
		$this->set_pending_delivery( false );
639
		$this->save();
640
641 3
		return true;
642 3
	}
643
644
	/*
645
	|--------------------------------------------------------------------------
646
	| Getters
647
	|--------------------------------------------------------------------------
648
	*/
649
650
	/**
651
	 * Get the friendly name for the webhook.
652
	 *
653 3
	 * @since  2.2.0
654 3
	 * @param  string $context What the value is for.
655
	 *                         Valid values are 'view' and 'edit'.
656 3
	 * @return string
657
	 */
658
	public function get_name( $context = 'view' ) {
659
		return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() );
660
	}
661
662
	/**
663
	 * Get the webhook status.
664
	 *
665
	 * - 'active' - delivers payload.
666
	 * - 'paused' - does not deliver payload, paused by admin.
667 3
	 * - 'disabled' - does not delivery payload, paused automatically due to consecutive failures.
668 3
	 *
669
	 * @since  2.2.0
670
	 * @param  string $context What the value is for.
671
	 *                         Valid values are 'view' and 'edit'.
672
	 * @return string status
673
	 */
674
	public function get_status( $context = 'view' ) {
675
		return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() );
676
	}
677
678
	/**
679 3
	 * Get webhopk created date.
680 3
	 *
681
	 * @since  3.2.0
682
	 * @param  string $context  What the value is for.
683
	 *                          Valid values are 'view' and 'edit'.
684
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
685
	 */
686
	public function get_date_created( $context = 'view' ) {
687
		return $this->get_prop( 'date_created', $context );
688
	}
689
690
	/**
691
	 * Get webhopk modified date.
692
	 *
693
	 * @since  3.2.0
694
	 * @param  string $context  What the value is for.
695 2
	 *                          Valid values are 'view' and 'edit'.
696 2
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
697
	 */
698
	public function get_date_modified( $context = 'view' ) {
699
		return $this->get_prop( 'date_modified', $context );
700
	}
701
702
	/**
703
	 * Get the secret used for generating the HMAC-SHA256 signature.
704
	 *
705
	 * @since  2.2.0
706
	 * @param  string $context What the value is for.
707
	 *                         Valid values are 'view' and 'edit'.
708 3
	 * @return string
709 3
	 */
710
	public function get_secret( $context = 'view' ) {
711
		return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() );
712
	}
713
714
	/**
715
	 * Get the webhook topic, e.g. `order.created`.
716
	 *
717
	 * @since  2.2.0
718
	 * @param  string $context What the value is for.
719
	 *                         Valid values are 'view' and 'edit'.
720
	 * @return string
721 1
	 */
722 1
	public function get_topic( $context = 'view' ) {
723
		return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() );
724
	}
725
726
	/**
727
	 * Get the delivery URL.
728
	 *
729
	 * @since  2.2.0
730
	 * @param  string $context What the value is for.
731 3
	 *                         Valid values are 'view' and 'edit'.
732 3
	 * @return string
733
	 */
734
	public function get_delivery_url( $context = 'view' ) {
735
		return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() );
736 3
	}
737
738
	/**
739
	 * Get the user ID for this webhook.
740
	 *
741
	 * @since  2.2.0
742
	 * @param  string $context What the value is for.
743
	 *                         Valid values are 'view' and 'edit'.
744
	 * @return int
745 2
	 */
746 2
	public function get_user_id( $context = 'view' ) {
747
		return $this->get_prop( 'user_id', $context );
748
	}
749
750
	/**
751
	 * API version.
752
	 *
753
	 * @since  3.0.0
754
	 * @param  string $context What the value is for.
755
	 *                         Valid values are 'view' and 'edit'.
756 5
	 * @return string
757 5
	 */
758
	public function get_api_version( $context = 'view' ) {
759 5
		$version = $this->get_prop( 'api_version', $context );
760
761
		return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3';
762
	}
763 5
764
	/**
765
	 * Get the failure count.
766
	 *
767
	 * @since  2.2.0
768
	 * @param  string $context What the value is for.
769
	 *                         Valid values are 'view' and 'edit'.
770
	 * @return int
771
	 */
772 2
	public function get_failure_count( $context = 'view' ) {
773 2
		return $this->get_prop( 'failure_count', $context );
774
	}
775
776
	/**
777
	 * Get pending delivery.
778
	 *
779
	 * @since  3.2.0
780
	 * @param  string $context What the value is for.
781
	 *                         Valid values are 'view' and 'edit'.
782 2
	 * @return bool
783 2
	 */
784
	public function get_pending_delivery( $context = 'view' ) {
785
		return $this->get_prop( 'pending_delivery', $context );
786
	}
787
788
	/*
789
	|--------------------------------------------------------------------------
790
	| Setters
791
	|--------------------------------------------------------------------------
792 2
	 */
793 2
794 1
	/**
795
	 * Set webhook name.
796
	 *
797 2
	 * @since 3.2.0
798
	 * @param string $name Webhook name.
799
	 */
800
	public function set_name( $name ) {
801
		$this->set_prop( 'name', $name );
802
	}
803
804
	/**
805
	 * Set webhook created date.
806 3
	 *
807 3
	 * @since 3.2.0
808
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
809
	 *                                  If the DateTime string has no timezone or offset,
810
	 *                                  WordPress site timezone will be assumed.
811
	 *                                  Null if their is no date.
812
	 */
813
	public function set_date_created( $date = null ) {
814
		$this->set_date_prop( 'date_created', $date );
815
	}
816 2
817 2
	/**
818
	 * Set webhook modified date.
819
	 *
820
	 * @since 3.2.0
821
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
822
	 *                                  If the DateTime string has no timezone or offset,
823
	 *                                  WordPress site timezone will be assumed.
824
	 *                                  Null if their is no date.
825
	 */
826
	public function set_date_modified( $date = null ) {
827
		$this->set_date_prop( 'date_modified', $date );
828
	}
829
830
	/**
831
	 * Set status.
832
	 *
833 1
	 * @since 3.2.0
834
	 * @param string $status Status.
835 1
	 */
836
	public function set_status( $status ) {
837
		if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) {
838
			$status = 'disabled';
839
		}
840
841
		$this->set_prop( 'status', $status );
842
	}
843
844
	/**
845
	 * Set the secret used for generating the HMAC-SHA256 signature.
846
	 *
847
	 * @since 2.2.0
848
	 * @param string $secret Secret.
849
	 */
850
	public function set_secret( $secret ) {
851
		$this->set_prop( 'secret', $secret );
852
	}
853
854
	/**
855
	 * Set the webhook topic and associated hooks.
856
	 * The topic resource & event are also saved separately.
857
	 *
858
	 * @since 2.2.0
859
	 * @param string $topic Webhook topic.
860
	 */
861
	public function set_topic( $topic ) {
862
		$topic = wc_clean( $topic );
863
864
		if ( ! wc_is_webhook_valid_topic( $topic ) ) {
0 ignored issues
show
Bug introduced by
It seems like $topic defined by wc_clean($topic) on line 862 can also be of type array; however, wc_is_webhook_valid_topic() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
865
			$topic = '';
866
		}
867
868
		$this->set_prop( 'topic', $topic );
869
	}
870
871
	/**
872
	 * Set the delivery URL.
873
	 *
874
	 * @since 2.2.0
875
	 * @param string $url Delivery URL.
876
	 */
877
	public function set_delivery_url( $url ) {
878
		$this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) );
879
	}
880
881
	/**
882
	 * Set user ID.
883
	 *
884
	 * @since 3.2.0
885
	 * @param int $user_id User ID.
886
	 */
887
	public function set_user_id( $user_id ) {
888
		$this->set_prop( 'user_id', (int) $user_id );
889
	}
890
891
	/**
892
	 * Set API version.
893 1
	 *
894
	 * @since 3.0.0
895 1
	 * @param int|string $version REST API version.
896
	 */
897
	public function set_api_version( $version ) {
898
		if ( ! is_numeric( $version ) ) {
899
			$version = $this->data_store->get_api_version_number( $version );
900
		}
901
902
		$this->set_prop( 'api_version', (int) $version );
903
	}
904 2
905 2
	/**
906 1
	 * Set pending delivery.
907
	 *
908 1
	 * @since 3.2.0
909
	 * @param bool $pending_delivery Set true if is pending for delivery.
910
	 */
911 2
	public function set_pending_delivery( $pending_delivery ) {
912
		$this->set_prop( 'pending_delivery', (bool) $pending_delivery );
913
	}
914
915
	/**
916
	 * Set failure count.
917
	 *
918
	 * @since 3.2.0
919
	 * @param bool $failure_count Total of failures.
920 3
	 */
921 3
	public function set_failure_count( $failure_count ) {
922
		$this->set_prop( 'failure_count', intval( $failure_count ) );
923 3
	}
924
925
	/*
926
	|--------------------------------------------------------------------------
927
	| Non-CRUD Getters
928
	|--------------------------------------------------------------------------
929
	*/
930
931
	/**
932 2
	 * Get the associated hook names for a topic.
933 2
	 *
934
	 * @since  2.2.0
935 2
	 * @param  string $topic Topic name.
936
	 * @return array
937
	 */
938
	private function get_topic_hooks( $topic ) {
939
		$topic_hooks = array(
940
			'coupon.created'   => array(
941
				'woocommerce_process_shop_coupon_meta',
942
				'woocommerce_new_coupon',
943 1
			),
944 1
			'coupon.updated'   => array(
945 1
				'woocommerce_process_shop_coupon_meta',
946
				'woocommerce_update_coupon',
947 1
			),
948
			'coupon.deleted'   => array(
949
				'wp_trash_post',
950
			),
951
			'coupon.restored'  => array(
952
				'untrashed_post',
953
			),
954
			'customer.created' => array(
955
				'user_register',
956
				'woocommerce_created_customer',
957
				'woocommerce_new_customer',
958
			),
959
			'customer.updated' => array(
960
				'profile_update',
961
				'woocommerce_update_customer',
962
			),
963
			'customer.deleted' => array(
964
				'delete_user',
965
			),
966
			'order.created'    => array(
967
				'woocommerce_new_order',
968
			),
969
			'order.updated'    => array(
970
				'woocommerce_update_order',
971
				'woocommerce_order_refunded',
972
			),
973
			'order.deleted'    => array(
974
				'wp_trash_post',
975
			),
976
			'order.restored'   => array(
977
				'untrashed_post',
978
			),
979
			'product.created'  => array(
980
				'woocommerce_process_product_meta',
981
				'woocommerce_new_product',
982
				'woocommerce_new_product_variation',
983
			),
984
			'product.updated'  => array(
985
				'woocommerce_process_product_meta',
986
				'woocommerce_update_product',
987
				'woocommerce_update_product_variation',
988
			),
989
			'product.deleted'  => array(
990
				'wp_trash_post',
991
			),
992
			'product.restored' => array(
993
				'untrashed_post',
994
			),
995
		);
996
997
		$topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );
998
999
		return isset( $topic_hooks[ $topic ] ) ? $topic_hooks[ $topic ] : array();
1000
	}
1001
1002
	/**
1003
	 * Get the hook names for the webhook.
1004
	 *
1005
	 * @since  2.2.0
1006
	 * @return array
1007
	 */
1008
	public function get_hooks() {
1009
		if ( 'action' === $this->get_resource() ) {
1010
			$hooks = array( $this->get_event() );
1011
		} else {
1012
			$hooks = $this->get_topic_hooks( $this->get_topic() );
1013
		}
1014
1015
		return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() );
1016
	}
1017
1018
	/**
1019
	 * Get the resource for the webhook, e.g. `order`.
1020
	 *
1021
	 * @since  2.2.0
1022
	 * @return string
1023
	 */
1024
	public function get_resource() {
1025
		$topic = explode( '.', $this->get_topic() );
1026
1027
		return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() );
1028
	}
1029
1030
	/**
1031
	 * Get the event for the webhook, e.g. `created`.
1032
	 *
1033
	 * @since  2.2.0
1034
	 * @return string
1035
	 */
1036
	public function get_event() {
1037
		$topic = explode( '.', $this->get_topic() );
1038
1039
		return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_id() );
1040
	}
1041
1042
	/**
1043
	 * Get the webhook i18n status.
1044
	 *
1045
	 * @return string
1046
	 */
1047
	public function get_i18n_status() {
1048
		$status   = $this->get_status();
1049
		$statuses = wc_get_webhook_statuses();
1050
1051
		return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status;
1052
	}
1053
}
1054