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.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 1.7.0.9 All 211 releases
charitable / includes / shortcodes / class-charitable-email-shortcode.php

class-charitable-email-shortcode.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/shortcodes/class-charitable-email-shortcode.php

296 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email shortcode class.
4 *
5 * @package Charitable/Shortcodes/Email
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.5.0
10 * @version 1.5.0
11 * @version 1.8.9.5 Added recursion prevention for email shortcodes.
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( ! class_exists( 'Charitable_Email_Shortcode' ) ) :
20
21 /**
22 * Charitable_Email_Shortcode class.
23 *
24 * @since 1.5.0
25 */
26 class Charitable_Email_Shortcode {
27
28 /**
29 * Static object instance.
30 *
31 * @since 1.5.0
32 *
33 * @var Charitable_Email_Shortcode
34 */
35 private static $instance;
36
37 /**
38 * The `Charitable_Email_Fields` instance.
39 *
40 * @since 1.5.0
41 *
42 * @var Charitable_Email_Fields
43 */
44 private $fields;
45
46 /**
47 * Processing stack to track active shortcodes and prevent recursion.
48 *
49 * @since 1.8.9.5
50 *
51 * @var array
52 */
53 private static $processing_stack = array();
54
55 /**
56 * Processing depth counter for recursion detection.
57 *
58 * @since 1.8.9.5
59 *
60 * @var int
61 */
62 private static $processing_depth = 0;
63
64 /**
65 * Set up class instance.
66 *
67 * @since 1.5.0
68 *
69 * @param Charitable_Email_Fields $fields The Charitable_Email_Fields instance.
70 */
71 private function __construct( Charitable_Email_Fields $fields ) {
72 $this->fields = $fields;
73 }
74
75 /**
76 * Set up the class instance.
77 *
78 * @since 1.5.0
79 *
80 * @param Charitable_Email $email The email object.
81 * @return void
82 */
83 public static function init( Charitable_Email $email ) {
84 $fields = new Charitable_Email_Fields( $email, false );
85 self::$instance = new self( $fields );
86 }
87
88 /**
89 * Set up the class instance, with preview set to true.
90 *
91 * @since 1.5.0
92 *
93 * @param Charitable_Email $email The email object.
94 * @return void
95 */
96 public static function init_preview( Charitable_Email $email ) {
97 $fields = new Charitable_Email_Fields( $email, true );
98 self::$instance = new self( $fields );
99 }
100
101 /**
102 * Flush the class instance.
103 *
104 * @since 1.5.0
105 *
106 * @return void
107 */
108 public static function flush() {
109 self::$instance = null;
110 }
111
112 /**
113 * The callback method for the campaigns shortcode.
114 *
115 * This receives the user-defined attributes and passes the logic off to the class.
116 *
117 * @since 1.5.0
118 *
119 * @param array $atts User-defined shortcode attributes.
120 * @return string
121 */
122 public static function display( $atts ) {
123 if ( ! isset( self::$instance ) ) {
124 charitable_get_deprecated()->doing_it_wrong(
125 __METHOD__,
126 __( '[charitable_email] cannot be called until a class instance has been created.', 'charitable' ),
127 '1.5.0'
128 );
129 return;
130 }
131
132 $defaults = array(
133 'show' => '',
134 'preview' => self::$instance->fields->is_preview(),
135 );
136
137 $args = apply_filters( 'charitable_email_shortcode_args', wp_parse_args( $atts, $defaults ), $atts, $defaults );
138
139 if ( ! isset( $args['show'] ) ) {
140 return '';
141 }
142
143 // Recursion prevention - Added in 1.8.9.5
144 $recursion_result = self::check_recursion( $args['show'], $args );
145 if ( false !== $recursion_result ) {
146 return $recursion_result;
147 }
148
149 // Add to processing stack
150 self::push_processing_stack( $args['show'] );
151
152 // Process the shortcode
153 $result = self::$instance->get( $args['show'], $args );
154
155 // Remove from processing stack
156 self::pop_processing_stack( $args['show'] );
157
158 return $result;
159 }
160
161 /**
162 * Return the value for a particular variable.
163 *
164 * @since 1.5.0
165 *
166 * @param string $field The field.
167 * @param array $args Mixed arguments.
168 * @return string
169 */
170 public function get( $field, $args ) {
171 $value = apply_filters( 'charitable_email_shortcode_get_value', $this->fields->get_field_value( $field, $args ) );
172 return $value;
173 }
174
175 /**
176 * Check for recursion and return fallback content if detected.
177 *
178 * @since 1.8.9.5
179 *
180 * @param string $show The field to show.
181 * @param array $args Mixed arguments.
182 * @return string|false False if no recursion, fallback content if recursion detected.
183 */
184 private static function check_recursion( $show, $args ) {
185 // Check depth limit
186 $max_depth = apply_filters( 'charitable_email_recursion_depth_limit', 3 );
187 if ( self::$processing_depth >= $max_depth ) {
188 self::log_recursion_attempt( 'depth', $show, self::$processing_depth, $max_depth );
189 return self::get_context_specific_fallback( $show, 'depth_limit' );
190 }
191
192 // Check for direct recursion (same field already being processed)
193 $stack_key = $show;
194 if ( in_array( $stack_key, self::$processing_stack, true ) ) {
195 self::log_recursion_attempt( 'circular', $show, count( self::$processing_stack ), $max_depth );
196 return self::get_context_specific_fallback( $show, 'circular_reference' );
197 }
198
199 return false; // No recursion detected
200 }
201
202 /**
203 * Add a field to the processing stack.
204 *
205 * @since 1.8.9.5
206 *
207 * @param string $show The field being processed.
208 */
209 private static function push_processing_stack( $show ) {
210 self::$processing_stack[] = $show;
211 self::$processing_depth++;
212 }
213
214 /**
215 * Remove a field from the processing stack.
216 *
217 * @since 1.8.9.5
218 *
219 * @param string $show The field being processed.
220 */
221 private static function pop_processing_stack( $show ) {
222 $index = array_search( $show, self::$processing_stack, true );
223 if ( false !== $index ) {
224 unset( self::$processing_stack[ $index ] );
225 self::$processing_stack = array_values( self::$processing_stack ); // Re-index array
226 }
227 self::$processing_depth = max( 0, self::$processing_depth - 1 );
228 }
229
230 /**
231 * Get context-specific fallback content when recursion is detected.
232 *
233 * @since 1.8.9.5
234 *
235 * @param string $show The field that caused recursion.
236 * @param string $reason The reason for fallback (depth_limit, circular_reference).
237 * @return string Fallback content.
238 */
239 private static function get_context_specific_fallback( $show, $reason ) {
240 $fallbacks = array(
241 'offline_instructions' => __( 'Payment instructions will be provided separately.', 'charitable' ),
242 'donation_summary' => __( 'Donation summary unavailable.', 'charitable' ),
243 'donor' => __( 'Donor information unavailable.', 'charitable' ),
244 'donor_email' => __( 'Email unavailable.', 'charitable' ),
245 'site_name' => get_bloginfo( 'name' ),
246 'campaign_title' => __( 'Campaign information unavailable.', 'charitable' ),
247 );
248
249 // Get specific fallback or generic one
250 $fallback = isset( $fallbacks[ $show ] ) ? $fallbacks[ $show ] : __( 'Content temporarily unavailable.', 'charitable' );
251
252 /**
253 * Filter the fallback content for recursive email shortcodes.
254 *
255 * @since 1.8.9.5
256 *
257 * @param string $fallback The default fallback content.
258 * @param string $show The field that caused recursion.
259 * @param string $reason The reason for fallback.
260 */
261 return apply_filters( 'charitable_email_shortcode_recursion_fallback', $fallback, $show, $reason );
262 }
263
264 /**
265 * Log recursion attempts for debugging.
266 *
267 * @since 1.8.9.5
268 *
269 * @param string $type Type of recursion (circular, depth).
270 * @param string $show The field that caused recursion.
271 * @param int $current Current depth/stack size.
272 * @param int $limit The configured limit.
273 */
274 private static function log_recursion_attempt( $type, $show, $current, $limit ) {
275 // Only log if both CHARITABLE_DEBUG and CHARITABLE_DEBUG_EMAILS are enabled
276 if ( ! charitable_is_debug() || ! defined( 'CHARITABLE_DEBUG_EMAILS' ) || ! CHARITABLE_DEBUG_EMAILS ) {
277 return;
278 }
279
280 $message = sprintf(
281 '[CHARITABLE_EMAIL_DEBUG] Recursion prevented: %s recursion detected for shortcode [charitable_email show="%s"]. Current: %d, Limit: %d, Stack: [%s]',
282 $type,
283 $show,
284 $current,
285 $limit,
286 implode( ', ', self::$processing_stack )
287 );
288
289 if ( charitable_is_debug() ) {
290 error_log( $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
291 }
292 }
293 }
294
295 endif;
296