| 1 |
<?php |
| 2 |
/** |
| 3 |
* Class that sets up the Campaign builder preview abilities. |
| 4 |
* |
| 5 |
* @package Charitable |
| 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 |
*/ |
| 12 |
|
| 13 |
// Exit if accessed directly. |
| 14 |
if ( ! defined( 'ABSPATH' ) ) { |
| 15 |
exit; |
| 16 |
} |
| 17 |
|
| 18 |
if ( ! class_exists( 'Campaign_Builder_Preview' ) ) : |
| 19 |
|
| 20 |
/** |
| 21 |
* Campaign preview. |
| 22 |
* |
| 23 |
* @since 1.8.0 |
| 24 |
* @version 1.8.8.6 |
| 25 |
*/ |
| 26 |
class Campaign_Builder_Preview { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Class name is used by other code. Changing it would break existing functionality. |
| 27 |
|
| 28 |
/** |
| 29 |
* One is the loneliest number that you'll ever do. |
| 30 |
* |
| 31 |
* @since 1.8.0 |
| 32 |
* |
| 33 |
* @var object |
| 34 |
*/ |
| 35 |
private static $instance; |
| 36 |
|
| 37 |
/** |
| 38 |
* Whether a campaign preview render is already in progress anywhere up the call stack. |
| 39 |
* |
| 40 |
* Static on purpose, and this is the whole point of it. This class is instantiated in more |
| 41 |
* than one place - once from the plugin bootstrap and again from |
| 42 |
* templates/campaign/builder/content.php, which the render itself includes - and every |
| 43 |
* instance whose is_preview_page() passes registers its own the_content callback. WordPress |
| 44 |
* keys object callbacks by spl_object_hash, so the remove_filter() around the render can only |
| 45 |
* ever detach the *calling* instance's callback. The instance created during the render is a |
| 46 |
* new object identity, so it re-arms the hook the level above just disarmed, and each nesting |
| 47 |
* level adds another handler: measured at 2, 3, 4 and climbing, exhausting 256M within a |
| 48 |
* handful of renders (HelpScout #11576). A per-instance guard cannot see that; a class-level |
| 49 |
* one can. |
| 50 |
* |
| 51 |
* @since 1.8.12.4 |
| 52 |
* |
| 53 |
* @var bool |
| 54 |
*/ |
| 55 |
private static $is_rendering = false; |
| 56 |
|
| 57 |
/** |
| 58 |
* Campaign data. |
| 59 |
* |
| 60 |
* @since 1.8.0 |
| 61 |
* |
| 62 |
* @var array |
| 63 |
*/ |
| 64 |
public $campaign_data; |
| 65 |
|
| 66 |
/** |
| 67 |
* Constructor. |
| 68 |
* |
| 69 |
* @since 1.8.0 |
| 70 |
*/ |
| 71 |
public function __construct() { |
| 72 |
|
| 73 |
if ( ! $this->is_preview_page() ) { |
| 74 |
return; |
| 75 |
} |
| 76 |
|
| 77 |
$this->hooks(); |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Main Instance. |
| 82 |
* |
| 83 |
* @since 1.8.0 |
| 84 |
* |
| 85 |
* @return Charitable_Builder |
| 86 |
*/ |
| 87 |
public static function get_instance() { |
| 88 |
if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Campaign_Builder_Preview ) ) { |
| 89 |
|
| 90 |
self::$instance = new Campaign_Builder_Preview(); |
| 91 |
|
| 92 |
} |
| 93 |
|
| 94 |
return self::$instance; |
| 95 |
} |
| 96 |
|
| 97 |
/** |
| 98 |
* Init |
| 99 |
* |
| 100 |
* @since 1.8.0 |
| 101 |
* |
| 102 |
* @return void |
| 103 |
*/ |
| 104 |
public function init() { |
| 105 |
} |
| 106 |
|
| 107 |
/** |
| 108 |
* Check if current page request meets requirements for campaign preview page. |
| 109 |
* |
| 110 |
* @since 1.8.0 |
| 111 |
* |
| 112 |
* @return bool |
| 113 |
*/ |
| 114 |
public function is_preview_page() { |
| 115 |
|
| 116 |
// Only proceed for the campaign preview page. |
| 117 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 118 |
if ( empty( $_GET['charitable_campaign_preview'] ) ) { |
| 119 |
return false; |
| 120 |
} |
| 121 |
|
| 122 |
// Check for logged-in user with correct capabilities. |
| 123 |
if ( ! is_user_logged_in() ) { |
| 124 |
return false; |
| 125 |
} |
| 126 |
|
| 127 |
$campaign_id = isset( $_GET['p'] ) ? absint( $_GET['p'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 128 |
$campaign_id = ( 0 === $campaign_id && ! empty( $_GET['charitable_campaign_preview'] ) ) ? absint( $_GET['charitable_campaign_preview'] ) : $campaign_id; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 129 |
|
| 130 |
if ( 0 === $campaign_id && ! empty( $_GET['charitable_campaign_preview'] ) && 1 === intval( $_GET['charitable_campaign_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 131 |
global $post; |
| 132 |
if ( ! empty( $post->post_id ) ) { |
| 133 |
$campaign_id = absint( $post->post_id ); |
| 134 |
} |
| 135 |
} |
| 136 |
|
| 137 |
// check if this custom post type actually exists. |
| 138 |
global $wpdb; |
| 139 |
if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE ID=%s AND post_status != 'trash'", array( $campaign_id ) ) ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching |
| 140 |
return false; |
| 141 |
} |
| 142 |
|
| 143 |
// check permissions. |
| 144 |
if ( ! charitable_current_user_can( 'edit_post', $campaign_id ) ) { // this was 'view_campaign_single'. |
| 145 |
return false; |
| 146 |
} |
| 147 |
|
| 148 |
// Fetch campaign details. |
| 149 |
$this->campaign_data = get_post_meta( $campaign_id, 'campaign_settings_v2', true ); |
| 150 |
|
| 151 |
// Check valid campaign was found. |
| 152 |
if ( empty( $this->campaign_data ) || ( empty( $this->campaign_data['id'] ) && empty( absint( $_GET['charitable_campaign_preview'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 153 |
return false; |
| 154 |
} |
| 155 |
|
| 156 |
return true; |
| 157 |
} |
| 158 |
|
| 159 |
/** |
| 160 |
* Hooks. |
| 161 |
* |
| 162 |
* @since 1.8.0 |
| 163 |
*/ |
| 164 |
public function hooks() { |
| 165 |
|
| 166 |
add_action( 'pre_get_posts', [ $this, 'pre_get_posts' ] ); |
| 167 |
add_filter( 'the_title', [ $this, 'the_title' ], 100, 1 ); |
| 168 |
add_filter( 'the_content', [ $this, 'the_content' ], 999 ); |
| 169 |
// Accept the second argument get_the_excerpt passes (the WP_Post being excerpted) so the |
| 170 |
// handler can tell whose excerpt it is and leave other posts alone. See the_content(). |
| 171 |
add_filter( 'get_the_excerpt', [ $this, 'the_content' ], 999, 2 ); |
| 172 |
add_filter( 'home_template_hierarchy', [ $this, 'force_page_template_hierarchy' ], 999 ); |
| 173 |
add_filter( 'frontpage_template_hierarchy', [ $this, 'force_page_template_hierarchy' ], 999 ); |
| 174 |
add_filter( 'post_thumbnail_html', '__return_empty_string' ); |
| 175 |
add_filter( 'charitable_campaign_can_receive_donations', [ $this, 'enable_campaign_form' ], 10, 2 ); |
| 176 |
} |
| 177 |
|
| 178 |
/** |
| 179 |
* The campaign donation form should be enabled for a campaign preview page, so on a preview page it's "ok" for a campaign to recieve a donation. |
| 180 |
* |
| 181 |
* @param boolean $can Whether the campaign form accepts donations. |
| 182 |
* @param mixed $the_object Charitable_Campaign. |
| 183 |
* |
| 184 |
* @return boolean Whether the campaign form can be enabled for the given object. |
| 185 |
*/ |
| 186 |
public function enable_campaign_form( $can, $the_object ) { // phpcs:ignore |
| 187 |
return true; |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Modify query, limit to one post. |
| 192 |
* |
| 193 |
* @since 1.8.0 |
| 194 |
* |
| 195 |
* @param \WP_Query $query The WP_Query instance. |
| 196 |
*/ |
| 197 |
public function pre_get_posts( $query ) { |
| 198 |
|
| 199 |
if ( is_admin() || ! $query->is_main_query() ) { |
| 200 |
return; |
| 201 |
} |
| 202 |
|
| 203 |
$query->set( 'page_id', '' ); |
| 204 |
$query->set( 'post_type', 'campaign' ); |
| 205 |
$query->set( 'post__in', empty( $this->campaign_data['id'] ) ? [] : [ (int) $this->campaign_data['id'] ] ); |
| 206 |
$query->set( 'posts_per_page', 1 ); |
| 207 |
} |
| 208 |
|
| 209 |
/** |
| 210 |
* Customize campaign preview page title. |
| 211 |
* |
| 212 |
* @since 1.5.1 |
| 213 |
* |
| 214 |
* @param string $title Page title. |
| 215 |
* |
| 216 |
* @return string |
| 217 |
*/ |
| 218 |
public function the_title( $title ) { |
| 219 |
|
| 220 |
if ( in_the_loop() ) { |
| 221 |
$title = sprintf( /* translators: %s - campaign title. */ |
| 222 |
esc_html__( '%s Preview', 'charitable' ), |
| 223 |
! empty( $this->campaign_data['settings']['campaign_title'] ) ? sanitize_text_field( $this->campaign_data['settings']['campaign_title'] ) : esc_html__( 'Campaign', 'charitable' ) |
| 224 |
); |
| 225 |
} |
| 226 |
|
| 227 |
return $title; |
| 228 |
} |
| 229 |
|
| 230 |
/** |
| 231 |
* The ID of the campaign this request is previewing. |
| 232 |
* |
| 233 |
* A brand-new campaign that has never been saved has no id in its payload, in which case the |
| 234 |
* only place the ID exists is the query argument, so fall back to that. |
| 235 |
* |
| 236 |
* @since 1.8.12.4 |
| 237 |
* |
| 238 |
* @return int Campaign ID, or 0 if none could be resolved. |
| 239 |
*/ |
| 240 |
public function get_preview_campaign_id() { |
| 241 |
|
| 242 |
$campaign_id = isset( $this->campaign_data['id'] ) ? absint( $this->campaign_data['id'] ) : 0; |
| 243 |
|
| 244 |
if ( 0 === $campaign_id && ! empty( $_GET['charitable_campaign_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 245 |
$campaign_id = absint( $_GET['charitable_campaign_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 246 |
} |
| 247 |
|
| 248 |
return $campaign_id; |
| 249 |
} |
| 250 |
|
| 251 |
/** |
| 252 |
* Customize campaign preview page content. |
| 253 |
* |
| 254 |
* @since 1.8.0 |
| 255 |
* @version 1.8.12.3 Detach from the content filters while the campaign renders, to prevent an infinite the_content loop that exhausts memory. |
| 256 |
* @version 1.8.12.4 Leave other posts' excerpts alone. This handler is also attached to |
| 257 |
* get_the_excerpt, and returned the whole rendered campaign for whatever post |
| 258 |
* was being excerpted, so any excerpt elsewhere on the preview page (a query |
| 259 |
* loop, a posts list) was replaced by a duplicate of the campaign. |
| 260 |
* |
| 261 |
* @param string $content The incoming content or excerpt. |
| 262 |
* @param WP_Post|null $post Only supplied by get_the_excerpt: the post being excerpted. |
| 263 |
* Null on the_content, which passes a single argument. |
| 264 |
* |
| 265 |
* @return string |
| 266 |
*/ |
| 267 |
public function the_content( $content = '', $post = null ) { |
| 268 |
|
| 269 |
/* |
| 270 |
* get_the_excerpt names the post it is asking about, so when it is asking about anything |
| 271 |
* other than the campaign being previewed, hand the excerpt straight back untouched. |
| 272 |
* Without this the handler ignored the question and answered with the full campaign render |
| 273 |
* every time, so a preview page containing any other post's excerpt showed the campaign |
| 274 |
* again in its place. |
| 275 |
* |
| 276 |
* Deliberately keyed on the argument rather than on in_the_loop() or get_the_ID(): both of |
| 277 |
* those read the global post, which under a block theme is only set up when core's |
| 278 |
* singular-template loop workaround fires (it is gated on is_singular(), |
| 279 |
* wp-includes/block-template.php). The passed WP_Post is reliable regardless of theme. |
| 280 |
* |
| 281 |
* the_content is left alone on purpose. It passes no post argument, and core's |
| 282 |
* core/post-content block calls get_the_content() with no ID precisely so the queried |
| 283 |
* object wins, so that path is expected to act on the ambient post. |
| 284 |
*/ |
| 285 |
if ( $post instanceof WP_Post && absint( $post->ID ) !== $this->get_preview_campaign_id() ) { |
| 286 |
return $content; |
| 287 |
} |
| 288 |
|
| 289 |
/* |
| 290 |
* A render is already running further up the stack, so this application of the filter is |
| 291 |
* a re-entry. Hand the content back untouched instead of starting a second render. |
| 292 |
* |
| 293 |
* This is what actually stops the loop. The remove_filter() further down cannot, because |
| 294 |
* it only detaches THIS instance's callback, and the instance created inside the render |
| 295 |
* (content.php, "preview page check") re-arms the hook under a fresh object identity. See |
| 296 |
* the note on self::$is_rendering. |
| 297 |
*/ |
| 298 |
if ( self::$is_rendering ) { |
| 299 |
return $content; |
| 300 |
} |
| 301 |
|
| 302 |
/* |
| 303 |
* Never render the campaign into an excerpt. This catches BOTH the direct |
| 304 |
* get_the_excerpt application and the indirect one: core's wp_trim_excerpt() re-applies |
| 305 |
* the_content from inside the get_the_excerpt filter, so an SEO or Open Graph plugin |
| 306 |
* asking for a meta description costs a full campaign render every time it asks. That is |
| 307 |
* what exhausted 256M on the reporting site, and it is why deactivating SiteSEO there |
| 308 |
* made the preview work (HelpScout #11576). doing_filter() sees the enclosing filter, so |
| 309 |
* it recognises the nested the_content application that a check on the current filter |
| 310 |
* alone would miss. |
| 311 |
* |
| 312 |
* Deliberately not bounded by a render counter instead: whichever caller asked first |
| 313 |
* would win, and an SEO plugin building meta tags in wp_head asks before the body |
| 314 |
* renders, which would leave the preview body empty. |
| 315 |
*/ |
| 316 |
if ( doing_filter( 'get_the_excerpt' ) ) { |
| 317 |
|
| 318 |
/* |
| 319 |
* Two different applications reach here while an excerpt is being built: the |
| 320 |
* get_the_excerpt filter itself, and the the_content filter that core's |
| 321 |
* wp_trim_excerpt() applies from inside it. Only the first is asking us for a |
| 322 |
* summary. The second is asking "what is this post's content", about whatever post |
| 323 |
* is being excerpted, which may not be the campaign at all, so answering it with the |
| 324 |
* campaign's description would put the campaign's words into an unrelated post's |
| 325 |
* excerpt. Hand that one straight back. |
| 326 |
* |
| 327 |
* Note wp_trim_excerpt() only takes that branch when the post has no excerpt of its |
| 328 |
* own (formatting.php, '' === trim( $text )), which is why a post WITH an excerpt |
| 329 |
* never exposed this. |
| 330 |
*/ |
| 331 |
if ( 'get_the_excerpt' !== current_filter() ) { |
| 332 |
return $content; |
| 333 |
} |
| 334 |
|
| 335 |
/* |
| 336 |
* Answer with the campaign's own description rather than nothing, so an SEO or Open |
| 337 |
* Graph plugin still gets a usable meta description on a preview page. This reads the |
| 338 |
* settings array is_preview_page() already loaded, so it costs no render and no query, |
| 339 |
* and charitable_find_description_in_campaign_settings() ends on a Charitable filter |
| 340 |
* rather than the_content, so it cannot re-enter this handler. |
| 341 |
* |
| 342 |
* Tags are stripped and the result trimmed because the caller wants a summary, not |
| 343 |
* markup. 55 words is about the length a meta description survives at. |
| 344 |
*/ |
| 345 |
if ( ! function_exists( 'charitable_find_description_in_campaign_settings' ) ) { |
| 346 |
return $content; |
| 347 |
} |
| 348 |
|
| 349 |
$charitable_preview_description = charitable_find_description_in_campaign_settings( $this->campaign_data, 0 ); |
| 350 |
|
| 351 |
if ( ! is_string( $charitable_preview_description ) ) { |
| 352 |
return $content; |
| 353 |
} |
| 354 |
|
| 355 |
$charitable_preview_description = wp_strip_all_tags( $charitable_preview_description ); |
| 356 |
|
| 357 |
if ( '' === trim( $charitable_preview_description ) ) { |
| 358 |
return $content; |
| 359 |
} |
| 360 |
|
| 361 |
return wp_trim_words( $charitable_preview_description, 55, '…' ); |
| 362 |
} |
| 363 |
|
| 364 |
if ( ! isset( $this->campaign_data['id'] ) ) { |
| 365 |
return ''; |
| 366 |
} |
| 367 |
|
| 368 |
if ( ! charitable_current_user_can( 'edit_posts', $this->campaign_data['id'] ) ) { |
| 369 |
return ''; |
| 370 |
} |
| 371 |
|
| 372 |
$links = []; |
| 373 |
|
| 374 |
if ( charitable_current_user_can( 'edit_posts', $this->campaign_data['id'] ) ) { |
| 375 |
$links[] = [ |
| 376 |
'url' => esc_url( |
| 377 |
add_query_arg( |
| 378 |
[ |
| 379 |
'page' => 'charitable-campaign-builder', |
| 380 |
'view' => 'design', |
| 381 |
'campaign_id' => absint( $this->campaign_data['id'] ), |
| 382 |
], |
| 383 |
admin_url( 'admin.php' ) |
| 384 |
) |
| 385 |
), |
| 386 |
'text' => esc_html__( 'Edit Campaign', 'charitable' ), |
| 387 |
]; |
| 388 |
} |
| 389 |
|
| 390 |
if ( charitable_current_user_can( 'read', $this->campaign_data['id'] ) ) { |
| 391 |
$links[] = [ |
| 392 |
'url' => esc_url( |
| 393 |
add_query_arg( |
| 394 |
[ |
| 395 |
'post_type' => 'donation', |
| 396 |
'campaign_id' => absint( $this->campaign_data['id'] ), |
| 397 |
], |
| 398 |
admin_url( 'edit.php' ) |
| 399 |
) |
| 400 |
), |
| 401 |
'text' => esc_html__( 'View Donations', 'charitable' ), |
| 402 |
]; |
| 403 |
} |
| 404 |
|
| 405 |
$links[] = [ |
| 406 |
'url' => esc_url( |
| 407 |
charitable_utm_link( 'https://www.wpcharitable.com/documentation/', 'Campaign Preview', 'Documentation' ) |
| 408 |
), |
| 409 |
'text' => esc_html__( 'Documentation', 'charitable' ), |
| 410 |
]; |
| 411 |
|
| 412 |
if ( ! empty( $_GET['new_window'] ) ) { // phpcs:ignore |
| 413 |
$links[] = [ |
| 414 |
'url' => 'javascript:window.close();', |
| 415 |
'text' => esc_html__( 'Close this window', 'charitable' ), |
| 416 |
]; |
| 417 |
} |
| 418 |
|
| 419 |
$content = '<div class="charitable-preview-messages"><p>'; |
| 420 |
|
| 421 |
if ( 'publish' === get_post_status( $this->campaign_data['id'] ) ) : |
| 422 |
|
| 423 |
if ( ! empty( $_GET['charitable_campaign_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 424 |
|
| 425 |
$content .= '<p>'; |
| 426 |
|
| 427 |
$content .= sprintf( |
| 428 |
wp_kses( |
| 429 |
/* translators: %s - Charitable doc link. */ |
| 430 |
__( 'You are viewing a preview of a <a href="%s" target="_blank">published campaign</a>. If this does not match your campaign page in the campaign builder, save your changes and then refresh this page. Your current theme can also effect the look of this page.', 'charitable' ), |
| 431 |
[ |
| 432 |
'a' => [ |
| 433 |
'href' => [], |
| 434 |
'target' => [], |
| 435 |
'rel' => [], |
| 436 |
], |
| 437 |
] |
| 438 |
), |
| 439 |
esc_url( get_permalink( $this->campaign_data['id'] ) ) |
| 440 |
); |
| 441 |
|
| 442 |
$content .= '</p>'; |
| 443 |
|
| 444 |
$content .= '<p>' . esc_html__( 'Some functionality and elements (like donation buttons, forms, social sharing, etc.) are disabled.', 'charitable' ) . '</p>'; |
| 445 |
|
| 446 |
} else { |
| 447 |
|
| 448 |
$content .= '<p>'; |
| 449 |
|
| 450 |
$content .= sprintf( |
| 451 |
wp_kses( |
| 452 |
/* translators: %s - Charitable doc link. */ |
| 453 |
__( 'This is a preview of the latest saved revision of your campaign page. If this does not match your campaign page in the campaign builder, save your changes and then refresh this page. Your current theme can also effect the look of this page.', 'charitable' ), |
| 454 |
[ |
| 455 |
'a' => [ |
| 456 |
'href' => [], |
| 457 |
'target' => [], |
| 458 |
'rel' => [], |
| 459 |
], |
| 460 |
] |
| 461 |
), |
| 462 |
esc_url( get_permalink( $this->campaign_data['id'] ) ) |
| 463 |
); |
| 464 |
|
| 465 |
$content .= '</p>'; |
| 466 |
|
| 467 |
$content .= '<p>' . esc_html__( 'Some functionality and elements (like donation buttons, forms, social sharing, etc.) are disabled.', 'charitable' ) . '</p>'; |
| 468 |
|
| 469 |
} |
| 470 |
|
| 471 |
array_unshift( |
| 472 |
$links, |
| 473 |
[ |
| 474 |
'url' => esc_url( |
| 475 |
get_permalink( $this->campaign_data['id'] ) |
| 476 |
), |
| 477 |
'text' => esc_html__( 'View Live Campaign', 'charitable' ), |
| 478 |
] |
| 479 |
); |
| 480 |
|
| 481 |
elseif ( 'draft' === get_post_status( $this->campaign_data['id'] ) ) : |
| 482 |
|
| 483 |
$content .= '<p>'; |
| 484 |
|
| 485 |
$content .= sprintf( |
| 486 |
wp_kses( |
| 487 |
/* translators: %s - Charitable doc link. */ |
| 488 |
__( 'This is a preview of the latest saved revision of your campaign page. If this does not match your campaign page in the campaign builder, save your changes and then refresh this page. Your current theme can also effect the look of this page.', 'charitable' ), |
| 489 |
[ |
| 490 |
'a' => [ |
| 491 |
'href' => [], |
| 492 |
'target' => [], |
| 493 |
'rel' => [], |
| 494 |
], |
| 495 |
] |
| 496 |
), |
| 497 |
esc_url( get_permalink( $this->campaign_data['id'] ) ) |
| 498 |
); |
| 499 |
|
| 500 |
$content .= '</p>'; |
| 501 |
|
| 502 |
$content .= '<p>' . esc_html__( 'Some functionality and elements (like donation buttons, forms, social sharing, etc.) are disabled.', 'charitable' ) . '</p>'; |
| 503 |
|
| 504 |
endif; |
| 505 |
|
| 506 |
if ( ! empty( $links ) ) { |
| 507 |
$content .= '<span class="charitable-preview-notice-links">'; |
| 508 |
|
| 509 |
foreach ( $links as $key => $link ) { |
| 510 |
$content .= '<a href="' . $link['url'] . '">' . $link['text'] . '</a>'; |
| 511 |
$l = array_keys( $links ); |
| 512 |
|
| 513 |
if ( end( $l ) !== $key ) { |
| 514 |
$content .= ' <span style="display:inline-block;margin:0 6px;opacity: 0.5">|</span> '; |
| 515 |
} |
| 516 |
} |
| 517 |
|
| 518 |
$content .= '</span>'; |
| 519 |
} |
| 520 |
$content .= '</p>'; |
| 521 |
|
| 522 |
$content .= '</div>'; |
| 523 |
|
| 524 |
// If there is no campaign ID in the campaign_data, this MIGHT be an initial preview on a |
| 525 |
// new campaign, in which case the ID is only in the URL. Same resolution the excerpt guard |
| 526 |
// above uses, so the two cannot disagree about which campaign is being previewed. |
| 527 |
$campaign_id = $this->get_preview_campaign_id(); |
| 528 |
|
| 529 |
// Mark a render as in progress for every instance of this class, not just this one, so a |
| 530 |
// re-application of the_content from inside the render short-circuits at the guard above. |
| 531 |
// This is the load-bearing part; the remove_filter() below is belt and braces that only |
| 532 |
// covers this instance's own callback. |
| 533 |
// Detach this handler from the content filters before rendering the campaign. |
| 534 |
// The campaign render re-applies the_content (via the campaign's get_content), and |
| 535 |
// with certain content-processing plugins active that re-application would re-enter |
| 536 |
// this method and render the campaign again, looping until PHP's memory limit is |
| 537 |
// exhausted (HelpScout #11576). The campaign endpoint already guards its own render |
| 538 |
// the same way. The finally re-adds the filters unconditionally, so even if the |
| 539 |
// render throws they are never left detached for the rest of the request. |
| 540 |
remove_filter( 'the_content', [ $this, 'the_content' ], 999 ); |
| 541 |
remove_filter( 'get_the_excerpt', [ $this, 'the_content' ], 999 ); |
| 542 |
|
| 543 |
try { |
| 544 |
// Inside the try so the finally below always clears it. Set outside, a throw between |
| 545 |
// the assignment and the try would leave every later application short-circuiting. |
| 546 |
self::$is_rendering = true; |
| 547 |
|
| 548 |
$content .= do_shortcode( '[campaign version="2" id=' . $campaign_id . ']' ); |
| 549 |
} finally { |
| 550 |
self::$is_rendering = false; |
| 551 |
add_filter( 'the_content', [ $this, 'the_content' ], 999 ); |
| 552 |
// The 2 matters. Re-adding without it silently downgrades this hook to one argument |
| 553 |
// for the rest of the request, so the excerpt guard above would stop receiving the |
| 554 |
// WP_Post and would go back to answering every excerpt with the campaign render. |
| 555 |
add_filter( 'get_the_excerpt', [ $this, 'the_content' ], 999, 2 ); |
| 556 |
} |
| 557 |
|
| 558 |
return $content; |
| 559 |
} |
| 560 |
|
| 561 |
/** |
| 562 |
* Force page template types. |
| 563 |
* |
| 564 |
* @since 1.8.0 |
| 565 |
* |
| 566 |
* @param array $templates A list of template candidates, in descending order of priority. |
| 567 |
* |
| 568 |
* @return array |
| 569 |
*/ |
| 570 |
public function force_page_template_hierarchy( $templates ) { // phpcs:ignore |
| 571 |
|
| 572 |
return [ 'page.php', 'single.php', 'index.php' ]; |
| 573 |
} |
| 574 |
|
| 575 |
/** |
| 576 |
* Adjust value of the {page_title} smart tag. |
| 577 |
* |
| 578 |
* @since 1.7.7 |
| 579 |
* |
| 580 |
* @param string $content Content. |
| 581 |
* @param array $campaign_data Campaign data. |
| 582 |
* @param array $fields List of fields. |
| 583 |
* @param string $entry_id Entry ID. |
| 584 |
* @param object $smart_tag_object The smart tag object or the Generic object for those cases when class unregistered. |
| 585 |
* |
| 586 |
* @return string |
| 587 |
*/ |
| 588 |
public function smart_tags_process_page_title_value( $content, $campaign_data, $fields, $entry_id, $smart_tag_object ) { // phpcs:ignore |
| 589 |
|
| 590 |
return sprintf( /* translators: %s - campaign title. */ |
| 591 |
esc_html__( '%s Preview', 'charitable' ), |
| 592 |
! empty( $campaign_data['settings']['campaign_title'] ) ? sanitize_text_field( $campaign_data['settings']['campaign_title'] ) : esc_html__( 'Campaign', 'charitable' ) |
| 593 |
); |
| 594 |
} |
| 595 |
|
| 596 |
/** |
| 597 |
* Force page template types. |
| 598 |
* |
| 599 |
* @since 1.5.1 |
| 600 |
* @deprecated 1.8.0 |
| 601 |
* |
| 602 |
* @return string |
| 603 |
*/ |
| 604 |
public function template_include() { |
| 605 |
|
| 606 |
_deprecated_function( __METHOD__, '1.8.0 of the Charitable plugin' ); |
| 607 |
|
| 608 |
return locate_template( [ 'page.php', 'single.php', 'index.php' ] ); |
| 609 |
} |
| 610 |
} |
| 611 |
|
| 612 |
endif; |
| 613 |
|
| 614 |
new Campaign_Builder_Preview(); |
| 615 |
|