PluginProbe
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe / trunk
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe vtrunk
4.17.3 trunk 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.6.2 2.6.3 4.10.0 4.11.1 4.12.2 4.14.1 4.14.2 4.14.3 4.15.0 4.16.0 All 59 releases
stripe / src / Webhook / NoneReceivedNotice.php

NoneReceivedNotice.php in Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe trunk, at src/Webhook/NoneReceivedNotice.php

449 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Webhook: None received notice
4 *
5 * @package SimplePay
6 * @subpackage Core
7 * @copyright Copyright (c) 2021, WP Simple Pay, LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 4.4.1
10 */
11
12 namespace SimplePay\Core\Webhook;
13
14 use SimplePay\Core\EventManagement\SubscriberInterface;
15 use SimplePay\Core\License\LicenseAwareInterface;
16 use SimplePay\Core\License\LicenseAwareTrait;
17 use SimplePay\Core\NotificationInbox\Notification;
18 use SimplePay\Core\NotificationInbox\NotificationAwareInterface;
19 use SimplePay\Core\NotificationInbox\NotificationAwareTrait;
20 use SimplePay\Core\NotificationInbox\NotificationRepository;
21 use SimplePay\Core\Settings;
22 use SimplePay\Pro\Webhooks\Database\Query as WebhookDatabase;
23
24 /**
25 * NoneReceivedNotice class.
26 *
27 * @since 4.4.1
28 */
29 class NoneReceivedNotice implements SubscriberInterface, LicenseAwareInterface, NotificationAwareInterface {
30
31 use LicenseAwareTrait;
32 use NotificationAwareTrait;
33
34 /**
35 * The most recent event timestamp.
36 *
37 * @since 4.4.1
38 * @var int|null|false
39 */
40 private $most_recent_event = false;
41
42 /**
43 * {@inheritdoc}
44 */
45 public function get_subscribed_events() {
46 if ( true === $this->license->is_lite() ) {
47 return array();
48 }
49
50 $check_received_events = true;
51
52 /**
53 * Filters if the webhooks should be checked for received events to notify
54 * the site admin about configuration issues.
55 *
56 * @since 4.4.1
57 *
58 * @param bool $check_received_events Whether to check for received events.
59 */
60 $check_received_events = apply_filters(
61 'simpay_webhooks_check_received_events',
62 $check_received_events
63 );
64
65 $dismissed = (bool) get_option(
66 'simpay_webhook_event_expected_dismiss',
67 false
68 );
69
70 if ( false === $check_received_events || true === $dismissed ) {
71 return array();
72 }
73
74 $subscribers = array(
75 // Log when an incoming event should be expected.
76 'simpay_after_checkout_session_from_payment_form_request' =>
77 'log_expected_event',
78 'simpay_after_paymentintent_from_payment_form_request' =>
79 'log_expected_event',
80 'simpay_after_subscription_from_payment_form_request' =>
81 'log_expected_event',
82 'simpay_after_charge_from_payment_form_request' =>
83 'log_expected_event',
84
85 // Show a notice in the setting.
86 '__unstable_simpay_before_webhook_setting' =>
87 'maybe_show_setting_notice',
88
89 // Clear the "expected" flag when a user claims they have verified their settings.
90 'wpsp_transition_notification_dismissed' =>
91 array( 'clear_expected_event_flag', 10, 3 ),
92
93 // Allow permanently dismissing the notice/notification.
94 'admin_init' =>
95 array(
96 array( 'dismiss_expected_event' ),
97 ),
98 );
99
100 // Alert via Notification Inbox.
101 $subscribers['admin_init'][] = array( 'maybe_add_notification' );
102
103 // Clear the expectations when the Stripe account changes.
104 $subscribers['simpay_stripe_account_connected'] = 'reset_expectations';
105
106 return $subscribers;
107 }
108
109 /**
110 * Logs that a webhook event should be received in the near future.
111 *
112 * @since 4.4.1
113 *
114 * @return void
115 */
116 public function log_expected_event() {
117 $option_key = sprintf(
118 'simpay_webhook_event_expected_%s',
119 simpay_is_test_mode() ? 'test' : 'live'
120 );
121
122 update_option( $option_key, time() );
123 }
124
125 /**
126 * Adds a notification if an event has not been received.
127 *
128 * @since 4.4.5
129 *
130 * @return void
131 */
132 public function maybe_add_notification() {
133 // Dismiss the notification if an event was received.
134 if ( true === $this->received_expected_event() ) {
135 $this->notifications->dismiss( 'webhook-event-expected' );
136
137 return;
138 }
139
140 // Restore (or add) the notification if it was not received.
141 $this->notifications->restore(
142 array(
143 'type' => 'error',
144 'source' => 'internal',
145 'title' => __(
146 'An expected webhook event was not received.',
147 'stripe'
148 ),
149 'slug' => 'webhook-event-expected',
150 'content' => __(
151 'An expected webhook event has not been received. Please ensure you have properly configured your webhook endpoint in Stripe to avoid interruption of functionality.',
152 'stripe'
153 ),
154 'actions' => array(
155 array(
156 'type' => 'primary',
157 'text' => __( 'Webhook Settings', 'stripe' ),
158 'url' => Settings\get_url(
159 array(
160 'section' => 'stripe',
161 'subsection' => 'webhooks',
162 )
163 ),
164 ),
165 array(
166 'type' => 'secondary',
167 'text' => __( 'Learn More', 'stripe' ),
168 'url' => simpay_docs_link(
169 __( 'Learn More', 'stripe' ),
170 'webhooks',
171 'notification-inbox',
172 true
173 ),
174 ),
175 ),
176 'conditions' => array(),
177 'start' => date( 'Y-m-d H:i:s', time() ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
178 'end' => date( 'Y-m-d H:i:s', time() + YEAR_IN_SECONDS ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
179 )
180 );
181 }
182
183 /**
184 * Ooutputs a notice in the settings if no expected event was received.
185 *
186 * @since 4.4.1
187 *
188 * @return void
189 */
190 public function maybe_show_setting_notice() {
191 if ( true === $this->received_expected_event() ) {
192 return;
193 }
194
195 $docs_url = simpay_docs_link(
196 'Learn more',
197 'webhooks',
198 'webhook-settings',
199 true
200 );
201
202 $base_url = Settings\get_url(
203 array(
204 'section' => 'stripe',
205 'subsection' => 'webhooks',
206 )
207 );
208
209 $verify_url = add_query_arg(
210 array(
211 'action' => 'simpay_verify_webhook',
212 'nonce' => wp_create_nonce( 'simpay_verify_webhook' ),
213 ),
214 $base_url
215 );
216
217 $dismiss_url = add_query_arg(
218 array(
219 'action' => 'simpay_dismiss_webhook',
220 'nonce' => wp_create_nonce( 'simpay_dismiss_webhook' ),
221 ),
222 $base_url
223 );
224
225 // @todo use a ViewLoader
226 include_once SIMPLE_PAY_DIR . '/views/admin-webhooks-none-received.php';
227 }
228
229 /**
230 * Clears the expected event flag when a user dismisses the notification.
231 *
232 * This will hide the notices until the next expected event is received.
233 *
234 * @since 4.4.1
235 *
236 * @param mixed $old_value Old value.
237 * @param mixed $new_value New value.
238 * @param int $notification_id Notification ID.
239 * @return void
240 */
241 public function clear_expected_event_flag( $old_value, $new_value, $notification_id ) {
242 if ( ! current_user_can( 'manage_options' ) ) {
243 return;
244 }
245
246 $notification = $this->notifications->get( $notification_id );
247
248 if ( ! $notification instanceof Notification ) {
249 return;
250 }
251
252 // Ensure we are dismissing the relevant notification.
253 if ( 'webhook-event-expected' !== $notification->slug ) {
254 return;
255 }
256
257 if ( '0' !== $old_value ) {
258 return;
259 }
260
261 // Clear the flag.
262 $option_key = sprintf(
263 'simpay_webhook_event_expected_%s',
264 simpay_is_test_mode() ? 'test' : 'live'
265 );
266
267 delete_option( $option_key );
268 }
269
270 /**
271 * Clears webhook expectattions (and dismisses a notification).
272 *
273 * @since 4.7.0
274 *
275 * @return void
276 */
277 public function reset_expectations() {
278 // Clear the flag(s).
279 delete_option( 'simpay_webhook_event_expected_live' );
280 delete_option( 'simpay_webhook_event_expected_test' );
281
282 // Dismiss the notice.
283 $this->notifications->dismiss( 'webhook-event-expected' );
284 }
285
286 /**
287 * Permanately dismisses the "No webhooks received" notice.
288 *
289 * @since 4.4.2
290 *
291 * @return void
292 */
293 public function dismiss_expected_event() {
294 if ( ! current_user_can( 'manage_options' ) ) {
295 return;
296 }
297
298 if (
299 ! isset( $_GET['action'] ) ||
300 'simpay_dismiss_webhook' !== sanitize_text_field( $_GET['action'] )
301 ) {
302 return;
303 }
304
305 $nonce = isset( $_GET['nonce'] )
306 ? sanitize_text_field( $_GET['nonce'] )
307 : '';
308
309 if ( ! wp_verify_nonce( $nonce, 'simpay_dismiss_webhook' ) ) {
310 return;
311 }
312
313 // Clear expected event.
314 $option_key = sprintf(
315 'simpay_webhook_event_expected_%s',
316 simpay_is_test_mode() ? 'test' : 'live'
317 );
318
319 delete_option( $option_key );
320
321 // Flag permanent dismissal.
322 update_option( 'simpay_webhook_event_expected_dismiss', true );
323
324 $settings_url = Settings\get_url(
325 array(
326 'section' => 'stripe',
327 'subsection' => 'webhooks',
328 )
329 );
330
331 wp_safe_redirect( esc_url_raw( $settings_url ) );
332 exit;
333 }
334
335 /**
336 * Determines if the most recently received webhook event falls within the expected
337 * timeframe after the last time an event was expected.
338 *
339 * @since 4.1.1
340 *
341 * @return bool
342 */
343 public function received_expected_event() {
344 $timeframe = MINUTE_IN_SECONDS * 15;
345 $expected = $this->get_most_recent_expected_event();
346
347 // No event was expected, then we can't assume anything went wrong.
348 if ( null === $expected ) {
349 return true;
350 }
351
352 // The timeframe from the expected event has not passed yet, assume things will still work.
353 if ( time() < ( $expected + $timeframe ) ) {
354 return true;
355 }
356
357 // If there is no received event after the expected event log but the timeframe
358 // has passed then something went wrong.
359 $received = $this->get_most_recent_received_event();
360
361 if ( null === $received ) {
362 return false;
363 }
364
365 return $received > $expected;
366 }
367
368 /**
369 * Returns the timestamp of the latest received webhook event.
370 *
371 * @since 4.4.1
372 *
373 * @return null|int
374 */
375 private function get_most_recent_received_event() {
376 if ( false !== $this->most_recent_event ) {
377 return $this->most_recent_event;
378 }
379
380 $expected = $this->get_most_recent_expected_event();
381
382 // No event is expected so we can't use that as our starting point.
383 if ( null === $expected ) {
384 $this->most_recent_event = null;
385 return $this->most_recent_event;
386 }
387
388 // @todo Use dependency injection when there is a proper repository model.
389 $db = new WebhookDatabase();
390 $livemode = simpay_is_livemode();
391
392 $webhooks = $db->query(
393 array(
394 'number' => 1,
395 'livemode' => $livemode,
396 'date_query' => array(
397 'after' => date( 'Y-m-d H:i:s', $expected ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
398 ),
399 )
400 );
401
402 if ( empty( $webhooks ) ) {
403 $this->most_recent_event = null;
404 return $this->most_recent_event;
405 }
406
407 /** @var array<\SimplePay\Pro\Webhooks\Database\Row> $webhooks */
408
409 $created = current( $webhooks )
410 ? current( $webhooks )->date_created
411 : null;
412
413 if ( null === $created ) {
414 $this->most_recent_event = null;
415 return $this->most_recent_event;
416 }
417
418 /** @var string $created */
419
420 $this->most_recent_event = strtotime( $created ) ? strtotime( $created ) : null;
421
422 return $this->most_recent_event;
423 }
424
425 /**
426 * Returns the timestamp of when the latest event that expects a webhook event occured.
427 *
428 * @since 4.4.1
429 *
430 * @return null|int
431 */
432 private function get_most_recent_expected_event() {
433 $option_key = sprintf(
434 'simpay_webhook_event_expected_%s',
435 simpay_is_test_mode() ? 'test' : 'live'
436 );
437
438 $expected = get_option( $option_key, null );
439
440 if ( null === $expected ) {
441 return null;
442 }
443
444 /** @var string $expected */
445
446 return (int) $expected;
447 }
448 }
449