PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.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.11.3, at includes/square/gateway/class-charitable-square-webhook-processor.php

1,520 lines 50.7 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 if ( charitable_is_debug( 'square' ) ) {
968 // phpcs:disable
969 error_log( 'Square Webhook - Refund Amount: ' . $refund_amount );
970 // phpcs:enable
971 }
972
973 $refund_status = $charge['refund']['status']; // should be COMPLETED.
974 if ( charitable_is_debug( 'square' ) ) {
975 // phpcs:disable
976 error_log( 'Square Webhook - Refund Status: ' . $refund_status );
977 // phpcs:enable
978 }
979
980 $order_id = $charge['refund']['order_id'];
981 if ( charitable_is_debug( 'square' ) ) {
982 // phpcs:disable
983 error_log( 'Square Webhook - Order ID: ' . $order_id );
984 // phpcs:enable
985 }
986
987 $payment_id = $charge['refund']['payment_id'];
988 if ( charitable_is_debug( 'square' ) ) {
989 // phpcs:disable
990 error_log( 'Square Webhook - Payment ID: ' . $payment_id );
991 // phpcs:enable
992 }
993
994 // We need to search post meta to find a _gateway_payment_id that matches the payment_id.
995 $sql = $wpdb->prepare(
996 "SELECT post_id
997 FROM $wpdb->postmeta
998 WHERE meta_key = '_gateway_payment_id'
999 AND meta_value = %s",
1000 $payment_id
1001 );
1002
1003 $donation_id = $wpdb->get_col( $sql ); // phpcs:ignore
1004
1005 if ( charitable_is_debug( 'square' ) ) {
1006 // phpcs:disable
1007 error_log( '[Square Core] Webhook - Donation ID: ' . print_r( $donation_id, true ) );
1008 // phpcs:enable
1009 }
1010
1011 if ( empty( $donation_id ) ) {
1012 if ( charitable_is_debug( 'square' ) ) {
1013 // phpcs:disable
1014 error_log( '[Square Core] Webhook - No donation found for payment ID: ' . $payment_id );
1015 // phpcs:enable
1016 }
1017 return __( 'Donation Webhook: Refund processed', 'charitable' );
1018 }
1019
1020 $donation = charitable_get_donation( $donation_id[0] );
1021
1022 if ( charitable_is_debug( 'square' ) ) {
1023 // phpcs:disable
1024 error_log( '[Square Core] Webhook - Donation found: ' . print_r( $donation, true ) );
1025 // phpcs:enable
1026 }
1027
1028 $donation->process_refund( $refund_amount, __( 'Donation refunded from the Square dashboard.', 'charitable' ) );
1029
1030 return __( 'Donation Webhook: Refund processed', 'charitable' );
1031 }
1032
1033 /**
1034 * Process the subscription.created webhook.
1035 *
1036 * @since 1.8.7
1037 *
1038 * @param object $event The Square event object.
1039 * @return string Response message
1040 */
1041 public function process_subscription_created( $event ) {
1042 if ( ! $this->is_recurring_installed() ) {
1043 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable' );
1044 }
1045
1046 if ( charitable_is_debug( 'square' ) ) {
1047 // phpcs:disable
1048 error_log( 'process_subscription_created - starting' );
1049 error_log( print_r( $event, true ) );
1050 // phpcs:enable
1051 }
1052
1053 $object = $event['data']['object'];
1054 $subscription = $this->get_subscription_for_webhook_object( $object['subscription'] );
1055
1056 if ( charitable_is_debug( 'square' ) ) {
1057 // phpcs:disable
1058 error_log( 'process_subscription_created - subscription' );
1059 error_log( print_r( $subscription, true ) );
1060 error_log( 'process_subscription_created - object' );
1061 error_log( print_r( $object, true ) );
1062 // phpcs:enable
1063 }
1064
1065 if ( empty( $subscription ) ) {
1066 return __( 'Subscription Webhook: Missing subscription', 'charitable' );
1067 }
1068
1069 $square_status = $this->get_subscription_status( $object['subscription']['status'] );
1070 $current_status = $subscription->get_status();
1071
1072 if ( charitable_is_debug( 'square' ) ) {
1073 // phpcs:disable
1074 error_log( 'process_subscription_created - square_status' );
1075 error_log( print_r( $square_status, true ) );
1076 error_log( 'process_subscription_created - current_status' );
1077 error_log( print_r( $current_status, true ) );
1078 // phpcs:enable
1079 }
1080
1081 if ( $square_status !== $current_status && 'charitable-completed' !== $current_status ) {
1082 $subscription->update_status( $square_status );
1083 if ( charitable_is_debug( 'square' ) ) {
1084 // phpcs:disable
1085 error_log( 'process_subscription_created - updated the status of the subscription to ' . $square_status );
1086 // phpcs:enable
1087 }
1088 }
1089
1090 return __( 'Subscription Webhook: Recurring donation updated', 'charitable' );
1091 }
1092
1093 /**
1094 * Process the subscription.updated webhook.
1095 *
1096 * @since 1.8.7
1097 *
1098 * @param object $event The Square event object.
1099 * @return string Response message
1100 */
1101 public function process_subscription_updated( $event ) {
1102 global $wpdb;
1103
1104 if ( ! $this->is_recurring_installed() ) {
1105 return __( 'Subscription Webhook: Unable to process without Charitable Recurring extension.', 'charitable' );
1106 }
1107
1108 if ( charitable_is_debug( 'square' ) ) {
1109 // phpcs:disable
1110 error_log( 'process_subscription_updated - incoming event' );
1111 error_log( print_r( $event, true ) );
1112 // phpcs:enable
1113 }
1114
1115 $object = $event['data']['object'];
1116
1117 // Early return if subscription data is missing.
1118 if ( empty( $object['subscription'] ) ) {
1119 if ( charitable_is_debug( 'square' ) ) {
1120 // phpcs:disable
1121 error_log( 'process_subscription_updated - Missing subscription data in webhook' );
1122 // phpcs:enable
1123 }
1124 return __( 'Subscription Webhook: Missing subscription data', 'charitable' );
1125 }
1126
1127 $subscription = $this->get_subscription_for_webhook_object( $object['subscription'] );
1128
1129 if ( charitable_is_debug( 'square' ) ) {
1130 // phpcs:disable
1131 error_log( 'process_subscription_updated - subscription' );
1132 error_log( print_r( $subscription, true ) );
1133 // phpcs:enable
1134 }
1135
1136 // Early return if no subscription found.
1137 if ( ! $subscription ) {
1138 if ( charitable_is_debug( 'square' ) ) {
1139 // phpcs:disable
1140 error_log( 'process_subscription_updated - No subscription found for ID: ' . $object['subscription']['id'] );
1141 // phpcs:enable
1142 }
1143 return __( 'Subscription Webhook: No subscription found', 'charitable' );
1144 }
1145
1146 $square_status = $this->get_subscription_status( $object['subscription']['status'] );
1147 $current_status = $subscription->get_status();
1148 $subscription_id = $subscription->get_donation_id();
1149
1150 $cancel_confirmed = false;
1151
1152 // The subscription could be ACTIVE but still "cancelled". Because the subscription is not immediately cancelled, we need to check the canceled_date.
1153 if ( isset( $object['subscription']['canceled_date'] ) && 'active' === strtolower( $object['subscription']['status'] ) ) {
1154 // Check and see if there is metadata for the subscription for a "canncelled to be confirmed".
1155 $cancelled_to_be_confirmed = get_post_meta( $subscription_id, '_square_cancelled_to_be_confirmed', true );
1156 if ( 'true' === strtolower( $cancelled_to_be_confirmed ) ) {
1157 $log = new Charitable_Square_Recurring_Donation_Log( $subscription );
1158 $log->log_custom_message( 'Subscription cancelled via Square via admin.' );
1159 $square_status = 'charitable-cancelled';
1160 $cancel_confirmed = true;
1161 }
1162 }
1163
1164 if ( charitable_is_debug( 'square' ) ) {
1165 // phpcs:disable
1166 error_log( 'process_subscription_updated - square_status' );
1167 error_log( print_r( $square_status, true ) );
1168 error_log( 'process_subscription_updated - current_status' );
1169 error_log( print_r( $current_status, true ) );
1170 error_log( 'process_subscription_updated - subscription_id' );
1171 error_log( print_r( $subscription_id, true ) );
1172 // phpcs:enable
1173 }
1174
1175 if ( $square_status !== $current_status ) {
1176
1177 $subscription->update_status( $square_status );
1178 if ( 'charitable-cancelled' === $square_status && class_exists( 'Charitable_Square_Recurring_Donation_Log' ) && ! $cancel_confirmed ) {
1179 $log = new Charitable_Square_Recurring_Donation_Log( $subscription );
1180 $log->log_custom_message( 'Subscription cancelled via Square.' );
1181 }
1182
1183 if ( charitable_is_debug( 'square' ) ) {
1184 // phpcs:disable
1185 error_log( 'process_subscription_updated - updated the status of the subscription to ' . $square_status );
1186 // phpcs:enable
1187 }
1188
1189 if ( 'charitable-cancelled' === $square_status ) {
1190
1191 // If the status is 'suspended', we need to fail the last pending donation.
1192 // Find the lastest child post of the parent post (which has an ID of $subscription_id).
1193 $sql = $wpdb->prepare(
1194 "SELECT ID
1195 FROM $wpdb->posts
1196 WHERE post_parent = %s
1197 ORDER BY post_date DESC
1198 LIMIT 1",
1199 $subscription_id
1200 );
1201 $donation_id = $wpdb->get_col( $sql ); // phpcs:ignore
1202
1203 if ( ! empty( $donation_id ) ) {
1204
1205 if ( charitable_is_debug( 'square' ) ) {
1206 // phpcs:disable
1207 error_log( 'process_subscription_updated - sql was: ' . $sql );
1208 error_log( 'process_subscription_updated - donation_id' );
1209 error_log( print_r( $donation_id[0], true ) );
1210 // phpcs:enable
1211 }
1212
1213 if ( ! empty( $donation_id[0] ) && is_numeric( $donation_id[0] ) && $donation_id[0] > 0 ) {
1214 $donation = charitable_get_donation( intval( $donation_id[0] ) );
1215 $donation->update_status( 'charitable-failed' );
1216
1217 $log = new Charitable_Square_Donation_Log( $donation );
1218 $log->log_custom_message( 'Subscription cancelled. Failed the last pending donation.' );
1219 }
1220 }
1221 }
1222 }
1223
1224 return __( 'Subscription Webhook: Recurring donation updated', 'charitable' );
1225 }
1226
1227 /**
1228 * Return a recurring donation object for a particular invoice, or false if
1229 * none is found.
1230 *
1231 * @since 1.8.7
1232 *
1233 * @param object $the_object The invoice or subscription object received from Square.
1234 * @return Charitable_Recurring_Donation|false
1235 */
1236 private function get_subscription_for_webhook_object( $the_object ) {
1237 $subscription_id = ! empty( $the_object['id'] ) ? $the_object['id'] : '';
1238
1239 // This charitable_recurring_get_subscription_by_gateway_id() function exits in the Charitable Recurring extension.
1240 if ( function_exists( 'charitable_recurring_get_subscription_by_gateway_id' ) ) {
1241 return charitable_recurring_get_subscription_by_gateway_id( $subscription_id, 'square' );
1242 }
1243
1244 return false;
1245 }
1246
1247 /**
1248 * Given a Square subscription status, return the corresponding Charitable status.
1249 *
1250 * @since 1.8.7
1251 *
1252 * @param string $status Square subscription status.
1253 * @return string
1254 */
1255 public function get_subscription_status( $status ) {
1256 switch ( strtolower( $status ) ) {
1257 case 'incomplete':
1258 case 'trialing':
1259 return 'charitable-pending';
1260
1261 case 'active':
1262 return 'charitable-active';
1263
1264 case 'past_due':
1265 return 'charitable-cancel';
1266
1267 case 'canceled':
1268 case 'unpaid':
1269 case 'incomplete_expired':
1270 case 'deactivated':
1271 return 'charitable-cancelled';
1272 }
1273 }
1274
1275 /**
1276 * When payment failures for a particular payment intent, update the failure count.
1277 *
1278 * After three failures, cancel the payment intent.
1279 *
1280 * @since 1.4.9
1281 *
1282 * @param Charitable_Abstract_Donation $donation The donation to be updated.
1283 * @param string $payment_intent The payment intent id.
1284 * @return void
1285 */
1286 public function update_payment_failure_count( Charitable_Abstract_Donation $donation, $payment_intent ) {
1287 $failure_count = (int) get_post_meta( $donation->ID, '_square_payment_intent_failure_count', true );
1288 $failure_count += 1; // phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1289
1290 /* Update the failure count. */
1291 update_post_meta( $donation->ID, '_square_payment_intent_failure_count', $failure_count );
1292
1293 /**
1294 * Filter the threshold number of failures after which a payment intent
1295 * should be cancelled.
1296 *
1297 * @since 1.4.9
1298 *
1299 * @param int $threshold The threshold number.
1300 */
1301 $threshold = apply_filters( 'charitable_square_payment_failure_cancellation_threshold', 3 );
1302
1303 /* The threshold has been reached, so cancel the payment intent. */
1304 if ( $threshold <= $failure_count ) {
1305 $intent = new Charitable_Square_Payment_Intent( $payment_intent );
1306 $intent->cancel();
1307
1308 /* Add a log message. */
1309 $donation->log()->add(
1310 sprintf(
1311 /* translators: %d: threshold */
1312 __( 'The payment intent has been cancelled after %d failed payment attempts.', 'charitable' ),
1313 $threshold
1314 )
1315 );
1316 }
1317 }
1318
1319 /**
1320 * Check whether Recurring Donations is active.
1321 *
1322 * @since 1.8.7
1323 *
1324 * @return boolean
1325 */
1326 private function is_recurring_installed() {
1327 return class_exists( 'Charitable_Recurring' );
1328 }
1329
1330 /**
1331 * For an IPN request, get the validated incoming event object.
1332 *
1333 * @since 1.8.7
1334 * @since 1.4.0 Returns an array instead of an object.
1335 * @since 1.8.7 Debug logging.
1336 *
1337 * @return false|array If valid, returns an object. Otherwise false.
1338 */
1339 private static function get_validated_incoming_event() {
1340 $body = @file_get_contents( 'php://input' ); // phpcs:ignore
1341
1342 // Debug logging.
1343 if ( charitable_is_debug( 'square' ) ) {
1344 // phpcs:disable
1345 error_log( 'Raw webhook body: ' . $body );
1346 error_log( 'Request method: ' . $_SERVER['REQUEST_METHOD'] );
1347 error_log( 'Content type: ' . ( isset( $_SERVER['CONTENT_TYPE'] ) ? $_SERVER['CONTENT_TYPE'] : 'not set' ) );
1348 // phpcs:enable
1349 }
1350
1351 /*
1352 * Verify Square's webhook signature before trusting the payload.
1353 *
1354 * Square signs every webhook by computing an HMAC-SHA256 over
1355 * ( notification_url . request_body ) using the subscription's
1356 * signature key, and sends the base64 result in the
1357 * x-square-hmacsha256-signature header. We recompute it with the
1358 * stored signing secret and reject anything that does not match.
1359 * Without this an unauthenticated caller could POST a forged event
1360 * to the public charitable-listener=square_core endpoint and tamper
1361 * with donation, refund, and subscription records.
1362 *
1363 * @since 1.8.11.3
1364 */
1365 $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.
1366 $mode = Charitable_Square_Helpers::get_mode();
1367
1368 /*
1369 * The signing secret is stored per-mode by the webhooks manager.
1370 * Lite saves it under the `gateways_square` group; read the
1371 * `gateways_square_core` group first for forward-compatibility and
1372 * fall back to `gateways_square` where it actually lives.
1373 */
1374 $core_settings = charitable_get_option( 'gateways_square_core' );
1375 $legacy_settings = charitable_get_option( 'gateways_square' );
1376 $signature_key = '';
1377 if ( is_array( $core_settings ) && ! empty( $core_settings[ $mode ]['webhooks-secret'] ) ) {
1378 $signature_key = $core_settings[ $mode ]['webhooks-secret'];
1379 } elseif ( is_array( $legacy_settings ) && ! empty( $legacy_settings[ $mode ]['webhooks-secret'] ) ) {
1380 $signature_key = $legacy_settings[ $mode ]['webhooks-secret'];
1381 }
1382
1383 /*
1384 * Verify against the exact URL registered with Square
1385 * ( Charitable_Square_WebhooksManager registers with
1386 * get_webhook_url() ), so sites using the CHARITABLE_SQUARE_WHURL
1387 * override still validate. Fall back to the REST URL if needed.
1388 */
1389 $notification_url = Charitable_Square_Helpers::get_webhook_url();
1390 if ( empty( $notification_url ) ) {
1391 $notification_url = Charitable_Square_Helpers::get_webhook_url_for_rest();
1392 }
1393
1394 if ( charitable_is_debug( 'square' ) ) {
1395 // phpcs:disable
1396 error_log( 'Square webhook signature header: ' . ( ! empty( $signature_header ) ? 'present' : 'missing' ) );
1397 error_log( 'Square webhook signing secret: ' . ( ! empty( $signature_key ) ? 'stored' : 'not stored' ) );
1398 // phpcs:enable
1399 }
1400
1401 if ( ! empty( $signature_key ) && ! empty( $signature_header ) ) {
1402
1403 /*
1404 * Resolve the Square SDK webhook helper. Lite bundles the SDK
1405 * under the unprefixed \Square namespace; also accept a
1406 * prefixed (scoped) build so a future packaging change cannot
1407 * silently disable verification.
1408 */
1409 $helper_class = '';
1410 if ( class_exists( '\Square\Utils\WebhooksHelper' ) ) {
1411 $helper_class = '\Square\Utils\WebhooksHelper';
1412 } elseif ( class_exists( '\CharitableSquareSDK\Utils\WebhooksHelper' ) ) {
1413 $helper_class = '\CharitableSquareSDK\Utils\WebhooksHelper';
1414 }
1415
1416 if ( '' === $helper_class ) {
1417 // Fail closed: a signed request we cannot verify is treated
1418 // as untrusted. Partial verification is worse than none.
1419 error_log( 'Square webhook REJECTED: WebhooksHelper class unavailable, cannot verify signature.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1420 return false;
1421 }
1422
1423 try {
1424 $is_valid = $helper_class::verifySignature( $body, $signature_header, $signature_key, $notification_url );
1425 } catch ( \Throwable $e ) {
1426 // Fail closed on any error/exception from the SDK: a signed
1427 // request we could not verify is treated as untrusted.
1428 if ( charitable_is_debug( 'square' ) ) {
1429 error_log( 'Square webhook signature verification error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1430 }
1431 return false;
1432 }
1433
1434 if ( ! $is_valid ) {
1435 if ( charitable_is_debug( 'square' ) ) {
1436 error_log( 'Square webhook REJECTED: signature verification failed.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1437 }
1438 return false;
1439 }
1440
1441 if ( charitable_is_debug( 'square' ) ) {
1442 error_log( 'Square webhook signature verified OK.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1443 }
1444 } elseif ( ! empty( $signature_key ) && empty( $signature_header ) ) {
1445 // A signing secret is configured but the request is unsigned: reject.
1446 if ( charitable_is_debug( 'square' ) ) {
1447 error_log( 'Square webhook REJECTED: signing secret configured but request has no signature header.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1448 }
1449 return false;
1450 }
1451
1452 /*
1453 * No signing secret stored (webhook subscription not yet registered
1454 * via the Square settings screen). Fall through to the legacy
1455 * merchant_id presence check for backwards compatibility; such sites
1456 * remain unverified until they register webhooks.
1457 */
1458 if ( empty( $signature_key ) && charitable_is_debug( 'square' ) ) {
1459 error_log( 'Square webhook WARNING: no signing secret stored - signature verification skipped. Register webhooks via Square settings to enable verification.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1460 }
1461
1462 $event = json_decode( $body, true );
1463
1464 // Debug logging for decoded event.
1465 if ( charitable_is_debug( 'square' ) ) {
1466 // phpcs:disable
1467 error_log( 'Decoded event: ' . print_r( $event, true ) );
1468 if ( json_last_error() !== JSON_ERROR_NONE ) {
1469 error_log( 'JSON decode error: ' . json_last_error_msg() );
1470 }
1471 // phpcs:enable
1472 }
1473
1474 if ( ! is_array( $event ) || ! array_key_exists( 'merchant_id', $event ) ) {
1475 if ( charitable_is_debug( 'square' ) ) {
1476 // phpcs:disable
1477 error_log( 'Invalid event format or missing merchant_id' );
1478 // phpcs:enable
1479 }
1480 return false;
1481 }
1482
1483 return $event;
1484 }
1485
1486 /**
1487 * Create instance of the class.
1488 *
1489 * @since 1.8.7
1490 *
1491 * @return Charitable_Square_Webhook_Processor
1492 */
1493 public static function get_instance() {
1494 if ( null === self::$instance ) {
1495 self::$instance = new self( true );
1496 }
1497 return self::$instance;
1498 }
1499
1500 /**
1501 * Get the donation amount including fees if fee relief is enabled.
1502 *
1503 * @since 1.8.8.4
1504 *
1505 * @param Charitable_Donation $donation The donation object.
1506 * @return float
1507 */
1508 private function get_donation_amount_with_fees( $donation ) {
1509 // Check if fee relief is enabled and donor opted to cover fees
1510 if ( $donation->get( 'cover_fees' ) ) {
1511 // Use the total donation amount including fees
1512 return \Charitable_Currency::get_instance()->cast_to_decimal_format( $donation->get( 'total_donation_with_fees' ) );
1513 }
1514
1515 // Return the standard donation amount
1516 return $donation->get_total_donation_amount( true );
1517 }
1518 }
1519 endif;
1520