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 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
← All changes | includes/shortcodes/class-charitable-campaign-shortcode.php +100 -1 1.8.12.1 → 1.8.13 View file →
@@ -7,8 +7,9 @@
7 7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 9 * @since 1.8.0
10 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.
11 12 */
12 13
13 14 // Exit if accessed directly.
14 15 if ( ! defined( 'ABSPATH' ) ) {
@@ -29,8 +30,36 @@
29 30 */
30 31 private $campaign_data;
31 32
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 + /**
33 62 * Display the shortcode output. This is the callback method for the campaigns shortcode.
34 63 *
35 64 * @since 1.8.0
36 65 *
@@ -44,8 +73,64 @@
44 73 );
45 74
46 75 $args = shortcode_atts( $default, $atts, 'campaign' );
47 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 +
48 133 // Get the campaign data from the ID that should be passed along in the shortcode.
49 134 $args['campaign'] = self::get_campaign( $args );
50 135 $args['campaign_data'] = self::get_campaign_data( $args );
51 136
@@ -90,9 +175,23 @@
90 175 $template->set_view_args( $view_args );
91 176
92 177 ob_start();
93 178
94 - $template->render();
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 + }
95 194
96 195 $html = ob_get_clean();
97 196
98 197 // If the $args['elementor'] is true, then replace the campaign-loop with the elementor campaign-loop.