PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.3
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / square / gateway / class-charitable-square-webhook-processor.php

class-charitable-square-webhook-processor.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.3, at includes/square/gateway/class-charitable-square-webhook-processor.php

1,565 lines 52.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class responsible for processing webhooks.
4 *
5 * @package Charitable Square/Classes/Charitable_Square_Webhook_Processor
6 * @author WP Charitable
7 * @copyright Copyright (c) 2021-2022, WP Charitable
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.7
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Square_Webhook_Processor' ) ) :
18
19 /**
20 * Charitable_Square_Webhook_Processor
21 *
22 * @since 1.8.7
23 */
24 class Charitable_Square_Webhook_Processor {
25
26 /**
27 * Create instance of the class.
28 *
29 * @since 1.8.7
30 *
31 * @var Charitable_Square_Webhook_Processor
32 */
33 private static $instance;
34
35 /**
36 * Event object.
37 *
38 * @since 1.8.7
39 *
40 * @var \Square\Event
41 */
42 protected $event;
43
44 /**
45 * Gateway helper.
46 *
47 * @since 1.8.7
48 *
49 * @var Charitable_Gateway_Square
50 */
51 protected $gateway;
52
53 /**
54 * Square Event object.
55 *
56 * @deprecated
57 *
58 * @since 1.8.7
59 *
60 * @var \Square\Event
61 */
62 protected $square_event;
63
64 /**
65 * Create class object.
66 *
67 * @since 1.8.7
68 *
69 * @param \Square\Event $event The Square event object.
70 */
71 public function __construct( $event ) {
72 $this->event = $event;
73 $this->gateway = new Charitable_Gateway_Square();
74 }
75
76 /**
77 * Process an incoming Square IPN.
78 *
79 * @since 1.8.7
80 *
81 * @return void
82 */
83 public static function process() {
84
85 if ( charitable_is_debug( 'square' ) ) {
86 // phpcs:disable
87 error_log( 'Charitable_Square_Webhook_Processor PROCESS START ' );
88 // phpcs:enable
89 }
90
91 // Debug: Log that the Square Core webhook processor is being called.
92 if ( charitable_is_debug( 'square' ) ) {
93 error_log( '[Square Core] Webhook processor called' ); // phpcs:ignore
94 }
95
96 /* Retrieve and validate the request's body. */
97 $event = self::get_validated_incoming_event();
98
99 // Debug: Log the event type if it's a refund.
100 if ( charitable_is_debug( 'square' ) && $event && isset( $event['type'] ) ) {
101 // phpcs:disable
102 error_log( '[Square Core] Webhook event type: ' . $event['type'] );
103 // phpcs:enable
104 }
105
106 if ( ! $event ) {
107 status_header( 500 );
108 if ( charitable_is_debug( 'square' ) ) {
109 // phpcs:disable
110 error_log( 'Charitable_Square_Webhook_Processor PROCESS FUNCTION INVALID SQUARE EVENT - 500 return' );
111 // phpcs:enable
112 }
113 die( __( 'Invalid Square event.', 'charitable' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
114 }
115
116 try {
117 if ( charitable_is_debug( 'square' ) ) {
118 // phpcs:disable
119 error_log( 'Charitable_Square_Webhook_Processor PROCESS FUNCTION TRY' );
120 error_log( 'the event is ' . print_r( $event, true ) );
121 // phpcs:enable
122 }
123
124 $event = self::construct_from_event( $event );
125
126 } catch ( \UnexpectedValueException $e ) {
127
128 status_header( 400 );
129 if ( charitable_is_debug( 'square' ) ) {
130 // phpcs:disable
131 error_log( 'Charitable_Square_Webhook_Processor PROCESS FUNCTION TRY CATCH' );
132 error_log( 'the event is ' . print_r( $event, true ) );
133 // phpcs:enable
134 }
135 die( __( 'Unable to construct Square object with payload.', 'charitable' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
136
137 }
138
139 $processor = new Charitable_Square_Webhook_Processor( $event );
140
141 if ( charitable_is_debug( 'square' ) ) {
142 // phpcs:disable
143 error_log( 'Charitable_Square_Webhook_Processor PROCESS FUNCTION RUN' );
144 // phpcs:enable
145 }
146
147 $processor->run();
148 }
149
150 /**
151 * This parses the returning event from Square and sets the event property.
152 * Under review if it's needed.
153 *
154 * @since 1.8.7
155 *
156 * @param string $event Decoded JSON object from Square.
157 */
158 public static function construct_from_event( $event ) {
159 return $event;
160 }
161
162 /**
163 * Run the processor.
164 *
165 * @since 1.8.7
166 *
167 * @return void
168 */
169 public function run() {
170 $this->set_square_api_key();
171
172 try {
173 status_header( 200 );
174 $this->run_event_processors();
175
176 die( __( 'Webhook processed.', 'charitable' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
177 } catch ( Exception $e ) {
178 if ( charitable_is_debug( 'square' ) ) {
179 // phpcs:disable
180 error_log( $e->getMessage() );
181 // phpcs:enable
182 }
183 status_header( 500 );
184
185 die( __( 'Error while retrieving event.', 'charitable' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
186 } //end try
187 }
188
189 /**
190 * Set Square API key, which is actually the access token.
191 *
192 * @since 1.8.7
193 *
194 * @return boolean True if the API key is set. False otherwise.
195 */
196 public function set_square_api_key() {
197 $keys = $this->gateway->get_keys( charitable_get_option( 'test_mode' ) );
198
199 if ( empty( $keys['access_token'] ) ) {
200 return false;
201 }
202
203 return $this->gateway->setup_api( $keys['access_token'] );
204 }
205
206 /**
207 * Get the account ID for the site.
208 *
209 * @since 1.8.7
210 *
211 * @return string|null Account ID if successfull. Null if the account couldn't be retrieved from Square.
212 */
213 public function get_site_account_id() {
214 $account_id = $this->gateway->get_account_id( 'account_id' );
215
216 if ( empty( $account_id ) ) {
217 if ( charitable_is_debug( 'square' ) ) {
218 // phpcs:disable
219 error_log( 'get_site_account_id - no account id found' );
220 // phpcs:enable
221 }
222 }
223
224 return $account_id;
225 }
226
227 /**
228 * Checks whether the current event is from a Connect webhook and
229 * is for a transaction taking place directly on a connected account.
230 *
231 * @since 1.4.0
232 *
233 * @return boolean
234 */
235 public function is_connect_webhook_for_connected_account() {
236 return isset( $this->event->account ) && $this->get_site_account_id() != $this->event->account;
237 }
238
239 /**
240 * Get the options array to pass when retrieving the event from Square.
241 *
242 * @since 1.8.7
243 *
244 * @return array
245 */
246 public function get_options() {
247 if ( isset( $this->event->account ) ) {
248 return array(
249 'square_account' => $this->event->account,
250 );
251 }
252
253 return array();
254 }
255
256 /**
257 * Sets up any default event processors.
258 *
259 * @since 1.8.7
260 *
261 * @return void
262 */
263 public function run_event_processors() {
264 /**
265 * Default event processors.
266 *
267 * @since 1.8.7
268 *
269 * @param array $processors Array of Square event types and associated callback functions.
270 */
271 $default_processors = apply_filters(
272 'charitable_square_default_event_processors',
273 array(
274 'refund.updated' => array( $this, 'process_refund' ),
275 'payment.created' => array( $this, 'process_payment_created' ),
276 'payment.updated' => array( $this, 'process_payment_updated' ),
277 'subscription.created' => array( $this, 'process_subscription_created' ),
278 'subscription.updated' => array( $this, 'process_subscription_updated' ),
279 'invoice.payment_made' => array( $this, 'process_invoice_payment_made' ),
280 )
281 );
282
283 // Critical items.
284 $event_type = $this->event['type']; // example: payment.updated.
285 $event_id = $this->event['event_id']; // example: 6a8f5f28-54a1-4eb0-a98a-3111513fd4fc.
286 $merchant_id = $this->event['merchant_id']; // example: 1234567890.
287 $event_created_at = $this->event['created_at']; // example: 2020-02-06T21:27:34.308Z.
288 $event_data_type = $this->event['data']['type']; // example: payment.
289 $event_data_id = $this->event['data']['id']; // example: hYy9pRFVxpDsO1FB05SunFWUe9JZY.
290
291 if ( charitable_is_debug( 'square' ) ) {
292 // phpcs:disable
293 error_log( 'run_event_processors' );
294 error_log( print_r( $default_processors, true ) );
295 error_log( 'event type' );
296 error_log( print_r( $this->event['type'], true ) );
297 // phpcs:enable
298 }
299
300 /* Check if this event can be handled by one of our built-in event processors. */
301 if ( array_key_exists( $this->event['type'], $default_processors ) ) {
302
303 if ( charitable_is_debug( 'square' ) ) {
304 error_log( 'array_key_exists' ); // phpcs:ignore
305 }
306
307 /**
308 * Double-check that this isn't a legacy Square webhook.
309 *
310 * @since 1.8.7
311 */
312 if ( ! charitable_square_legacy_mode() ) {
313
314 if ( charitable_is_debug( 'square' ) ) {
315 error_log( 'double checked this is not a legacy webhook' ); // phpcs:ignore
316 }
317
318 $message = call_user_func( $default_processors[ $this->event['type'] ], $this->event );
319
320 /* Kill processing with a message returned by the event processor. */
321 die( $message ); // phpcs:ignore
322 }
323 }
324
325 /**
326 * Fire an action hook to process the event.
327 *
328 * Note that this will only fire for webhooks that have not already been processed by one
329 * of the default webhook handlers above.
330 *
331 * @since 1.0.0
332 *
333 * @param string $event_type Type of event.
334 * @param \Square\Event $event Square event object.
335 */
336 if ( charitable_is_debug( 'square' ) ) {
337 // phpcs:disable
338 error_log( 'before charitable_square_ipn_event' );
339 // phpcs:enable
340 }
341
342 do_action( 'charitable_square_ipn_event', $this->event['type'], $this->event );
343
344 if ( charitable_is_debug( 'square' ) ) {
345 // phpcs:disable
346 error_log( 'after charitable_square_ipn_event' );
347 // phpcs:enable
348 }
349 }
350
351 /**
352 * Process the payment_created webhook.
353 * This isn't used in the current implementation.
354 *
355 * @see https://developer.squareup.com/reference/square/payments-api/webhooks/payment.created
356 * @since 1.8.7
357 *
358 * @param object $event The Square event object.
359 */
360 public function process_payment_created( $event ) {
361 if ( charitable_is_debug( 'square' ) ) {
362 // phpcs:disable
363 error_log( 'process_payment_created - starting' );
364 error_log( print_r( $event, true ) );
365 // phpcs:enable
366 }
367 }
368
369 /**
370 * Process the invoice_payment_made webhook.
371 *
372 * @see https://developer.squareup.com/reference/square/invoices-api/webhooks/invoice.payment_made
373 * @since 1.8.7
374 *
375 * @param object $event The Square event object.
376 * @return string Response message
377 */
378 public function process_invoice_payment_made( $event ) {
379 global $wpdb;
380
381 if ( charitable_is_debug( 'square' ) ) {
382 // phpcs:disable
383 error_log( 'process_invoice_payment_made - starting' );
384 error_log( print_r( $event, true ) );
385 // phpcs:enable
386 }
387 $object = $event['data']['object']['invoice'];
388
389 $order_id = $object['order_id'];
390 $subscription_id = $object['subscription_id'];
391 $status = $object['status']; // 'PAID'
392 $invoice_number = $object['invoice_number'];
393 $invoice_id = $object['id'];
394 $email = $object['primary_recipient']['email_address'];
395 $first_name = $object['primary_recipient']['given_name'];
396 $last_name = $object['primary_recipient']['family_name'];
397
398 // We need to find the recurring donation with the subscription_id.
399 $recurring_donation_id = $wpdb->get_col( // phpcs:ignore
400 $wpdb->prepare(
401 "SELECT post_id
402 FROM $wpdb->postmeta
403 WHERE meta_key = '_gateway_subscription_id'
404 AND meta_value = %s
405 LIMIT 1",
406 $subscription_id
407 )
408 );
409 // If any exist, get the first one. Otherwise, return an error.
410 if ( count( $recurring_donation_id ) > 0 ) {
411 $recurring_donation_id = $recurring_donation_id[0];
412 } else {
413 if ( charitable_is_debug( 'square' ) ) {
414 // phpcs:disable
415 error_log( 'process_invoice_payment_made - no recurring donation found for subscription_id: ' . $subscription_id );
416 error_log( print_r( $recurring_donation_id, true ) );
417 // phpcs:enable
418 }
419 return __( 'Invoice Webhook: No recurring donation found for subscription ID', 'charitable' );
420 }
421
422 // Get the recurring donation object.
423 $recurring_donation = charitable_get_donation( $recurring_donation_id );
424
425 // If the recurring donation is not found, return an error.
426 if ( ! $recurring_donation ) {
427 if ( charitable_is_debug( 'square' ) ) {
428 // phpcs:disable
429 error_log( 'process_invoice_payment_made - no recurring donation found for subscription_id: ' . $subscription_id );
430 // phpcs:enable
431 }
432 return __( 'Invoice Webhook: No recurring donation found for subscription ID', 'charitable' );
433 }
434
435 if ( charitable_is_debug( 'square' ) ) {
436 // phpcs:disable
437 error_log( 'recurring donation found' );
438 error_log( print_r( $recurring_donation, true ) );
439 // phpcs:enable
440 }
441
442 // If the recurring donation is not active, return an error.
443 if ( 'charitable-active' !== $recurring_donation->get_status() ) {
444 if ( charitable_is_debug( 'square' ) ) {
445 // phpcs:disable
446 error_log( 'process_invoice_payment_made - recurring donation is not active' );
447 // phpcs:enable
448 }
449 return __( 'Invoice Webhook: Recurring donation is not active', 'charitable' );
450 }
451
452 // Get the information so we can create a new one-time donation that has a post parent of the recurring donation.
453 // Updated in 1.8.8.4 to use the new method.
454 $amount = $this->get_donation_amount_with_fees( $recurring_donation );
455 $donations = $recurring_donation->get_campaign_donations();
456
457 if ( charitable_is_debug( 'square' ) ) {
458 // phpcs:disable
459 error_log( 'process_invoice_payment_made - donations' );
460 error_log( print_r( $donations, true ) );
461 // phpcs:enable
462 }
463
464 $amount = $donations[0]->amount;
465 $campaign_id = $donations[0]->campaign_id;
466 $campaign_name = $donations[0]->campaign_name;
467 $donor_id = $recurring_donation->get_donor_id();
468
469 if ( charitable_is_debug( 'square' ) ) {
470 // phpcs:disable
471 error_log( 'process_invoice_payment_made - donor_id' );
472 error_log( print_r( $donor_id, true ) );
473 // phpcs:enable
474 }
475
476 // We need to see if this is the first time a donation has been made for this subscription.
477 // Because that means in Charitable, there is an initial (pending) donation that needs to be completed.
478 $first_donation = $wpdb->get_col( // phpcs:ignore
479 $wpdb->prepare(
480 "SELECT post_id
481 FROM $wpdb->postmeta
482 WHERE meta_key = '_square_subscription_donation_first_donation'
483 AND meta_value = %d
484 LIMIT 1",
485 $recurring_donation_id
486 )
487 );
488 if ( count( $first_donation ) > 0 ) {
489 // First donation exists! Update!
490
491 $first_donation_id = $first_donation[0];
492 $first_donation = charitable_get_donation( $first_donation_id );
493
494 // This donation needs to be completed.
495 $first_donation->update_status( 'charitable-completed' );
496
497 if ( $order_id ) {
498 $first_donation->set_gateway_transaction_id( $order_id );
499 $message = sprintf( '%s: %s', __( 'Square Order / Transaction ID processor1', 'charitable' ), $order_id );
500 $first_donation->log()->add( $message );
501 }
502
503 if ( $invoice_id && $invoice_number ) {
504 $message = sprintf( '%s: %s', __( 'Square Invoice ID and Number', 'charitable' ), $invoice_id . ' - ' . $invoice_number );
505 $first_donation->log()->add( $message );
506 }
507
508 // We need to delete the meta key so that we don't do this again.
509 delete_post_meta( $first_donation_id, '_square_subscription_donation_first_donation' );
510
511 if ( charitable_is_debug( 'square' ) ) {
512 // phpcs:disable
513 error_log( 'donation upgraded to completed' );
514 error_log( print_r( $first_donation, true ) );
515 // phpcs:enable
516 }
517 } else {
518 // No first donation exists! Create!
519
520 // Translate Square status to Charitable status.
521 switch ( $status ) {
522 case 'PAID':
523 $status = 'charitable-completed';
524 break;
525 case 'FAILED':
526 $status = 'charitable-failed';
527 break;
528 default:
529 $status = 'charitable-pending';
530 break;
531 }
532
533 // Create the new donation.
534 $new_donation_id = charitable_create_donation(
535 array(
536 'status' => $status,
537 'gateway' => 'square',
538 'note' => '',
539 'campaigns' => array(
540 array(
541 'campaign_id' => $campaign_id,
542 'amount' => $amount,
543 ),
544 ),
545 'user' => array(
546 'first_name' => $first_name,
547 'last_name' => $last_name,
548 'email' => $email,
549 ),
550 )
551 );
552
553 $new_donation = new Charitable_Donation( $new_donation_id );
554
555 // Update post parent of the new donation to the recurring donation.
556 // Use WP functions to update post parent.
557 wp_update_post(
558 array(
559 'ID' => $new_donation_id,
560 'post_parent' => $recurring_donation_id,
561 )
562 );
563
564 // Set the gateway subscription id of the new donation to the subscription_id.
565 $new_donation->set_gateway_subscription_id( $subscription_id );
566
567 if ( $order_id ) {
568 $new_donation->set_gateway_transaction_id( $order_id );
569 $message = sprintf( '%s: %s', __( 'Square Order / Transaction ID processor2', 'charitable' ), $order_id );
570 $new_donation->log()->add( $message );
571 }
572
573 if ( $invoice_id && $invoice_number ) {
574 $message = sprintf( '%s: %s', __( 'Square Invoice ID and Number', 'charitable' ), $invoice_id . ' - ' . $invoice_number );
575 $new_donation->log()->add( $message );
576 }
577
578 if ( charitable_is_debug( 'square' ) ) {
579 // phpcs:disable
580 error_log( 'new donation created' );
581 error_log( print_r( $new_donation, true ) );
582 // phpcs:enable
583 }
584 }
585
586 return __( 'Donation Webhook: Invoice payment received', 'charitable' );
587 }
588
589 /**
590 * Process the payment_updated webhook.
591 *
592 * @see https://developer.squareup.com/reference/square/refunds-api/webhooks/payment.updated
593 * @since 1.8.7
594 *
595 * @param object $event The Square event object.
596 * @return string Response message
597 */
598 public function process_payment_updated( $event ) {
599 if ( charitable_is_debug( 'square' ) ) {
600 // phpcs:disable
601 error_log( 'process_payment_updated - starting' );
602 // phpcs:enable
603 }
604
605 $charge = (array) $event['data']['object']['payment'];
606
607 if ( ! isset( $charge['order_id'] ) ) {
608 if ( charitable_is_debug( 'square' ) ) {
609 // phpcs:disable
610 error_log( 'Square Webhook - No order ID found... here is the charge' );
611 error_log( print_r( $charge, true ) );
612 error_log( 'Square Webhook - No order ID found... here is the event' );
613 error_log( print_r( $event, true ) );
614 // phpcs:enable
615 }
616 return __( 'Donation Webhook: Missing order ID', 'charitable' );
617 }
618
619 $order_id = $charge['order_id'];
620 $customer_id = isset( $charge['customer_id'] ) ? $charge['customer_id'] : '';
621 if ( empty( $customer_id ) ) {
622 if ( charitable_is_debug( 'square' ) ) {
623 // phpcs:disable
624 error_log( 'Square Webhook - No customer ID found... here is the charge' );
625 error_log( print_r( $charge, true ) );
626 // phpcs:enable
627 }
628 }
629 $donation_id = Charitable_Square_Gateway_Processor::get_donation_with_gateway_transaction_id( $order_id );
630
631 if ( ! $donation_id ) {
632 if ( charitable_is_debug( 'square' ) ) {
633 // phpcs:disable
634 error_log( 'Square Webhook - No donation found for order ID: ' . $order_id );
635 error_log( 'Square Webhook - Going to try to find the donation with the customer ID: ' . $customer_id );
636 // phpcs:enable
637 }
638 // If this is a recurring donation, we have to find the last donation made by the customer (using the customer_id).
639 // This is likely the donation that created the subscription. Once we have that, we can add the order_id (also known as the transaction_id) to this single donation for future reference.
640 $donation_id = Charitable_Square_Gateway_Processor::get_donation_with_gateway_customer_id( $customer_id );
641 if ( ! $donation_id ) {
642 if ( charitable_is_debug( 'square' ) ) {
643 // phpcs:disable
644 error_log( 'Square Webhook - No donation found for order ID: ' . $order_id );
645 // phpcs:enable
646 }
647 return __( 'Donation Webhook: No donation found for order ID', 'charitable' );
648 } elseif ( charitable_is_debug( 'square' ) ) {
649 // phpcs:disable
650 error_log( 'Square Webhook - Found donation for customer ID: ' . $customer_id );
651 // phpcs:enable
652
653 }
654 }
655
656 $donation = new Charitable_Donation( $donation_id );
657
658 // If there isn't a gateway transaction id (order_id), we need to add it to the donation.
659 if ( ! $donation->get_gateway_transaction_id() ) {
660 if ( charitable_is_debug( 'square' ) ) {
661 // phpcs:disable
662 error_log( 'Square Webhook - Adding gateway transaction ID to donation (LIKELY A RECURRING DONATION): ' . $order_id );
663 // phpcs:enable
664 }
665 $donation->set_gateway_transaction_id( $order_id );
666 }
667
668 if ( ! $donation ) {
669 if ( charitable_is_debug( 'square' ) ) {
670 // phpcs:disable
671 error_log( 'Square Webhook - No donation found for donation ID: ' . $donation_id );
672 // phpcs:enable
673 }
674 return __( 'Donation Webhook: No donation found for donation ID', 'charitable' );
675 }
676
677 $payment_data = array(
678 'payment_status' => ! empty( $charge['status'] ) ? $charge['status'] : 'unknown',
679 'payment_id' => ! empty( $charge['id'] ) ? $charge['id'] : '',
680 'card_details_status' => ! empty( $charge['card_details']['status'] ) ? $charge['card_details']['status'] : '',
681 'receipt_url' => ! empty( $charge['receipt_url'] ) ? $charge['receipt_url'] : '',
682 );
683
684 if ( $charge['status'] === 'unknown' || $charge['receipt_url'] === '' ) {
685 if ( charitable_is_debug( 'square' ) ) {
686 // phpcs:disable
687 error_log( 'Square Webhook - Unknown payment status or receipt URL' );
688 error_log( print_r( $charge, true ) );
689 error_log( 'Square Webhook - Unknown payment status or receipt URL' );
690 error_log( print_r( $event, true ) );
691 // phpcs:enable
692 }
693 return __( 'Donation Webhook: Unknown payment status or receipt URL', 'charitable' );
694 }
695
696 /* Update the donation log. */
697 $log = new Charitable_Square_Donation_Log( $donation );
698
699 if ( charitable_is_debug( 'square' ) ) {
700 // phpcs:disable
701 error_log( 'made it to where we update the status of the donation to ' . $payment_data['payment_status'] );
702 error_log( 'the donation has a status of ' . $donation->get_status() . ' and the payment status is ' . strtolower( $payment_data['payment_status'] ) );
703 // phpcs:enable
704 }
705
706 $current_donation_status = $donation->get_status();
707
708 // If this is a refunded donation, we don't need to do anything.
709 if ( ! empty( $charge['refunded_money'] ) && ! empty( $charge['refunded_money']['amount'] ) ) {
710 if ( charitable_is_debug( 'square' ) ) {
711 // phpcs:disable
712 error_log( 'process_payment_updated - Refunded donation' );
713 error_log( print_r( $charge, true ) );
714 // phpcs:enable
715 }
716 return __( 'Donation Webhook: Payment ignored (Refunded donation)', 'charitable' );
717 }
718
719 // Test the overall payment transaction status.
720 switch ( strtolower( $payment_data['payment_status'] ) ) {
721 // Payment completed successfully.
722 case 'completed':
723 if ( charitable_is_debug( 'square' ) ) {
724 // phpcs:disable
725 error_log( 'process_payment_updated - card details status ' . strtolower( $payment_data['card_details_status'] ) );
726 // phpcs:enable
727 }
728 switch ( strtolower( $payment_data['card_details_status'] ) ) {
729 case 'authorized':
730 if ( charitable_is_debug( 'square' ) ) {
731 // phpcs:disable
732 error_log( 'process_payment_updated - authorized' );
733 // phpcs:enable
734 }
735 // Funds are held but not captured.
736 if ( 'charitable-pending' !== $current_donation_status ) {
737 $donation->update_status( 'charitable-pending' );
738 $log->log_payment_update_details( $payment_data, $charge );
739 if ( charitable_is_debug( 'square' ) ) {
740 // phpcs:disable
741 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
742 // phpcs:enable
743 }
744 } elseif ( charitable_is_debug( 'square' ) ) {
745 // phpcs:disable
746 error_log( 'the donation already has a status of charitable-pending' );
747 // phpcs:enable
748
749 }
750 break;
751 case 'captured':
752 if ( charitable_is_debug( 'square' ) ) {
753 // phpcs:disable
754 error_log( 'process_payment_updated - captured' );
755 // phpcs:enable
756 }
757 // The payment has fully completed its lifecycle
758 // The funds have been successfully captured from the card. ALL GOOD!
759 if ( 'charitable-completed' !== $current_donation_status ) {
760 $donation->update_status( 'charitable-completed' );
761 $log->log_payment_update_details( $payment_data, $charge );
762 if ( charitable_is_debug( 'square' ) ) {
763 // phpcs:disable
764 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
765 // phpcs:enable
766 }
767 } elseif ( charitable_is_debug( 'square' ) ) {
768 // phpcs:disable
769 error_log( 'the donation already has a status of charitable-completed' );
770 // phpcs:enable
771
772 }
773 break;
774 case 'failed':
775 if ( charitable_is_debug( 'square' ) ) {
776 // phpcs:disable
777 error_log( 'process_payment_updated - failed' );
778 // phpcs:enable
779 }
780 // Card processing failed.
781 if ( 'charitable-failed' !== $current_donation_status ) {
782 $donation->update_status( 'charitable-failed' );
783 $log->log_payment_update_details( $payment_data, $charge );
784 if ( charitable_is_debug( 'square' ) ) {
785 // phpcs:disable
786 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
787 // phpcs:enable
788 }
789 } elseif ( charitable_is_debug( 'square' ) ) {
790 // phpcs:disable
791 error_log( 'the donation already has a status of charitable-failed' );
792 // phpcs:enable
793
794 }
795 break;
796 case 'voided':
797 if ( charitable_is_debug( 'square' ) ) {
798 // phpcs:disable
799 error_log( 'process_payment_updated - voided' );
800 // phpcs:enable
801 }
802 // The payment was voided.
803 if ( 'charitable-failed' !== $current_donation_status ) {
804 $donation->update_status( 'charitable-failed' );
805 $log->log_payment_update_details( $payment_data, $charge );
806 if ( charitable_is_debug( 'square' ) ) {
807 // phpcs:disable
808 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
809 // phpcs:enable
810 }
811 } elseif ( charitable_is_debug( 'square' ) ) {
812 // phpcs:disable
813 error_log( 'the donation already has a status of charitable-failed' );
814 // phpcs:enable
815
816 }
817 break;
818 default:
819 // The card details status is unknown.
820 $log->log_payment_update_details( $payment_data, $charge );
821 if ( charitable_is_debug( 'square' ) ) {
822 // phpcs:disable
823 error_log( 'process_payment_updated - could not do anything with this payment status: ' . $payment_data['payment_status'] );
824 // phpcs:enable
825 }
826 break;
827 }
828 break;
829 // Payment was canceled.
830 case 'canceled':
831 case 'failed':
832 if ( 'charitable-failed' !== $current_donation_status ) {
833 $donation->update_status( 'charitable-failed' );
834 $log->log_payment_update_details( $payment_data, $charge );
835 if ( charitable_is_debug( 'square' ) ) {
836 // phpcs:disable
837 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
838 // phpcs:enable
839 }
840 } elseif ( charitable_is_debug( 'square' ) ) {
841 // phpcs:disable
842 error_log( 'the donation already has a status of charitable-failed' );
843 // phpcs:enable
844
845 }
846 break;
847 // Payment approved but not yet completed.
848 case 'approved':
849 if ( 'charitable-pending' !== $current_donation_status ) {
850 $donation->update_status( 'charitable-pending' );
851 $log->log_payment_update_details( $payment_data, $charge );
852 if ( charitable_is_debug( 'square' ) ) {
853 // phpcs:disable
854 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
855 // phpcs:enable
856 }
857 } elseif ( charitable_is_debug( 'square' ) ) {
858 // phpcs:disable
859 error_log( 'the donation already has a status of charitable-pending' );
860 // phpcs:enable
861
862 }
863 break;
864 // Payment is pending.
865 case 'pending':
866 if ( 'charitable-pending' !== $current_donation_status ) {
867 $donation->update_status( 'charitable-pending' );
868 $log->log_payment_update_details( $payment_data, $charge );
869 if ( charitable_is_debug( 'square' ) ) {
870 // phpcs:disable
871 error_log( 'the donation has changed to a status of ' . $donation->get_status() );
872 // phpcs:enable
873 }
874 } elseif ( charitable_is_debug( 'square' ) ) {
875 // phpcs:disable
876 error_log( 'the donation already has a status of charitable-pending' );
877 // phpcs:enable
878
879 }
880 break;
881 default:
882 if ( charitable_is_debug( 'square' ) ) {
883 // phpcs:disable
884 error_log( 'process_payment_updated - could not do anything with this payment status: ' . $payment_data['payment_status'] );
885 // phpcs:enable
886 }
887 break;
888 }
889
890 return __( 'Donation Webhook: Payment updated', 'charitable' );
891 }
892
893 /**
894 * Process a refund initiated via the Square dashboard.
895 *
896 * @see https://developer.squareup.com/docs/refunds-api/webhooks
897 * Example webhook payload for refund.created event:
898 * Example webhook payload for refund.created event:
899 * {
900 * "merchant_id": "6SSW7HV8K2ST5",
901 * "type": "refund.updated",
902 * "event_id": "bc316346-6691-4243-88ed-6d651a0d0c47",
903 * "created_at": "2020-02-06T22:14:16.421Z",
904 * "data": {
905 * "type": "refund",
906 * "id": "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P",
907 * "object": {
908 * "refund": {
909 * "id": "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P",
910 * "created_at": "2020-02-06T21:27:41.836Z",
911 * "updated_at": "2020-02-06T22:14:16.381Z",
912 * "amount_money": {
913 * "amount": 1000,
914 * "currency": "USD"
915 * },
916 * "status": "COMPLETED",
917 * "processing_fee": [
918 * {
919 * "effective_at": "2020-02-06T23:27:31.000Z",
920 * "type": "INITIAL",
921 * "amount_money": {
922 * "amount": -59,
923 * "currency": "USD"
924 * }
925 * }
926 * ],
927 * "location_id": "NAQ1FHV6ZJ8YV",
928 * "order_id": "haOyDuHiqtAXMk0d8pDKXpL7Jg4F",
929 * "payment_id": "KkAkhdMsgzn59SM8A89WgKwekxLZY",
930 * "version": 10
931 * }
932 * }
933 * }
934 * }
935 *
936 * @since 1.8.7
937 *
938 * @param object $event The Square event object.
939 * @return string Response message
940 */
941 public function process_refund( $event ) {
942 global $wpdb;
943 $charge = (array) $event['data']['object'];
944
945 /**
946 * If we're missing a donation ID, stop processing.
947 * This probably isn't a Charitable payment.
948 */
949 if ( ! isset( $charge['refund']['id'] ) ) {
950 return __( 'Donation Webhook: Missing transaction ID', 'charitable' );
951 }
952
953 if ( charitable_is_debug( 'square' ) ) {
954 // phpcs:disable
955 error_log( 'Square Webhook - Processing refund' );
956 // phpcs:enable
957 }
958
959 $transaction_id = $charge['refund']['id'];
960 if ( charitable_is_debug( 'square' ) ) {
961 // phpcs:disable
962 error_log( 'Square Webhook - Transaction ID: ' . $transaction_id );
963 // phpcs:enable
964 }
965
966 $refund_amount = $charge['refund']['amount_money']['amount'];
967 $refund_currency = $charge['refund']['amount_money']['currency'];
968
969 // Square sends amounts in the smallest currency unit (cents for
970 // USD, yen for JPY, etc.). process_refund() expects the site's
971 // display unit, so divide by 100 unless this is a zero-decimal
972 // currency.
973 if ( ! Charitable_Square_Gateway_Processor::is_zero_decimal_currency( $refund_currency ) ) {
974 $refund_amount = $refund_amount / 100;
975 }
976
977 if ( charitable_is_debug( 'square' ) ) {
978 // phpcs:disable
979 error_log( 'Square Webhook - Refund Amount: ' . $refund_amount );
980 // phpcs:enable
981 }
982
983 $refund_status = $charge['refund']['status']; // should be COMPLETED.
984 if ( charitable_is_debug( 'square' ) ) {
985 // phpcs:disable
986 error_log( 'Square Webhook - Refund Status: ' . $refund_status );
987 // phpcs:enable
988 }
989
990 $order_id = $charge['refund']['order_id'];
991 if ( charitable_is_debug( 'square' ) ) {
992 // phpcs:disable
993 error_log( 'Square Webhook - Order ID: ' . $order_id );
994 // phpcs:enable
995 }
996
997 $payment_id = $charge['refund']['payment_id'];
998 if ( charitable_is_debug( 'square' ) ) {
999 // phpcs:disable
1000 error_log( 'Square Webhook - Payment ID: ' . $payment_id );
1001 // phpcs:enable
1002 }
1003
1004 // We need to search post meta to find a _gateway_payment_id that matches the payment_id.
1005 $sql = $wpdb->prepare(
1006 "SELECT post_id
1007 FROM $wpdb->postmeta
1008 WHERE meta_key = '_gateway_payment_id'
1009 AND meta_value = %s",
1010 $payment_id
1011 );
1012
1013 $donation_id = $wpdb->get_col( $sql ); // phpcs:ignore
1014
1015 if ( charitable_is_debug( 'square' ) ) {
1016 // phpcs:disable
1017 error_log( '[Square Core] Webhook - Donation ID: ' . print_r( $donation_id, true ) );
1018 // phpcs:enable
1019 }
1020
1021 if ( empty( $donation_id ) ) {
1022 if ( charitable_is_debug( 'square' ) ) {
1023 // phpcs:disable
1024 error_log( '[Square Core] Webhook - No donation found for payment ID: ' . $payment_id );
1025 // phpcs:enable
1026 }
1027 return __( 'Donation Webhook: Refund processed', 'charitable' );
1028 }
1029
1030 $donation = charitable_get_donation( $donation_id[0] );
1031
1032 if ( charitable_is_debug( 'square' ) ) {
1033 // phpcs:disable
1034 error_log( '[Square Core] Webhook - Donation found: ' . print_r( $donation, true ) );
1035 // phpcs:enable
1036 }
1037
1038 $donation->process_refund( $refund_amount, __( 'Donation refunded from the Square dashboard.', 'charitable' ) );
1039
1040 return __( 'Donation Webhook: Refund processed', 'charitable' );
1041 }
1042
1043 /**
1044 * Process the subscription.created webhook.
1045 *
1046 * @since 1.8.7
1047 *
1048 * @param object $event The Square event object.
1049 * @return string Response message
1050 */
1051 public function process_subscription_created( $event ) {
1052 if ( ! $this->is_recurring_installed() ) {
1053 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable' );
1054 }
1055
1056 if ( charitable_is_debug( 'square' ) ) {
1057 // phpcs:disable
1058 error_log( 'process_subscription_created - starting' );
1059 error_log( print_r( $event, true ) );
1060 // phpcs:enable
1061 }
1062
1063 $object = $event['data']['object'];
1064 $subscription = $this->get_subscription_for_webhook_object( $object['subscription'] );
1065
1066 if ( charitable_is_debug( 'square' ) ) {
1067 // phpcs:disable
1068 error_log( 'process_subscription_created - subscription' );
1069 error_log( print_r( $subscription, true ) );
1070 error_log( 'process_subscription_created - object' );
1071 error_log( print_r( $object, true ) );
1072 // phpcs:enable
1073 }
1074
1075 if ( empty( $subscription ) ) {
1076 return __( 'Subscription Webhook: Missing subscription', 'charitable' );
1077 }
1078
1079 $square_status = $this->get_subscription_status( $object['subscription']['status'] );
1080 $current_status = $subscription->get_status();
1081
1082 if ( charitable_is_debug( 'square' ) ) {
1083 // phpcs:disable
1084 error_log( 'process_subscription_created - square_status' );
1085 error_log( print_r( $square_status, true ) );
1086 error_log( 'process_subscription_created - current_status' );
1087 error_log( print_r( $current_status, true ) );
1088 // phpcs:enable
1089 }
1090
1091 if ( $square_status !== $current_status && 'charitable-completed' !== $current_status ) {
1092 $subscription->update_status( $square_status );
1093 if ( charitable_is_debug( 'square' ) ) {
1094 // phpcs:disable
1095 error_log( 'process_subscription_created - updated the status of the subscription to ' . $square_status );
1096 // phpcs:enable
1097 }
1098 }
1099
1100 return __( 'Subscription Webhook: Recurring donation updated', 'charitable' );
1101 }
1102
1103 /**
1104 * Process the subscription.updated webhook.
1105 *
1106 * @since 1.8.7
1107 *
1108 * @param object $event The Square event object.
1109 * @return string Response message
1110 */
1111 public function process_subscription_updated( $event ) {
1112 global $wpdb;
1113
1114 if ( ! $this->is_recurring_installed() ) {
1115 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable' );
1116 }
1117
1118 if ( charitable_is_debug( 'square' ) ) {
1119 // phpcs:disable
1120 error_log( 'process_subscription_updated - incoming event' );
1121 error_log( print_r( $event, true ) );
1122 // phpcs:enable
1123 }
1124
1125 $object = $event['data']['object'];
1126
1127 // Early return if subscription data is missing.
1128 if ( empty( $object['subscription'] ) ) {
1129 if ( charitable_is_debug( 'square' ) ) {
1130 // phpcs:disable
1131 error_log( 'process_subscription_updated - Missing subscription data in webhook' );
1132 // phpcs:enable
1133 }
1134 return __( 'Subscription Webhook: Missing subscription data', 'charitable' );
1135 }
1136
1137 $subscription = $this->get_subscription_for_webhook_object( $object['subscription'] );
1138
1139 if ( charitable_is_debug( 'square' ) ) {
1140 // phpcs:disable
1141 error_log( 'process_subscription_updated - subscription' );
1142 error_log( print_r( $subscription, true ) );
1143 // phpcs:enable
1144 }
1145
1146 // Early return if no subscription found.
1147 if ( ! $subscription ) {
1148 if ( charitable_is_debug( 'square' ) ) {
1149 // phpcs:disable
1150 error_log( 'process_subscription_updated - No subscription found for ID: ' . $object['subscription']['id'] );
1151 // phpcs:enable
1152 }
1153 return __( 'Subscription Webhook: No subscription found', 'charitable' );
1154 }
1155
1156 $square_status = $this->get_subscription_status( $object['subscription']['status'] );
1157 $current_status = $subscription->get_status();
1158 $subscription_id = $subscription->get_donation_id();
1159
1160 $cancel_confirmed = false;
1161
1162 // The subscription could be ACTIVE but still "cancelled". Because the subscription is not immediately cancelled, we need to check the canceled_date.
1163 if ( isset( $object['subscription']['canceled_date'] ) && 'active' === strtolower( $object['subscription']['status'] ) ) {
1164 // Check and see if there is metadata for the subscription for a "canncelled to be confirmed".
1165 $cancelled_to_be_confirmed = get_post_meta( $subscription_id, '_square_cancelled_to_be_confirmed', true );
1166 if ( 'true' === strtolower( $cancelled_to_be_confirmed ) ) {
1167 $log = new Charitable_Square_Recurring_Donation_Log( $subscription );
1168 $log->log_custom_message( 'Subscription cancelled via Square via admin.' );
1169 $square_status = 'charitable-cancelled';
1170 $cancel_confirmed = true;
1171 }
1172 }
1173
1174 if ( charitable_is_debug( 'square' ) ) {
1175 // phpcs:disable
1176 error_log( 'process_subscription_updated - square_status' );
1177 error_log( print_r( $square_status, true ) );
1178 error_log( 'process_subscription_updated - current_status' );
1179 error_log( print_r( $current_status, true ) );
1180 error_log( 'process_subscription_updated - subscription_id' );
1181 error_log( print_r( $subscription_id, true ) );
1182 // phpcs:enable
1183 }
1184
1185 if ( $square_status !== $current_status ) {
1186
1187 $subscription->update_status( $square_status );
1188 if ( 'charitable-cancelled' === $square_status && class_exists( 'Charitable_Square_Recurring_Donation_Log' ) && ! $cancel_confirmed ) {
1189 $log = new Charitable_Square_Recurring_Donation_Log( $subscription );
1190 $log->log_custom_message( 'Subscription cancelled via Square.' );
1191 }
1192
1193 if ( charitable_is_debug( 'square' ) ) {
1194 // phpcs:disable
1195 error_log( 'process_subscription_updated - updated the status of the subscription to ' . $square_status );
1196 // phpcs:enable
1197 }
1198
1199 if ( 'charitable-cancelled' === $square_status ) {
1200
1201 // If the status is 'suspended', we need to fail the last pending donation.
1202 // Find the lastest child post of the parent post (which has an ID of $subscription_id).
1203 $sql = $wpdb->prepare(
1204 "SELECT ID
1205 FROM $wpdb->posts
1206 WHERE post_parent = %s
1207 ORDER BY post_date DESC
1208 LIMIT 1",
1209 $subscription_id
1210 );
1211 $donation_id = $wpdb->get_col( $sql ); // phpcs:ignore
1212
1213 if ( ! empty( $donation_id ) ) {
1214
1215 if ( charitable_is_debug( 'square' ) ) {
1216 // phpcs:disable
1217 error_log( 'process_subscription_updated - sql was: ' . $sql );
1218 error_log( 'process_subscription_updated - donation_id' );
1219 error_log( print_r( $donation_id[0], true ) );
1220 // phpcs:enable
1221 }
1222
1223 if ( ! empty( $donation_id[0] ) && is_numeric( $donation_id[0] ) && $donation_id[0] > 0 ) {
1224 $donation = charitable_get_donation( intval( $donation_id[0] ) );
1225 $donation->update_status( 'charitable-failed' );
1226
1227 $log = new Charitable_Square_Donation_Log( $donation );
1228 $log->log_custom_message( 'Subscription cancelled. Failed the last pending donation.' );
1229 }
1230 }
1231 }
1232 }
1233
1234 return __( 'Subscription Webhook: Recurring donation updated', 'charitable' );
1235 }
1236
1237 /**
1238 * Return a recurring donation object for a particular invoice, or false if
1239 * none is found.
1240 *
1241 * @since 1.8.7
1242 *
1243 * @param object $the_object The invoice or subscription object received from Square.
1244 * @return Charitable_Recurring_Donation|false
1245 */
1246 private function get_subscription_for_webhook_object( $the_object ) {
1247 $subscription_id = ! empty( $the_object['id'] ) ? $the_object['id'] : '';
1248
1249 // This charitable_recurring_get_subscription_by_gateway_id() function exits in the Charitable Recurring extension.
1250 if ( function_exists( 'charitable_recurring_get_subscription_by_gateway_id' ) ) {
1251 return charitable_recurring_get_subscription_by_gateway_id( $subscription_id, 'square' );
1252 }
1253
1254 return false;
1255 }
1256
1257 /**
1258 * Given a Square subscription status, return the corresponding Charitable status.
1259 *
1260 * @since 1.8.7
1261 *
1262 * @param string $status Square subscription status.
1263 * @return string
1264 */
1265 public function get_subscription_status( $status ) {
1266 switch ( strtolower( $status ) ) {
1267 case 'incomplete':
1268 case 'trialing':
1269 return 'charitable-pending';
1270
1271 case 'active':
1272 return 'charitable-active';
1273
1274 case 'past_due':
1275 return 'charitable-cancel';
1276
1277 case 'canceled':
1278 case 'unpaid':
1279 case 'incomplete_expired':
1280 case 'deactivated':
1281 return 'charitable-cancelled';
1282 }
1283 }
1284
1285 /**
1286 * When payment failures for a particular payment intent, update the failure count.
1287 *
1288 * After three failures, cancel the payment intent.
1289 *
1290 * @since 1.4.9
1291 *
1292 * @param Charitable_Abstract_Donation $donation The donation to be updated.
1293 * @param string $payment_intent The payment intent id.
1294 * @return void
1295 */
1296 public function update_payment_failure_count( Charitable_Abstract_Donation $donation, $payment_intent ) {
1297 $failure_count = (int) get_post_meta( $donation->ID, '_square_payment_intent_failure_count', true );
1298 $failure_count += 1; // phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1299
1300 /* Update the failure count. */
1301 update_post_meta( $donation->ID, '_square_payment_intent_failure_count', $failure_count );
1302
1303 /**
1304 * Filter the threshold number of failures after which a payment intent
1305 * should be cancelled.
1306 *
1307 * @since 1.4.9
1308 *
1309 * @param int $threshold The threshold number.
1310 */
1311 $threshold = apply_filters( 'charitable_square_payment_failure_cancellation_threshold', 3 );
1312
1313 /* The threshold has been reached, so cancel the payment intent. */
1314 if ( $threshold <= $failure_count ) {
1315 $intent = new Charitable_Square_Payment_Intent( $payment_intent );
1316 $intent->cancel();
1317
1318 /* Add a log message. */
1319 $donation->log()->add(
1320 sprintf(
1321 /* translators: %d: threshold */
1322 __( 'The payment intent has been cancelled after %d failed payment attempts.', 'charitable' ),
1323 $threshold
1324 )
1325 );
1326 }
1327 }
1328
1329 /**
1330 * Check whether Recurring Donations is active.
1331 *
1332 * @since 1.8.7
1333 *
1334 * @return boolean
1335 */
1336 private function is_recurring_installed() {
1337 return class_exists( 'Charitable_Recurring' );
1338 }
1339
1340 /**
1341 * For an IPN request, get the validated incoming event object.
1342 *
1343 * @since 1.8.7
1344 * @since 1.4.0 Returns an array instead of an object.
1345 * @since 1.8.7 Debug logging.
1346 *
1347 * @return false|array If valid, returns an object. Otherwise false.
1348 */
1349 private static function get_validated_incoming_event() {
1350 $body = @file_get_contents( 'php://input' ); // phpcs:ignore
1351
1352 // Debug logging.
1353 if ( charitable_is_debug( 'square' ) ) {
1354 // phpcs:disable
1355 error_log( 'Raw webhook body: ' . $body );
1356 error_log( 'Request method: ' . $_SERVER['REQUEST_METHOD'] );
1357 error_log( 'Content type: ' . ( isset( $_SERVER['CONTENT_TYPE'] ) ? $_SERVER['CONTENT_TYPE'] : 'not set' ) );
1358 // phpcs:enable
1359 }
1360
1361 /*
1362 * Verify Square's webhook signature before trusting the payload.
1363 *
1364 * Square signs every webhook by computing an HMAC-SHA256 over
1365 * ( notification_url . request_body ) using the subscription's
1366 * signature key, and sends the base64 result in the
1367 * x-square-hmacsha256-signature header. We recompute it with the
1368 * stored signing secret and reject anything that does not match.
1369 * Without this an unauthenticated caller could POST a forged event
1370 * to the public charitable-listener=square_core endpoint and tamper
1371 * with donation, refund, and subscription records.
1372 *
1373 * @since 1.8.11.3
1374 */
1375 $signature_header = isset( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) ? wp_unslash( $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- A cryptographic signature is compared byte-for-byte and must not be altered by sanitization.
1376 $mode = Charitable_Square_Helpers::get_mode();
1377
1378 /*
1379 * The signing secret is stored per-mode by the webhooks manager.
1380 * Lite saves it under the `gateways_square` group; read the
1381 * `gateways_square_core` group first for forward-compatibility and
1382 * fall back to `gateways_square` where it actually lives.
1383 */
1384 $core_settings = charitable_get_option( 'gateways_square_core' );
1385 $legacy_settings = charitable_get_option( 'gateways_square' );
1386 $signature_key = '';
1387 if ( is_array( $core_settings ) && ! empty( $core_settings[ $mode ]['webhooks-secret'] ) ) {
1388 $signature_key = $core_settings[ $mode ]['webhooks-secret'];
1389 } elseif ( is_array( $legacy_settings ) && ! empty( $legacy_settings[ $mode ]['webhooks-secret'] ) ) {
1390 $signature_key = $legacy_settings[ $mode ]['webhooks-secret'];
1391 }
1392
1393 /*
1394 * Verify against the exact URL registered with Square
1395 * ( Charitable_Square_WebhooksManager registers with
1396 * get_webhook_url() ), so sites using the CHARITABLE_SQUARE_WHURL
1397 * override still validate. Fall back to the REST URL if needed.
1398 */
1399 $notification_url = Charitable_Square_Helpers::get_webhook_url();
1400 if ( empty( $notification_url ) ) {
1401 $notification_url = Charitable_Square_Helpers::get_webhook_url_for_rest();
1402 }
1403
1404 if ( charitable_is_debug( 'square' ) ) {
1405 // phpcs:disable
1406 error_log( 'Square webhook signature header: ' . ( ! empty( $signature_header ) ? 'present' : 'missing' ) );
1407 error_log( 'Square webhook signing secret: ' . ( ! empty( $signature_key ) ? 'stored' : 'not stored' ) );
1408 // phpcs:enable
1409 }
1410
1411 if ( ! empty( $signature_key ) && ! empty( $signature_header ) ) {
1412
1413 /*
1414 * Resolve the Square SDK webhook helper. Lite bundles the SDK
1415 * under the unprefixed \Square namespace; also accept a
1416 * prefixed (scoped) build so a future packaging change cannot
1417 * silently disable verification.
1418 */
1419 $helper_class = '';
1420 if ( class_exists( '\Square\Utils\WebhooksHelper' ) ) {
1421 $helper_class = '\Square\Utils\WebhooksHelper';
1422 } elseif ( class_exists( '\CharitableSquareSDK\Utils\WebhooksHelper' ) ) {
1423 $helper_class = '\CharitableSquareSDK\Utils\WebhooksHelper';
1424 }
1425
1426 if ( '' === $helper_class ) {
1427 // Fail closed: a signed request we cannot verify is treated
1428 // as untrusted. Partial verification is worse than none.
1429 error_log( 'Square webhook REJECTED: WebhooksHelper class unavailable, cannot verify signature.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1430 return false;
1431 }
1432
1433 try {
1434 $is_valid = $helper_class::verifySignature( $body, $signature_header, $signature_key, $notification_url );
1435 } catch ( \Throwable $e ) {
1436 // Fail closed on any error/exception from the SDK: a signed
1437 // request we could not verify is treated as untrusted.
1438 if ( charitable_is_debug( 'square' ) ) {
1439 error_log( 'Square webhook signature verification error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1440 }
1441 return false;
1442 }
1443
1444 if ( ! $is_valid ) {
1445 if ( charitable_is_debug( 'square' ) ) {
1446 error_log( 'Square webhook REJECTED: signature verification failed.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1447 }
1448 return false;
1449 }
1450
1451 if ( charitable_is_debug( 'square' ) ) {
1452 error_log( 'Square webhook signature verified OK.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1453 }
1454 } elseif ( ! empty( $signature_key ) && empty( $signature_header ) ) {
1455 // A signing secret is configured but the request is unsigned: reject.
1456 if ( charitable_is_debug( 'square' ) ) {
1457 error_log( 'Square webhook REJECTED: signing secret configured but request has no signature header.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1458 }
1459 return false;
1460 }
1461
1462 /*
1463 * No signing secret stored. Historically the processor fell through
1464 * here to a merchant_id presence check and ACCEPTED the (unsigned)
1465 * event, which let an unauthenticated caller forge Square webhooks on
1466 * any site that had not yet stored a signing secret. Fail closed: the
1467 * event is refused unless enforcement is explicitly disabled.
1468 *
1469 * Sites connected through the Square settings screen store a signing
1470 * secret during onboarding, so they never reach this branch and are
1471 * unaffected. For the rare site that has webhooks enabled but no
1472 * stored secret, refused events stop donations/refunds from
1473 * reconciling, which the hourly Charitable_Square_WebhooksHealthCheck
1474 * detects and surfaces as an admin notice prompting a reconnect (which
1475 * re-registers the subscription and stores a fresh signing secret,
1476 * restoring verification).
1477 *
1478 * @since 1.8.12
1479 */
1480 if ( empty( $signature_key ) ) {
1481
1482 /**
1483 * Filter whether to enforce Square webhook signature verification
1484 * when no signing secret is stored.
1485 *
1486 * Defaults to true (fail closed). Returning false restores the
1487 * legacy behaviour of accepting unsigned events and re-opens the
1488 * forgery vector, so it should only be used as a temporary escape
1489 * hatch while reconnecting Square.
1490 *
1491 * @since 1.8.12
1492 *
1493 * @param bool $enforce Whether to reject unsigned events. Default true.
1494 */
1495 $enforce_signature = (bool) apply_filters( 'charitable_square_webhook_enforce_signature', true );
1496
1497 if ( $enforce_signature ) {
1498 error_log( 'Square webhook REJECTED: no signing secret stored. Reconnect Square (Settings > Payment > Square) to register webhooks and enable signature verification. To temporarily accept unsigned events, use the charitable_square_webhook_enforce_signature filter.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1499 return false;
1500 }
1501
1502 if ( charitable_is_debug( 'square' ) ) {
1503 error_log( 'Square webhook WARNING: no signing secret stored and signature enforcement disabled via the charitable_square_webhook_enforce_signature filter - accepting unsigned event.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1504 }
1505 }
1506
1507 $event = json_decode( $body, true );
1508
1509 // Debug logging for decoded event.
1510 if ( charitable_is_debug( 'square' ) ) {
1511 // phpcs:disable
1512 error_log( 'Decoded event: ' . print_r( $event, true ) );
1513 if ( json_last_error() !== JSON_ERROR_NONE ) {
1514 error_log( 'JSON decode error: ' . json_last_error_msg() );
1515 }
1516 // phpcs:enable
1517 }
1518
1519 if ( ! is_array( $event ) || ! array_key_exists( 'merchant_id', $event ) ) {
1520 if ( charitable_is_debug( 'square' ) ) {
1521 // phpcs:disable
1522 error_log( 'Invalid event format or missing merchant_id' );
1523 // phpcs:enable
1524 }
1525 return false;
1526 }
1527
1528 return $event;
1529 }
1530
1531 /**
1532 * Create instance of the class.
1533 *
1534 * @since 1.8.7
1535 *
1536 * @return Charitable_Square_Webhook_Processor
1537 */
1538 public static function get_instance() {
1539 if ( null === self::$instance ) {
1540 self::$instance = new self( true );
1541 }
1542 return self::$instance;
1543 }
1544
1545 /**
1546 * Get the donation amount including fees if fee relief is enabled.
1547 *
1548 * @since 1.8.8.4
1549 *
1550 * @param Charitable_Donation $donation The donation object.
1551 * @return float
1552 */
1553 private function get_donation_amount_with_fees( $donation ) {
1554 // Check if fee relief is enabled and donor opted to cover fees
1555 if ( $donation->get( 'cover_fees' ) ) {
1556 // Use the total donation amount including fees
1557 return \Charitable_Currency::get_instance()->cast_to_decimal_format( $donation->get( 'total_donation_with_fees' ) );
1558 }
1559
1560 // Return the standard donation amount
1561 return $donation->get_total_donation_amount( true );
1562 }
1563 }
1564 endif;
1565