PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.0
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 +99 -525 trunk1.1.0 View file →
@@ -9,9 +9,8 @@
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;
14 13 use SureDonation\Inc\Payments\Payment_Helper;
15 14
16 15 // Exit if accessed directly.
17 16 if ( ! defined( 'ABSPATH' ) ) {
@@ -204,40 +203,8 @@
204 203 return update_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, wp_json_encode( $meta ) );
205 204 }
206 205
207 206 /**
208 - * Whether a (possibly nested) block tree contains a block of the given name.
209 - *
210 - * Walks parse_blocks() output, descending into innerBlocks so a block nested
211 - * inside a layout wrapper (Group/Columns) is still found. Note that a block
212 - * inside a synced pattern is not reachable: those parse as `core/block` with
213 - * no innerBlocks.
214 - *
215 - * Lives here rather than on Form_Renderer or Payment_Helper — both need it,
216 - * they sit in unrelated namespaces, and this is a generic block utility with
217 - * no rendering or payment semantics.
218 - *
219 - * @param array<int|string, mixed> $blocks Parsed blocks (parse_blocks output).
220 - * @param string $target Block name to look for.
221 - * @return bool
222 - * @since 1.5.1
223 - */
224 - public static function block_tree_contains( $blocks, $target ) {
225 - foreach ( $blocks as $block ) {
226 - if ( ! is_array( $block ) ) {
227 - continue;
228 - }
229 - if ( isset( $block['blockName'] ) && $block['blockName'] === $target ) {
230 - return true;
231 - }
232 - if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && self::block_tree_contains( $block['innerBlocks'], $target ) ) {
233 - return true;
234 - }
235 - }
236 - return false;
237 - }
238 -
239 - /**
240 207 * Checks if current value is string or else returns default value
241 208 *
242 209 * @param mixed $data data which need to be checked if is string.
243 210 * @return string
@@ -471,39 +438,13 @@
471 438 return $slug . '-' . $index;
472 439 }
473 440
474 441 /**
475 - * Generate a unique block ID for a server-created block.
476 - *
477 - * Mirrors the client-side generateBlockId() used in each block's edit.js
478 - * (a 7-character base36 string). Blocks created programmatically (e.g. the
479 - * default form auto-generated when a campaign is published) never run the
480 - * editor, so they would otherwise have no block_id. The server-side payment
481 - * validation config is keyed on block_id, so without one no config is stored
482 - * and donations fail with "Invalid form configuration." until the form is
483 - * opened and saved in the editor.
484 - *
485 - * @return string A 7-character base36 identifier.
486 - * @since 1.1.1
487 - */
488 - public static function generate_block_id() {
489 - $chars = '0123456789abcdefghijklmnopqrstuvwxyz';
490 - $block_id = '';
491 - for ( $i = 0; $i < 7; $i++ ) {
492 - $block_id .= $chars[ wp_rand( 0, 35 ) ];
493 - }
494 - return $block_id;
495 - }
496 -
497 - /**
498 442 * Get client IP address for logging purposes.
499 443 *
500 - * Uses REMOTE_ADDR only — forwarded headers (HTTP_X_FORWARDED_FOR,
501 - * HTTP_CLIENT_IP) are deliberately ignored because they are trivially
502 - * spoofable. Note: behind a proxy/CDN that does not restore the real client
503 - * IP, this returns the proxy's address. Suitable for informational logging
504 - * and best-effort geolocation only — do NOT use for security-critical IP
505 - * validation.
444 + * Checks forwarded headers first (for proxied/load-balanced environments)
445 + * then falls back to REMOTE_ADDR. This is suitable for informational
446 + * logging only — do NOT use for security-critical IP validation.
506 447 *
507 448 * @return string Client IP address.
508 449 * @since 0.0.1
509 450 */
@@ -575,89 +516,54 @@
575 516 */
576 517 public static function get_allowed_form_html() {
577 518 // Note: data-* wildcard doesn't work in wp_kses, so we list each data attribute explicitly.
578 519 $common_data_attrs = [
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,
623 - // Dropdown (tom-select) field.
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,
630 - // Phone (intl-tel-input) field.
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,
520 + 'data-block-id' => true,
521 + 'data-form-id' => true,
522 + 'data-gateway' => true,
523 + 'data-stripe-key' => true,
524 + 'data-currency' => true,
525 + 'data-payment-mode' => true,
526 + 'data-amount-type' => true,
527 + 'data-fixed-amount' => true,
528 + 'data-payment-type' => true,
529 + 'data-customer-name-field' => true,
530 + 'data-customer-email-field' => true,
531 + 'data-nonce' => true,
532 + 'data-variable-amount-field' => true,
533 + 'data-minimum-amount' => true,
534 + 'data-subscription-plan-name' => true,
535 + 'data-subscription-interval' => true,
536 + 'data-subscription-billing-cycles' => true,
537 + 'data-currency-symbol' => true,
538 + 'data-message-format' => true,
539 + 'data-payment-methods' => true,
540 + 'data-method' => true,
541 + 'data-slug' => true,
542 + 'data-required' => true,
543 + 'data-fee-percentage' => true,
544 + 'data-fee-fixed' => true,
545 + 'data-fee-mode' => true,
546 + 'data-gateway-fees' => true,
547 + 'data-invalid-email-msg' => true,
548 + 'data-sd-mask' => true,
549 + 'data-custom-sd-mask' => true,
637 550 ];
638 551
639 - $allowed = [
640 - 'div' => array_merge(
552 + return [
553 + 'div' => array_merge(
641 554 [
642 555 'id' => true,
643 556 'class' => true,
644 557 'style' => true,
645 558 'role' => true,
646 - 'tabindex' => true,
647 559 'aria-live' => true,
648 560 'aria-atomic' => true,
649 - 'aria-hidden' => true,
650 561 '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,
656 562 ],
657 563 $common_data_attrs
658 564 ),
659 - 'form' => array_merge(
565 + 'form' => array_merge(
660 566 [
661 567 'id' => true,
662 568 'class' => true,
663 569 'method' => true,
@@ -664,22 +570,22 @@
664 570 'action' => true,
665 571 ],
666 572 $common_data_attrs
667 573 ),
668 - 'fieldset' => [
574 + 'fieldset' => [
669 575 'id' => true,
670 576 'class' => true,
671 577 ],
672 - 'legend' => [
578 + 'legend' => [
673 579 'id' => true,
674 580 'class' => true,
675 581 ],
676 - 'label' => [
582 + 'label' => [
677 583 'id' => true,
678 584 'class' => true,
679 585 'for' => true,
680 586 ],
681 - 'input' => array_merge(
587 + 'input' => array_merge(
682 588 [
683 589 'id' => true,
684 590 'class' => true,
685 591 'type' => true,
@@ -693,20 +599,15 @@
693 599 'checked' => true,
694 600 'disabled' => true,
695 601 'readonly' => true,
696 602 'required' => true,
697 - 'tabindex' => true,
698 - 'autocomplete' => true,
699 - 'inputmode' => true,
700 603 'aria-describedby' => true,
701 604 'aria-required' => true,
702 605 'aria-hidden' => true,
703 - // Payment-type chooser radios point at the amount panel they reveal.
704 - 'aria-controls' => true,
705 606 ],
706 607 $common_data_attrs
707 608 ),
708 - 'button' => array_merge(
609 + 'button' => array_merge(
709 610 [
710 611 'id' => true,
711 612 'class' => true,
712 613 'type' => true,
@@ -713,9 +614,9 @@
713 614 'disabled' => true,
714 615 ],
715 616 $common_data_attrs
716 617 ),
717 - 'select' => array_merge(
618 + 'select' => array_merge(
718 619 [
719 620 'id' => true,
720 621 'class' => true,
721 622 'name' => true,
@@ -720,23 +621,19 @@
720 621 'class' => true,
721 622 'name' => true,
722 623 'disabled' => true,
723 624 'required' => true,
724 - 'multiple' => true,
725 - 'tabindex' => true,
726 - 'autocomplete' => true,
727 625 'aria-describedby' => true,
728 626 'aria-required' => true,
729 627 ],
730 628 $common_data_attrs
731 629 ),
732 - 'option' => [
630 + 'option' => [
733 631 'value' => true,
734 - 'class' => true,
735 632 'selected' => true,
736 633 'disabled' => true,
737 634 ],
738 - 'textarea' => array_merge(
635 + 'textarea' => array_merge(
739 636 [
740 637 'id' => true,
741 638 'class' => true,
742 639 'name' => true,
@@ -751,9 +648,9 @@
751 648 'aria-required' => true,
752 649 ],
753 650 $common_data_attrs
754 651 ),
755 - 'span' => array_merge(
652 + 'span' => array_merge(
756 653 [
757 654 'id' => true,
758 655 'class' => true,
759 656 'style' => true,
@@ -760,39 +657,15 @@
760 657 'aria-hidden' => true,
761 658 ],
762 659 $common_data_attrs
763 660 ),
764 - 'p' => [
661 + 'p' => [
765 662 'id' => true,
766 663 'class' => true,
767 664 'style' => true,
768 665 'role' => true,
769 666 ],
770 - 'h1' => [
771 - 'id' => true,
772 - 'class' => true,
773 - ],
774 - 'h2' => [
775 - 'id' => true,
776 - 'class' => true,
777 - ],
778 - 'h3' => [
779 - 'id' => true,
780 - 'class' => true,
781 - ],
782 - 'h4' => [
783 - 'id' => true,
784 - 'class' => true,
785 - ],
786 - 'h5' => [
787 - 'id' => true,
788 - 'class' => true,
789 - ],
790 - 'h6' => [
791 - 'id' => true,
792 - 'class' => true,
793 - ],
794 - 'a' => [
667 + 'a' => [
795 668 'id' => true,
796 669 'class' => true,
797 670 'href' => true,
798 671 'target' => true,
@@ -798,60 +671,25 @@
798 671 'target' => true,
799 672 'rel' => true,
800 673 'style' => true,
801 674 ],
802 - 'strong' => [
675 + 'strong' => [
803 676 'class' => true,
804 677 ],
805 - 'em' => [
678 + 'em' => [
806 679 'class' => true,
807 680 ],
808 - 'ol' => [
681 + 'ol' => [
809 682 'class' => true,
810 683 ],
811 - 'ul' => [
684 + 'ul' => [
812 685 'class' => true,
813 686 ],
814 - 'li' => [
687 + 'li' => [
815 688 'class' => true,
816 689 ],
817 - 'br' => [],
818 - 'hr' => [
819 - 'class' => true,
820 - ],
821 - // img/figure/figcaption back the Image block (inc/blocks/image) — the
822 - // render depends on these entries, so don't drop them in a cleanup.
823 - 'img' => [
824 - 'src' => true,
825 - 'fetchpriority' => true,
826 - 'srcset' => true,
827 - 'sizes' => true,
828 - 'alt' => true,
829 - 'class' => true,
830 - 'style' => true,
831 - 'width' => true,
832 - 'height' => true,
833 - 'loading' => true,
834 - 'decoding' => true,
835 - 'title' => true,
836 - // Lazy-load optimizers (WP Rocket, Perfmatters, Optimole, the
837 - // Bricks theme, …) rewrite wp_get_attachment_image() output into
838 - // these data-* attributes with a data: placeholder in src; allow
839 - // them so kses doesn't strip the real URLs the lazy JS swaps back.
840 - 'data-src' => true,
841 - 'data-srcset' => true,
842 - 'data-sizes' => true,
843 - 'data-lazy-src' => true,
844 - 'data-lazy-srcset' => true,
845 - 'data-lazy-sizes' => true,
846 - ],
847 - 'figure' => [
848 - 'class' => true,
849 - ],
850 - 'figcaption' => [
851 - 'class' => true,
852 - ],
853 - 'svg' => [
690 + 'br' => [],
691 + 'svg' => [
854 692 'class' => true,
855 693 'width' => true,
856 694 'height' => true,
857 695 'viewbox' => true,
@@ -858,9 +696,9 @@
858 696 'fill' => true,
859 697 'xmlns' => true,
860 698 'aria-hidden' => true,
861 699 ],
862 - 'circle' => [
700 + 'circle' => [
863 701 'cx' => true,
864 702 'cy' => true,
865 703 'r' => true,
866 704 'stroke' => true,
@@ -866,9 +704,9 @@
866 704 'stroke' => true,
867 705 'stroke-width' => true,
868 706 'fill' => true,
869 707 ],
870 - 'rect' => [
708 + 'rect' => [
871 709 'x' => true,
872 710 'y' => true,
873 711 'width' => true,
874 712 'height' => true,
@@ -875,9 +713,9 @@
875 713 'rx' => true,
876 714 'stroke' => true,
877 715 'stroke-width' => true,
878 716 ],
879 - 'path' => [
717 + 'path' => [
880 718 'class' => true,
881 719 'd' => true,
882 720 'stroke' => true,
883 721 'stroke-width' => true,
@@ -885,19 +723,8 @@
885 723 'stroke-linejoin' => true,
886 724 'fill' => true,
887 725 ],
888 726 ];
889 -
890 - /**
891 - * Filter the allowed HTML tags/attributes for SureDonation form markup.
892 - *
893 - * Lets extensions (e.g. the SureDonation Pro date/time pickers) permit the
894 - * extra tags or data attributes their fields render.
895 - *
896 - * @since 1.1.1
897 - * @param array<string, array<string, bool>> $allowed Allowed tags/attributes.
898 - */
899 - return apply_filters( 'suredonation_allowed_form_html', $allowed );
900 727 }
901 728
902 729 /**
903 730 * Get the nonce action string for a donation form.
@@ -945,36 +772,21 @@
945 772 ? $data['message']
946 773 : esc_html__( 'Thank you for your donation!', 'suredonation' );
947 774
948 775 return [
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 ) ),
776 + 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
777 + 'confirmationType' => $confirmation_type,
778 + 'successTitle' => esc_html__( 'Thank You!', 'suredonation' ),
779 + 'successMessage' => wp_kses_post( self::get_string_value( $success_message ) ),
953 780 // Shown when payment succeeded at the gateway but our server-side
954 781 // finalize did not complete; the webhook will finalize it, so the
955 782 // donor must not be prompted to pay again.
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'],
783 + 'processingMessage' => esc_html__( 'Payment received. We are finalizing your donation and will email you a confirmation shortly. Please do not pay again.', 'suredonation' ),
784 + 'redirectUrl' => ! empty( $redirect_url ) ? esc_url( self::get_string_value( $redirect_url ) ) : '',
785 + 'submissionAction' => $data['submission_action'],
965 786 // translators: %s: formatted fee amount with currency symbol.
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' ),
975 - // Currency symbol placement for client-side amount/fee formatting.
976 - 'currencySignPosition' => Payment_Helper::get_currency_sign_position(),
787 + 'feeIncludesText' => __( '(includes %s processing fee)', 'suredonation' ),
788 + 'amountPlaceholder' => __( 'Complete the form to view the amount.', 'suredonation' ),
977 789 ];
978 790 }
979 791
980 792 /**
@@ -1067,9 +879,9 @@
1067 879 'title' => __( 'Success Badge', 'suredonation' ),
1068 880 ],
1069 881 ];
1070 882
1071 - $smart_tags = [
883 + return [
1072 884 'confirmation' => $confirmation_tags,
1073 885 'email' => array_merge(
1074 886 $confirmation_tags,
1075 887 [
@@ -1085,8 +897,16 @@
1085 897 'tag' => '{admin_url}',
1086 898 'title' => __( 'Admin URL', 'suredonation' ),
1087 899 ],
1088 900 [
901 + 'tag' => '{subscription_id}',
902 + 'title' => __( 'Subscription ID', 'suredonation' ),
903 + ],
904 + [
905 + 'tag' => '{subscription_interval}',
906 + 'title' => __( 'Subscription Interval', 'suredonation' ),
907 + ],
908 + [
1089 909 'tag' => '{offline_instructions}',
1090 910 'title' => __( 'Offline Instructions', 'suredonation' ),
1091 911 ],
1092 912 ]
@@ -1123,21 +943,19 @@
1123 943 'tag' => '{payment_method}',
1124 944 'title' => __( 'Payment Method', 'suredonation' ),
1125 945 ],
1126 946 [
947 + 'tag' => '{subscription_id}',
948 + 'title' => __( 'Subscription ID', 'suredonation' ),
949 + ],
950 + [
951 + 'tag' => '{subscription_interval}',
952 + 'title' => __( 'Subscription Interval', 'suredonation' ),
953 + ],
954 + [
1127 955 'tag' => '{refund_amount}',
1128 956 'title' => __( 'Refund Amount', 'suredonation' ),
1129 957 ],
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 - ],
1140 958 ],
1141 959 ],
1142 960 [
1143 961 'label' => __( 'General Tags', 'suredonation' ),
@@ -1183,68 +1001,8 @@
1183 1001 'title' => __( 'Admin Email', 'suredonation' ),
1184 1002 ],
1185 1003 ],
1186 1004 ];
1187 -
1188 - // Recurring tags resolve to nothing without Pro, so a free-only site was
1189 - // being offered two tags it could never use. They stay here rather than
1190 - // moving into Pro so that activating Pro does not depend on shipping a
1191 - // matching Pro release; anything Pro adds beyond these comes through the
1192 - // filter below.
1193 - if ( defined( 'SUREDONATION_PRO_VER' ) ) {
1194 - $smart_tags['email_grouped'][0]['tags'][] = [
1195 - 'tag' => '{subscription_id}',
1196 - 'title' => __( 'Recurring Donation ID', 'suredonation' ),
1197 - ];
1198 - $smart_tags['email_grouped'][0]['tags'][] = [
1199 - 'tag' => '{subscription_interval}',
1200 - 'title' => __( 'Frequency', 'suredonation' ),
1201 - ];
1202 - }
1203 -
1204 - /**
1205 - * Filter the grouped smart tags offered in the email notification editor.
1206 - *
1207 - * The list is what an admin can insert, so anything registering a tag
1208 - * resolver via `suredonation_email_smart_tags` needs to advertise it here
1209 - * too. Without this, Pro could resolve recurring tags but had no way to
1210 - * surface them, and free listed subscription tags that could never
1211 - * resolve for a free-only site.
1212 - *
1213 - * @param array<int, array<string, mixed>> $groups Grouped tag definitions.
1214 - * @since 1.5.1
1215 - */
1216 - $grouped = apply_filters( 'suredonation_email_smart_tag_groups', $smart_tags['email_grouped'] );
1217 -
1218 - // The filter feeds the editor's tag picker, which iterates groups and
1219 - // their tags. A callback returning a non-array — or groups without a
1220 - // `tags` array — would fatal there rather than in whatever added it, so
1221 - // the shape is re-checked before it is handed on.
1222 - if ( is_array( $grouped ) ) {
1223 - $smart_tags['email_grouped'] = array_values(
1224 - array_filter(
1225 - $grouped,
1226 - static function ( $group ) {
1227 - return is_array( $group ) && isset( $group['tags'] ) && is_array( $group['tags'] );
1228 - }
1229 - )
1230 - );
1231 - }
1232 -
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 );
1247 1005 }
1248 1006
1249 1007 /**
1250 1008 * Map a payment gateway slug to a human-readable label.
@@ -1280,9 +1038,9 @@
1280 1038 /**
1281 1039 * Render a styled payment-status badge for the donation confirmation.
1282 1040 *
1283 1041 * @param string $status Payment status (e.g. completed, pending, failed).
1284 - * @return array Badge HTML.
1042 + * @return string Badge HTML.
1285 1043 * @since 1.0.0
1286 1044 */
1287 1045 public static function get_payment_status_config( $status ) {
1288 1046 $status = strtolower( trim( (string) $status ) );
@@ -1408,112 +1166,8 @@
1408 1166 );
1409 1167 }
1410 1168
1411 1169 /**
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 - /**
1516 1170 * Default confirmation message template (receipt layout with smart tags).
1517 1171 *
1518 1172 * @return string Message HTML template.
1519 1173 * @since 1.0.0
@@ -1524,9 +1178,9 @@
1524 1178 /* translators: {donor_name} is a smart tag replaced with the donor's name. */
1525 1179 . esc_html__( 'Thank you {donor_name} for your Donation', 'suredonation' )
1526 1180 . '</h2>'
1527 1181 . '<p class="sd-receipt-subtitle" style="text-align: center;">'
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' )
1182 + . esc_html__( 'Your contribution means a lot. We have sent an email to your registered account along with a receipt for your donation.', 'suredonation' )
1529 1183 . '</p>{donation_receipt}';
1530 1184 }
1531 1185
1532 1186 /**
@@ -1532,28 +1186,17 @@
1532 1186 /**
1533 1187 * Build the rendered confirmation/thank-you HTML for a donation.
1534 1188 *
1535 1189 * Resolves the form's confirmation message template against the donation's
1536 - * real data (smart tags) so the frontend can display the receipt. The
1537 - * billing interval is lifted out of the nested donation_data column, which
1538 - * is the only field of the set that is not stored as a column of its own.
1190 + * real data (smart tags) so the frontend can display the receipt.
1539 1191 *
1540 - * @param int $donation_id Donation ID.
1541 - * @param array<string, mixed>|null $donation Donation row to render from.
1542 - * Defaults to reading it. Pass one
1543 - * when the caller already holds the
1544 - * row, or when the row on disk does
1545 - * not yet reflect the state being
1546 - * reported to the donor.
1192 + * @param int $donation_id Donation ID.
1547 1193 * @return string Sanitized confirmation HTML, or '' on failure.
1548 1194 * @since 1.0.0
1549 1195 */
1550 - public static function render_confirmation_message( $donation_id, $donation = null ) {
1196 + public static function render_confirmation_message( $donation_id ) {
1197 + $donation = Donations::get( $donation_id );
1551 1198 if ( ! is_array( $donation ) ) {
1552 - $donation = Donations::get( $donation_id );
1553 - }
1554 -
1555 - if ( ! is_array( $donation ) ) {
1556 1199 return '';
1557 1200 }
1558 1201
1559 1202 $form_id = isset( $donation['form_id'] ) ? absint( $donation['form_id'] ) : 0;
@@ -1561,32 +1204,19 @@
1561 1204
1562 1205 $settings = self::get_form_confirmation_settings( $form_id );
1563 1206 $template = ! empty( $settings['message'] ) ? $settings['message'] : self::get_default_confirmation_message();
1564 1207
1565 - // The billing interval is the one field the donation row does not carry
1566 - // as a column; it is written a level down inside donation_data, so it
1567 - // has to be lifted out before the tag map can see it.
1568 - $stored = $donation['donation_data'] ?? [];
1569 - if ( is_string( $stored ) && '' !== $stored ) {
1570 - $stored = json_decode( $stored, true );
1571 - }
1572 - $stored = is_array( $stored ) ? $stored : [];
1573 -
1574 1208 $donation_data = [
1575 - 'id' => $donation_id,
1576 - 'donor_name' => $donation['donor_name'] ?? '',
1577 - 'donor_email' => $donation['donor_email'] ?? '',
1578 - 'amount' => $donation['amount'] ?? 0,
1579 - 'fees_covered' => $donation['fees_covered'] ?? 0,
1580 - 'currency' => $donation['currency'] ?? Payment_Helper::get_currency(),
1581 - 'gateway' => $donation['gateway'] ?? '',
1582 - 'payment_status' => $donation['payment_status'] ?? '',
1583 - 'transaction_id' => $donation['transaction_id'] ?? '',
1584 - 'donation_type' => $donation['donation_type'] ?? 'one-time',
1585 - // Recurring donations resolve these two; a one-time donation has
1586 - // neither, and the tag map already renders a missing value as empty.
1587 - 'subscription_id' => $donation['subscription_id'] ?? '',
1588 - 'subscription_interval' => $stored['subscription_interval'] ?? '',
1209 + 'id' => $donation_id,
1210 + 'donor_name' => $donation['donor_name'] ?? '',
1211 + 'donor_email' => $donation['donor_email'] ?? '',
1212 + 'amount' => $donation['amount'] ?? 0,
1213 + 'fees_covered' => $donation['fees_covered'] ?? 0,
1214 + 'currency' => $donation['currency'] ?? Payment_Helper::get_currency(),
1215 + 'gateway' => $donation['gateway'] ?? '',
1216 + 'payment_status' => $donation['payment_status'] ?? '',
1217 + 'transaction_id' => $donation['transaction_id'] ?? '',
1218 + 'donation_type' => $donation['donation_type'] ?? 'one-time',
1589 1219 ];
1590 1220
1591 1221 $campaign = $campaign_id ? get_post( $campaign_id ) : null;
1592 1222
@@ -1592,62 +1222,6 @@
1592 1222
1593 1223 $rendered = Email_Handler::process_smart_tags( $template, $donation_data, $campaign );
1594 1224
1595 1225 return wp_kses_post( $rendered );
1596 - }
1597 -
1598 - /**
1599 - * Check whether the OttoKit (formerly SureTriggers) plugin is active and
1600 - * authenticated with the OttoKit SaaS.
1601 - *
1602 - * @return bool True when OttoKit is installed, active and connected.
1603 - * @since 1.2.0
1604 - */
1605 - public static function is_suretriggers_ready() {
1606 - if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) {
1607 - // Plugin is deactivated or not installed at all.
1608 - return false;
1609 - }
1610 -
1611 - $suretriggers_data = get_option( 'suretrigger_options', [] );
1612 - if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) {
1613 - // OttoKit is not authenticated yet.
1614 - return false;
1615 - }
1616 -
1617 - return true;
1618 - }
1619 -
1620 - /**
1621 - * Get OttoKit (formerly SureTriggers) integration metadata.
1622 - *
1623 - * Shared by the admin app and the donation form editor so both surface the
1624 - * same install/activate/connect state.
1625 - *
1626 - * @return array<string,mixed> Integration metadata.
1627 - * @since 1.2.0
1628 - */
1629 - public static function get_ottokit_integration() {
1630 - $plugin_file = 'suretriggers/suretriggers.php';
1631 -
1632 - if ( ! function_exists( 'is_plugin_active' ) ) {
1633 - include_once ABSPATH . 'wp-admin/includes/plugin.php';
1634 - }
1635 -
1636 - $status = 'Install';
1637 - if ( is_plugin_active( $plugin_file ) ) {
1638 - $status = 'Activated';
1639 - } elseif ( array_key_exists( $plugin_file, get_plugins() ) ) {
1640 - $status = 'Installed';
1641 - }
1642 -
1643 - return [
1644 - 'title' => 'OttoKit',
1645 - 'slug' => 'suretriggers',
1646 - 'path' => $plugin_file,
1647 - 'status' => $status,
1648 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Filter is owned by the OttoKit plugin.
1649 - 'connected' => apply_filters( 'suretriggers_is_user_connected', '' ),
1650 - 'connection_url' => admin_url( 'admin.php?page=suretriggers' ),
1651 - ];
1652 1226 }
1653 1227 }