PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/helper.php +218 -58 1.4.0 → 1.6.1 View file →
@@ -9,8 +9,9 @@
9 9
10 10 use SureDonation\Inc\API\Settings_API;
11 11 use SureDonation\Inc\Database\Tables\Donations;
12 12 use SureDonation\Inc\Emails\Email_Handler;
13 +use SureDonation\Inc\Field_Validation;
13 14 use SureDonation\Inc\Payments\Payment_Helper;
14 15
15 16 // Exit if accessed directly.
16 17 if ( ! defined( 'ABSPATH' ) ) {
@@ -217,9 +218,9 @@
217 218 *
218 219 * @param array<int|string, mixed> $blocks Parsed blocks (parse_blocks output).
219 220 * @param string $target Block name to look for.
220 221 * @return bool
221 - * @since 1.4.0
222 + * @since 1.5.1
222 223 */
223 224 public static function block_tree_contains( $blocks, $target ) {
224 225 foreach ( $blocks as $block ) {
225 226 if ( ! is_array( $block ) ) {
@@ -574,54 +575,66 @@
574 575 */
575 576 public static function get_allowed_form_html() {
576 577 // Note: data-* wildcard doesn't work in wp_kses, so we list each data attribute explicitly.
577 578 $common_data_attrs = [
578 - 'data-block-id' => true,
579 - 'data-form-id' => true,
580 - 'data-gateway' => true,
581 - 'data-stripe-key' => true,
582 - 'data-currency' => true,
583 - 'data-payment-mode' => true,
584 - 'data-amount-type' => true,
585 - 'data-fixed-amount' => true,
586 - 'data-payment-type' => true,
587 - 'data-customer-name-field' => true,
588 - 'data-customer-email-field' => true,
589 - 'data-nonce' => true,
590 - 'data-variable-amount-field' => true,
591 - 'data-minimum-amount' => true,
592 - 'data-subscription-plan-name' => true,
593 - 'data-subscription-interval' => true,
594 - 'data-subscription-billing-cycles' => true,
595 - 'data-currency-symbol' => true,
596 - 'data-message-format' => true,
597 - 'data-payment-methods' => true,
598 - 'data-payment-available' => true,
599 - 'data-method' => true,
600 - 'data-slug' => true,
601 - 'data-required' => true,
602 - 'data-fee-percentage' => true,
603 - 'data-fee-fixed' => true,
604 - 'data-fee-mode' => true,
605 - 'data-gateway-fees' => true,
606 - 'data-invalid-email-msg' => true,
607 - 'data-invalid-url-msg' => true,
608 - 'data-sd-mask' => true,
609 - 'data-custom-sd-mask' => true,
579 + 'data-block-id' => true,
580 + 'data-form-id' => true,
581 + 'data-gateway' => true,
582 + 'data-stripe-key' => true,
583 + 'data-currency' => true,
584 + 'data-payment-mode' => true,
585 + 'data-amount-type' => true,
586 + 'data-fixed-amount' => true,
587 + 'data-payment-type' => true,
588 + 'data-customer-name-field' => true,
589 + 'data-customer-email-field' => true,
590 + 'data-nonce' => true,
591 + 'data-variable-amount-field' => true,
592 + 'data-minimum-amount' => true,
593 + 'data-subscription-plan-name' => true,
594 + 'data-subscription-interval' => true,
595 + 'data-subscription-billing-cycles' => true,
596 + // Dual-mode ("both") payment block: per-choice amount configuration read by
597 + // the chooser when the donor switches between one-time and recurring.
598 + 'data-original-payment-type' => true,
599 + 'data-default-payment-choice' => true,
600 + 'data-one-time-amount-type' => true,
601 + 'data-one-time-fixed-amount' => true,
602 + 'data-one-time-minimum-amount' => true,
603 + 'data-one-time-variable-amount-field' => true,
604 + 'data-subscription-amount-type' => true,
605 + 'data-subscription-fixed-amount' => true,
606 + 'data-subscription-minimum-amount' => true,
607 + 'data-subscription-variable-amount-field' => true,
608 + 'data-currency-symbol' => true,
609 + 'data-message-format' => true,
610 + 'data-payment-methods' => true,
611 + 'data-payment-available' => true,
612 + 'data-method' => true,
613 + 'data-slug' => true,
614 + 'data-required' => true,
615 + 'data-fee-percentage' => true,
616 + 'data-fee-fixed' => true,
617 + 'data-fee-mode' => true,
618 + 'data-gateway-fees' => true,
619 + 'data-invalid-email-msg' => true,
620 + 'data-invalid-url-msg' => true,
621 + 'data-sd-mask' => true,
622 + 'data-custom-sd-mask' => true,
610 623 // Dropdown (tom-select) field.
611 - 'data-multiple' => true,
612 - 'data-searchable' => true,
613 - 'data-preselected' => true,
614 - 'data-min-selection' => true,
615 - 'data-max-selection' => true,
616 - 'data-placeholder' => true,
624 + 'data-multiple' => true,
625 + 'data-searchable' => true,
626 + 'data-preselected' => true,
627 + 'data-min-selection' => true,
628 + 'data-max-selection' => true,
629 + 'data-placeholder' => true,
617 630 // Phone (intl-tel-input) field.
618 - 'data-default-country' => true,
619 - 'data-auto-country' => true,
620 - 'data-enable-country-filter' => true,
621 - 'data-country-filter-type' => true,
622 - 'data-include-countries' => true,
623 - 'data-exclude-countries' => true,
631 + 'data-default-country' => true,
632 + 'data-auto-country' => true,
633 + 'data-enable-country-filter' => true,
634 + 'data-country-filter-type' => true,
635 + 'data-include-countries' => true,
636 + 'data-exclude-countries' => true,
624 637 ];
625 638
626 639 $allowed = [
627 640 'div' => array_merge(
@@ -634,8 +647,13 @@
634 647 'aria-live' => true,
635 648 'aria-atomic' => true,
636 649 'aria-hidden' => true,
637 650 'aria-labelledby' => true,
651 + 'aria-label' => true,
652 + // The dual-mode payment chooser hides the inactive amount panel with
653 + // `hidden`; without it here kses strips the attribute and both panels
654 + // render at once.
655 + 'hidden' => true,
638 656 ],
639 657 $common_data_attrs
640 658 ),
641 659 'form' => array_merge(
@@ -681,8 +699,10 @@
681 699 'inputmode' => true,
682 700 'aria-describedby' => true,
683 701 'aria-required' => true,
684 702 'aria-hidden' => true,
703 + // Payment-type chooser radios point at the amount panel they reveal.
704 + 'aria-controls' => true,
685 705 ],
686 706 $common_data_attrs
687 707 ),
688 708 'button' => array_merge(
@@ -925,23 +945,36 @@
925 945 ? $data['message']
926 946 : esc_html__( 'Thank you for your donation!', 'suredonation' );
927 947
928 948 return [
929 - 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
930 - 'confirmationType' => $confirmation_type,
931 - 'successTitle' => esc_html__( 'Thank You!', 'suredonation' ),
932 - 'successMessage' => wp_kses_post( self::get_string_value( $success_message ) ),
949 + 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
950 + 'confirmationType' => $confirmation_type,
951 + 'successTitle' => esc_html__( 'Thank You!', 'suredonation' ),
952 + 'successMessage' => wp_kses_post( self::get_string_value( $success_message ) ),
933 953 // Shown when payment succeeded at the gateway but our server-side
934 954 // finalize did not complete; the webhook will finalize it, so the
935 955 // donor must not be prompted to pay again.
936 - 'processingMessage' => esc_html__( 'Payment received. We are finalizing your donation and will email you a confirmation shortly. Please do not pay again.', 'suredonation' ),
937 - 'redirectUrl' => ! empty( $redirect_url ) ? esc_url( self::get_string_value( $redirect_url ) ) : '',
938 - 'submissionAction' => $data['submission_action'],
956 + 'processingMessage' => esc_html__( 'Payment received. We are finalizing your donation and will email you a confirmation shortly. Please do not pay again.', 'suredonation' ),
957 + // Shown when the card form itself could not be rendered — almost
958 + // always because the connected Stripe account is not allowed to
959 + // charge cards. Deliberately says nothing about the account: the
960 + // cause is the site's to fix, and the gateway's own wording would
961 + // put its account state on a public page.
962 + 'cardUnavailableMessage' => esc_html__( 'Card payments are unavailable right now. Please choose another payment method or contact the site owner.', 'suredonation' ),
963 + 'redirectUrl' => ! empty( $redirect_url ) ? esc_url( self::get_string_value( $redirect_url ) ) : '',
964 + 'submissionAction' => $data['submission_action'],
939 965 // translators: %s: formatted fee amount with currency symbol.
940 - 'feeIncludesText' => __( '(includes %s processing fee)', 'suredonation' ),
941 - 'amountPlaceholder' => __( 'Complete the form to view the amount.', 'suredonation' ),
966 + 'feeIncludesText' => __( '(includes %s processing fee)', 'suredonation' ),
967 + 'amountPlaceholder' => __( 'Complete the form to view the amount.', 'suredonation' ),
968 + // Shown when a failed recurring confirmation forces the Stripe
969 + // Payment Element to rebuild after switching to one-time — see
970 + // StripeGateway.updatePaymentType(). Assigned via textContent
971 + // (GatewayBase.showError()), which doesn't decode HTML entities,
972 + // so this must not be esc_html__() or an apostrophe in
973 + // translation would render as the literal "&#039;".
974 + 'reenterCardMessage' => __( 'Please re-enter your card details to continue.', 'suredonation' ),
942 975 // Currency symbol placement for client-side amount/fee formatting.
943 - 'currencySignPosition' => Payment_Helper::get_currency_sign_position(),
976 + 'currencySignPosition' => Payment_Helper::get_currency_sign_position(),
944 977 ];
945 978 }
946 979
947 980 /**
@@ -1093,8 +1126,18 @@
1093 1126 [
1094 1127 'tag' => '{refund_amount}',
1095 1128 'title' => __( 'Refund Amount', 'suredonation' ),
1096 1129 ],
1130 + [
1131 + 'tag' => '{form_fields}',
1132 + 'title' => __( 'Form Fields', 'suredonation' ),
1133 + // Resolves to a block-level receipt card. The editor
1134 + // offers this same list for Subject, From Name and
1135 + // Reply-To, all of which are run through
1136 + // process_smart_tags() — inserting it there would put
1137 + // raw markup in a mail header. Body editor only.
1138 + 'bodyOnly' => true,
1139 + ],
1097 1140 ],
1098 1141 ],
1099 1142 [
1100 1143 'label' => __( 'General Tags', 'suredonation' ),
@@ -1167,9 +1210,9 @@
1167 1210 * surface them, and free listed subscription tags that could never
1168 1211 * resolve for a free-only site.
1169 1212 *
1170 1213 * @param array<int, array<string, mixed>> $groups Grouped tag definitions.
1171 - * @since 1.4.0
1214 + * @since 1.5.1
1172 1215 */
1173 1216 $grouped = apply_filters( 'suredonation_email_smart_tag_groups', $smart_tags['email_grouped'] );
1174 1217
1175 1218 // The filter feeds the editor's tag picker, which iterates groups and
@@ -1186,9 +1229,22 @@
1186 1229 )
1187 1230 );
1188 1231 }
1189 1232
1190 - return $smart_tags;
1233 + /**
1234 + * Filter the smart-tag catalogue grouped by context.
1235 + *
1236 + * Lets extensions register additional contexts (e.g. a 'pdf' group for
1237 + * PDF receipt templates) or extend existing ones. This catalogue only
1238 + * drives tag-picker UIs; tag resolution happens in
1239 + * Email_Handler::process_smart_tags() and its
1240 + * 'suredonation_email_smart_tags' filter, so new tags must be
1241 + * registered there as well to take effect.
1242 + *
1243 + * @param array<string, array<int, array<string, mixed>>> $smart_tags Smart tags grouped by context.
1244 + * @since 1.5.0
1245 + */
1246 + return apply_filters( 'suredonation_smart_tags', $smart_tags );
1191 1247 }
1192 1248
1193 1249 /**
1194 1250 * Map a payment gateway slug to a human-readable label.
@@ -1352,8 +1408,112 @@
1352 1408 );
1353 1409 }
1354 1410
1355 1411 /**
1412 + * Translate a stored checkbox value for display.
1413 + *
1414 + * Checkbox fields persist the canonical, untranslated tokens in
1415 + * Field_Validation::CHECKBOX_VALUES so the stored column stays comparable
1416 + * across locales and survives an export/re-import. Anything shown to a human
1417 + * runs through here; the CSV export deliberately does not, so the exported
1418 + * column keeps the canonical token.
1419 + *
1420 + * Values that are not a checkbox token are returned untouched, so this is
1421 + * safe to apply to a mixed field set.
1422 + *
1423 + * @param string $value Stored field value.
1424 + * @return string Display value.
1425 + * @since 1.5.1
1426 + */
1427 + public static function format_checkbox_field_value( $value ) {
1428 + $value = self::get_string_value( $value );
1429 +
1430 + switch ( $value ) {
1431 + case Field_Validation::CHECKBOX_VALUES['yes']:
1432 + return _x( 'Yes', 'checkbox field value', 'suredonation' );
1433 + case Field_Validation::CHECKBOX_VALUES['no']:
1434 + return _x( 'No', 'checkbox field value', 'suredonation' );
1435 + default:
1436 + return $value;
1437 + }
1438 + }
1439 +
1440 + /**
1441 + * Render the donation's submitted form fields as receipt rows.
1442 + *
1443 + * The values persisted under donation_data['fields'] (see
1444 + * Donations::set_submitted_fields) already surface on the entry screen and
1445 + * in exports; this renders the same set for the email templates, behind the
1446 + * {form_fields} smart tag. Returns '' when the donation has none, so a
1447 + * template carrying the tag is unchanged for forms with no extra fields.
1448 + *
1449 + * SECURITY: the return value is substituted into email HTML by the
1450 + * {form_fields} smart tag, and that tag is exempt from the escaping pass in
1451 + * Email_Handler::process_smart_tags() because core tags are compared by value
1452 + * and left alone. The esc_html() calls below are therefore the only thing
1453 + * between donor-submitted text and an admin's mailbox — both the label and
1454 + * the value must stay escaped here. See the regression test in
1455 + * tests/unit/inc/test-helper.php.
1456 + *
1457 + * @param array<mixed> $fields Stored fields as label/value/group entries.
1458 + * @return string Rendered markup, or '' when there is nothing to show.
1459 + * @since 1.5.1
1460 + */
1461 + public static function render_submitted_fields( $fields ) {
1462 + if ( empty( $fields ) || ! is_array( $fields ) ) {
1463 + return '';
1464 + }
1465 +
1466 + $rows_html = '';
1467 + foreach ( $fields as $field ) {
1468 + if ( ! is_array( $field ) ) {
1469 + continue;
1470 + }
1471 +
1472 + $label = self::get_string_value( $field['label'] ?? '' );
1473 + $value = self::format_checkbox_field_value( $field['value'] ?? '' );
1474 + $group = self::get_string_value( $field['group'] ?? '' );
1475 +
1476 + if ( '' === $label && '' === $value ) {
1477 + continue;
1478 + }
1479 +
1480 + // Sub-fields (e.g. the Address block's parts) are stored with their
1481 + // parent block's label as the group; prefix it so "Street Address"
1482 + // reads as "Address: Street Address" rather than losing its context.
1483 + if ( '' !== $group ) {
1484 + // str_replace (not sprintf) because the format is translator
1485 + // editable and this runs inside the gateway webhook handlers — a
1486 + // stray literal % would make sprintf throw a ValueError on PHP 8,
1487 + // 500 the webhook and trigger gateway retries. Same rule as
1488 + // Field_Validation's message formatting.
1489 + $label = str_replace(
1490 + [ '%1$s', '%2$s' ],
1491 + [ $group, $label ],
1492 + /* translators: 1: parent field label, 2: sub-field label. */
1493 + _x( '%1$s: %2$s', 'parent field label: sub-field label', 'suredonation' )
1494 + );
1495 + }
1496 +
1497 + $rows_html .= sprintf(
1498 + '<div class="sd-receipt-row"><span class="sd-receipt-row__label">%1$s</span><span class="sd-receipt-row__value">%2$s</span></div>',
1499 + esc_html( $label ),
1500 + esc_html( $value )
1501 + );
1502 + }
1503 +
1504 + if ( '' === $rows_html ) {
1505 + return '';
1506 + }
1507 +
1508 + return sprintf(
1509 + '<div class="sd-receipt-card"><h3 class="sd-receipt-card__title">%1$s</h3><div class="sd-receipt-rows">%2$s</div></div>',
1510 + esc_html__( 'Form Details', 'suredonation' ),
1511 + $rows_html
1512 + );
1513 + }
1514 +
1515 + /**
1356 1516 * Default confirmation message template (receipt layout with smart tags).
1357 1517 *
1358 1518 * @return string Message HTML template.
1359 1519 * @since 1.0.0
@@ -1364,9 +1524,9 @@
1364 1524 /* translators: {donor_name} is a smart tag replaced with the donor's name. */
1365 1525 . esc_html__( 'Thank you {donor_name} for your Donation', 'suredonation' )
1366 1526 . '</h2>'
1367 1527 . '<p class="sd-receipt-subtitle" style="text-align: center;">'
1368 - . esc_html__( 'Your contribution means a lot. We have sent an email to your registered account along with a receipt for your donation.', 'suredonation' )
1528 + . esc_html__( 'Your contribution means a lot. We have sent a confirmation email to your registered address with the details of your donation.', 'suredonation' )
1369 1529 . '</p>{donation_receipt}';
1370 1530 }
1371 1531
1372 1532 /**