PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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
← All changes | includes/forms/class-charitable-donation-form.php +453 -69 1.7.0.9 → 1.8.13 View file →
@@ -3,9 +3,9 @@
3 3 * Donation form model class.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Donation_Form
6 6 * @author David Bisset
7 - * @copyright Copyright (c) 2022, WP Charitable LLC
7 + * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 9 * @since 1.0.0
10 10 * @version 1.6.60
11 11 */
@@ -105,12 +105,22 @@
105 105 */
106 106 protected $valid;
107 107
108 108 /**
109 + * Cached merged fields array.
110 + *
111 + * @since 1.0.0
112 + *
113 + * @var array
114 + */
115 + protected $merged_fields;
116 +
117 + /**
109 118 * Create a donation form object.
110 119 *
111 120 * @since 1.0.0
112 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.
113 123 *
114 124 * @param Charitable_Campaign|null $campaign Optional. Campaign receiving the donation, or NULL if
115 125 * the campaign will be selected in the form.
116 126 */
@@ -117,21 +127,30 @@
117 127 public function __construct( Charitable_Campaign $campaign = null ) {
118 128 $this->campaign = $campaign;
119 129 $this->id = uniqid();
120 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();
121 140 $this->setup_payment_fields();
122 141 $this->check_test_mode();
142 + $this->stripe_key_check();
143 + $this->square_connection_check();
123 144
124 145 /* For backwards-compatibility */
125 146 add_action( 'charitable_form_field', array( $this, 'render_field' ), 10, 6 );
126 147 /* added in 1.7.0.4 */
127 148 if ( 'below_donation_title' === charitable_get_option( 'donation_form_minimal_amount_notice_display', false ) ) {
128 - add_action( 'charitable_after_donation_amount_wrapper_header', array( $this, 'minimum_donation_amount_notice' ), 10, 2 );
129 - } else if ( '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 ) ) {
130 - add_action( 'charitable_before_donation_amount_wrapper_header', array( $this, 'minimum_donation_amount_notice' ), 10, 2 );
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 );
131 152 }
132 -
133 -
134 153 }
135 154
136 155 /**
137 156 * Returns the campaign associated with this donation form object.
@@ -145,8 +164,19 @@
145 164 return $this->campaign;
146 165 }
147 166
148 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 + /**
149 179 * Return the current user.
150 180 *
151 181 * @since 1.0.0
152 182 *
@@ -164,8 +194,9 @@
164 194 /**
165 195 * Returns the set value for a particular user field.
166 196 *
167 197 * @since 1.0.0
198 + * @version 1.8.9.1
168 199 *
169 200 * @param string $key The field key.
170 201 * @param string $default Optional. The value that will be used if none is set.
171 202 * @return mixed
@@ -170,11 +201,13 @@
170 201 * @param string $default Optional. The value that will be used if none is set.
171 202 * @return mixed
172 203 */
173 204 public function get_user_value( $key, $default = '' ) {
205 + // phpcs:disable WordPress.Security.NonceVerification.Missing
174 206 if ( isset( $_POST[ $key ] ) ) {
175 - return $_POST[ $key ];
207 + return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
176 208 }
209 + // phpcs:enable WordPress.Security.NonceVerification.Missing
177 210
178 211 $donation_id = $this->get_validated_donation_id();
179 212
180 213 if ( $donation_id ) {
@@ -292,25 +325,25 @@
292 325 */
293 326 public function get_user_account_fields() {
294 327 $account_fields = array(
295 328 'user_pass' => array(
296 - 'label' => __( 'Password', 'charitable' ),
297 - 'type' => 'password',
298 - 'priority' => 4,
299 - 'required' => true,
329 + 'label' => __( 'Password', 'charitable' ),
330 + 'type' => 'password',
331 + 'priority' => 4,
332 + 'required' => true,
300 333 'requires_registration' => true,
301 - 'data_type' => 'user',
334 + 'data_type' => 'user',
302 335 ),
303 336 );
304 337
305 338 if ( apply_filters( 'charitable_donor_usernames', false ) ) {
306 339 $account_fields['user_login'] = array(
307 - 'label' => __( 'Username', 'charitable' ),
308 - 'type' => 'text',
309 - 'priority' => 2,
310 - 'required' => true,
340 + 'label' => __( 'Username', 'charitable' ),
341 + 'type' => 'text',
342 + 'priority' => 2,
343 + 'required' => true,
311 344 'requires_registration' => true,
312 - 'data_type' => 'user',
345 + 'data_type' => 'user',
313 346 );
314 347 }
315 348
316 349 return $account_fields;
@@ -350,8 +383,9 @@
350 383 /**
351 384 * Return the donation form fields.
352 385 *
353 386 * @since 1.0.0
387 + * @version 1.8.3.5
354 388 *
355 389 * @return array[]
356 390 */
357 391 public function get_fields() {
@@ -356,12 +390,13 @@
356 390 */
357 391 public function get_fields() {
358 392 $fields = array(
359 393 'donation_fields' => array(
360 - 'legend' => _x( 'Your Donation', 'donation form amount section header', 'charitable' ),
361 - 'type' => 'donation-amount-wrapper',
362 - 'fields' => $this->get_donation_fields(),
363 - 'priority' => 20,
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,
364 399 ),
365 400 'details_fields' => array(
366 401 'legend' => __( 'Details', 'charitable' ),
367 402 'type' => 'details-fields',
@@ -415,9 +450,9 @@
415 450 $gateways = array();
416 451 $has_gateway_fields = false;
417 452
418 453 foreach ( $gateways_helper->get_active_gateways() as $gateway_id => $gateway_class ) {
419 - $gateway = new $gateway_class;
454 + $gateway = new $gateway_class();
420 455 $gateway_fields = $this->add_credit_card_fields( array(), $gateway );
421 456
422 457 /**
423 458 * Filter the gateway fields.
@@ -474,8 +509,17 @@
474 509 'form_id' => $this->get_form_identifier(),
475 510 ),
476 511 ''
477 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 + );
478 522 } else {
479 523 $content = charitable_template_donation_form_current_amount_text( $amount, $this->get_form_identifier(), $this->get_campaign()->ID );
480 524 }
481 525
@@ -501,9 +545,10 @@
501 545
502 546 /**
503 547 * Render the donation form.
504 548 *
505 - * @since 1.0.0
549 + * @since 1.0.0
550 + * @version 1.8.3.6 added get_form_template()
506 551 *
507 552 * @return void
508 553 */
509 554 public function render() {
@@ -509,10 +554,11 @@
509 554 public function render() {
510 555 charitable_template(
511 556 'donation-form/form-donation.php',
512 557 array(
513 - 'campaign' => $this->get_campaign(),
514 - 'form' => $this,
558 + 'campaign' => $this->get_campaign(),
559 + 'form_template' => $this->get_form_template(),
560 + 'form' => $this,
515 561 )
516 562 );
517 563 }
518 564
@@ -523,19 +569,23 @@
523 569 * the donation ID is checked to ensure it corresponds to a donation
524 570 * and one that the current user should have access to.
525 571 *
526 572 * @since 1.5.14
573 + * @version 1.8.9.1
527 574 *
528 575 * @return int
529 576 */
530 577 public function get_validated_donation_id() {
578 + // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
531 579 if ( array_key_exists( 'donation_id', $_GET ) ) {
532 - $donation_id = $_GET['donation_id'];
580 + $donation_id = intval( wp_unslash( $_GET['donation_id'] ) );
533 581 } elseif ( array_key_exists( 'ID', $_POST ) ) {
534 - $donation_id = $_POST['ID'];
582 + $donation_id = intval( wp_unslash( $_POST['ID'] ) );
535 583 } else {
584 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
536 585 return 0;
537 586 }
587 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
538 588
539 589 if ( charitable_user_can_access_donation( $donation_id ) ) {
540 590 return $donation_id;
541 591 }
@@ -662,8 +712,17 @@
662 712 charitable_get_notices()->add_error(
663 713 __( 'Unfortunately, we were unable to verify your form submission. Please reload the page and try again.', 'charitable' )
664 714 );
665 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 +
666 725 $ret = false;
667 726 }
668 727
669 728 /**
@@ -689,14 +748,24 @@
689 748
690 749 /* Don't process donations with dummy emails. */
691 750 $submitted = $this->get_submitted_value( 'email' );
692 751 if ( ! is_null( $submitted ) && ! is_email( $submitted ) ) {
693 - charitable_get_notices()->add_error( sprintf(
752 + charitable_get_notices()->add_error(
753 + sprintf(
694 754 /* translators: %s: email address */
695 - __( '%s is not a valid email address.', 'charitable' ),
696 - $submitted
697 - ) );
755 + __( '%s is not a valid email address.', 'charitable' ),
756 + $submitted
757 + )
758 + );
698 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 +
699 768 $ret = false;
700 769 }
701 770
702 771 /**
@@ -734,9 +803,8 @@
734 803 * @param boolean $ret The result to be returned. True or False.
735 804 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
736 805 */
737 806 return apply_filters( 'charitable_validate_donation_form_submission_gateway_check', $ret, $this );
738 -
739 807 }
740 808
741 809 /**
742 810 * Checks whether the set amount is valid.
@@ -756,29 +824,58 @@
756 824
757 825 if ( $minimum > 0 && $amount < $minimum ) {
758 826 charitable_get_notices()->add_error(
759 827 sprintf(
828 + /* translators: %s: minimum donation amount */
760 829 __( 'You must donate more than %s.', 'charitable' ),
761 830 charitable_format_money( $minimum, false, true )
762 831 )
763 832 );
764 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 +
765 842 $ret = false;
766 843 } elseif ( $maximum && $maximum < $amount ) {
767 844 charitable_get_notices()->add_error(
768 845 sprintf(
846 + /* translators: %s: maximum donation amount */
769 847 __( 'The maximum donation amount permitted is %s.', 'charitable' ),
770 848 charitable_format_money( $maximum, false, true )
771 849 )
772 850 );
773 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 +
774 860 $ret = false;
775 861 } elseif ( $minimum === 0 && $amount <= 0 && ! apply_filters( 'charitable_permit_0_donation', false ) ) {
776 - charitable_get_notices()->add_error( sprintf(
777 - __( 'You must donate more than %s.', 'charitable' ),
778 - charitable_format_money( $minimum )
779 - ) );
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 + );
780 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 +
781 878 $ret = false;
782 879 }
783 880
784 881 /**
@@ -789,9 +886,8 @@
789 886 * @param boolean $ret The result to be returned. True or False.
790 887 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
791 888 */
792 889 return apply_filters( 'charitable_validate_donation_form_submission_amount_check', $ret, $this );
793 -
794 890 }
795 891
796 892 /**
797 893 * Return the donation values.
@@ -975,9 +1071,9 @@
975 1071 // default location of the minimum donation amount.
976 1072 $fields['donation_fields']['fields']['minimum_donation_amount'] = array(
977 1073 'legend' => __( 'Mininum Donation', 'charitable' ),
978 1074 'type' => 'content',
979 - 'content' => $this->minimum_donation_amount_notice( true, $minimum_amount ),
1075 + 'content' => $this->minimum_max_donation_amount_notice( true, $minimum_amount ),
980 1076 'priority' => 80,
981 1077 );
982 1078
983 1079 /**
@@ -996,27 +1092,36 @@
996 1092
997 1093 return $fields;
998 1094 }
999 1095
1000 - public function minimum_donation_amount_notice( $return = true, $minimum_amount = false ) {
1001 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 +
1002 1108 if ( false === $minimum_amount ) {
1003 1109 $minimum_amount = get_post_meta( $this->get_campaign()->ID, '_campaign_minimum_donation_amount', true );
1004 1110 }
1005 1111
1006 1112 // do not display a message in the donation form if the field is zero or empty.
1007 - if ( false === $minimum_amount || '' === trim( $minimum_amount ) ) {
1113 + if ( false === $minimum_amount || '' === trim( $minimum_amount ) || 0 === intval( $minimum_amount ) ) {
1008 1114 return '';
1009 1115 } else {
1010 - $message = '<p class="minimum-donation-amount-text">' . __( 'The minimum donation for this campaign is ', 'charitable' ) . charitable_format_money( $minimum_amount, false, true ) . '.</p>';
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 );
1011 1117 }
1012 1118
1013 1119 if ( $return ) {
1014 1120 return $message;
1015 1121 } else {
1016 - echo $message;
1122 + echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1017 1123 }
1018 -
1019 1124 }
1020 1125
1021 1126
1022 1127 /**
@@ -1043,9 +1148,9 @@
1043 1148 $terms_fields['contact_consent'] = array(
1044 1149 'type' => 'checkbox',
1045 1150 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
1046 1151 'priority' => 8,
1047 - 'required' => false,
1152 + 'required' => charitable_get_option( 'contact_consent_required', false ),
1048 1153 'data_type' => 'meta',
1049 1154 );
1050 1155 }
1051 1156
@@ -1078,16 +1183,23 @@
1078 1183 if ( empty( $terms_fields ) ) {
1079 1184 return $fields;
1080 1185 }
1081 1186
1082 - return array_merge( $fields, array(
1083 - 'terms_fields' => array(
1084 - 'legend' => __( 'Terms and Conditions', 'charitable' ),
1085 - 'type' => 'fieldset',
1086 - 'fields' => $terms_fields,
1087 - 'priority' => 80,
1088 - ),
1089 - ) );
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 + );
1090 1202 }
1091 1203
1092 1204 /**
1093 1205 * Return the terms and conditions text.
@@ -1176,17 +1288,20 @@
1176 1288 /**
1177 1289 * Return the donation amount.
1178 1290 *
1179 1291 * @since 1.0.0
1292 + * @version 1.8.9.1
1180 1293 *
1181 1294 * @return float
1182 1295 */
1183 1296 public static function get_donation_amount() {
1184 - $amount = isset( $_POST['donation_amount'] ) ? $_POST['donation_amount'] : 0;
1297 + // phpcs:disable WordPress.Security.NonceVerification.Missing
1298 + $amount = isset( $_POST['donation_amount'] ) ? sanitize_text_field( wp_unslash( $_POST['donation_amount'] ) ) : 0;
1185 1299
1186 1300 if ( 0 === $amount || 'custom' == $amount ) {
1187 - $amount = isset( $_POST['custom_donation_amount'] ) ? $_POST['custom_donation_amount'] : 0;
1301 + $amount = isset( $_POST['custom_donation_amount'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_donation_amount'] ) ) : 0;
1188 1302 }
1303 + // phpcs:enable WordPress.Security.NonceVerification.Missing
1189 1304
1190 1305 /**
1191 1306 * Only sanitize the amount if it's a number (no letters).
1192 1307 *
@@ -1234,8 +1349,9 @@
1234 1349 /**
1235 1350 * Set a field's initial value.
1236 1351 *
1237 1352 * @since 1.5.0
1353 + * @version 1.8.9.1
1238 1354 *
1239 1355 * @param array $field Field definition.
1240 1356 * @param string $key The key of the field.
1241 1357 * @return array
@@ -1241,9 +1357,11 @@
1241 1357 * @return array
1242 1358 */
1243 1359 protected function set_field_value( $field, $key ) {
1244 1360 $donation_id = $this->get_validated_donation_id();
1245 - $submitted = isset( $_POST[ $key ] ) ? $_POST[ $key ] : null;
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
1246 1364
1247 1365 /* Mark a checkbox as checked. */
1248 1366 if ( 'checkbox' === $field['type'] ) {
1249 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
@@ -1249,18 +1367,16 @@
1249 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
1250 1368
1251 1369 if ( ! is_null( $submitted ) ) {
1252 1370 $field['checked'] = $submitted == $checked_value;
1253 - } elseif( $donation_id ) {
1371 + } elseif ( $donation_id ) {
1254 1372 $field['checked'] = charitable_get_donation( $donation_id )->get( $key ) == $checked_value;
1255 1373 }
1256 - } else {
1257 - if ( ! is_null( $submitted ) ) {
1374 + } elseif ( ! is_null( $submitted ) ) {
1258 1375 $field['value'] = $submitted;
1259 - } elseif ( $donation_id ) {
1260 - $donation = charitable_get_donation( $donation_id );
1261 - $field['value'] = $donation ? $donation->get( $key ) : '';
1262 - }
1376 + } elseif ( $donation_id ) {
1377 + $donation = charitable_get_donation( $donation_id );
1378 + $field['value'] = $donation ? $donation->get( $key ) : '';
1263 1379 }
1264 1380
1265 1381 if ( array_key_exists( 'value', $field ) ) {
1266 1382 return $field;
@@ -1332,10 +1448,11 @@
1332 1448 * @return string
1333 1449 */
1334 1450 protected function get_test_mode_active_notice() {
1335 1451 $notice = $this->get_credentialed_notice(
1336 - __( 'Test mode is active.', 'charitable' ),
1337 - sprintf( '<a href="%s">%s</a>.',
1452 + __( 'Test mode is enabled.', 'charitable' ),
1453 + sprintf(
1454 + '<a href="%s">%s</a>.',
1338 1455 admin_url( 'admin.php?page=charitable-settings&tab=gateways' ),
1339 1456 __( 'Disable Test Mode', 'charitable' )
1340 1457 )
1341 1458 );
@@ -1343,8 +1460,272 @@
1343 1460 return apply_filters( 'charitable_test_mode_active_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1344 1461 }
1345 1462
1346 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 + /**
1347 1728 * Return the donor value fields.
1348 1729 *
1349 1730 * @since 1.0.0
1350 1731 *
@@ -1390,9 +1771,9 @@
1390 1771 * Returns true if required fields are missing.
1391 1772 *
1392 1773 * @since 1.0.0
1393 1774 *
1394 - * @param array $required_fields
1775 + * @param array $required_fields
1395 1776 * @return boolean
1396 1777 */
1397 1778 protected function is_missing_required_fields( $required_fields ) {
1398 1779 if ( is_user_logged_in() ) {
@@ -1399,11 +1780,14 @@
1399 1780 return false;
1400 1781 }
1401 1782
1402 1783 if ( is_null( $this->get_submitted_value( 'gateway' ) ) ) {
1403 - charitable_get_notices()->add_error( sprintf( '<p>%s</p>',
1404 - __( 'Your donation could not be processed. No payment gateway was selected.', 'charitable' )
1405 - ) );
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 + );
1406 1790
1407 1791 return false;
1408 1792 }
1409 1793
@@ -1415,9 +1799,9 @@
1415 1799 *
1416 1800 * @since 1.0.0
1417 1801 * @since 1.5.0 Deprecated. This is handled by `Charitable_Public_Form_View` now.
1418 1802 *
1419 - * @param string|false $custom_template
1803 + * @param string|false $custom_template Custom template.
1420 1804 * @return string|false|Charitable_Template
1421 1805 */
1422 1806 public function use_custom_templates( $custom_template ) {
1423 1807 charitable_get_deprecated()->deprecated_function(