| @@ -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'] ) ) : |