PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.3
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / shortcodes / class-charitable-campaign-shortcode.php

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

375 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Campaign v2 shortcode model.
4 *
5 * @package Charitable/Classes/Charitable_Campaign_Shortcode
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.8.0
10 * @version. 1.8.0
11 * @version 1.8.12.2 Add an infinite-recursion guard so a self-referencing [campaign] shortcode cannot exhaust memory.
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( ! class_exists( 'Charitable_Campaign_Shortcode' ) ) :
20
21 /**
22 * Campaign Model
23 */
24 class Charitable_Campaign_Shortcode {
25
26 /**
27 * Campaign data.
28 *
29 * @var array $campaign_data The campaign data.
30 */
31 private $campaign_data;
32
33 /**
34 * Campaign IDs that are currently mid-render (the recursion stack).
35 *
36 * Used to stop a campaign from rendering itself forever. A campaign whose
37 * content embeds a [campaign] shortcode pointing back at the same campaign
38 * (directly, or via a mutual A->B->A reference) would otherwise re-enter
39 * self::display() on every level, nesting an output buffer each time until
40 * PHP's memory limit is exhausted and the request fatals. Keyed by campaign
41 * ID for O(1) lookups; a set (not a bool) so mutual references are caught too.
42 *
43 * @since 1.8.12.2
44 *
45 * @var array<int,bool>
46 */
47 private static $rendering = array();
48
49 /**
50 * Whether the given campaign is already being rendered further up the stack.
51 *
52 * @since 1.8.12.2
53 *
54 * @param int $campaign_id The campaign ID.
55 * @return bool
56 */
57 public static function is_rendering( $campaign_id ) {
58 return isset( self::$rendering[ (int) $campaign_id ] );
59 }
60
61 /**
62 * Display the shortcode output. This is the callback method for the campaigns shortcode.
63 *
64 * @since 1.8.0
65 *
66 * @param array $atts The user-defined shortcode attributes.
67 * @return string
68 */
69 public static function display( $atts ) {
70
71 $default = array(
72 'id' => '',
73 );
74
75 $args = shortcode_atts( $default, $atts, 'campaign' );
76
77 // Guard against infinite recursion. If this campaign is already being rendered
78 // higher up the stack, a field inside it embeds a [campaign] shortcode that points
79 // back at the same campaign (directly, or via a mutual A->B->A reference). Rendering
80 // it again here would loop until the memory limit is exhausted, fatalling the request.
81 // Bail with a harmless placeholder.
82 $recursion_guard_id = absint( $args['id'] );
83
84 /*
85 * Defence in depth for the id-less form, which absint( '' ) would otherwise
86 * reduce to 0 and exclude from both the check below and the flagging further
87 * down.
88 *
89 * A bare [campaign] does resolve to the current post -- get_campaign() passes
90 * the empty string through to charitable_get_campaign(), and WP's get_post()
91 * falls back to $GLOBALS['post'] on an empty argument (verified: it returns
92 * the surrounding campaign). It cannot recurse today only because
93 * get_campaign_data() reads campaign_settings_v2 via get_post_meta( '' ),
94 * which gets no such fallback and returns empty, so no fields render and no
95 * nested shortcode ever executes -- a bare [campaign] emits an empty wrapper.
96 *
97 * That makes this belt and braces rather than a live fix: the only thing
98 * preventing the loop is an asymmetry between get_post() and get_post_meta().
99 * Give get_campaign_data() the same fallback and the vector opens, so resolve
100 * the ID the guard would need before that can happen.
101 */
102 if ( ! $recursion_guard_id ) {
103 $recursion_guard_id = absint( get_the_ID() );
104 }
105
106 if ( $recursion_guard_id && self::is_rendering( $recursion_guard_id ) ) {
107
108 // Invisible in the rendered page, but leaves a trace in the source for debugging.
109 $notice = '<!-- charitable: skipped self-referencing [campaign] shortcode for campaign ' . $recursion_guard_id . ' to prevent an infinite render loop. -->';
110
111 // In the builder preview / admin, surface a visible notice so the author understands
112 // why the embedded campaign did not render, instead of silently dropping it.
113 $is_preview = is_admin() || ! empty( $_GET['charitable_campaign_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
114
115 if ( $is_preview ) {
116 $notice .= '<div class="charitable-notice charitable-notice-info"><p style="margin:0;">' .
117 esc_html__( 'This campaign embeds a shortcode that points back at itself, so it was not rendered here to prevent an infinite loop. Remove or change that shortcode field.', 'charitable' ) .
118 '</p></div>';
119 }
120
121 /**
122 * Filter the placeholder returned when a self-referencing [campaign] shortcode is skipped.
123 *
124 * @since 1.8.12.2
125 *
126 * @param string $notice The placeholder markup.
127 * @param int $campaign_id The campaign ID that would have recursed.
128 * @param array $args The parsed shortcode arguments.
129 */
130 return apply_filters( 'charitable_campaign_shortcode_recursion_notice', $notice, $recursion_guard_id, $args );
131 }
132
133 // Get the campaign data from the ID that should be passed along in the shortcode.
134 $args['campaign'] = self::get_campaign( $args );
135 $args['campaign_data'] = self::get_campaign_data( $args );
136
137 // Get the template_id, which should be a part of the campaign data, fall back to the default if you have to.
138 $args['template_id'] = ! empty( $args['campaign_data']['template_id'] ) ? esc_attr( $args['campaign_data']['template_id'] ) : charitable_campaign_builder_default_template();
139
140 $args['template'] = self::get_template( $args['template_id'] );
141
142 self::prefix_add_footer_styles( $args['template_id'], $args['campaign_data'] );
143
144 /**
145 * Replace the default template with your own.
146 *
147 * If you replace the template with your own, it needs to be an instance of Charitable_Template.
148 *
149 * @since 1.0.0
150 *
151 * @param false|Charitable_Template The template. If false (the default), we will use our own template.
152 * @param array $args All the parsed arguments.
153 */
154 $template = apply_filters( 'charitable_campaign_shortcode_template', false, $args );
155
156 /* Fall back to default Charitable_Template if no template returned or if template was not object of 'Charitable_Template' class. */
157 if ( ! is_object( $template ) || ! is_a( $template, 'Charitable_Template' ) ) {
158 $template = new Charitable_Template( 'campaign/builder/content.php', false );
159 }
160
161 if ( ! $template->template_file_exists() ) {
162 return false;
163 }
164
165 /**
166 * Modify the view arguments that are passed to the campaigns shortcode template.
167 *
168 * @since 1.0.0
169 *
170 * @param array $view_args The arguments to pass.
171 * @param array $args All the parsed arguments.
172 */
173 $view_args = apply_filters( 'charitable_campaign_shortcode_view_args', $args, $template );
174
175 $template->set_view_args( $view_args );
176
177 ob_start();
178
179 try {
180 // Flag this campaign as rendering so any nested [campaign] shortcode that points
181 // back at it short-circuits in the recursion guard above instead of looping. The
182 // matching unset in the finally below runs even if rendering throws, so a campaign
183 // is never left flagged as rendering for the rest of the request.
184 if ( $recursion_guard_id ) {
185 self::$rendering[ $recursion_guard_id ] = true;
186 }
187
188 $template->render();
189 } finally {
190 if ( $recursion_guard_id ) {
191 unset( self::$rendering[ $recursion_guard_id ] );
192 }
193 }
194
195 $html = ob_get_clean();
196
197 // If the $args['elementor'] is true, then replace the campaign-loop with the elementor campaign-loop.
198 if ( isset( $args['elementor'] ) && $args['elementor'] ) {
199 $html = str_replace( 'campaign-loop', 'elementor-campaign-loop', $html );
200 }
201
202 /**
203 * Customize the output of the shortcode.
204 *
205 * @since 1.0.0
206 *
207 * @param string $content The content to be displayed.
208 * @param array $args All the parsed arguments.
209 * @return string
210 */
211 return apply_filters( 'charitable_campaign_shortcode', $html, $args );
212 }
213
214 /**
215 * Return campaigns to display in the campaigns shortcode.
216 *
217 * @since 1.0.0
218 *
219 * @param array $args The query arguments to be used to retrieve campaigns.
220 * @return WP_Query
221 */
222 public static function get_campaign( $args ) {
223
224 $campaign = charitable_get_campaign( $args['id'] );
225
226 return $campaign;
227 }
228
229 /**
230 * Get campaign data.
231 *
232 * @since 1.0.0
233 *
234 * @param array $args The query arguments to be used to retrieve campaigns.
235 * @return WP_Query
236 */
237 public static function get_campaign_data( $args ) {
238
239 $settings = get_post_meta( $args['id'], 'campaign_settings_v2', true );
240
241 if ( ! empty( $settings ) ) {
242 return $settings;
243 }
244
245 return $args;
246 }
247
248 /**
249 * Return the template metadata to help display in the campaigns shortcode.
250 *
251 * @since 1.0.0
252 *
253 * @param string $template_id The template ID.
254 * @return array|false
255 */
256 public static function get_template_data( $template_id ) {
257
258 $settings = get_post_meta( $template_id, 'campaign_settings_v2', true );
259
260 if ( ! empty( $settings ) ) {
261 return $settings;
262 }
263
264 return false;
265 }
266
267 /**
268 * Load template specific CSS in the footer.
269 *
270 * @since 1.8.0
271 * @version 1.8.1 Added $template_id blank check.
272 * @version 1.8.2 removed $min from frotend styles.
273 *
274 * @param string $template_id Template slug.
275 * @param array $campaign_data Saved campaign data.
276 */
277 public static function prefix_add_footer_styles( $template_id = 'basic', $campaign_data = false ) {
278
279 if ( '' === $template_id ) {
280 return;
281 }
282
283 $min = charitable_get_min_suffix();
284 $version = charitable()->get_version();
285 $assets_dir = charitable()->get_path( 'assets', false );
286
287 // Pass along the theme color overides to the theme. Start by defining the base/default theme values as stored in settings.
288 $query_args = array(
289 'p' => ! empty( $campaign_data['color_base_primary'] ) ? charitable_sanitize_hex( $campaign_data['color_base_primary'], false ) : false,
290 's' => ! empty( $campaign_data['color_base_secondary'] ) ? charitable_sanitize_hex( $campaign_data['color_base_secondary'], false ) : false,
291 't' => ! empty( $campaign_data['color_base_tertiary'] ) ? charitable_sanitize_hex( $campaign_data['color_base_tertiary'], false ) : false,
292 'b' => ! empty( $campaign_data['color_base_button'] ) ? charitable_sanitize_hex( $campaign_data['color_base_button'], false ) : false,
293 );
294
295 // Now check for overrides in the settings.
296 $layout_theme_settings = isset( $campaign_data['layout']['advanced'] ) ? $campaign_data['layout']['advanced'] : array();
297
298 $query_args['p'] = ! empty( $layout_theme_settings['theme_color_primary'] ) ? charitable_sanitize_hex( $layout_theme_settings['theme_color_primary'], false ) : $query_args['p'];
299 $query_args['s'] = ! empty( $layout_theme_settings['theme_color_secondary'] ) ? charitable_sanitize_hex( $layout_theme_settings['theme_color_secondary'], false ) : $query_args['s'];
300 $query_args['t'] = ! empty( $layout_theme_settings['theme_color_tertiary'] ) ? charitable_sanitize_hex( $layout_theme_settings['theme_color_tertiary'], false ) : $query_args['t'];
301 $query_args['b'] = ! empty( $layout_theme_settings['theme_color_button'] ) ? charitable_sanitize_hex( $layout_theme_settings['theme_color_button'], false ) : $query_args['b'];
302
303 $template_frontend_css_url = add_query_arg(
304 apply_filters(
305 'charitable_builder_template_frontend_styles_' . $template_id,
306 $query_args,
307 $template_id,
308 $campaign_data
309 ),
310 rest_url( 'charitable/v1/campaign-css/' . $template_id )
311 );
312
313 wp_enqueue_style(
314 'charitable-campaign-theme-base',
315 apply_filters(
316 'charitable_builder_template_frontend_base_styles_' . $template_id,
317 $assets_dir . 'css/campaign-builder/themes/frontend/base' . $min . '.css',
318 $template_id,
319 $campaign_data
320 ),
321 array(),
322 $version
323 );
324
325 wp_enqueue_style(
326 'charitable-campaign-theme-' . $template_id,
327 $template_frontend_css_url,
328 array( 'charitable-campaign-theme-base' ),
329 false // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NoExplicitVersion
330 );
331 }
332
333 /**
334 * Load template classes.
335 *
336 * @since 1.8.0
337 *
338 * @param string $template_id The template ID.
339 * @return array|false
340 */
341 public static function get_template( $template_id = 'basic' ) {
342
343 /**
344 * Allows developers to disable loading of some builder panels.
345 *
346 * @since 1.8.0
347 *
348 * @param array $panels Panels slugs array.
349 */
350 $classes = apply_filters(
351 'charitable_builder_template_classes_',
352 array(
353 'templates',
354 )
355 );
356
357 foreach ( $classes as $class ) {
358 $class = sanitize_file_name( $class );
359 $file = require_once charitable()->get_path( 'includes' ) . "admin/campaign-builder/templates/class-{$class}.php";
360
361 if ( file_exists( $file ) ) {
362 require_once $file;
363 }
364 }
365
366 $builder_template = new Charitable_Campaign_Builder_Templates();
367 $templates_data = $builder_template->get_templates_data( $template_id );
368 $template_data = isset( $templates_data['templates'][ $template_id ] ) ? $templates_data['templates'][ $template_id ] : false;
369
370 return $template_data;
371 }
372 }
373
374 endif;
375