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 +525 -58 1.7.0.2 → 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,13 +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 );
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 + }
126 153 }
127 154
128 155 /**
129 156 * Returns the campaign associated with this donation form object.
@@ -137,8 +164,19 @@
137 164 return $this->campaign;
138 165 }
139 166
140 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 + /**
141 179 * Return the current user.
142 180 *
143 181 * @since 1.0.0
144 182 *
@@ -156,8 +194,9 @@
156 194 /**
157 195 * Returns the set value for a particular user field.
158 196 *
159 197 * @since 1.0.0
198 + * @version 1.8.9.1
160 199 *
161 200 * @param string $key The field key.
162 201 * @param string $default Optional. The value that will be used if none is set.
163 202 * @return mixed
@@ -162,11 +201,13 @@
162 201 * @param string $default Optional. The value that will be used if none is set.
163 202 * @return mixed
164 203 */
165 204 public function get_user_value( $key, $default = '' ) {
205 + // phpcs:disable WordPress.Security.NonceVerification.Missing
166 206 if ( isset( $_POST[ $key ] ) ) {
167 - return $_POST[ $key ];
207 + return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
168 208 }
209 + // phpcs:enable WordPress.Security.NonceVerification.Missing
169 210
170 211 $donation_id = $this->get_validated_donation_id();
171 212
172 213 if ( $donation_id ) {
@@ -284,25 +325,25 @@
284 325 */
285 326 public function get_user_account_fields() {
286 327 $account_fields = array(
287 328 'user_pass' => array(
288 - 'label' => __( 'Password', 'charitable' ),
289 - 'type' => 'password',
290 - 'priority' => 4,
291 - 'required' => true,
329 + 'label' => __( 'Password', 'charitable' ),
330 + 'type' => 'password',
331 + 'priority' => 4,
332 + 'required' => true,
292 333 'requires_registration' => true,
293 - 'data_type' => 'user',
334 + 'data_type' => 'user',
294 335 ),
295 336 );
296 337
297 338 if ( apply_filters( 'charitable_donor_usernames', false ) ) {
298 339 $account_fields['user_login'] = array(
299 - 'label' => __( 'Username', 'charitable' ),
300 - 'type' => 'text',
301 - 'priority' => 2,
302 - 'required' => true,
340 + 'label' => __( 'Username', 'charitable' ),
341 + 'type' => 'text',
342 + 'priority' => 2,
343 + 'required' => true,
303 344 'requires_registration' => true,
304 - 'data_type' => 'user',
345 + 'data_type' => 'user',
305 346 );
306 347 }
307 348
308 349 return $account_fields;
@@ -342,8 +383,9 @@
342 383 /**
343 384 * Return the donation form fields.
344 385 *
345 386 * @since 1.0.0
387 + * @version 1.8.3.5
346 388 *
347 389 * @return array[]
348 390 */
349 391 public function get_fields() {
@@ -348,12 +390,13 @@
348 390 */
349 391 public function get_fields() {
350 392 $fields = array(
351 393 'donation_fields' => array(
352 - 'legend' => _x( 'Your Donation', 'donation form amount section header', 'charitable' ),
353 - 'type' => 'donation-amount-wrapper',
354 - 'fields' => $this->get_donation_fields(),
355 - '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,
356 399 ),
357 400 'details_fields' => array(
358 401 'legend' => __( 'Details', 'charitable' ),
359 402 'type' => 'details-fields',
@@ -375,8 +418,10 @@
375 418 );
376 419
377 420 $fields = $this->maybe_add_terms_conditions_fields( $fields );
378 421
422 + $fields = $this->maybe_add_minimum_donation_amount( $fields );
423 +
379 424 /**
380 425 * Filter the donation form fields.
381 426 *
382 427 * @since 1.0.0
@@ -405,9 +450,9 @@
405 450 $gateways = array();
406 451 $has_gateway_fields = false;
407 452
408 453 foreach ( $gateways_helper->get_active_gateways() as $gateway_id => $gateway_class ) {
409 - $gateway = new $gateway_class;
454 + $gateway = new $gateway_class();
410 455 $gateway_fields = $this->add_credit_card_fields( array(), $gateway );
411 456
412 457 /**
413 458 * Filter the gateway fields.
@@ -464,8 +509,17 @@
464 509 'form_id' => $this->get_form_identifier(),
465 510 ),
466 511 ''
467 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 + );
468 522 } else {
469 523 $content = charitable_template_donation_form_current_amount_text( $amount, $this->get_form_identifier(), $this->get_campaign()->ID );
470 524 }
471 525
@@ -491,9 +545,10 @@
491 545
492 546 /**
493 547 * Render the donation form.
494 548 *
495 - * @since 1.0.0
549 + * @since 1.0.0
550 + * @version 1.8.3.6 added get_form_template()
496 551 *
497 552 * @return void
498 553 */
499 554 public function render() {
@@ -499,10 +554,11 @@
499 554 public function render() {
500 555 charitable_template(
501 556 'donation-form/form-donation.php',
502 557 array(
503 - 'campaign' => $this->get_campaign(),
504 - 'form' => $this,
558 + 'campaign' => $this->get_campaign(),
559 + 'form_template' => $this->get_form_template(),
560 + 'form' => $this,
505 561 )
506 562 );
507 563 }
508 564
@@ -513,19 +569,23 @@
513 569 * the donation ID is checked to ensure it corresponds to a donation
514 570 * and one that the current user should have access to.
515 571 *
516 572 * @since 1.5.14
573 + * @version 1.8.9.1
517 574 *
518 575 * @return int
519 576 */
520 577 public function get_validated_donation_id() {
578 + // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
521 579 if ( array_key_exists( 'donation_id', $_GET ) ) {
522 - $donation_id = $_GET['donation_id'];
580 + $donation_id = intval( wp_unslash( $_GET['donation_id'] ) );
523 581 } elseif ( array_key_exists( 'ID', $_POST ) ) {
524 - $donation_id = $_POST['ID'];
582 + $donation_id = intval( wp_unslash( $_POST['ID'] ) );
525 583 } else {
584 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
526 585 return 0;
527 586 }
587 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
528 588
529 589 if ( charitable_user_can_access_donation( $donation_id ) ) {
530 590 return $donation_id;
531 591 }
@@ -652,8 +712,17 @@
652 712 charitable_get_notices()->add_error(
653 713 __( 'Unfortunately, we were unable to verify your form submission. Please reload the page and try again.', 'charitable' )
654 714 );
655 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 +
656 725 $ret = false;
657 726 }
658 727
659 728 /**
@@ -679,14 +748,24 @@
679 748
680 749 /* Don't process donations with dummy emails. */
681 750 $submitted = $this->get_submitted_value( 'email' );
682 751 if ( ! is_null( $submitted ) && ! is_email( $submitted ) ) {
683 - charitable_get_notices()->add_error( sprintf(
752 + charitable_get_notices()->add_error(
753 + sprintf(
684 754 /* translators: %s: email address */
685 - __( '%s is not a valid email address.', 'charitable' ),
686 - $submitted
687 - ) );
755 + __( '%s is not a valid email address.', 'charitable' ),
756 + $submitted
757 + )
758 + );
688 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 +
689 768 $ret = false;
690 769 }
691 770
692 771 /**
@@ -724,9 +803,8 @@
724 803 * @param boolean $ret The result to be returned. True or False.
725 804 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
726 805 */
727 806 return apply_filters( 'charitable_validate_donation_form_submission_gateway_check', $ret, $this );
728 -
729 807 }
730 808
731 809 /**
732 810 * Checks whether the set amount is valid.
@@ -746,29 +824,58 @@
746 824
747 825 if ( $minimum > 0 && $amount < $minimum ) {
748 826 charitable_get_notices()->add_error(
749 827 sprintf(
828 + /* translators: %s: minimum donation amount */
750 829 __( 'You must donate more than %s.', 'charitable' ),
751 830 charitable_format_money( $minimum, false, true )
752 831 )
753 832 );
754 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 +
755 842 $ret = false;
756 843 } elseif ( $maximum && $maximum < $amount ) {
757 844 charitable_get_notices()->add_error(
758 845 sprintf(
846 + /* translators: %s: maximum donation amount */
759 847 __( 'The maximum donation amount permitted is %s.', 'charitable' ),
760 848 charitable_format_money( $maximum, false, true )
761 849 )
762 850 );
763 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 +
764 860 $ret = false;
765 861 } elseif ( $minimum === 0 && $amount <= 0 && ! apply_filters( 'charitable_permit_0_donation', false ) ) {
766 - charitable_get_notices()->add_error( sprintf(
767 - __( 'You must donate more than %s.', 'charitable' ),
768 - charitable_format_money( $minimum )
769 - ) );
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 + );
770 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 +
771 878 $ret = false;
772 879 }
773 880
774 881 /**
@@ -779,9 +886,8 @@
779 886 * @param boolean $ret The result to be returned. True or False.
780 887 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
781 888 */
782 889 return apply_filters( 'charitable_validate_donation_form_submission_amount_check', $ret, $this );
783 -
784 890 }
785 891
786 892 /**
787 893 * Return the donation values.
@@ -936,8 +1042,90 @@
936 1042 return $this->user_has_required_fields;
937 1043 }
938 1044
939 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 + /**
940 1128 * Maybe add terms and conditions fields to the form.
941 1129 *
942 1130 * @since 1.6.0
943 1131 *
@@ -960,9 +1148,9 @@
960 1148 $terms_fields['contact_consent'] = array(
961 1149 'type' => 'checkbox',
962 1150 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
963 1151 'priority' => 8,
964 - 'required' => false,
1152 + 'required' => charitable_get_option( 'contact_consent_required', false ),
965 1153 'data_type' => 'meta',
966 1154 );
967 1155 }
968 1156
@@ -995,16 +1183,23 @@
995 1183 if ( empty( $terms_fields ) ) {
996 1184 return $fields;
997 1185 }
998 1186
999 - return array_merge( $fields, array(
1000 - 'terms_fields' => array(
1001 - 'legend' => __( 'Terms and Conditions', 'charitable' ),
1002 - 'type' => 'fieldset',
1003 - 'fields' => $terms_fields,
1004 - 'priority' => 80,
1005 - ),
1006 - ) );
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 + );
1007 1202 }
1008 1203
1009 1204 /**
1010 1205 * Return the terms and conditions text.
@@ -1093,17 +1288,20 @@
1093 1288 /**
1094 1289 * Return the donation amount.
1095 1290 *
1096 1291 * @since 1.0.0
1292 + * @version 1.8.9.1
1097 1293 *
1098 1294 * @return float
1099 1295 */
1100 1296 public static function get_donation_amount() {
1101 - $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;
1102 1299
1103 1300 if ( 0 === $amount || 'custom' == $amount ) {
1104 - $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;
1105 1302 }
1303 + // phpcs:enable WordPress.Security.NonceVerification.Missing
1106 1304
1107 1305 /**
1108 1306 * Only sanitize the amount if it's a number (no letters).
1109 1307 *
@@ -1151,8 +1349,9 @@
1151 1349 /**
1152 1350 * Set a field's initial value.
1153 1351 *
1154 1352 * @since 1.5.0
1353 + * @version 1.8.9.1
1155 1354 *
1156 1355 * @param array $field Field definition.
1157 1356 * @param string $key The key of the field.
1158 1357 * @return array
@@ -1158,9 +1357,11 @@
1158 1357 * @return array
1159 1358 */
1160 1359 protected function set_field_value( $field, $key ) {
1161 1360 $donation_id = $this->get_validated_donation_id();
1162 - $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
1163 1364
1164 1365 /* Mark a checkbox as checked. */
1165 1366 if ( 'checkbox' === $field['type'] ) {
1166 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
@@ -1166,18 +1367,16 @@
1166 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
1167 1368
1168 1369 if ( ! is_null( $submitted ) ) {
1169 1370 $field['checked'] = $submitted == $checked_value;
1170 - } elseif( $donation_id ) {
1371 + } elseif ( $donation_id ) {
1171 1372 $field['checked'] = charitable_get_donation( $donation_id )->get( $key ) == $checked_value;
1172 1373 }
1173 - } else {
1174 - if ( ! is_null( $submitted ) ) {
1374 + } elseif ( ! is_null( $submitted ) ) {
1175 1375 $field['value'] = $submitted;
1176 - } elseif ( $donation_id ) {
1177 - $donation = charitable_get_donation( $donation_id );
1178 - $field['value'] = $donation ? $donation->get( $key ) : '';
1179 - }
1376 + } elseif ( $donation_id ) {
1377 + $donation = charitable_get_donation( $donation_id );
1378 + $field['value'] = $donation ? $donation->get( $key ) : '';
1180 1379 }
1181 1380
1182 1381 if ( array_key_exists( 'value', $field ) ) {
1183 1382 return $field;
@@ -1249,10 +1448,11 @@
1249 1448 * @return string
1250 1449 */
1251 1450 protected function get_test_mode_active_notice() {
1252 1451 $notice = $this->get_credentialed_notice(
1253 - __( 'Test mode is active.', 'charitable' ),
1254 - sprintf( '<a href="%s">%s</a>.',
1452 + __( 'Test mode is enabled.', 'charitable' ),
1453 + sprintf(
1454 + '<a href="%s">%s</a>.',
1255 1455 admin_url( 'admin.php?page=charitable-settings&tab=gateways' ),
1256 1456 __( 'Disable Test Mode', 'charitable' )
1257 1457 )
1258 1458 );
@@ -1260,8 +1460,272 @@
1260 1460 return apply_filters( 'charitable_test_mode_active_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1261 1461 }
1262 1462
1263 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 + /**
1264 1728 * Return the donor value fields.
1265 1729 *
1266 1730 * @since 1.0.0
1267 1731 *
@@ -1307,9 +1771,9 @@
1307 1771 * Returns true if required fields are missing.
1308 1772 *
1309 1773 * @since 1.0.0
1310 1774 *
1311 - * @param array $required_fields
1775 + * @param array $required_fields
1312 1776 * @return boolean
1313 1777 */
1314 1778 protected function is_missing_required_fields( $required_fields ) {
1315 1779 if ( is_user_logged_in() ) {
@@ -1316,11 +1780,14 @@
1316 1780 return false;
1317 1781 }
1318 1782
1319 1783 if ( is_null( $this->get_submitted_value( 'gateway' ) ) ) {
1320 - charitable_get_notices()->add_error( sprintf( '<p>%s</p>',
1321 - __( 'Your donation could not be processed. No payment gateway was selected.', 'charitable' )
1322 - ) );
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 + );
1323 1790
1324 1791 return false;
1325 1792 }
1326 1793
@@ -1332,9 +1799,9 @@
1332 1799 *
1333 1800 * @since 1.0.0
1334 1801 * @since 1.5.0 Deprecated. This is handled by `Charitable_Public_Form_View` now.
1335 1802 *
1336 - * @param string|false $custom_template
1803 + * @param string|false $custom_template Custom template.
1337 1804 * @return string|false|Charitable_Template
1338 1805 */
1339 1806 public function use_custom_templates( $custom_template ) {
1340 1807 charitable_get_deprecated()->deprecated_function(