PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.2
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 / forms / class-charitable-donation-form.php

class-charitable-donation-form.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.2, at includes/forms/class-charitable-donation-form.php

1,817 lines 52.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Donation form model class.
4 *
5 * @package Charitable/Classes/Charitable_Donation_Form
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.6.60
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Donation_Form' ) ) :
19
20 /**
21 * Charitable_Donation_Form
22 *
23 * @since 1.0.0
24 */
25 class Charitable_Donation_Form extends Charitable_Form implements Charitable_Donation_Form_Interface {
26
27 /**
28 * The current campaign.
29 *
30 * @since 1.0.0
31 *
32 * @var Charitable_Campaign
33 */
34 protected $campaign;
35
36 /**
37 * The current user, or false if the user is not logged in.
38 *
39 * @since 1.0.0
40 *
41 * @var Charitable_User|false
42 */
43 protected $user;
44
45 /**
46 * Form fields.
47 *
48 * @since 1.0.0
49 *
50 * @var array
51 */
52 protected $form_fields;
53
54 /**
55 * Nonce action.
56 *
57 * @since 1.0.0
58 *
59 * @var string
60 */
61 protected $nonce_action = 'charitable_donation';
62
63 /**
64 * Nonce name.
65 *
66 * @since 1.0.0
67 *
68 * @var string
69 */
70 protected $nonce_name = '_charitable_donation_nonce';
71
72 /**
73 * Action to be executed upon form submission.
74 *
75 * @since 1.0.0
76 *
77 * @var string
78 */
79 protected $form_action = 'make_donation';
80
81 /**
82 * Value to indicate whether the user has all required fields filled out.
83 *
84 * @since 1.0.0
85 *
86 * @var boolean
87 */
88 protected $user_has_required_fields;
89
90 /**
91 * Flag thrown when the form submission has been validated.
92 *
93 * @since 1.0.0
94 *
95 * @var boolean
96 */
97 protected $validated = false;
98
99 /**
100 * Whether the form submission is valid.
101 *
102 * @since 1.0.0
103 *
104 * @var boolean
105 */
106 protected $valid;
107
108 /**
109 * Cached merged fields array.
110 *
111 * @since 1.0.0
112 *
113 * @var array
114 */
115 protected $merged_fields;
116
117 /**
118 * Create a donation form object.
119 *
120 * @since 1.0.0
121 * @since 1.5.0 $campaign argument became optional. Previously it was required.
122 * @since 1.8.2 Added minimum donation amount notice display option.
123 *
124 * @param Charitable_Campaign|null $campaign Optional. Campaign receiving the donation, or NULL if
125 * the campaign will be selected in the form.
126 */
127 public function __construct( Charitable_Campaign $campaign = null ) {
128 $this->campaign = $campaign;
129 $this->id = uniqid();
130
131 // paypal_commerce_connection_check() registers the filters that hide
132 // PayPal Commerce from the donor-facing gateway list when no seller
133 // is connected, and that swap the no-active-gateways notice for a
134 // donor-friendly fallback. Both filters must register BEFORE
135 // setup_payment_fields() evaluates the active-gateway count — that
136 // branch only fires (and thus only triggers the friendly fallback)
137 // when the gateway count drops to zero, which depends on the filter
138 // already being wired.
139 $this->paypal_commerce_connection_check();
140 $this->setup_payment_fields();
141 $this->check_test_mode();
142 $this->stripe_key_check();
143 $this->square_connection_check();
144
145 /* For backwards-compatibility */
146 add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
147 /* added in 1.7.0.4 */
148 if ( 'below_donation_title' === charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) ) {
149 add_action( 'charitable_after_donation_amount_wrapper_header', array( $this, 'minimum_max_donation_amount_notice' ), 10, 2 );
150 } elseif ( 'above_donation_title' === charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) || 'after_donation_title' === charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) ) {
151 add_action( 'charitable_before_donation_amount_wrapper_header', array( $this, 'minimum_max_donation_amount_notice' ), 10, 2 );
152 }
153 }
154
155 /**
156 * Returns the campaign associated with this donation form object.
157 *
158 * @since 1.0.0
159 * @since 1.5.0 May now return NULL when the donation form was set up without a campaign.
160 *
161 * @return Charitable_Campaign|null
162 */
163 public function get_campaign() {
164 return $this->campaign;
165 }
166
167 /**
168 * Return the form template, which is the look. Right now, the default is false with "minimal as the beta.
169 *
170 * @since 1.8.3.5
171 *
172 * @return string
173 */
174 public function get_form_template() {
175 return charitable_get_option( 'donation_form_template', false );
176 }
177
178 /**
179 * Return the current user.
180 *
181 * @since 1.0.0
182 *
183 * @return Charitable_User|false Object if the user is logged in. False otherwise.
184 */
185 public function get_user() {
186 if ( ! isset( $this->user ) ) {
187 $user = wp_get_current_user();
188 $this->user = $user->ID ? new Charitable_User( $user ) : false;
189 }
190
191 return $this->user;
192 }
193
194 /**
195 * Returns the set value for a particular user field.
196 *
197 * @since 1.0.0
198 * @version 1.8.9.1
199 *
200 * @param string $key The field key.
201 * @param string $default Optional. The value that will be used if none is set.
202 * @return mixed
203 */
204 public function get_user_value( $key, $default = '' ) {
205 // phpcs:disable WordPress.Security.NonceVerification.Missing
206 if ( isset( $_POST[ $key ] ) ) {
207 return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
208 }
209 // phpcs:enable WordPress.Security.NonceVerification.Missing
210
211 $donation_id = $this->get_validated_donation_id();
212
213 if ( $donation_id ) {
214 $donation = charitable_get_donation( $donation_id );
215 $value = $donation->get_donor()->get_donor_meta( $key );
216
217 if ( $value ) {
218 return $value;
219 }
220 }
221
222 if ( ! $this->get_user() || ! $this->get_user()->has_prop( $key ) ) {
223 return $default;
224 }
225
226 return $this->get_user()->get( $key );
227 }
228
229 /**
230 * Returns the fields related to the person making the donation.
231 *
232 * @since 1.0.0
233 *
234 * @return array
235 */
236 public function get_user_fields() {
237 /**
238 * Filter the donor fields.
239 *
240 * @since 1.0.0
241 *
242 * @param array $fields Set of donor fields.
243 * @param Charitable_Donation_Form $form Instance of `Charitable_Donation_Form`.
244 */
245 $fields = apply_filters( 'charitable_donation_form_user_fields', $this->get_sanitized_donation_fields( 'user' ), $this );
246 $fields = $this->hide_non_required_user_fields( $fields );
247
248 uasort( $fields, 'charitable_priority_sort' );
249
250 return $fields;
251 }
252
253 /**
254 * Returns the fields related to the person making the donation.
255 *
256 * @since 1.6.0
257 *
258 * @return array
259 */
260 public function get_meta_fields() {
261 /**
262 * Filter the meta fields.
263 *
264 * @since 1.6.0
265 *
266 * @param array $fields Set of meta fields.
267 * @param Charitable_Donation_Form $form Instance of `Charitable_Donation_Form`.
268 */
269 $fields = apply_filters( 'charitable_donation_form_meta_fields', $this->get_sanitized_donation_fields( 'meta' ), $this );
270
271 uasort( $fields, 'charitable_priority_sort' );
272
273 return $fields;
274 }
275
276 /**
277 * Return donation fields for a particular section.
278 *
279 * @since 1.6.0
280 * @since 1.6.1 Changed access to public.
281 *
282 * @param string $section The section of the donation form we need fields for.
283 * @return array
284 */
285 public function get_sanitized_donation_fields( $section ) {
286 $fields = charitable()->donation_fields()->get_donation_form_fields( $section );
287
288 if ( empty( $fields ) ) {
289 return array();
290 }
291
292 $keys = array_keys( $fields );
293
294 return array_combine(
295 $keys,
296 array_map( array( $this, 'set_field_value' ), wp_list_pluck( $fields, 'donation_form' ), $keys )
297 );
298 }
299
300 /**
301 * Only show the required user fields if that option was enabled by the site admin.
302 *
303 * @since 1.2.0
304 *
305 * @param array $fields The user fields.
306 * @return array[]
307 */
308 public function hide_non_required_user_fields( $fields ) {
309 if ( ! charitable_get_option( 'donation_form_minimal_fields', false ) ) {
310 return $fields;
311 }
312
313 return array_filter( $fields, array( $this, 'filter_required_fields' ) );
314 }
315
316 /**
317 * Return fields used for account creation.
318 *
319 * By default, this just returns the password field. You can include a username
320 * field with ...
321 *
322 * @since 1.0.0
323 *
324 * @return array
325 */
326 public function get_user_account_fields() {
327 $account_fields = array(
328 'user_pass' => array(
329 'label' => __( 'Password', 'charitable' ),
330 'type' => 'password',
331 'priority' => 4,
332 'required' => true,
333 'requires_registration' => true,
334 'data_type' => 'user',
335 ),
336 );
337
338 if ( apply_filters( 'charitable_donor_usernames', false ) ) {
339 $account_fields['user_login'] = array(
340 'label' => __( 'Username', 'charitable' ),
341 'type' => 'text',
342 'priority' => 2,
343 'required' => true,
344 'requires_registration' => true,
345 'data_type' => 'user',
346 );
347 }
348
349 return $account_fields;
350 }
351
352 /**
353 * Returns the donation fields.
354 *
355 * @since 1.0.0
356 *
357 * @return array[]
358 */
359 public function get_donation_fields() {
360 $fields = array(
361 'donation_amount' => array(
362 'type' => 'donation-amount',
363 'priority' => 4,
364 'required' => false,
365 ),
366 );
367
368 /**
369 * Filter the donation amount fields.
370 *
371 * @since 1.0.0
372 *
373 * @param array $fields The list of fields.
374 * @param Charitable_Donation_Form $form Instance of `Charitable_Donation_Form`.
375 */
376 $fields = apply_filters( 'charitable_donation_form_donation_fields', $fields, $this );
377
378 uasort( $fields, 'charitable_priority_sort' );
379
380 return $fields;
381 }
382
383 /**
384 * Return the donation form fields.
385 *
386 * @since 1.0.0
387 * @version 1.8.3.5
388 *
389 * @return array[]
390 */
391 public function get_fields() {
392 $fields = array(
393 'donation_fields' => array(
394 'legend' => _x( 'Your Donation', 'donation form amount section header', 'charitable' ),
395 'type' => 'donation-amount-wrapper',
396 'fields' => $this->get_donation_fields(),
397 'priority' => 20,
398 'multi_currency' => true,
399 ),
400 'details_fields' => array(
401 'legend' => __( 'Details', 'charitable' ),
402 'type' => 'details-fields',
403 'class' => 'fieldset',
404 'priority' => 40,
405 'fields' => array(
406 'donor_fields' => array(
407 'type' => 'donor-fields',
408 'fields' => $this->get_user_fields(),
409 'priority' => 44,
410 ),
411 'meta_fields' => array(
412 'type' => 'meta-fields',
413 'fields' => $this->get_meta_fields(),
414 'priority' => 48,
415 ),
416 ),
417 ),
418 );
419
420 $fields = $this->maybe_add_terms_conditions_fields( $fields );
421
422 $fields = $this->maybe_add_minimum_donation_amount( $fields );
423
424 /**
425 * Filter the donation form fields.
426 *
427 * @since 1.0.0
428 *
429 * @param array $fields The donation form fields.
430 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
431 */
432 $fields = apply_filters( 'charitable_donation_form_fields', $fields, $this );
433
434 uasort( $fields, 'charitable_priority_sort' );
435
436 return $fields;
437 }
438
439 /**
440 * Add payment fields to the donation form if necessary.
441 *
442 * @since 1.0.0
443 *
444 * @param array[] $fields All existing fields in the donation form.
445 * @return array[]
446 */
447 public function add_payment_fields( $fields ) {
448 $gateways_helper = charitable_get_helper( 'gateways' );
449 $default_gateway = $gateways_helper->get_default_gateway();
450 $gateways = array();
451 $has_gateway_fields = false;
452
453 foreach ( $gateways_helper->get_active_gateways() as $gateway_id => $gateway_class ) {
454 $gateway = new $gateway_class();
455 $gateway_fields = $this->add_credit_card_fields( array(), $gateway );
456
457 /**
458 * Filter the gateway fields.
459 *
460 * @since 1.0.0
461 *
462 * @param array $gateway_fields List of gateway fields.
463 * @param Charitable_Gateway $gateway Instance of `Charitable_Gateway`.
464 */
465 $gateway_fields = apply_filters( 'charitable_donation_form_gateway_fields', $gateway_fields, $gateway );
466 $has_gateway_fields = $has_gateway_fields || ! empty( $gateway_fields );
467 $gateways[ $gateway_id ] = array(
468 'label' => $gateway->get_label(),
469 'fields' => $gateway_fields,
470 );
471 }
472
473 /* Add the payment section if there are gateway fields to be filled out. */
474 if ( $has_gateway_fields || count( $gateways ) > 1 ) {
475 $fields['payment_fields'] = array(
476 'type' => 'gateway-fields',
477 'legend' => __( 'Payment', 'charitable' ),
478 'default' => $default_gateway,
479 'gateways' => $gateways,
480 'priority' => 60,
481 );
482 }
483
484 return $fields;
485 }
486
487 /**
488 * Include a paragraph showing the currently set donation
489 * amount before the amount form, if one is set.
490 *
491 * @since 1.4.14
492 * @since 1.5.0 $fields argument deprecated and return
493 * type changed to string.
494 *
495 * @return string
496 */
497 public function maybe_show_current_donation_amount() {
498 if ( ! $this->get_campaign() ) {
499 return '';
500 }
501
502 $amount = $this->get_campaign()->get_donation_amount_in_session();
503
504 if ( ! $amount ) {
505 $content = charitable_template_from_session_content(
506 'donation_form_current_amount_text',
507 array(
508 'campaign_id' => $this->get_campaign()->ID,
509 'form_id' => $this->get_form_identifier(),
510 ),
511 ''
512 );
513 } elseif ( defined( 'CHARITABLE_DISABLE_SHOW_CURRENT_DONATION_AMOUNT' ) && CHARITABLE_DISABLE_SHOW_CURRENT_DONATION_AMOUNT ) {
514 $content = charitable_template_from_session_content(
515 'donation_form_current_amount_text',
516 array(
517 'campaign_id' => $this->get_campaign()->ID,
518 'form_id' => $this->get_form_identifier(),
519 ),
520 ''
521 );
522 } else {
523 $content = charitable_template_donation_form_current_amount_text( $amount, $this->get_form_identifier(), $this->get_campaign()->ID );
524 }
525
526 return $content;
527 }
528
529 /**
530 * Add credit card fields to the donation form if this gateway requires it.
531 *
532 * @since 1.0.0
533 *
534 * @param array[] $fields Current gateway fields. Deprecated as of 1.5.0.
535 * @param Charitable_Gateway $gateway Instance of `Charitable_Gateway`.
536 * @return array[]
537 */
538 public function add_credit_card_fields( $fields, Charitable_Gateway $gateway ) {
539 if ( $gateway->supports( 'credit-card' ) ) {
540 $fields = array_merge( $fields, $gateway->get_credit_card_fields() );
541 }
542
543 return $fields;
544 }
545
546 /**
547 * Render the donation form.
548 *
549 * @since 1.0.0
550 * @version 1.8.3.6 added get_form_template()
551 *
552 * @return void
553 */
554 public function render() {
555 charitable_template(
556 'donation-form/form-donation.php',
557 array(
558 'campaign' => $this->get_campaign(),
559 'form_template' => $this->get_form_template(),
560 'form' => $this,
561 )
562 );
563 }
564
565 /**
566 * Return the validated donation ID.
567 *
568 * The donation is picked from a GET parameter or a POST. Either way,
569 * the donation ID is checked to ensure it corresponds to a donation
570 * and one that the current user should have access to.
571 *
572 * @since 1.5.14
573 * @version 1.8.9.1
574 *
575 * @return int
576 */
577 public function get_validated_donation_id() {
578 // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
579 if ( array_key_exists( 'donation_id', $_GET ) ) {
580 $donation_id = intval( wp_unslash( $_GET['donation_id'] ) );
581 } elseif ( array_key_exists( 'ID', $_POST ) ) {
582 $donation_id = intval( wp_unslash( $_POST['ID'] ) );
583 } else {
584 // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
585 return 0;
586 }
587 // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
588
589 if ( charitable_user_can_access_donation( $donation_id ) ) {
590 return $donation_id;
591 }
592
593 return 0;
594 }
595
596 /**
597 * Adds hidden fields to the start of the donation form.
598 *
599 * @since 1.5.0
600 *
601 * @return array
602 */
603 public function get_hidden_fields() {
604 $fields = parent::get_hidden_fields();
605
606 if ( ! is_null( $this->campaign ) ) {
607 $fields['campaign_id'] = $this->campaign->ID;
608 $fields['description'] = get_the_title( $this->campaign->ID );
609 }
610
611 $fields['ID'] = $this->get_validated_donation_id();
612
613 /**
614 * Filter the hidden fields in the donation form.
615 *
616 * @since 1.0.0
617 *
618 * @param string[] $fields The hidden fields as key=>value pairs.
619 * @param Charitable_Donation_Form $form The donation form instance.
620 */
621 return apply_filters( 'charitable_donation_form_hidden_fields', $fields, $this );
622 }
623
624 /**
625 * Set the gateway as a hidden field when there is only one gateway.
626 *
627 * @since 1.0.0
628 *
629 * @param string[] $fields The hidden fields as key=>value pairs.
630 * @return string[]
631 */
632 public function add_hidden_gateway_field( $fields ) {
633 $gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
634
635 if ( count( $gateways ) !== 1 ) {
636 return $fields;
637 }
638
639 $gateway_keys = array_keys( $gateways );
640
641 $fields['gateway'] = $gateway_keys[0];
642
643 return $fields;
644 }
645
646 /**
647 * Add a password field to the end of the form.
648 *
649 * @since 1.0.0
650 *
651 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
652 * @return void
653 */
654 public function add_password_field( $form ) {
655 if ( ! $form->is_current_form( $this->id ) ) {
656 return;
657 }
658
659 /* Make sure we are not logged in. */
660 if ( 0 !== wp_get_current_user()->ID ) {
661 return;
662 }
663
664 charitable_template( 'donation-form/user-login-fields.php' );
665 }
666
667 /**
668 * Validate the form submission.
669 *
670 * @since 1.0.0
671 *
672 * @return boolean
673 */
674 public function validate_submission() {
675 /* If we have already validated the submission, return the value. */
676 if ( $this->validated ) {
677 return $this->valid;
678 }
679
680 $this->validated = true;
681
682 $this->valid = $this->validate_security_check()
683 && $this->check_required_fields( $this->get_merged_fields() )
684 && $this->validate_email()
685 && $this->validate_amount()
686 && $this->validate_gateway();
687
688 /**
689 * Filter the overall validation result.
690 *
691 * @since 1.0.0
692 *
693 * @param boolean $ret The result to be returned. True or False.
694 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
695 */
696 $this->valid = apply_filters( 'charitable_validate_donation_form_submission', $this->valid, $this );
697
698 return $this->valid;
699 }
700
701 /**
702 * Checks whether the security checks (nonce and honeypot) pass.
703 *
704 * @since 1.4.6
705 *
706 * @return boolean
707 */
708 public function validate_security_check() {
709 $ret = true;
710
711 if ( ! $this->validate_nonce() || ! $this->validate_honeypot() ) {
712 charitable_get_notices()->add_error(
713 __( 'Unfortunately, we were unable to verify your form submission. Please reload the page and try again.', 'charitable' )
714 );
715
716 // Log the security validation failure
717 $error_details = ! $this->validate_nonce() ? 'nonce_failed' : 'honeypot_failed';
718 $context = array(
719 'campaign_id' => $this->campaign ? $this->campaign->ID : 0,
720 'amount' => $this->get_submitted_value( 'donation_amount' ),
721 'gateway' => $this->get_submitted_value( 'gateway' ),
722 );
723 charitable_log_form_error( 'security_failure', $error_details, $context );
724
725 $ret = false;
726 }
727
728 /**
729 * Filter the security validation result.
730 *
731 * @since 1.4.7
732 *
733 * @param boolean $ret The result to be returned. True or False.
734 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
735 */
736 return apply_filters( 'charitable_validate_donation_form_submission_security_check', $ret, $this );
737 }
738
739 /**
740 * Checks whether the submitted email is valid.
741 *
742 * @since 1.4.6
743 *
744 * @return boolean
745 */
746 public function validate_email() {
747 $ret = true;
748
749 /* Don't process donations with dummy emails. */
750 $submitted = $this->get_submitted_value( 'email' );
751 if ( ! is_null( $submitted ) && ! is_email( $submitted ) ) {
752 charitable_get_notices()->add_error(
753 sprintf(
754 /* translators: %s: email address */
755 __( '%s is not a valid email address.', 'charitable' ),
756 $submitted
757 )
758 );
759
760 // Log the email validation failure
761 $context = array(
762 'campaign_id' => $this->campaign ? $this->campaign->ID : 0,
763 'amount' => $this->get_submitted_value( 'donation_amount' ),
764 'gateway' => $this->get_submitted_value( 'gateway' ),
765 );
766 charitable_log_form_error( 'validation_failure', 'invalid_email', $context );
767
768 $ret = false;
769 }
770
771 /**
772 * Filter the email validation result.
773 *
774 * @since 1.4.7
775 *
776 * @param boolean $ret The result to be returned. True or False.
777 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
778 */
779 return apply_filters( 'charitable_validate_donation_form_submission_email_check', $ret, $this );
780 }
781
782 /**
783 * Checks whether the submitted gateway is valid.
784 *
785 * @since 1.4.6
786 *
787 * @return boolean
788 */
789 public function validate_gateway() {
790 $ret = true;
791
792 /* Validate the gateway. */
793 if ( ! Charitable_Gateways::get_instance()->is_valid_gateway( $this->get_submitted_value( 'gateway' ) ) ) {
794 charitable_get_notices()->add_error( __( 'The gateway submitted is not valid.', 'charitable' ) );
795 $ret = false;
796 }
797
798 /**
799 * Filter the gateway validation result.
800 *
801 * @since 1.4.7
802 *
803 * @param boolean $ret The result to be returned. True or False.
804 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
805 */
806 return apply_filters( 'charitable_validate_donation_form_submission_gateway_check', $ret, $this );
807 }
808
809 /**
810 * Checks whether the set amount is valid.
811 *
812 * @since 1.4.6
813 *
814 * @return boolean
815 */
816 public function validate_amount() {
817 $ret = true;
818
819 /* Ensure that a valid amount has been submitted. */
820 $submitted = $this->get_submitted_values();
821 $minimum = charitable_get_minimum_donation_amount( $submitted['campaign_id'] );
822 $maximum = charitable_get_maximum_donation_amount( $submitted['campaign_id'] );
823 $amount = self::get_donation_amount();
824
825 if ( $minimum > 0 && $amount < $minimum ) {
826 charitable_get_notices()->add_error(
827 sprintf(
828 /* translators: %s: minimum donation amount */
829 __( 'You must donate more than %s.', 'charitable' ),
830 charitable_format_money( $minimum, false, true )
831 )
832 );
833
834 // Log the minimum amount validation failure
835 $context = array(
836 'campaign_id' => $submitted['campaign_id'],
837 'amount' => $amount,
838 'gateway' => $this->get_submitted_value( 'gateway' ),
839 );
840 charitable_log_form_error( 'validation_failure', 'amount_below_minimum', $context );
841
842 $ret = false;
843 } elseif ( $maximum && $maximum < $amount ) {
844 charitable_get_notices()->add_error(
845 sprintf(
846 /* translators: %s: maximum donation amount */
847 __( 'The maximum donation amount permitted is %s.', 'charitable' ),
848 charitable_format_money( $maximum, false, true )
849 )
850 );
851
852 // Log the maximum amount validation failure
853 $context = array(
854 'campaign_id' => $submitted['campaign_id'],
855 'amount' => $amount,
856 'gateway' => $this->get_submitted_value( 'gateway' ),
857 );
858 charitable_log_form_error( 'validation_failure', 'amount_above_maximum', $context );
859
860 $ret = false;
861 } elseif ( $minimum === 0 && $amount <= 0 && ! apply_filters( 'charitable_permit_0_donation', false ) ) {
862 charitable_get_notices()->add_error(
863 sprintf(
864 /* translators: %s: minimum donation amount */
865 __( 'You must donate more than %s.', 'charitable' ),
866 charitable_format_money( $minimum )
867 )
868 );
869
870 // Log the zero amount validation failure
871 $context = array(
872 'campaign_id' => $submitted['campaign_id'],
873 'amount' => $amount,
874 'gateway' => $this->get_submitted_value( 'gateway' ),
875 );
876 charitable_log_form_error( 'validation_failure', 'zero_amount_not_permitted', $context );
877
878 $ret = false;
879 }
880
881 /**
882 * Filter the amount validation result.
883 *
884 * @since 1.4.7
885 *
886 * @param boolean $ret The result to be returned. True or False.
887 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
888 */
889 return apply_filters( 'charitable_validate_donation_form_submission_amount_check', $ret, $this );
890 }
891
892 /**
893 * Return the donation values.
894 *
895 * @since 1.0.0
896 *
897 * @return array
898 */
899 public function get_donation_values() {
900 $submitted = $this->get_submitted_values();
901 $values = array(
902 'ID' => $this->get_validated_donation_id(),
903 'user_id' => get_current_user_id(),
904 'gateway' => $submitted['gateway'],
905 'campaigns' => array(
906 array(
907 'campaign_id' => $submitted['campaign_id'],
908 'amount' => self::get_donation_amount(),
909 ),
910 ),
911 );
912
913 if ( 0 !== $values['ID'] ) {
914 $values['donation_key'] = charitable_get_donation( $values['ID'] )->get_donation_key();
915 }
916
917 foreach ( $this->get_merged_fields() as $key => $field ) {
918 if ( isset( $field['data_type'] ) || 'gateways' == $key ) {
919 if ( 'gateways' == $key ) {
920 foreach ( $field as $gateway_id => $gateway_fields ) {
921 foreach ( $gateway_fields as $key => $field ) {
922 if ( ! isset( $field['type'] ) ) {
923 continue;
924 }
925
926 $field_type = $field['type'];
927 $default = 'checkbox' == $field_type ? false : '';
928 $value = isset( $submitted[ $key ] ) ? $submitted[ $key ] : $default;
929
930 /* Strip extra spaces from the credit card number. */
931 if ( 'cc_number' == $key ) {
932 $value = trim( str_replace( ' ', '', $value ) );
933 }
934
935 $values['gateways'][ $gateway_id ][ $key ] = $value;
936 }
937 }
938 } elseif ( isset( $field['type'] ) ) {
939 $data_type = $field['data_type'];
940 $field_type = $field['type'];
941 $default = 'checkbox' == $field_type ? false : '';
942
943 $values[ $data_type ][ $key ] = isset( $submitted[ $key ] ) ? $submitted[ $key ] : $default;
944 }
945 }
946 }
947
948 /**
949 * Filter the submitted donation form values, before any processing happens.
950 *
951 * @since 1.0.0
952 *
953 * @param array $values The structured submitted values.
954 * @param array $submitted The raw submitted values (i.e. $_POST).
955 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
956 */
957 return apply_filters( 'charitable_donation_form_submission_values', $values, $submitted, $this );
958 }
959
960 /**
961 * Returns all fields as a merged array.
962 *
963 * @since 1.0.0
964 *
965 * @return array[]
966 */
967 public function get_merged_fields() {
968 if ( ! isset( $this->merged_fields ) ) {
969 $this->merged_fields = array();
970
971 foreach ( $this->get_fields() as $section_id => $section ) {
972 $this->merged_fields = array_merge( $this->merged_fields, $this->get_fields_from_section( $section_id, $section ) );
973 }
974 }
975
976 return $this->merged_fields;
977 }
978
979 /**
980 * Return the fields from a particular section.
981 *
982 * This will loop recursively, so if the section has a "fields" property,
983 * it will loop over those fields and call itself again for each of those.
984 *
985 * @since 1.6.0
986 *
987 * @param string $section_id The key of the section, or field.
988 * @param array $section The section arguments.
989 * @return array
990 */
991 public function get_fields_from_section( $section_id, $section ) {
992 if ( 'payment_fields' == $section_id ) {
993 $section_fields = array();
994
995 foreach ( $section['gateways'] as $gateway_id => $gateway_section ) {
996 if ( isset( $gateway_section['fields'] ) ) {
997 $section_fields['gateways'][ $gateway_id ] = $gateway_section['fields'];
998 }
999 }
1000
1001 return $section_fields;
1002 }
1003
1004 if ( array_key_exists( 'fields', $section ) ) {
1005 $section_fields = array();
1006
1007 foreach ( $section['fields'] as $section_id => $section ) {
1008 $section_fields = array_merge( $section_fields, $this->get_fields_from_section( $section_id, $section ) );
1009 }
1010
1011 return $section_fields;
1012 }
1013
1014 return array( $section_id => $section );
1015 }
1016
1017 /**
1018 * Checks whether the user has all required fields.
1019 *
1020 * @since 1.2.0
1021 *
1022 * @return boolean
1023 */
1024 public function user_has_required_fields() {
1025 if ( ! isset( $this->user_has_required_fields ) ) {
1026
1027 foreach ( $this->get_user_fields() as $field ) {
1028
1029 if ( ! isset( $field['required'] ) || false == $field['required'] ) {
1030 continue;
1031 }
1032
1033 if ( empty( $field['value'] ) ) {
1034 $this->user_has_required_fields = false;
1035 return $this->user_has_required_fields;
1036 }
1037 }
1038
1039 $this->user_has_required_fields = true;
1040 }
1041
1042 return $this->user_has_required_fields;
1043 }
1044
1045 /**
1046 * Maybe add minimum donation message to form.
1047 *
1048 * @since 1.7.0.4
1049 *
1050 * @param array $fields The registered donation form fields.
1051 * @return array
1052 */
1053 public function maybe_add_minimum_donation_amount( $fields ) {
1054
1055 if ( false === $this->get_campaign()->ID || false === $fields || ! isset( $fields['donation_fields']['fields'] ) ) {
1056 return $fields;
1057 }
1058
1059 if ( false === charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) || 'below_amount_selection' !== charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) ) {
1060 return $fields;
1061 }
1062
1063 $terms_fields = array();
1064 $minimum_amount = get_post_meta( $this->get_campaign()->ID, '_campaign_minimum_donation_amount', true );
1065
1066 // do not display a message in the donation form if the field is zero or empty.
1067 if ( false === $minimum_amount || '' === trim( $minimum_amount ) ) {
1068 return $fields;
1069 }
1070
1071 // default location of the minimum donation amount.
1072 $fields['donation_fields']['fields']['minimum_donation_amount'] = array(
1073 'legend' => __( 'Mininum Donation', 'charitable' ),
1074 'type' => 'content',
1075 'content' => $this->minimum_max_donation_amount_notice( true, $minimum_amount ),
1076 'priority' => 80,
1077 );
1078
1079 /**
1080 * Filter the minimum donation notification fields.
1081 *
1082 * @since 1.7.0.3
1083 *
1084 * @param array $terms_fields List of terms fields.
1085 * @param Charitable_Donation_Form $form Instance of `Charitable_Donation_Form`.
1086 */
1087 $minimum_fields = apply_filters( 'charitable_donation_minimum_fields', $fields, $this );
1088
1089 if ( empty( $minimum_fields ) ) {
1090 return $fields;
1091 }
1092
1093 return $fields;
1094 }
1095
1096
1097 /**
1098 * Generate minimum donation notice (with filter for changing text more easily).
1099 *
1100 * @since 1.8.0
1101 *
1102 * @param string $return False if echo, true if return.
1103 * @param string $minimum_amount Minimum amount likely set by user in admin settings.
1104 * @return array
1105 */
1106 public function minimum_max_donation_amount_notice( $return = true, $minimum_amount = false ) {
1107
1108 if ( false === $minimum_amount ) {
1109 $minimum_amount = get_post_meta( $this->get_campaign()->ID, '_campaign_minimum_donation_amount', true );
1110 }
1111
1112 // do not display a message in the donation form if the field is zero or empty.
1113 if ( false === $minimum_amount || '' === trim( $minimum_amount ) || 0 === intval( $minimum_amount ) ) {
1114 return '';
1115 } else {
1116 $message = apply_filters( 'charitable_donationa_amount_notice', '<p class="minimum-donation-amount-text">' . __( 'The minimum donation for this campaign is ', 'charitable' ) . charitable_format_money( $minimum_amount, false, true ) . '.</p>', $minimum_amount );
1117 }
1118
1119 if ( $return ) {
1120 return $message;
1121 } else {
1122 echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1123 }
1124 }
1125
1126
1127 /**
1128 * Maybe add terms and conditions fields to the form.
1129 *
1130 * @since 1.6.0
1131 *
1132 * @param array $fields The registered donation form fields.
1133 * @return array
1134 */
1135 public function maybe_add_terms_conditions_fields( $fields ) {
1136 $terms_fields = array();
1137 $show_consent = (int) charitable_get_option( 'contact_consent', 0 ) && Charitable_Upgrade::get_instance()->upgrade_has_been_completed( 'upgrade_donor_tables' );
1138
1139 if ( charitable_is_privacy_policy_activated() ) {
1140 $terms_fields['privacy_policy_text'] = array(
1141 'type' => 'content',
1142 'content' => '<p class="charitable-privacy-policy-text">' . charitable_get_privacy_policy_field_text() . '</p>',
1143 'priority' => 4,
1144 );
1145 }
1146
1147 if ( charitable_is_contact_consent_activated() ) {
1148 $terms_fields['contact_consent'] = array(
1149 'type' => 'checkbox',
1150 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
1151 'priority' => 8,
1152 'required' => charitable_get_option( 'contact_consent_required', false ),
1153 'data_type' => 'meta',
1154 );
1155 }
1156
1157 if ( charitable_is_terms_and_conditions_activated() ) {
1158 $terms_fields['terms_text'] = array(
1159 'type' => 'content',
1160 'content' => '<div class="charitable-terms-text">' . charitable_get_terms_and_conditions() . '</div>',
1161 'priority' => 12,
1162 );
1163
1164 $terms_fields['accept_terms'] = array(
1165 'type' => 'checkbox',
1166 'label' => charitable_get_terms_and_conditions_field_label(),
1167 'priority' => 16,
1168 'required' => true,
1169 'data_type' => 'meta',
1170 );
1171 }
1172
1173 /**
1174 * Filter the terms and conditions fields.
1175 *
1176 * @since 1.6.0
1177 *
1178 * @param array $terms_fields List of terms fields.
1179 * @param Charitable_Donation_Form $form Instance of `Charitable_Donation_Form`.
1180 */
1181 $terms_fields = apply_filters( 'charitable_donation_form_terms_fields', $terms_fields, $this );
1182
1183 if ( empty( $terms_fields ) ) {
1184 return $fields;
1185 }
1186
1187 return array_merge(
1188 $fields,
1189 array(
1190 'terms_fields' => array(
1191 'legend' => __( 'Terms and Conditions', 'charitable' ),
1192 'type' => 'fieldset',
1193 'fields' => $terms_fields,
1194 // Terms must be accepted before the donor commits to
1195 // a payment method, so order this fieldset just above
1196 // the Payment fieldset (priority 60). Donors see
1197 // consent → payment, not payment → consent.
1198 'priority' => 56,
1199 ),
1200 )
1201 );
1202 }
1203
1204 /**
1205 * Return the terms and conditions text.
1206 *
1207 * @deprecated 2.0.0
1208 *
1209 * @since 1.6.0
1210 * @since 1.6.14 Deprecated.
1211 *
1212 * @param int $terms_page The ID of the terms page.
1213 * @return string
1214 */
1215 public function get_terms( $terms_page ) {
1216 charitable_get_deprecated()->deprecated_function(
1217 __METHOD__,
1218 '1.6.14',
1219 'charitable_get_terms_and_conditions'
1220 );
1221
1222 return charitable_get_terms_and_conditions();
1223 }
1224
1225 /**
1226 * Return the terms text, with [terms] replaced by a link to the terms and conditions.
1227 *
1228 * @deprecated 2.0.0
1229 *
1230 * @since 1.6.0
1231 * @since 1.6.14 Deprecated.
1232 *
1233 * @param int $terms_page The ID of the terms page.
1234 * @return string
1235 */
1236 public function get_parsed_terms_text( $terms_page ) {
1237 charitable_get_deprecated()->deprecated_function(
1238 __METHOD__,
1239 '1.6.14',
1240 'charitable_get_terms_and_conditions_field_label'
1241 );
1242
1243 return charitable_get_terms_and_conditions_field_label();
1244 }
1245
1246 /**
1247 * Return the privacy policy text, with [privacy_policy] replaced by a link to the privacy policy page.
1248 *
1249 * @deprecated 2.0.0
1250 *
1251 * @since 1.6.0
1252 * @since 1.6.14 Deprecated.
1253 *
1254 * @param int $privacy_page The ID of the privacy policy page.
1255 * @return string
1256 */
1257 public function get_parsed_privacy_text( $privacy_page ) {
1258 charitable_get_deprecated()->deprecated_function(
1259 __METHOD__,
1260 '1.6.14',
1261 'charitable_get_privacy_policy_field_text'
1262 );
1263
1264 return charitable_get_privacy_policy_field_text();
1265 }
1266
1267 /**
1268 * Returns whether the form should hide the user fields.
1269 *
1270 * @since 1.6.0
1271 *
1272 * @return boolean
1273 */
1274 public function should_hide_user_fields() {
1275 $hide_fields = $this->get_user() && $this->user_has_required_fields() && ! is_customize_preview();
1276
1277 /**
1278 * Filter whether the user fields should be hidden.
1279 *
1280 * @since 1.6.0
1281 *
1282 * @param boolean $hide_fields Whether the fields should be hidden.
1283 * @param Charitable_Donation_Form $form The instance of `Charitable_Donation_Form`.
1284 */
1285 return apply_filters( 'charitable_hide_fields_for_logged_in_users', $hide_fields, $this );
1286 }
1287
1288 /**
1289 * Return the donation amount.
1290 *
1291 * @since 1.0.0
1292 * @version 1.8.9.1
1293 *
1294 * @return float
1295 */
1296 public static function get_donation_amount() {
1297 // phpcs:disable WordPress.Security.NonceVerification.Missing
1298 $amount = isset( $_POST['donation_amount'] ) ? sanitize_text_field( wp_unslash( $_POST['donation_amount'] ) ) : 0;
1299
1300 if ( 0 === $amount || 'custom' == $amount ) {
1301 $amount = isset( $_POST['custom_donation_amount'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_donation_amount'] ) ) : 0;
1302 }
1303 // phpcs:enable WordPress.Security.NonceVerification.Missing
1304
1305 /**
1306 * Only sanitize the amount if it's a number (no letters).
1307 *
1308 * With Recurring Donations, it's possible that amount might be 'recurring-custom',
1309 * which should not be sanitized.
1310 */
1311 if ( ! is_string( $amount ) || ! preg_match( '/[A-Za-z]/', $amount ) ) {
1312 $amount = charitable_get_currency_helper()->sanitize_monetary_amount( (string) $amount );
1313 }
1314
1315 /**
1316 * Filter the donation amount.
1317 *
1318 * @since 1.3.0
1319 *
1320 * @param float|WP_Error $amount The donation amount.
1321 */
1322 return apply_filters( 'charitable_donation_form_amount', $amount );
1323 }
1324
1325 /**
1326 * Set up payment fields based on the gateways that are installed and which one is default.
1327 *
1328 * @since 1.0.0
1329 *
1330 * @return void
1331 */
1332 protected function setup_payment_fields() {
1333 $active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
1334 $has_gateways = apply_filters( 'charitable_has_active_gateways', ! empty( $active_gateways ) );
1335
1336 /* If no gateways have been selected, display a notice and return the fields */
1337 if ( ! $has_gateways ) {
1338 charitable_get_notices()->add_error( $this->get_no_active_gateways_notice() );
1339 return;
1340 }
1341
1342 if ( count( $active_gateways ) == 1 ) {
1343 add_filter( 'charitable_donation_form_hidden_fields', array( $this, 'add_hidden_gateway_field' ) );
1344 }
1345
1346 add_action( 'charitable_donation_form_fields', array( $this, 'add_payment_fields' ) );
1347 }
1348
1349 /**
1350 * Set a field's initial value.
1351 *
1352 * @since 1.5.0
1353 * @version 1.8.9.1
1354 *
1355 * @param array $field Field definition.
1356 * @param string $key The key of the field.
1357 * @return array
1358 */
1359 protected function set_field_value( $field, $key ) {
1360 $donation_id = $this->get_validated_donation_id();
1361 // phpcs:disable WordPress.Security.NonceVerification.Missing
1362 $submitted = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : null;
1363 // phpcs:enable WordPress.Security.NonceVerification.Missing
1364
1365 /* Mark a checkbox as checked. */
1366 if ( 'checkbox' === $field['type'] ) {
1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
1368
1369 if ( ! is_null( $submitted ) ) {
1370 $field['checked'] = $submitted == $checked_value;
1371 } elseif ( $donation_id ) {
1372 $field['checked'] = charitable_get_donation( $donation_id )->get( $key ) == $checked_value;
1373 }
1374 } elseif ( ! is_null( $submitted ) ) {
1375 $field['value'] = $submitted;
1376 } elseif ( $donation_id ) {
1377 $donation = charitable_get_donation( $donation_id );
1378 $field['value'] = $donation ? $donation->get( $key ) : '';
1379 }
1380
1381 if ( array_key_exists( 'value', $field ) ) {
1382 return $field;
1383 }
1384
1385 if ( 'user' == $field['data_type'] ) {
1386 $field['value'] = $this->get_user_value( $key, $field['default'] );
1387 } else {
1388 $field['value'] = $field['default'];
1389 }
1390
1391 return $field;
1392 }
1393
1394 /**
1395 * Given a notice and an additional action that requires
1396 * admin credentials, return the appropriate notice for
1397 * the current user.
1398 *
1399 * @since 1.5.0
1400 *
1401 * @return string
1402 */
1403 protected function get_credentialed_notice( $notice, $admin_action ) {
1404 if ( current_user_can( 'manage_charitable_settings' ) ) {
1405 $notice .= '&nbsp;' . $admin_action;
1406 }
1407
1408 return $notice;
1409 }
1410
1411 /**
1412 * A formatted notice to advise that there are no gateways active.
1413 *
1414 * @since 1.0.0
1415 *
1416 * @return string
1417 */
1418 protected function get_no_active_gateways_notice() {
1419 $notice = $this->get_credentialed_notice(
1420 __( 'There are no active payment gateways.', 'charitable' ),
1421 sprintf( '<a href="%s">%s</a>.', admin_url( 'admin.php?page=charitable-settings&tab=gateways' ), _x( 'Enable one now', 'enable payment gateway', 'charitable' ) )
1422 );
1423
1424 return apply_filters( 'charitable_no_active_gateways_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1425 }
1426
1427 /**
1428 * Determine the status of Test Mode and display an alert if it is active
1429 *
1430 * @since 1.4.7
1431 *
1432 * @return void
1433 */
1434 protected function check_test_mode() {
1435 $in_test_mode = charitable_get_option( 'test_mode', 0 );
1436
1437 /* If test mode is enabled, and current user is an admin, display an alert on the form. */
1438 if ( $in_test_mode && current_user_can( 'manage_charitable_settings' ) ) {
1439 charitable_get_notices()->add_error( $this->get_test_mode_active_notice() );
1440 }
1441 }
1442
1443 /**
1444 * A formatted notice to advise that Test Mode is active.
1445 *
1446 * @since 1.4.7
1447 *
1448 * @return string
1449 */
1450 protected function get_test_mode_active_notice() {
1451 $notice = $this->get_credentialed_notice(
1452 __( 'Test mode is enabled.', 'charitable' ),
1453 sprintf(
1454 '<a href="%s">%s</a>.',
1455 admin_url( 'admin.php?page=charitable-settings&tab=gateways' ),
1456 __( 'Disable Test Mode', 'charitable' )
1457 )
1458 );
1459
1460 return apply_filters( 'charitable_test_mode_active_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1461 }
1462
1463 /**
1464 * Determine if there is a Stripe key set if Stripe is a payment gateway
1465 *
1466 * @since 1.8.2
1467 *
1468 * @return void
1469 */
1470 protected function stripe_key_check() {
1471
1472 if ( defined( 'CHARITABLE_DISABLE_STRIPE_KEY_CHECK' ) && CHARITABLE_DISABLE_STRIPE_KEY_CHECK ) {
1473 return;
1474 }
1475
1476 if ( ! class_exists( 'Charitable_Gateway_Stripe_AM' ) ) {
1477 return;
1478 }
1479
1480 $active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
1481 $has_gateways = apply_filters( 'charitable_has_active_gateways', ! empty( $active_gateways ) );
1482
1483 if ( ! $has_gateways ) {
1484 return;
1485 }
1486
1487 if ( ! is_array( $active_gateways ) || ! in_array( 'Charitable_Gateway_Stripe_AM', $active_gateways ) ) {
1488 return;
1489 }
1490
1491 // Stripe is enabled, check if the keys are set.
1492 $gateway = new Charitable_Gateway_Stripe_AM();
1493 $keys = $gateway->get_keys();
1494
1495 // If there is no public key, and current user is an admin, display an alert on the form. */
1496 if ( ( ! isset( $keys['public_key'] ) || empty( $keys['public_key'] ) ) && current_user_can( 'manage_charitable_settings' ) ) {
1497 charitable_get_notices()->add_error( $this->get_stripe_key_check_notice() );
1498 }
1499 }
1500
1501 /**
1502 * A formatted notice to advise that Test Mode is active.
1503 *
1504 * @since 1.8.2
1505 *
1506 * @return string
1507 */
1508 protected function get_stripe_key_check_notice() {
1509 $notice = $this->get_credentialed_notice(
1510 __( 'Stripe is enabled but some keys are missing.', 'charitable' ),
1511 sprintf(
1512 '<a href="%s">%s</a>.',
1513 admin_url( 'admin.php?page=charitable-settings&tab=gateways&group=gateways_stripe' ),
1514 __( 'View Settings', 'charitable' )
1515 )
1516 );
1517
1518 return apply_filters( 'charitable_stripe_key_check_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1519 }
1520
1521 /**
1522 * Check if Square connection is available for the current mode.
1523 *
1524 * @since 1.8.5
1525 *
1526 * @return void
1527 */
1528 protected function square_connection_check() {
1529 // Check if there's a Square mode connection warning
1530 $warning = get_option( 'charitable_square_mode_connection_warning' );
1531
1532 if ( ! $warning ) {
1533 return;
1534 }
1535
1536 // Check if Square gateway is active
1537 $active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
1538
1539 if ( ! is_array( $active_gateways ) || ! in_array( 'Charitable_Gateway_Square', $active_gateways ) ) {
1540 return;
1541 }
1542
1543 // Only show warning to admins
1544 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1545 return;
1546 }
1547
1548 // Display the warning
1549 charitable_get_notices()->add_error( $this->get_square_connection_warning_notice( $warning['mode'] ) );
1550 }
1551
1552 /**
1553 * A formatted notice to advise that Square connection is missing for the current mode.
1554 *
1555 * @since 1.8.5
1556 *
1557 * @param string $mode The mode that's missing connection.
1558 * @return string
1559 */
1560 protected function get_square_connection_warning_notice( $mode ) {
1561 $mode_label = 'test' === $mode ? __( 'test', 'charitable' ) : __( 'live', 'charitable' );
1562
1563 $notice = $this->get_credentialed_notice(
1564 sprintf(
1565 /* translators: %s: mode (test or live) */
1566 __( 'Square is enabled but not connected in %s mode.', 'charitable' ),
1567 $mode_label
1568 ),
1569 sprintf(
1570 '<a href="%s">%s</a>.',
1571 admin_url( 'admin.php?page=charitable-settings&tab=gateways&group=gateways_square_core' ),
1572 __( 'Connect Square Account', 'charitable' )
1573 )
1574 );
1575
1576 return apply_filters( 'charitable_square_connection_warning_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1577 }
1578
1579 /**
1580 * Tracks whether the donor-facing payment list was stripped of
1581 * PayPal Commerce because the seller account wasn't connected.
1582 * Drives the donor-facing "donations temporarily unavailable"
1583 * message when this is the only reason there are no gateways.
1584 *
1585 * @since 1.8.11
1586 * @var bool
1587 */
1588 protected static $paypal_commerce_hidden_for_disconnect = false;
1589
1590 /**
1591 * Determine PayPal Commerce's connection status and warn admins when
1592 * the gateway is enabled but no PayPal account is connected.
1593 *
1594 * Also wires the runtime filters that:
1595 * - hide PayPal Commerce from the donor-facing payment list when
1596 * it's not connected, so donors never see a broken option, and
1597 * - swap the generic "no active payment gateways" notice for a
1598 * donor-friendly "temporarily unavailable" message when PayPal
1599 * was the only enabled gateway.
1600 *
1601 * Donors never see the admin notice — capability-gated through
1602 * get_credentialed_notice() — but they do see the donor-side
1603 * graceful message.
1604 *
1605 * @since 1.8.11
1606 *
1607 * @return void
1608 */
1609 protected function paypal_commerce_connection_check() {
1610 if ( defined( 'CHARITABLE_DISABLE_PAYPAL_COMMERCE_CHECK' ) && CHARITABLE_DISABLE_PAYPAL_COMMERCE_CHECK ) {
1611 return;
1612 }
1613
1614 if ( ! class_exists( 'Charitable_Gateway_Paypal_Commerce' ) ) {
1615 return;
1616 }
1617
1618 $active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
1619 $has_gateways = apply_filters( 'charitable_has_active_gateways', ! empty( $active_gateways ) );
1620
1621 if ( ! $has_gateways ) {
1622 return;
1623 }
1624
1625 if ( ! is_array( $active_gateways ) || ! array_key_exists( 'paypal_commerce', $active_gateways ) ) {
1626 return;
1627 }
1628
1629 $gateway = new Charitable_Gateway_Paypal_Commerce();
1630
1631 if ( $gateway->is_seller_connected() ) {
1632 return;
1633 }
1634
1635 // Wire the runtime filters once per request — gateway list cleanup
1636 // and the donor-friendly fallback notice.
1637 add_filter( 'charitable_active_gateways', array( $this, 'filter_active_gateways_hide_disconnected_paypal_commerce' ) );
1638 add_filter( 'charitable_no_active_gateways_notice', array( $this, 'filter_no_active_gateways_notice_when_paypal_disconnected' ), 10, 2 );
1639
1640 // Admin-only notice in the form-level error list (matches Stripe pattern).
1641 if ( current_user_can( 'manage_charitable_settings' ) ) { // phpcs:ignore
1642 charitable_get_notices()->add_error( $this->get_paypal_commerce_connection_check_notice() );
1643 }
1644 }
1645
1646 /**
1647 * Build the admin-only notice text for the PayPal Commerce
1648 * "enabled but not connected" state. Donors never see this — the
1649 * admin-action link is only appended when the current user can
1650 * manage Charitable settings.
1651 *
1652 * @since 1.8.11
1653 *
1654 * @return string
1655 */
1656 protected function get_paypal_commerce_connection_check_notice() {
1657 $notice = $this->get_credentialed_notice(
1658 __( 'PayPal Commerce is enabled but no PayPal account is connected.', 'charitable' ),
1659 sprintf(
1660 '<a href="%s">%s</a>.',
1661 admin_url( 'admin.php?page=charitable-settings&tab=gateways&group=gateways_paypal_commerce' ),
1662 __( 'Connect with PayPal', 'charitable' )
1663 )
1664 );
1665
1666 return apply_filters( 'charitable_paypal_commerce_connection_check_notice', $notice, current_user_can( 'manage_charitable_settings' ) ); // phpcs:ignore
1667 }
1668
1669 /**
1670 * Remove PayPal Commerce from the active gateway list when its seller
1671 * isn't connected. Donors then never see the option on the donation
1672 * form, and the gateway-selector form field doesn't render a broken
1673 * radio that would have failed at PayPal-side anyway.
1674 *
1675 * Flips $paypal_commerce_hidden_for_disconnect so the companion
1676 * `charitable_no_active_gateways_notice` filter can swap the
1677 * standard message for the donor-friendly variant when PayPal was
1678 * the only enabled gateway.
1679 *
1680 * @since 1.8.11
1681 *
1682 * @param array $gateways Active gateway map (gateway_id => class name).
1683 * @return array
1684 */
1685 public function filter_active_gateways_hide_disconnected_paypal_commerce( $gateways ) {
1686 if ( ! is_array( $gateways ) || ! array_key_exists( 'paypal_commerce', $gateways ) ) {
1687 return $gateways;
1688 }
1689
1690 unset( $gateways['paypal_commerce'] );
1691 self::$paypal_commerce_hidden_for_disconnect = true;
1692
1693 return $gateways;
1694 }
1695
1696 /**
1697 * Replace the "no active payment gateways" message with a
1698 * donor-friendly fallback when PayPal Commerce is the only
1699 * enabled gateway and we just stripped it for being disconnected.
1700 * Admins still get an actionable link; donors see graceful copy.
1701 *
1702 * @since 1.8.11
1703 *
1704 * @param string $notice Default notice text.
1705 * @param bool $is_admin Whether the current user can manage Charitable settings.
1706 * @return string
1707 */
1708 public function filter_no_active_gateways_notice_when_paypal_disconnected( $notice, $is_admin = false ) {
1709 if ( ! self::$paypal_commerce_hidden_for_disconnect ) {
1710 return $notice;
1711 }
1712
1713 if ( $is_admin ) {
1714 return $this->get_credentialed_notice(
1715 __( 'PayPal Commerce is enabled but no PayPal account is connected.', 'charitable' ),
1716 sprintf(
1717 '<a href="%s">%s</a>.',
1718 admin_url( 'admin.php?page=charitable-settings&tab=gateways&group=gateways_paypal_commerce' ),
1719 __( 'Connect with PayPal', 'charitable' )
1720 )
1721 );
1722 }
1723
1724 return __( 'Online donations are temporarily unavailable. Please check back soon.', 'charitable' );
1725 }
1726
1727 /**
1728 * Return the donor value fields.
1729 *
1730 * @since 1.0.0
1731 *
1732 * @param array $submitted The submitted values.
1733 * @return string[]
1734 */
1735 protected function get_donor_value_fields( $submitted ) {
1736 $donor_fields = array();
1737
1738 if ( isset( $submitted['first_name'] ) ) {
1739 $donor_fields['first_name'] = $submitted['first_name'];
1740 }
1741
1742 if ( isset( $submitted['last_name'] ) ) {
1743 $donor_fields['last_name'] = $submitted['last_name'];
1744 }
1745
1746 if ( isset( $submitted['user_email'] ) ) {
1747 $donor_fields['email'] = $submitted['user_email'];
1748 }
1749
1750 return $donor_fields;
1751 }
1752
1753 /**
1754 * Checks whether the form submission contains profile fields.
1755 *
1756 * @since 1.0.0
1757 *
1758 * @return boolean
1759 */
1760 protected function has_profile_fields( $submitted, $user_fields ) {
1761 foreach ( $user_fields as $key => $field ) {
1762 if ( $field['requires_registration'] && isset( $submitted[ $key ] ) ) {
1763 return true;
1764 }
1765 }
1766
1767 return false;
1768 }
1769
1770 /**
1771 * Returns true if required fields are missing.
1772 *
1773 * @since 1.0.0
1774 *
1775 * @param array $required_fields
1776 * @return boolean
1777 */
1778 protected function is_missing_required_fields( $required_fields ) {
1779 if ( is_user_logged_in() ) {
1780 return false;
1781 }
1782
1783 if ( is_null( $this->get_submitted_value( 'gateway' ) ) ) {
1784 charitable_get_notices()->add_error(
1785 sprintf(
1786 '<p>%s</p>',
1787 __( 'Your donation could not be processed. No payment gateway was selected.', 'charitable' )
1788 )
1789 );
1790
1791 return false;
1792 }
1793
1794 return ! $this->check_required_fields( $required_fields );
1795 }
1796
1797 /**
1798 * Use custom template for some form fields.
1799 *
1800 * @since 1.0.0
1801 * @since 1.5.0 Deprecated. This is handled by `Charitable_Public_Form_View` now.
1802 *
1803 * @param string|false $custom_template Custom template.
1804 * @return string|false|Charitable_Template
1805 */
1806 public function use_custom_templates( $custom_template ) {
1807 charitable_get_deprecated()->deprecated_function(
1808 __METHOD__,
1809 '1.5.0'
1810 );
1811
1812 return $custom_template;
1813 }
1814 }
1815
1816 endif;
1817