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 | templates/campaign/builder/content.php +199 -23 1.8.11 → 1.8.13 View file →
@@ -19,15 +19,72 @@
19 19 // Data related to the template (and therefore the defining layout) to be used.
20 20
21 21 $charitable_campaign = $view_args['campaign']; // Charitable_Campaign Instance of `Charitable_Campaign`.
22 22
23 -if ( ! empty( $_GET['charitable_campaign_preview'] ) ) { //phpcs:ignore
23 +/*
24 + * Whether to honour the ?charitable_campaign_preview argument.
25 + *
26 + * That argument is unauthenticated and names an arbitrary post ID, so it is only trusted
27 + * for a user who can edit that specific campaign. Without this check, appending
28 + * ?charitable_campaign_preview=<other campaign id> to ANY campaign URL replaced the
29 + * rendered campaign's whole builder payload with that other campaign's, which gave two
30 + * real problems:
31 + *
32 + * - An unpublished campaign's headline, story and images leaked to any Contributor. The
33 + * render gate further down tests edit_posts, a site-wide primitive every Contributor
34 + * holds, not a per-campaign capability.
35 + * - A published campaign's content rendered under a different campaign's URL for
36 + * anonymous visitors, including its donation ask. That link is hand-outable and any
37 + * full-page cache that ignores unknown query args will store it against the wrong URL.
38 + *
39 + * Both were reachable before 1.8.12.4 for the 24 hours a preview transient lives after
40 + * each builder save; the saved-revision fallback added below would otherwise have made
41 + * them permanent.
42 + *
43 + * edit_post (singular) is the meta capability. The campaign post type is registered with
44 + * capability_type 'campaign' and map_meta_cap true, so WordPress maps it onto
45 + * edit_campaign / edit_others_campaigns for the specific post. This mirrors the check
46 + * Campaign_Builder_Preview::is_preview_page() already makes.
47 + *
48 + * When the check fails the argument is ignored completely and the normal view_args path
49 + * renders the campaign the caller actually asked for.
50 + */
51 +$charitable_preview_id = empty( $_GET['charitable_campaign_preview'] ) ? 0 : absint( $_GET['charitable_campaign_preview'] ); //phpcs:ignore
52 +$charitable_can_preview = $charitable_preview_id
53 + && Charitable::CAMPAIGN_POST_TYPE === get_post_type( $charitable_preview_id )
54 + && charitable_current_user_can( 'edit_post', $charitable_preview_id );
55 +
56 +if ( $charitable_can_preview ) {
24 57 // get the transient that is storing the temp settings information, as this is what we will use to display the preview.
25 - $charitable_campaign_data = get_transient( 'charitable_campaign_preview_' . intval( $_GET['charitable_campaign_preview'] ) ); //phpcs:ignore
58 + $charitable_campaign_data = get_transient( 'charitable_campaign_preview_' . $charitable_preview_id );
59 +
60 + /*
61 + * The transient carries the builder's *unsaved* changes and is written only by the
62 + * builder's save AJAX. Every other route to a preview arrives without it:
63 + *
64 + * - the Preview action on a draft in the Campaigns list table,
65 + * - WordPress's own preview_post_link for a campaign,
66 + * - any preview more than DAY_IN_SECONDS after the last builder save,
67 + * - an object cache that has evicted the entry (HelpScout #11576).
68 + *
69 + * With no fallback the whole render proceeded on false, which produced an empty
70 + * campaign body and logged a warning for every read of ['id']. Fall back to the
71 + * saved revision, which is what the preview notice already tells the user it is
72 + * showing.
73 + */
74 + if ( empty( $charitable_campaign_data ) ) {
75 + $charitable_campaign_data = get_post_meta( $charitable_preview_id, 'campaign_settings_v2', true );
76 + }
26 77 } else {
27 78 $charitable_campaign_data = empty( $view_args['campaign_data'] ) && ! empty( $view_args['id'] ) ? get_post_meta( intval( $view_args['id'] ), 'campaign_settings_v2', true ) : $view_args['campaign_data'];
28 79 }
29 80
81 +// A brand-new campaign has neither transient nor saved meta, so either branch can still
82 +// yield a non-array. Normalise before anything indexes it.
83 +if ( ! is_array( $charitable_campaign_data ) ) {
84 + $charitable_campaign_data = array();
85 +}
86 +
30 87 $charitable_template_data = isset( $view_args['template'] ) && is_array( $view_args['template'] ) ? $view_args['template'] : array();
31 88 $charitable_template_id = isset( $charitable_campaign_data['template_id'] ) && ! empty( $charitable_campaign_data['template_id'] ) ? sanitize_key( $charitable_campaign_data['template_id'] ) : charitable_campaign_builder_default_template();
32 89 $charitable_template_layout = isset( $charitable_template_data['layout'] ) ? $charitable_template_data['layout'] : array();
33 90
@@ -68,29 +125,63 @@
68 125
69 126
70 127 /* The Setup */
71 128
72 -/* Campaign Related */
129 +/*
130 + * $charitable_campaign, $charitable_campaign_data, $charitable_template_data,
131 + * $charitable_template_id and $charitable_template_layout are all resolved at the top of
132 + * this file and are still current here. Nothing between there and this point reassigns
133 + * $view_args or $_GET, or writes the preview transient or the campaign's post meta. The
134 + * is_admin() branch only reads them and echoes stylesheet links, assigning nothing but four
135 + * locals of its own; Campaign_Builder_Preview's constructor does query the database via
136 + * is_preview_page() before registering its hooks, but it touches none of this scope.
137 + * Verified at runtime across the draft-preview, published-preview and front-end paths.
138 + *
139 + * This block previously recomputed four of the five from scratch - everything except
140 + * $charitable_template_data, which was and still is assigned exactly once above. That
141 + * duplicate is where the second copy of the preview-transient read lived, and why the
142 + * transient fix originally had to be written twice.
143 + */
73 144
74 -$charitable_campaign = $view_args['campaign']; // Charitable_Campaign Instance of `Charitable_Campaign`.
145 +/*
146 + * Resolve the campaign's post ID independently of the campaign data payload. The three post
147 + * lookups below need nothing but the ID, and sourcing it from inside the payload is what
148 + * made them warn whenever the payload was missing.
149 + *
150 + * $view_args['id'] comes first because it is the campaign the caller actually asked for.
151 + * The payload's own 'id' is the least trustworthy source: it is written once at save time
152 + * and copied verbatim thereafter, so a WXR import, a staging-to-production migration or a
153 + * duplicate-post plugin can leave it pointing at an unrelated post, and the status of THAT
154 + * post would then decide whether this campaign renders.
155 + *
156 + * $charitable_preview_id is used only when $charitable_can_preview passed, so the
157 + * unauthenticated query argument cannot steer this either.
158 + *
159 + * Stays 0 only when there is no global post either, since get_the_ID() would otherwise have
160 + * supplied it. The status gate below has an explicit branch for that case rather than
161 + * relying on get_post_status( 0 ), which returns false with no post to fall back to.
162 + */
163 +$charitable_campaign_post_id = ! empty( $view_args['id'] ) ? intval( $view_args['id'] ) : 0;
75 164
76 -if ( ! empty( $_GET['charitable_campaign_preview'] ) ) { //phpcs:ignore
77 - // get the transient that is storing the temp settings information, as this is what we will use to display the preview.
78 - $charitable_campaign_data = get_transient( 'charitable_campaign_preview_' . intval( $_GET['charitable_campaign_preview'] ) ); //phpcs:ignore
79 -} else {
80 - $charitable_campaign_data = empty( $view_args['campaign_data'] ) && ! empty( $view_args['id'] ) ? get_post_meta( intval( $view_args['id'] ), 'campaign_settings_v2', true ) : $view_args['campaign_data'];
165 +if ( 0 === $charitable_campaign_post_id && ! empty( $charitable_campaign_data['id'] ) ) {
166 + $charitable_campaign_post_id = intval( $charitable_campaign_data['id'] );
81 167 }
82 168
169 +if ( 0 === $charitable_campaign_post_id && $charitable_can_preview ) {
170 + $charitable_campaign_post_id = $charitable_preview_id;
171 +}
172 +
173 +if ( 0 === $charitable_campaign_post_id ) {
174 + $charitable_campaign_post_id = intval( get_the_ID() );
175 +}
176 +
83 177 /* Template Related */
84 178
85 -$charitable_template_id = isset( $charitable_campaign_data['template_id'] ) && ! empty( $charitable_campaign_data['template_id'] ) ? sanitize_key( $charitable_campaign_data['template_id'] ) : charitable_campaign_builder_default_template();
86 -$charitable_template_layout = isset( $charitable_template_data['layout'] ) ? $charitable_template_data['layout'] : array();
87 -
88 179 $charitable_template_parent_id = ( isset( $charitable_template_data['meta']['parent_theme'] ) && ! empty( $charitable_template_data['meta']['parent_theme'] ) ) ? esc_attr( $charitable_template_data['meta']['parent_theme'] ) : false;
89 180 $charitable_template_wrap_css = false !== $charitable_template_parent_id ? 'template-' . $charitable_template_parent_id : '';
90 181 $charitable_template_wrap_css .= false !== $charitable_template_id ? ' template-' . $charitable_template_id : '';
91 182 $charitable_template_wrap_css .= ! empty( $view_args['campaign_data']['settings']['general']['form_css_class'] ) ? ' ' . esc_attr( $view_args['campaign_data']['settings']['general']['form_css_class'] ) : false;
92 -$charitable_template_wrap_css .= 'draft' === get_post_status( $charitable_campaign_data['id'] ) ? ' is-charitable-preview' : false; // this to give a css class only when the campaign is previewed.
183 +$charitable_template_wrap_css .= 'draft' === get_post_status( $charitable_campaign_post_id ) ? ' is-charitable-preview' : false; // this to give a css class only when the campaign is previewed.
93 184
94 185 /* Layout Related */
95 186
96 187 $charitable_row_counter = 0;
@@ -105,19 +196,61 @@
105 196 $charitable_css_classes = apply_filters( 'charitable_builder_campaign_content_css_classes', $charitable_css_classes, $charitable_template_data, $charitable_campaign_data );
106 197 $charitable_css_classes_output = implode( ' ', $charitable_css_classes );
107 198
108 199 // Get the post status - if this is a draft, we will display a notice to the admin or author, and not show this to the public.
109 -$charitable_post_status = get_post_status( $charitable_campaign_data['id'] );
110 -$charitable_post_author = get_post_field( 'post_author', $charitable_campaign_data['id'] );
200 +$charitable_post_status = get_post_status( $charitable_campaign_post_id );
201 +$charitable_post_author = get_post_field( 'post_author', $charitable_campaign_post_id );
111 202
112 -// Only display the message if the viewer if the view isn't viewing a preview from the campaign builder (maybe they are viewing this via shortcode on the frontend, etc.).
113 -if ( empty( $_GET['charitable_campaign_preview'] ) && ( false === $charitable_post_status || 'draft' === $charitable_post_status ) ) : //phpcs:ignore
203 +/*
204 + * Statuses that are not publicly published. These render only for the campaign's author or
205 + * a user who can edit that campaign, and carry the "not published yet" notice on the front
206 + * end.
207 + *
208 + * 'pending' and 'future' were missing before 1.8.12.4, so a campaign in either status
209 + * rendered nothing at all - the builder's preview page showed its notice bar above an empty
210 + * body. That became reachable once the Preview button stopped swallowing those statuses.
211 + * 'private' is deliberately not in this list: it is published but restricted, so it is
212 + * handled below with the read_post capability instead.
213 + */
214 +$charitable_unpublished_statuses = array( 'draft', 'pending', 'future' );
114 215
216 +/*
217 + * Whether the current user is the campaign's author.
218 + *
219 + * get_post_field() returns post_author as a *string*, while get_current_user_id() returns an
220 + * int, so the previous `$charitable_post_author === get_current_user_id()` could never match
221 + * and every check below fell through to the capability test alone. Comparing as integers is
222 + * plainly the intent (see the comments on each branch), and restores author access.
223 + *
224 + * The get_current_user_id() > 0 guard is load-bearing: a campaign whose post_author is 0 or
225 + * NULL would otherwise compare equal to a logged-out visitor's user ID of 0 and expose an
226 + * unpublished campaign publicly.
227 + */
228 +$charitable_is_campaign_author = get_current_user_id() > 0 && (int) $charitable_post_author === get_current_user_id();
229 +
230 +/*
231 + * Only display the message if the viewer isn't viewing a preview from the campaign builder
232 + * (maybe they are viewing this via shortcode on the frontend, etc.).
233 + *
234 + * Keyed off $charitable_can_preview rather than the raw query argument: a request carrying
235 + * ?charitable_campaign_preview for a campaign the user cannot edit is an ordinary page view,
236 + * not a preview, so it should still get the notice.
237 + */
238 +if ( ! $charitable_can_preview && ( false === $charitable_post_status || in_array( $charitable_post_status, $charitable_unpublished_statuses, true ) ) ) :
239 +
115 240 // if the user is the author of the post OR if they have permissions to view drafts, show the notice.
116 - if ( $charitable_post_author === get_current_user_id() || current_user_can( 'edit_posts' ) ) {
241 + if ( $charitable_is_campaign_author || current_user_can( 'edit_posts' ) ) {
117 242 ?>
118 243 <div class="charitable-notice charitable-notice-info">
119 - <p style="margin: 0;"><?php esc_html_e( 'This campaign is currently in draft mode. Only you can see it, and some functionality (like donation forms, donation buttons, etc.) might be disabled.', 'charitable' ); ?></p>
244 + <?php
245 + /*
246 + * Status-neutral wording. This branch now covers pending and future as well as
247 + * draft, so the previous "currently in draft mode" was inaccurate for a campaign
248 + * awaiting review or scheduled. The draft-specific strings it replaces are no
249 + * longer referenced.
250 + */
251 + ?>
252 + <p style="margin: 0;"><?php esc_html_e( 'This campaign has not been published yet. Only you can see it, and some functionality (like donation forms, donation buttons, etc.) might be disabled.', 'charitable' ); ?></p>
120 253 </div>
121 254 <?php
122 255 } else {
123 256 // show a generic message to the public.
@@ -122,9 +255,9 @@
122 255 } else {
123 256 // show a generic message to the public.
124 257 ?>
125 258 <div class="charitable-notice charitable-notice-info">
126 - <p style="margin: 0;"><?php esc_html_e( 'This campaign is currently in draft mode.', 'charitable' ); ?></p>
259 + <p style="margin: 0;"><?php esc_html_e( 'This campaign has not been published yet.', 'charitable' ); ?></p>
127 260 </div>
128 261 <?php
129 262 }
130 263
@@ -129,11 +262,43 @@
129 262 }
130 263
131 264 endif;
132 265
133 -// if the campaign/post is published OR if the user is the author of the post OR if they have permissions to edit posts, show the (slightly disabled) campaign.
134 -if ( 'publish' === $charitable_post_status || ( ( false === $charitable_post_status || 'draft' === $charitable_post_status ) && ( $charitable_post_author === get_current_user_id() || current_user_can( 'edit_posts' ) ) ) ) :
266 +/*
267 + * Whether to render the campaign at all, and to whom.
268 + *
269 + * - 'publish' is public.
270 + * - The unpublished states render for the author, or for a user who can edit THAT campaign.
271 + * - 'private' is published but access-restricted, so it asks whether the user can read that
272 + * specific post rather than whether they can edit campaigns generally.
273 + * - No resolvable post means there is nothing to disclose, so this keeps the pre-1.8.12.4
274 + * site-wide edit_posts test rather than inventing a per-post check with no post.
275 + * - Everything else - trash, auto-draft, inherit, and any status a third party registers -
276 + * renders nothing, as before.
277 + *
278 + * These use the singular edit_post / read_post META capabilities against
279 + * $charitable_campaign_post_id, not the plural edit_posts / read_private_posts primitives.
280 + * The campaign post type registers capability_type 'campaign' with map_meta_cap true, so
281 + * WordPress maps them onto edit_campaign / edit_others_campaigns / read_private_campaigns
282 + * for that one post. The plural forms were wrong twice over: edit_posts is held by every
283 + * Contributor for every campaign on the site, and read_private_posts is a *posts* primitive
284 + * that says nothing about campaigns, so a core Editor would have passed it while WordPress's
285 + * own read_post check on the campaign would have denied them.
286 + */
287 +if ( 'publish' === $charitable_post_status ) {
288 + $charitable_can_view_campaign = true;
289 +} elseif ( false === $charitable_post_status || 0 === $charitable_campaign_post_id ) {
290 + $charitable_can_view_campaign = $charitable_is_campaign_author || charitable_current_user_can( 'edit_posts' );
291 +} elseif ( 'private' === $charitable_post_status ) {
292 + $charitable_can_view_campaign = $charitable_is_campaign_author || charitable_current_user_can( 'read_post', $charitable_campaign_post_id );
293 +} elseif ( in_array( $charitable_post_status, $charitable_unpublished_statuses, true ) ) {
294 + $charitable_can_view_campaign = $charitable_is_campaign_author || charitable_current_user_can( 'edit_post', $charitable_campaign_post_id );
295 +} else {
296 + $charitable_can_view_campaign = false;
297 +}
135 298
299 +if ( $charitable_can_view_campaign ) :
300 +
136 301 /**
137 302 * Add something before the campaign builder content.
138 303 *
139 304 * @since 1.8.0
@@ -171,8 +336,19 @@
171 336 if ( '' !== $charitable_additional_css ) {
172 337 $charitable_row_css[] = $charitable_additional_css;
173 338 }
174 339
340 + $charitable_row_bg = ! empty( $charitable_row['background_image'] ) ? esc_url( $charitable_row['background_image'] ) : '';
341 + $charitable_row_style = '' !== $charitable_row_bg ? ' style="background-image:url(\'' . $charitable_row_bg . '\');"' : '';
342 + $charitable_row_locked = ! empty( $charitable_row['locked'] ) ? ' data-row-locked="1"' : '';
343 +
344 + if ( '' !== $charitable_row_bg ) {
345 + $charitable_row_css[] = 'has-background-image';
346 + }
347 + if ( ! empty( $charitable_row['locked'] ) ) {
348 + $charitable_row_css[] = 'is-locked';
349 + }
350 +
175 351 $charitable_row_css_classes = apply_filters(
176 352 'charitable_campaign_row_css',
177 353 $charitable_row_css,
178 354 $charitable_row,
@@ -184,9 +360,9 @@
184 360 ?>
185 361
186 362 <?php echo '<!-- row START -->'; ?>
187 363
188 - <div id="charitable-template-row-<?php echo intval( $charitable_row_id ); ?>" data-row-id="<?php echo intval( $charitable_row_id ); ?>" data-row-type="<?php echo esc_attr( $charitable_row_type ); ?>" class="<?php echo implode( ' ', array_map( 'esc_attr', $charitable_row_css_classes ) ); ?>">
364 + <div id="charitable-template-row-<?php echo intval( $charitable_row_id ); ?>" data-row-id="<?php echo intval( $charitable_row_id ); ?>" data-row-type="<?php echo esc_attr( $charitable_row_type ); ?>"<?php echo $charitable_row_locked; // phpcs:ignore ?> class="<?php echo implode( ' ', array_map( 'esc_attr', $charitable_row_css_classes ) ); ?>"<?php echo $charitable_row_style; // phpcs:ignore ?>>
189 365
190 366 <?php
191 367
192 368 if ( ! empty( $charitable_row['columns'] ) ) :