PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.0.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.0.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
suredonation / inc / helper.php

helper.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.0.0, at inc/helper.php

1,165 lines 34.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper Class - Utility functions for SureDonation
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc;
9
10 use SureDonation\Inc\Database\Tables\Donations;
11 use SureDonation\Inc\Emails\Email_Handler;
12 use SureDonation\Inc\Payments\Payment_Helper;
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Helper class.
21 * Provides utility functions for the plugin.
22 *
23 * @since 0.0.1
24 */
25 class Helper {
26 /**
27 * Option name for all SureDonation settings.
28 *
29 * @since 0.0.1
30 */
31 public const OPTION_NAME = 'suredonation_options';
32
33 /**
34 * Campaign meta key name.
35 *
36 * @since 0.0.1
37 */
38 public const SUREDONATION_CAMPAIGN_META_KEY = '_suredonation_campaign_meta';
39
40 /**
41 * Default campaign meta values.
42 *
43 * @since 0.0.1
44 * @var array<string, mixed>
45 */
46 private static $campaign_meta_defaults = [
47 'goal_type' => 'raised_amount',
48 'goal_amount' => 0,
49 'campaign_status' => 'active',
50 'email_settings' => [],
51 'require_terms' => false,
52 'terms_text' => '',
53 'thank_you_message' => '',
54 ];
55
56 /**
57 * Get a value from the suredonation_options array.
58 *
59 * @param string $key The key to retrieve.
60 * @param mixed $default_value Default value if key doesn't exist.
61 * @return mixed
62 * @since 0.0.1
63 */
64 public static function get_suredonation_option( $key, $default_value = null ) {
65 $options = get_option( self::OPTION_NAME, [] );
66
67 if ( ! is_array( $options ) ) {
68 $options = [];
69 }
70
71 return array_key_exists( $key, $options ) ? $options[ $key ] : $default_value;
72 }
73
74 /**
75 * Update a value in the suredonation_options array.
76 *
77 * @param string $key The key to update.
78 * @param mixed $value The value to set.
79 * @return bool True on success, false on failure.
80 * @since 0.0.1
81 */
82 public static function update_suredonation_option( $key, $value ) {
83 $options = get_option( self::OPTION_NAME, [] );
84
85 if ( ! is_array( $options ) ) {
86 $options = [];
87 }
88
89 $options[ $key ] = $value;
90
91 return update_option( self::OPTION_NAME, $options );
92 }
93
94 /**
95 * Get all campaign meta as an array.
96 *
97 * @param int $campaign_id Campaign post ID.
98 * @return array<string, mixed> Campaign meta values.
99 * @since 0.0.1
100 */
101 public static function get_campaign_meta( $campaign_id ) {
102 $raw = get_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, true );
103
104 $meta = ! empty( $raw ) && is_string( $raw ) ? json_decode( $raw, true ) : [];
105
106 if ( ! is_array( $meta ) ) {
107 $meta = [];
108 }
109
110 return array_merge( self::$campaign_meta_defaults, $meta );
111 }
112
113 /**
114 * Get a single campaign meta value.
115 *
116 * @param int $campaign_id Campaign post ID.
117 * @param string $key Meta key within the campaign meta array.
118 * @param mixed $default_value Default value if not set.
119 * @return mixed
120 * @since 0.0.1
121 */
122 public static function get_campaign_meta_value( $campaign_id, $key, $default_value = null ) {
123 $meta = self::get_campaign_meta( $campaign_id );
124
125 return $meta[ $key ] ?? $default_value;
126 }
127
128 /**
129 * Update campaign meta. Merges provided values with existing meta.
130 *
131 * @param int $campaign_id Campaign post ID.
132 * @param array<string, mixed> $values Key-value pairs to update.
133 * @return bool|int Meta ID on success, false on failure.
134 * @since 0.0.1
135 */
136 public static function update_campaign_meta( $campaign_id, $values ) {
137 $meta = self::get_campaign_meta( $campaign_id );
138 $meta = array_merge( $meta, $values );
139
140 return update_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, wp_json_encode( $meta ) );
141 }
142
143 /**
144 * Checks if current value is string or else returns default value
145 *
146 * @param mixed $data data which need to be checked if is string.
147 * @return string
148 * @since 0.0.1
149 */
150 public static function get_string_value( $data ) {
151 if ( is_scalar( $data ) ) {
152 return (string) $data;
153 }
154 if ( is_object( $data ) && method_exists( $data, '__toString' ) ) {
155 return $data->__toString();
156 }
157 if ( is_null( $data ) ) {
158 return '';
159 }
160 return '';
161 }
162
163 /**
164 * Checks if current value is number or else returns default value
165 *
166 * @param mixed $value data which need to be checked if is string.
167 * @param int $base value can be set is $data is not a string, defaults to empty string.
168 * @return int
169 * @since 0.0.1
170 */
171 public static function get_integer_value( $value, $base = 10 ) {
172 if ( is_numeric( $value ) ) {
173 return (int) $value;
174 }
175 if ( is_string( $value ) ) {
176 $trimmed_value = trim( $value );
177 return intval( $trimmed_value, $base );
178 }
179 return 0;
180 }
181
182 /**
183 * Safely converts a mixed value to float
184 *
185 * @param mixed $value The value to convert.
186 * @param float $default_value Default value if conversion fails.
187 * @return float
188 * @since 0.0.1
189 */
190 public static function get_float_value( $value, $default_value = 0.0 ) {
191 if ( is_numeric( $value ) ) {
192 return (float) $value;
193 }
194 return $default_value;
195 }
196
197 /**
198 * Safely get array value with type checking
199 *
200 * @param mixed $value The value to check.
201 * @param array<string, mixed> $default_value Default value if not an array.
202 * @return array<string, mixed>
203 * @since 0.0.1
204 */
205 public static function get_array_value( $value, $default_value = [] ) {
206 return is_array( $value ) ? $value : $default_value;
207 }
208
209 /**
210 * Check if current user has required capability.
211 *
212 * @param string $capability Capability to check (default: 'manage_options').
213 * @param array<mixed> $args Additional arguments for capability check.
214 * @return bool True if user has capability.
215 * @since 0.0.1
216 */
217 public static function current_user_can( $capability = '', $args = [] ) {
218 if ( ! function_exists( 'current_user_can' ) ) {
219 return false;
220 }
221
222 if ( ! is_string( $capability ) || empty( $capability ) ) {
223 $capability = 'manage_options';
224 }
225
226 return ! empty( $args )
227 ? current_user_can( $capability, ...$args )
228 : current_user_can( $capability );
229 }
230
231 /**
232 * Join an array of strings into a single string, filtering out empty values.
233 *
234 * @param array<string> $strings Array of strings to join.
235 * @param string $glue Separator to use (default: ' ').
236 * @return string Joined string.
237 * @since 0.0.1
238 */
239 public static function join_strings( $strings, $glue = ' ' ) {
240 if ( ! is_array( $strings ) ) {
241 return '';
242 }
243
244 $filtered = array_filter(
245 $strings,
246 static function ( $item ) {
247 return is_string( $item ) && '' !== trim( $item );
248 }
249 );
250
251 return implode( $glue, array_map( 'trim', $filtered ) );
252 }
253
254 /**
255 * Process blocks to generate unique slugs for SureDonation blocks.
256 *
257 * Recursively processes all blocks and generates slugs for those that
258 * don't have one set. Ensures all slugs are unique within the form.
259 *
260 * @param array<mixed> $blocks The blocks to process.
261 * @param array<string> $slugs Array of existing slugs (keyed by block_id).
262 * @param bool $updated Whether any blocks were updated.
263 * @param string $prefix Optional prefix for nested blocks.
264 * @return array{0: array<mixed>, 1: array<string>, 2: bool} Processed blocks, slugs, and updated flag.
265 * @since 0.0.1
266 */
267 public static function process_blocks( $blocks, $slugs = [], $updated = false, $prefix = '' ) {
268 if ( ! is_array( $blocks ) ) {
269 return [ [], $slugs, $updated ];
270 }
271 foreach ( $blocks as $index => $block ) {
272 if ( ! is_array( $block ) ) {
273 continue;
274 }
275 // Skip non-SureDonation blocks.
276 if ( ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) || strpos( $block['blockName'], 'suredonation/' ) !== 0 ) {
277 // Process inner blocks if any.
278 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
279 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix );
280 }
281 continue;
282 }
283
284 // Skip if no attrs or slug is already set and block_id is in slugs array.
285 if (
286 ! isset( $block['attrs'] ) ||
287 ! is_array( $block['attrs'] ) ||
288 (
289 ! empty( $block['attrs']['slug'] ) &&
290 isset( $block['attrs']['block_id'] ) &&
291 isset( $slugs[ $block['attrs']['block_id'] ] )
292 )
293 ) {
294 // Process inner blocks if any.
295 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
296 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix );
297 }
298 continue;
299 }
300
301 // Generate slug if empty.
302 if ( empty( $block['attrs']['slug'] ) ) {
303 $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix );
304 $updated = true;
305 }
306
307 // Track the slug if block_id is set.
308 if ( isset( $block['attrs']['block_id'] ) ) {
309 $slugs[ $block['attrs']['block_id'] ] = $blocks[ $index ]['attrs']['slug'];
310 }
311
312 // Process inner blocks recursively.
313 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
314 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks(
315 $block['innerBlocks'],
316 $slugs,
317 $updated,
318 $blocks[ $index ]['attrs']['slug']
319 );
320 }
321 }
322
323 return [ $blocks, $slugs, $updated ];
324 }
325
326 /**
327 * Generates a unique slug based on the provided block and existing slugs.
328 *
329 * @param array<mixed> $block The block data.
330 * @param array<string> $slugs The array of existing slugs.
331 * @param string $prefix Optional prefix for nested blocks.
332 * @return string The generated unique block slug.
333 * @since 0.0.1
334 */
335 public static function generate_unique_block_slug( $block, $slugs, $prefix = '' ) {
336 $slug = is_string( $block['blockName'] ?? '' ) ? str_replace( 'suredonation/', '', $block['blockName'] ) : '';
337
338 // Use label if available.
339 if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
340 $slug = sanitize_title( $block['attrs']['label'] );
341 }
342
343 // Add prefix for nested blocks.
344 if ( ! empty( $prefix ) ) {
345 $slug = $prefix . '-' . $slug;
346 }
347
348 return self::generate_unique_slug( $slug, $slugs );
349 }
350
351 /**
352 * Ensures that the slug is unique.
353 *
354 * If the slug is already taken, it appends a number to make it unique.
355 *
356 * @param string $slug The slug to make unique.
357 * @param array<string> $slugs Array of existing slugs.
358 * @return string The unique slug.
359 * @since 0.0.1
360 */
361 public static function generate_unique_slug( $slug, $slugs ) {
362 $slug = sanitize_title( $slug );
363
364 // Check if slug exists in the array values.
365 if ( ! in_array( $slug, $slugs, true ) ) {
366 return $slug;
367 }
368
369 // Append a number to make it unique.
370 $index = 1;
371 while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
372 ++$index;
373 }
374
375 return $slug . '-' . $index;
376 }
377
378 /**
379 * Get client IP address for logging purposes.
380 *
381 * Checks forwarded headers first (for proxied/load-balanced environments)
382 * then falls back to REMOTE_ADDR. This is suitable for informational
383 * logging only — do NOT use for security-critical IP validation.
384 *
385 * @return string Client IP address.
386 * @since 0.0.1
387 */
388 public static function get_client_ip() {
389 // Only trust REMOTE_ADDR — proxy headers (HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP)
390 // are trivially spoofable and should not be used for logging or security.
391 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
392
393 if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
394 return $ip;
395 }
396
397 return '';
398 }
399
400 /**
401 * Per-IP rate limiter for public (unauthenticated) submission endpoints.
402 *
403 * Uses a short-lived transient bucket keyed by action + client IP to
404 * throttle abuse (card-testing, DB/email flooding) on nopriv AJAX handlers.
405 * When the client IP cannot be determined the request is allowed, so
406 * legitimate donors are never blocked by a missing IP.
407 *
408 * @param string $action Unique action identifier namespacing the bucket.
409 * @param int $max Maximum attempts permitted within the window.
410 * @param int $window Window length in seconds.
411 * @return bool True if the request is within limits; false if the limit is exceeded.
412 * @since x.x.x
413 */
414 public static function check_rate_limit( $action, $max = 15, $window = MINUTE_IN_SECONDS ) {
415 $ip = self::get_client_ip();
416 if ( '' === $ip ) {
417 return true;
418 }
419
420 $key = 'suredonation_rl_' . md5( (string) $action . '|' . $ip );
421 $count = (int) get_transient( $key );
422
423 if ( $count >= $max ) {
424 return false;
425 }
426
427 set_transient( $key, $count + 1, $window );
428 return true;
429 }
430
431 /**
432 * Get sanitized request metadata (user agent and referer).
433 *
434 * @return array{user_agent: string, referer_url: string} Request metadata.
435 * @since 1.0.0
436 */
437 public static function get_request_meta() {
438 return [
439 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
440 'referer_url' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
441 ];
442 }
443
444 /**
445 * Get allowed HTML tags for form markup.
446 *
447 * The wp_kses_post() doesn't allow form elements, so we need a custom allowed tags array.
448 * This is safe because the markup is generated internally by trusted code that already
449 * escapes user input with esc_attr(), esc_html(), etc.
450 *
451 * @return array<string, array<string, bool>> Allowed HTML tags and attributes.
452 * @since 0.0.1
453 */
454 public static function get_allowed_form_html() {
455 // Note: data-* wildcard doesn't work in wp_kses, so we list each data attribute explicitly.
456 $common_data_attrs = [
457 'data-block-id' => true,
458 'data-form-id' => true,
459 'data-gateway' => true,
460 'data-stripe-key' => true,
461 'data-currency' => true,
462 'data-payment-mode' => true,
463 'data-amount-type' => true,
464 'data-fixed-amount' => true,
465 'data-payment-type' => true,
466 'data-customer-name-field' => true,
467 'data-customer-email-field' => true,
468 'data-nonce' => true,
469 'data-variable-amount-field' => true,
470 'data-minimum-amount' => true,
471 'data-subscription-plan-name' => true,
472 'data-subscription-interval' => true,
473 'data-subscription-billing-cycles' => true,
474 'data-currency-symbol' => true,
475 'data-message-format' => true,
476 'data-payment-methods' => true,
477 'data-method' => true,
478 'data-slug' => true,
479 'data-required' => true,
480 'data-fee-percentage' => true,
481 'data-fee-fixed' => true,
482 'data-fee-mode' => true,
483 'data-gateway-fees' => true,
484 'data-invalid-email-msg' => true,
485 'data-sd-mask' => true,
486 'data-custom-sd-mask' => true,
487 ];
488
489 return [
490 'div' => array_merge(
491 [
492 'id' => true,
493 'class' => true,
494 'style' => true,
495 'role' => true,
496 'aria-live' => true,
497 'aria-atomic' => true,
498 'aria-labelledby' => true,
499 ],
500 $common_data_attrs
501 ),
502 'form' => array_merge(
503 [
504 'id' => true,
505 'class' => true,
506 'method' => true,
507 'action' => true,
508 ],
509 $common_data_attrs
510 ),
511 'fieldset' => [
512 'id' => true,
513 'class' => true,
514 ],
515 'legend' => [
516 'id' => true,
517 'class' => true,
518 ],
519 'label' => [
520 'id' => true,
521 'class' => true,
522 'for' => true,
523 ],
524 'input' => array_merge(
525 [
526 'id' => true,
527 'class' => true,
528 'type' => true,
529 'name' => true,
530 'value' => true,
531 'placeholder' => true,
532 'min' => true,
533 'max' => true,
534 'step' => true,
535 'maxlength' => true,
536 'checked' => true,
537 'disabled' => true,
538 'readonly' => true,
539 'required' => true,
540 'aria-describedby' => true,
541 'aria-required' => true,
542 'aria-hidden' => true,
543 ],
544 $common_data_attrs
545 ),
546 'button' => array_merge(
547 [
548 'id' => true,
549 'class' => true,
550 'type' => true,
551 'disabled' => true,
552 ],
553 $common_data_attrs
554 ),
555 'select' => array_merge(
556 [
557 'id' => true,
558 'class' => true,
559 'name' => true,
560 'disabled' => true,
561 'required' => true,
562 'aria-describedby' => true,
563 'aria-required' => true,
564 ],
565 $common_data_attrs
566 ),
567 'option' => [
568 'value' => true,
569 'selected' => true,
570 'disabled' => true,
571 ],
572 'textarea' => array_merge(
573 [
574 'id' => true,
575 'class' => true,
576 'name' => true,
577 'rows' => true,
578 'cols' => true,
579 'placeholder' => true,
580 'maxlength' => true,
581 'disabled' => true,
582 'readonly' => true,
583 'required' => true,
584 'aria-describedby' => true,
585 'aria-required' => true,
586 ],
587 $common_data_attrs
588 ),
589 'span' => array_merge(
590 [
591 'id' => true,
592 'class' => true,
593 'style' => true,
594 'aria-hidden' => true,
595 ],
596 $common_data_attrs
597 ),
598 'p' => [
599 'id' => true,
600 'class' => true,
601 'style' => true,
602 'role' => true,
603 ],
604 'a' => [
605 'id' => true,
606 'class' => true,
607 'href' => true,
608 'target' => true,
609 'rel' => true,
610 'style' => true,
611 ],
612 'strong' => [
613 'class' => true,
614 ],
615 'em' => [
616 'class' => true,
617 ],
618 'ol' => [
619 'class' => true,
620 ],
621 'ul' => [
622 'class' => true,
623 ],
624 'li' => [
625 'class' => true,
626 ],
627 'br' => [],
628 'svg' => [
629 'class' => true,
630 'width' => true,
631 'height' => true,
632 'viewbox' => true,
633 'fill' => true,
634 'xmlns' => true,
635 'aria-hidden' => true,
636 ],
637 'circle' => [
638 'cx' => true,
639 'cy' => true,
640 'r' => true,
641 'stroke' => true,
642 'stroke-width' => true,
643 'fill' => true,
644 ],
645 'rect' => [
646 'x' => true,
647 'y' => true,
648 'width' => true,
649 'height' => true,
650 'rx' => true,
651 'stroke' => true,
652 'stroke-width' => true,
653 ],
654 'path' => [
655 'class' => true,
656 'd' => true,
657 'stroke' => true,
658 'stroke-width' => true,
659 'stroke-linecap' => true,
660 'stroke-linejoin' => true,
661 'fill' => true,
662 ],
663 ];
664 }
665
666 /**
667 * Get the nonce action string for a donation form.
668 *
669 * Shared between block render, shortcode render, and donation handler
670 * to ensure the nonce action is always consistent.
671 *
672 * @param int $campaign_id Campaign ID (0 for standalone forms).
673 * @return string Nonce action string.
674 * @since 1.0.0
675 */
676 public static function get_donation_nonce_action( $campaign_id ) {
677 // Note: This nonce is used by the generic donation-handler.php (form POST flow).
678 // Stripe and Offline AJAX handlers use a separate fixed nonce action
679 // 'suredonation_donation_form' generated in payment-markup.php — these are
680 // intentionally different nonce paths (form POST vs payment AJAX).
681 return $campaign_id ? 'suredonation_donation_' . $campaign_id : 'suredonation_donation_standalone';
682 }
683
684 /**
685 * Get form payment settings from post meta.
686 *
687 * Shared between the block and shortcode render paths to build
688 * the `window.suredonationPayment` frontend configuration object.
689 *
690 * @param int $form_id Form post ID.
691 * @return array<string, mixed> Payment settings array.
692 * @since 1.0.0
693 */
694 public static function get_form_payment_settings( $form_id ) {
695 $data = self::get_form_confirmation_settings( $form_id );
696
697 // Map confirmation type to frontend format.
698 $confirmation_type = 'message';
699 $redirect_url = '';
700 if ( 'custom url' === $data['confirmation_type'] ) {
701 $confirmation_type = 'redirect';
702 $redirect_url = $data['custom_url'];
703 } elseif ( 'different page' === $data['confirmation_type'] ) {
704 $confirmation_type = 'redirect';
705 $redirect_url = $data['page_url'];
706 }
707
708 $success_message = ! empty( $data['message'] )
709 ? $data['message']
710 : esc_html__( 'Thank you for your donation!', 'suredonation' );
711
712 return [
713 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
714 'confirmationType' => $confirmation_type,
715 'successTitle' => esc_html__( 'Thank You!', 'suredonation' ),
716 'successMessage' => wp_kses_post( self::get_string_value( $success_message ) ),
717 // Shown when payment succeeded at the gateway but our server-side
718 // finalize did not complete; the webhook will finalize it, so the
719 // donor must not be prompted to pay again.
720 'processingMessage' => esc_html__( 'Payment received. We are finalizing your donation and will email you a confirmation shortly. Please do not pay again.', 'suredonation' ),
721 'redirectUrl' => ! empty( $redirect_url ) ? esc_url( self::get_string_value( $redirect_url ) ) : '',
722 'submissionAction' => $data['submission_action'],
723 // translators: %s: formatted fee amount with currency symbol.
724 'feeIncludesText' => __( '(includes %s processing fee)', 'suredonation' ),
725 'amountPlaceholder' => __( 'Complete the form to view the amount.', 'suredonation' ),
726 ];
727 }
728
729 /**
730 * Get form confirmation settings from post meta.
731 *
732 * Reads from consolidated _suredonation_form_confirmation meta key.
733 *
734 * @param int $form_id Form post ID.
735 * @return array<string, string> Confirmation settings with defaults applied.
736 * @since 1.0.0
737 */
738 public static function get_form_confirmation_settings( $form_id ) {
739 $defaults = [
740 'confirmation_type' => 'same page',
741 'message' => '',
742 'submission_action' => 'hide form',
743 'custom_url' => '',
744 'page_url' => '',
745 ];
746
747 $raw = get_post_meta( $form_id, '_suredonation_form_confirmation', true );
748
749 if ( ! empty( $raw ) && is_string( $raw ) ) {
750 $data = json_decode( $raw, true );
751 if ( is_array( $data ) ) {
752 return wp_parse_args( $data, $defaults );
753 }
754 }
755
756 return $defaults;
757 }
758
759 /**
760 * Get smart tags definitions grouped by context.
761 *
762 * Centralized source of truth for all smart tag lists used across
763 * admin UI, form editor, and email settings.
764 *
765 * @return array<string, array<int, array<string, mixed>>> Smart tags grouped by context.
766 * @since 1.0.0
767 */
768 public static function get_smart_tags() {
769 $confirmation_tags = [
770 [
771 'tag' => '{donor_name}',
772 'title' => __( 'Donor Name', 'suredonation' ),
773 ],
774 [
775 'tag' => '{donor_email}',
776 'title' => __( 'Donor Email', 'suredonation' ),
777 ],
778 [
779 'tag' => '{amount}',
780 'title' => __( 'Donation Amount', 'suredonation' ),
781 ],
782 [
783 'tag' => '{campaign_name}',
784 'title' => __( 'Campaign Name', 'suredonation' ),
785 ],
786 [
787 'tag' => '{donation_date}',
788 'title' => __( 'Donation Date', 'suredonation' ),
789 ],
790 [
791 'tag' => '{transaction_id}',
792 'title' => __( 'Transaction ID', 'suredonation' ),
793 ],
794 [
795 'tag' => '{payment_method}',
796 'title' => __( 'Payment Method', 'suredonation' ),
797 ],
798 [
799 'tag' => '{site_title}',
800 'title' => __( 'Site Title', 'suredonation' ),
801 ],
802 [
803 'tag' => '{donation_total}',
804 'title' => __( 'Donation Total', 'suredonation' ),
805 ],
806 [
807 'tag' => '{payment_status}',
808 'title' => __( 'Payment Status', 'suredonation' ),
809 ],
810 [
811 'tag' => '{donation_receipt}',
812 'title' => __( 'Donation Receipt', 'suredonation' ),
813 ],
814 [
815 'tag' => '{success_badge}',
816 'title' => __( 'Success Badge', 'suredonation' ),
817 ],
818 ];
819
820 return [
821 'confirmation' => $confirmation_tags,
822 'email' => array_merge(
823 $confirmation_tags,
824 [
825 [
826 'tag' => '{admin_email}',
827 'title' => __( 'Admin Email', 'suredonation' ),
828 ],
829 [
830 'tag' => '{site_url}',
831 'title' => __( 'Site URL', 'suredonation' ),
832 ],
833 [
834 'tag' => '{admin_url}',
835 'title' => __( 'Admin URL', 'suredonation' ),
836 ],
837 [
838 'tag' => '{subscription_id}',
839 'title' => __( 'Subscription ID', 'suredonation' ),
840 ],
841 [
842 'tag' => '{subscription_interval}',
843 'title' => __( 'Subscription Interval', 'suredonation' ),
844 ],
845 [
846 'tag' => '{offline_instructions}',
847 'title' => __( 'Offline Instructions', 'suredonation' ),
848 ],
849 ]
850 ),
851 'email_grouped' => [
852 [
853 'label' => __( 'Donation Tags', 'suredonation' ),
854 'tags' => [
855 [
856 'tag' => '{donor_name}',
857 'title' => __( 'Donor Name', 'suredonation' ),
858 ],
859 [
860 'tag' => '{donor_email}',
861 'title' => __( 'Donor Email', 'suredonation' ),
862 ],
863 [
864 'tag' => '{amount}',
865 'title' => __( 'Donation Amount', 'suredonation' ),
866 ],
867 [
868 'tag' => '{campaign_name}',
869 'title' => __( 'Campaign Name', 'suredonation' ),
870 ],
871 [
872 'tag' => '{donation_date}',
873 'title' => __( 'Donation Date', 'suredonation' ),
874 ],
875 [
876 'tag' => '{transaction_id}',
877 'title' => __( 'Transaction ID', 'suredonation' ),
878 ],
879 [
880 'tag' => '{payment_method}',
881 'title' => __( 'Payment Method', 'suredonation' ),
882 ],
883 [
884 'tag' => '{subscription_id}',
885 'title' => __( 'Subscription ID', 'suredonation' ),
886 ],
887 [
888 'tag' => '{subscription_interval}',
889 'title' => __( 'Subscription Interval', 'suredonation' ),
890 ],
891 [
892 'tag' => '{refund_amount}',
893 'title' => __( 'Refund Amount', 'suredonation' ),
894 ],
895 ],
896 ],
897 [
898 'label' => __( 'General Tags', 'suredonation' ),
899 'tags' => [
900 [
901 'tag' => '{site_title}',
902 'title' => __( 'Site Title', 'suredonation' ),
903 ],
904 [
905 'tag' => '{admin_email}',
906 'title' => __( 'Admin Email', 'suredonation' ),
907 ],
908 [
909 'tag' => '{site_url}',
910 'title' => __( 'Site URL', 'suredonation' ),
911 ],
912 [
913 'tag' => '{admin_url}',
914 'title' => __( 'Admin URL', 'suredonation' ),
915 ],
916 [
917 'tag' => '{offline_instructions}',
918 'title' => __( 'Offline Instructions', 'suredonation' ),
919 ],
920 ],
921 ],
922 ],
923 'offline_instructions' => [
924 [
925 'tag' => '{campaign_name}',
926 'title' => __( 'Campaign Name', 'suredonation' ),
927 ],
928 [
929 'tag' => '{site_title}',
930 'title' => __( 'Site Title', 'suredonation' ),
931 ],
932 [
933 'tag' => '{site_url}',
934 'title' => __( 'Site URL', 'suredonation' ),
935 ],
936 [
937 'tag' => '{admin_email}',
938 'title' => __( 'Admin Email', 'suredonation' ),
939 ],
940 ],
941 ];
942 }
943
944 /**
945 * Map a payment gateway slug to a human-readable label.
946 *
947 * @param string $gateway Gateway slug (e.g. stripe, paypal, manual).
948 * @return string Display label.
949 * @since 1.0.0
950 */
951 public static function get_payment_method_label( $gateway ) {
952 switch ( $gateway ) {
953 case 'paypal':
954 return __( 'PayPal', 'suredonation' );
955 case 'manual':
956 case 'offline':
957 return __( 'Offline Donation', 'suredonation' );
958 case 'stripe':
959 return __( 'Stripe', 'suredonation' );
960 default:
961 return ucwords( str_replace( [ '_', '-' ], ' ', (string) $gateway ) );
962 }
963 }
964
965 /**
966 * Render the static "Success" badge used by the {success_badge} smart tag.
967 *
968 * @return string Badge HTML.
969 * @since 1.0.0
970 */
971 public static function render_success_badge() {
972 return '<span class="sd-success-box__badge">' . esc_html__( 'Success', 'suredonation' ) . '</span>';
973 }
974
975 /**
976 * Render a styled payment-status badge for the donation confirmation.
977 *
978 * @param string $status Payment status (e.g. completed, pending, failed).
979 * @return string Badge HTML.
980 * @since 1.0.0
981 */
982 public static function get_payment_status_config( $status ) {
983 $status = strtolower( trim( (string) $status ) );
984
985 $map = [
986 'completed' => [
987 'label' => __( 'Complete', 'suredonation' ),
988 'variant' => 'complete',
989 ],
990 'complete' => [
991 'label' => __( 'Complete', 'suredonation' ),
992 'variant' => 'complete',
993 ],
994 'pending' => [
995 'label' => __( 'Pending', 'suredonation' ),
996 'variant' => 'pending',
997 ],
998 'processing' => [
999 'label' => __( 'Processing', 'suredonation' ),
1000 'variant' => 'pending',
1001 ],
1002 'failed' => [
1003 'label' => __( 'Failed', 'suredonation' ),
1004 'variant' => 'failed',
1005 ],
1006 'refunded' => [
1007 'label' => __( 'Refunded', 'suredonation' ),
1008 'variant' => 'refunded',
1009 ],
1010 ];
1011
1012 return $map[ $status ] ?? [
1013 'label' => '' !== $status ? ucfirst( $status ) : __( 'Complete', 'suredonation' ),
1014 'variant' => 'pending',
1015 ];
1016 }
1017
1018 /**
1019 * Render a styled payment-status badge for the donation receipt row.
1020 *
1021 * @param string $status Payment status (e.g. completed, pending, failed).
1022 * @return string Badge HTML.
1023 * @since 1.0.0
1024 */
1025 public static function render_payment_status_badge( $status ) {
1026 $config = self::get_payment_status_config( $status );
1027 return sprintf(
1028 '<span class="sd-receipt-badge sd-receipt-badge--%1$s">%2$s</span>',
1029 esc_attr( $config['variant'] ),
1030 esc_html( $config['label'] )
1031 );
1032 }
1033
1034 /**
1035 * Render the donation receipt card used by the {donation_receipt} smart tag.
1036 *
1037 * @param array<string, mixed> $donation_data Donation data.
1038 * @param string $campaign_name Campaign name ('' for standalone forms).
1039 * @return string Receipt card HTML.
1040 * @since 1.0.0
1041 */
1042 public static function render_donation_receipt( $donation_data, $campaign_name = '' ) {
1043 $currency = isset( $donation_data['currency'] ) && is_string( $donation_data['currency'] ) ? $donation_data['currency'] : 'USD';
1044 $base_amount = isset( $donation_data['amount'] ) && is_numeric( $donation_data['amount'] ) ? (float) $donation_data['amount'] : 0.0;
1045 $fees_covered = isset( $donation_data['fees_covered'] ) && is_numeric( $donation_data['fees_covered'] ) ? (float) $donation_data['fees_covered'] : 0.0;
1046 $total = $base_amount + $fees_covered;
1047
1048 $donor_name = isset( $donation_data['donor_name'] ) && is_string( $donation_data['donor_name'] ) ? $donation_data['donor_name'] : '';
1049 $donor_email = isset( $donation_data['donor_email'] ) && is_string( $donation_data['donor_email'] ) ? $donation_data['donor_email'] : '';
1050 $gateway = isset( $donation_data['gateway'] ) && is_string( $donation_data['gateway'] ) ? $donation_data['gateway'] : '';
1051 $status = isset( $donation_data['payment_status'] ) && is_string( $donation_data['payment_status'] ) ? $donation_data['payment_status'] : '';
1052
1053 $rows = [
1054 [
1055 'label' => __( 'Donor Name', 'suredonation' ),
1056 'value' => esc_html( $donor_name ),
1057 ],
1058 [
1059 'label' => __( 'Donor Email', 'suredonation' ),
1060 'value' => esc_html( $donor_email ),
1061 ],
1062 ];
1063
1064 if ( '' !== $campaign_name ) {
1065 $rows[] = [
1066 'label' => __( 'Campaign Name', 'suredonation' ),
1067 'value' => esc_html( $campaign_name ),
1068 ];
1069 }
1070
1071 $rows[] = [
1072 'label' => __( 'Payment Status', 'suredonation' ),
1073 'value' => self::render_payment_status_badge( $status ),
1074 ];
1075 $rows[] = [
1076 'label' => __( 'Payment Method', 'suredonation' ),
1077 'value' => esc_html( self::get_payment_method_label( $gateway ) ),
1078 ];
1079 $rows[] = [
1080 'label' => __( 'Donation Amount', 'suredonation' ),
1081 'value' => esc_html( Payment_Helper::format_amount( $base_amount, $currency ) ),
1082 ];
1083
1084 $rows_html = '';
1085 foreach ( $rows as $row ) {
1086 $rows_html .= sprintf(
1087 '<div class="sd-receipt-row"><span class="sd-receipt-row__label">%1$s</span><span class="sd-receipt-row__value">%2$s</span></div>',
1088 esc_html( $row['label'] ),
1089 $row['value']
1090 );
1091 }
1092
1093 $rows_html .= sprintf(
1094 '<div class="sd-receipt-row sd-receipt-row--total"><span class="sd-receipt-row__label">%1$s</span><span class="sd-receipt-row__value">%2$s</span></div>',
1095 esc_html__( 'Donation Total', 'suredonation' ),
1096 esc_html( Payment_Helper::format_amount( $total, $currency ) )
1097 );
1098
1099 return sprintf(
1100 '<div class="sd-receipt-card"><h3 class="sd-receipt-card__title">%1$s</h3><div class="sd-receipt-rows">%2$s</div></div>',
1101 esc_html__( 'Donation Receipt', 'suredonation' ),
1102 $rows_html
1103 );
1104 }
1105
1106 /**
1107 * Default confirmation message template (receipt layout with smart tags).
1108 *
1109 * @return string Message HTML template.
1110 * @since 1.0.0
1111 */
1112 public static function get_default_confirmation_message() {
1113 return '<p style="text-align: center; margin: 0;">{success_badge}</p>'
1114 . '<h2 class="sd-receipt-title" style="text-align: center;">'
1115 /* translators: {donor_name} is a smart tag replaced with the donor's name. */
1116 . esc_html__( 'Thank you {donor_name} for your Donation', 'suredonation' )
1117 . '</h2>'
1118 . '<p class="sd-receipt-subtitle" style="text-align: center;">'
1119 . esc_html__( 'Your contribution means a lot. We have sent an email to your registered account along with a receipt for your donation.', 'suredonation' )
1120 . '</p>{donation_receipt}';
1121 }
1122
1123 /**
1124 * Build the rendered confirmation/thank-you HTML for a donation.
1125 *
1126 * Resolves the form's confirmation message template against the donation's
1127 * real data (smart tags) so the frontend can display the receipt.
1128 *
1129 * @param int $donation_id Donation ID.
1130 * @return string Sanitized confirmation HTML, or '' on failure.
1131 * @since 1.0.0
1132 */
1133 public static function render_confirmation_message( $donation_id ) {
1134 $donation = Donations::get( $donation_id );
1135 if ( ! is_array( $donation ) ) {
1136 return '';
1137 }
1138
1139 $form_id = isset( $donation['form_id'] ) ? absint( $donation['form_id'] ) : 0;
1140 $campaign_id = isset( $donation['campaign_id'] ) ? absint( $donation['campaign_id'] ) : 0;
1141
1142 $settings = self::get_form_confirmation_settings( $form_id );
1143 $template = ! empty( $settings['message'] ) ? $settings['message'] : self::get_default_confirmation_message();
1144
1145 $donation_data = [
1146 'id' => $donation_id,
1147 'donor_name' => $donation['donor_name'] ?? '',
1148 'donor_email' => $donation['donor_email'] ?? '',
1149 'amount' => $donation['amount'] ?? 0,
1150 'fees_covered' => $donation['fees_covered'] ?? 0,
1151 'currency' => $donation['currency'] ?? Payment_Helper::get_currency(),
1152 'gateway' => $donation['gateway'] ?? '',
1153 'payment_status' => $donation['payment_status'] ?? '',
1154 'transaction_id' => $donation['transaction_id'] ?? '',
1155 'donation_type' => $donation['donation_type'] ?? 'one-time',
1156 ];
1157
1158 $campaign = $campaign_id ? get_post( $campaign_id ) : null;
1159
1160 $rendered = Email_Handler::process_smart_tags( $template, $donation_data, $campaign );
1161
1162 return wp_kses_post( $rendered );
1163 }
1164 }
1165