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 +480 -64 1.7.0.3 → 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',
@@ -377,10 +420,8 @@
377 420 $fields = $this->maybe_add_terms_conditions_fields( $fields );
378 421
379 422 $fields = $this->maybe_add_minimum_donation_amount( $fields );
380 423
381 - // print_r( $fields ); exit;
382 -
383 424 /**
384 425 * Filter the donation form fields.
385 426 *
386 427 * @since 1.0.0
@@ -409,9 +450,9 @@
409 450 $gateways = array();
410 451 $has_gateway_fields = false;
411 452
412 453 foreach ( $gateways_helper->get_active_gateways() as $gateway_id => $gateway_class ) {
413 - $gateway = new $gateway_class;
454 + $gateway = new $gateway_class();
414 455 $gateway_fields = $this->add_credit_card_fields( array(), $gateway );
415 456
416 457 /**
417 458 * Filter the gateway fields.
@@ -468,8 +509,17 @@
468 509 'form_id' => $this->get_form_identifier(),
469 510 ),
470 511 ''
471 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 + );
472 522 } else {
473 523 $content = charitable_template_donation_form_current_amount_text( $amount, $this->get_form_identifier(), $this->get_campaign()->ID );
474 524 }
475 525
@@ -495,9 +545,10 @@
495 545
496 546 /**
497 547 * Render the donation form.
498 548 *
499 - * @since 1.0.0
549 + * @since 1.0.0
550 + * @version 1.8.3.6 added get_form_template()
500 551 *
501 552 * @return void
502 553 */
503 554 public function render() {
@@ -503,10 +554,11 @@
503 554 public function render() {
504 555 charitable_template(
505 556 'donation-form/form-donation.php',
506 557 array(
507 - 'campaign' => $this->get_campaign(),
508 - 'form' => $this,
558 + 'campaign' => $this->get_campaign(),
559 + 'form_template' => $this->get_form_template(),
560 + 'form' => $this,
509 561 )
510 562 );
511 563 }
512 564
@@ -517,19 +569,23 @@
517 569 * the donation ID is checked to ensure it corresponds to a donation
518 570 * and one that the current user should have access to.
519 571 *
520 572 * @since 1.5.14
573 + * @version 1.8.9.1
521 574 *
522 575 * @return int
523 576 */
524 577 public function get_validated_donation_id() {
578 + // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
525 579 if ( array_key_exists( 'donation_id', $_GET ) ) {
526 - $donation_id = $_GET['donation_id'];
580 + $donation_id = intval( wp_unslash( $_GET['donation_id'] ) );
527 581 } elseif ( array_key_exists( 'ID', $_POST ) ) {
528 - $donation_id = $_POST['ID'];
582 + $donation_id = intval( wp_unslash( $_POST['ID'] ) );
529 583 } else {
584 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
530 585 return 0;
531 586 }
587 + // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
532 588
533 589 if ( charitable_user_can_access_donation( $donation_id ) ) {
534 590 return $donation_id;
535 591 }
@@ -656,8 +712,17 @@
656 712 charitable_get_notices()->add_error(
657 713 __( 'Unfortunately, we were unable to verify your form submission. Please reload the page and try again.', 'charitable' )
658 714 );
659 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 +
660 725 $ret = false;
661 726 }
662 727
663 728 /**
@@ -683,14 +748,24 @@
683 748
684 749 /* Don't process donations with dummy emails. */
685 750 $submitted = $this->get_submitted_value( 'email' );
686 751 if ( ! is_null( $submitted ) && ! is_email( $submitted ) ) {
687 - charitable_get_notices()->add_error( sprintf(
752 + charitable_get_notices()->add_error(
753 + sprintf(
688 754 /* translators: %s: email address */
689 - __( '%s is not a valid email address.', 'charitable' ),
690 - $submitted
691 - ) );
755 + __( '%s is not a valid email address.', 'charitable' ),
756 + $submitted
757 + )
758 + );
692 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 +
693 768 $ret = false;
694 769 }
695 770
696 771 /**
@@ -728,9 +803,8 @@
728 803 * @param boolean $ret The result to be returned. True or False.
729 804 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
730 805 */
731 806 return apply_filters( 'charitable_validate_donation_form_submission_gateway_check', $ret, $this );
732 -
733 807 }
734 808
735 809 /**
736 810 * Checks whether the set amount is valid.
@@ -750,29 +824,58 @@
750 824
751 825 if ( $minimum > 0 && $amount < $minimum ) {
752 826 charitable_get_notices()->add_error(
753 827 sprintf(
828 + /* translators: %s: minimum donation amount */
754 829 __( 'You must donate more than %s.', 'charitable' ),
755 830 charitable_format_money( $minimum, false, true )
756 831 )
757 832 );
758 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 +
759 842 $ret = false;
760 843 } elseif ( $maximum && $maximum < $amount ) {
761 844 charitable_get_notices()->add_error(
762 845 sprintf(
846 + /* translators: %s: maximum donation amount */
763 847 __( 'The maximum donation amount permitted is %s.', 'charitable' ),
764 848 charitable_format_money( $maximum, false, true )
765 849 )
766 850 );
767 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 +
768 860 $ret = false;
769 861 } elseif ( $minimum === 0 && $amount <= 0 && ! apply_filters( 'charitable_permit_0_donation', false ) ) {
770 - charitable_get_notices()->add_error( sprintf(
771 - __( 'You must donate more than %s.', 'charitable' ),
772 - charitable_format_money( $minimum )
773 - ) );
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 + );
774 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 +
775 878 $ret = false;
776 879 }
777 880
778 881 /**
@@ -783,9 +886,8 @@
783 886 * @param boolean $ret The result to be returned. True or False.
784 887 * @param Charitable_Donation_Form $form This instance of `Charitable_Donation_Form`.
785 888 */
786 889 return apply_filters( 'charitable_validate_donation_form_submission_amount_check', $ret, $this );
787 -
788 890 }
789 891
790 892 /**
791 893 * Return the donation values.
@@ -940,11 +1042,11 @@
940 1042 return $this->user_has_required_fields;
941 1043 }
942 1044
943 1045 /**
944 - * Maybe add terms and conditions fields to the form.
1046 + * Maybe add minimum donation message to form.
945 1047 *
946 - * @since 1.6.0
1048 + * @since 1.7.0.4
947 1049 *
948 1050 * @param array $fields The registered donation form fields.
949 1051 * @return array
950 1052 */
@@ -953,8 +1055,12 @@
953 1055 if ( false === $this->get_campaign()->ID || false === $fields || ! isset( $fields['donation_fields']['fields'] ) ) {
954 1056 return $fields;
955 1057 }
956 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 +
957 1063 $terms_fields = array();
958 1064 $minimum_amount = get_post_meta( $this->get_campaign()->ID, '_campaign_minimum_donation_amount', true );
959 1065
960 1066 // do not display a message in the donation form if the field is zero or empty.
@@ -961,12 +1067,13 @@
961 1067 if ( false === $minimum_amount || '' === trim( $minimum_amount ) ) {
962 1068 return $fields;
963 1069 }
964 1070
1071 + // default location of the minimum donation amount.
965 1072 $fields['donation_fields']['fields']['minimum_donation_amount'] = array(
966 - 'legend' => __( 'Mininum Donation Test', 'charitable' ),
1073 + 'legend' => __( 'Mininum Donation', 'charitable' ),
967 1074 'type' => 'content',
968 - 'content' => '<p class="minimum-donation-amount-text">' . __( 'The minimum donation for this campaign is ' . charitable_format_money( $minimum_amount, false, true ), 'charitable' ) . '.</p>',
1075 + 'content' => $this->minimum_max_donation_amount_notice( true, $minimum_amount ),
969 1076 'priority' => 80,
970 1077 );
971 1078
972 1079 /**
@@ -987,8 +1094,38 @@
987 1094 }
988 1095
989 1096
990 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 + /**
991 1128 * Maybe add terms and conditions fields to the form.
992 1129 *
993 1130 * @since 1.6.0
994 1131 *
@@ -1011,9 +1148,9 @@
1011 1148 $terms_fields['contact_consent'] = array(
1012 1149 'type' => 'checkbox',
1013 1150 'label' => charitable_get_option( 'contact_consent_label', __( 'Yes, I am happy for you to contact me via email or phone.', 'charitable' ) ),
1014 1151 'priority' => 8,
1015 - 'required' => false,
1152 + 'required' => charitable_get_option( 'contact_consent_required', false ),
1016 1153 'data_type' => 'meta',
1017 1154 );
1018 1155 }
1019 1156
@@ -1046,16 +1183,23 @@
1046 1183 if ( empty( $terms_fields ) ) {
1047 1184 return $fields;
1048 1185 }
1049 1186
1050 - return array_merge( $fields, array(
1051 - 'terms_fields' => array(
1052 - 'legend' => __( 'Terms and Conditions', 'charitable' ),
1053 - 'type' => 'fieldset',
1054 - 'fields' => $terms_fields,
1055 - 'priority' => 80,
1056 - ),
1057 - ) );
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 + );
1058 1202 }
1059 1203
1060 1204 /**
1061 1205 * Return the terms and conditions text.
@@ -1144,17 +1288,20 @@
1144 1288 /**
1145 1289 * Return the donation amount.
1146 1290 *
1147 1291 * @since 1.0.0
1292 + * @version 1.8.9.1
1148 1293 *
1149 1294 * @return float
1150 1295 */
1151 1296 public static function get_donation_amount() {
1152 - $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;
1153 1299
1154 1300 if ( 0 === $amount || 'custom' == $amount ) {
1155 - $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;
1156 1302 }
1303 + // phpcs:enable WordPress.Security.NonceVerification.Missing
1157 1304
1158 1305 /**
1159 1306 * Only sanitize the amount if it's a number (no letters).
1160 1307 *
@@ -1202,8 +1349,9 @@
1202 1349 /**
1203 1350 * Set a field's initial value.
1204 1351 *
1205 1352 * @since 1.5.0
1353 + * @version 1.8.9.1
1206 1354 *
1207 1355 * @param array $field Field definition.
1208 1356 * @param string $key The key of the field.
1209 1357 * @return array
@@ -1209,9 +1357,11 @@
1209 1357 * @return array
1210 1358 */
1211 1359 protected function set_field_value( $field, $key ) {
1212 1360 $donation_id = $this->get_validated_donation_id();
1213 - $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
1214 1364
1215 1365 /* Mark a checkbox as checked. */
1216 1366 if ( 'checkbox' === $field['type'] ) {
1217 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
@@ -1217,18 +1367,16 @@
1217 1367 $checked_value = isset( $field['value'] ) ? $field['value'] : 1;
1218 1368
1219 1369 if ( ! is_null( $submitted ) ) {
1220 1370 $field['checked'] = $submitted == $checked_value;
1221 - } elseif( $donation_id ) {
1371 + } elseif ( $donation_id ) {
1222 1372 $field['checked'] = charitable_get_donation( $donation_id )->get( $key ) == $checked_value;
1223 1373 }
1224 - } else {
1225 - if ( ! is_null( $submitted ) ) {
1374 + } elseif ( ! is_null( $submitted ) ) {
1226 1375 $field['value'] = $submitted;
1227 - } elseif ( $donation_id ) {
1228 - $donation = charitable_get_donation( $donation_id );
1229 - $field['value'] = $donation ? $donation->get( $key ) : '';
1230 - }
1376 + } elseif ( $donation_id ) {
1377 + $donation = charitable_get_donation( $donation_id );
1378 + $field['value'] = $donation ? $donation->get( $key ) : '';
1231 1379 }
1232 1380
1233 1381 if ( array_key_exists( 'value', $field ) ) {
1234 1382 return $field;
@@ -1300,10 +1448,11 @@
1300 1448 * @return string
1301 1449 */
1302 1450 protected function get_test_mode_active_notice() {
1303 1451 $notice = $this->get_credentialed_notice(
1304 - __( 'Test mode is active.', 'charitable' ),
1305 - sprintf( '<a href="%s">%s</a>.',
1452 + __( 'Test mode is enabled.', 'charitable' ),
1453 + sprintf(
1454 + '<a href="%s">%s</a>.',
1306 1455 admin_url( 'admin.php?page=charitable-settings&tab=gateways' ),
1307 1456 __( 'Disable Test Mode', 'charitable' )
1308 1457 )
1309 1458 );
@@ -1311,8 +1460,272 @@
1311 1460 return apply_filters( 'charitable_test_mode_active_notice', $notice, current_user_can( 'manage_charitable_settings' ) );
1312 1461 }
1313 1462
1314 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 + /**
1315 1728 * Return the donor value fields.
1316 1729 *
1317 1730 * @since 1.0.0
1318 1731 *
@@ -1358,9 +1771,9 @@
1358 1771 * Returns true if required fields are missing.
1359 1772 *
1360 1773 * @since 1.0.0
1361 1774 *
1362 - * @param array $required_fields
1775 + * @param array $required_fields
1363 1776 * @return boolean
1364 1777 */
1365 1778 protected function is_missing_required_fields( $required_fields ) {
1366 1779 if ( is_user_logged_in() ) {
@@ -1367,11 +1780,14 @@
1367 1780 return false;
1368 1781 }
1369 1782
1370 1783 if ( is_null( $this->get_submitted_value( 'gateway' ) ) ) {
1371 - charitable_get_notices()->add_error( sprintf( '<p>%s</p>',
1372 - __( 'Your donation could not be processed. No payment gateway was selected.', 'charitable' )
1373 - ) );
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 + );
1374 1790
1375 1791 return false;
1376 1792 }
1377 1793
@@ -1383,9 +1799,9 @@
1383 1799 *
1384 1800 * @since 1.0.0
1385 1801 * @since 1.5.0 Deprecated. This is handled by `Charitable_Public_Form_View` now.
1386 1802 *
1387 - * @param string|false $custom_template
1803 + * @param string|false $custom_template Custom template.
1388 1804 * @return string|false|Charitable_Template
1389 1805 */
1390 1806 public function use_custom_templates( $custom_template ) {
1391 1807 charitable_get_deprecated()->deprecated_function(