PluginProbe
Booking Calendar / 11.4
Booking Calendar v11.4
11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 10.11 All 204 releases
booking / includes / page-form-builder / preview / bfb-preview.php

bfb-preview.php in Booking Calendar 11.4, at includes/page-form-builder/preview/bfb-preview.php

1,130 lines 32.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Booking Form Builder - Preview Service (Option A).
4 *
5 * - Creates and manages a single private "Booking Form Preview" page.
6 * - Handles AJAX to store a temporary snapshot of the current BFB structure.
7 * - Builds a secure preview URL for that page and injects the real booking shortcode.
8 * - Optionally exposes a filter hook to let BFB override form structure from snapshot.
9 *
10 * @package Booking Calendar
11 * @subpackage Form Builder
12 *
13 * @author wpdevelop
14 * @version 1.0.0
15 * @since 10.14.0
16 * @file: ../includes/page-form-builder/preview/bfb-preview.php
17 */
18
19 /**
20 == Preview pipeline ==
21 When click on update Preview, the client sends AJAX:
22 action = WPBC_AJX_BFB_SAVE_FORM_CONFIG
23 status = preview
24 structure = JSON.stringify( builder_structure )
25 settings = JSON.stringify( form_settings ) (options + css_vars)
26 and then either:
27 Advanced Mode text (if chosen / auto+dirty), OR
28 Builder export by calling WPBC_BFB_Exporter.export_all(structure, export_options) and sending:
29 advanced_form
30 content_form (from fields_data)
31 Server stores it temporarily in a transient via create_preview_session():
32 structure
33 advanced_form
34 content_form
35 Then it returns a secure URL, and the iframe loads that page.
36 On the preview page request, your service injects the booking shortcode and applies filters:
37 wpbc_bfb_get_form_structure
38 wpbc_bfb_get_form_advanced_form
39 wpbc_bfb_get_form_content_form
40 */
41
42 if ( ! defined( 'ABSPATH' ) ) {
43 exit;
44 }
45
46 /**
47 * Service for handling BFB preview page and snapshots.
48 */
49 class WPBC_BFB_Preview_Service {
50
51 /**
52 * Singleton instance.
53 *
54 * @var WPBC_BFB_Preview_Service|null
55 */
56 protected static $instance = null;
57
58 /**
59 * Currently active preview data for this request (if any).
60 *
61 * @var array|null
62 */
63 protected $current_preview_data = null;
64
65 /**
66 * Get singleton instance.
67 *
68 * @return WPBC_BFB_Preview_Service
69 */
70 public static function get_instance() {
71 if ( null === self::$instance ) {
72 self::$instance = new self();
73 }
74
75 return self::$instance;
76 }
77
78 /**
79 * Constructor (private, use get_instance()).
80 */
81 private function __construct() {
82
83 // Ensure preview page exists (lazy).
84 add_action( 'init', array( $this, 'ensure_preview_page_exists' ), 20 );
85
86 // Re-apply template after switching theme.
87 add_action( 'after_switch_theme', array( $this, 'ensure_preview_page_template' ), 20 );
88
89 // Inject preview content into preview page.
90 add_filter( 'the_content', array( $this, 'filter_the_content_for_preview' ) );
91
92 // Optionally hide preview page from Pages list in admin.
93 // add_action( 'pre_get_posts', array( $this, 'hide_preview_page_in_admin_list' ) );
94
95 // Enqueue JS/CSS on Builder admin page.
96 add_action( 'wpbc_enqueue_js_files_on_page_done', array( $this, 'enqueue_js_files' ) );
97
98 // Hide admin bar in preview iframe only.
99 add_action( 'after_setup_theme', array( $this, 'maybe_hide_admin_bar_for_preview' ) );
100
101 add_filter( 'wp_robots', array( $this, 'add_noindex_to_preview_page' ), 99 );
102 }
103
104 /**
105 * Ensure that the preview page exists and is stored in options.
106 *
107 * This runs on 'init' and can also be called manually.
108 *
109 * @return int|false Page ID on success, false on failure.
110 */
111 public function ensure_preview_page_exists() {
112
113 $option_key = $this->get_page_option_key();
114 $page_id = (int) get_option( $option_key );
115
116 if ( $page_id > 0 ) {
117
118 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
119 if ( is_admin() && ( defined( 'DOING_AJAX' ) ) && ( DOING_AJAX ) && ( isset( $_POST['action'] ) ) && ( 'WPBC_AJX_BFB_SAVE_FORM_CONFIG' === $_POST['action'] ) ) {
120 // Probably saving preview before making this preview, so we can check about ensure preview page.
121 } else {
122 return false;
123 }
124
125 $page = get_post( $page_id );
126
127 if ( $page instanceof WP_Post && 'page' === $page->post_type ) {
128
129 // Ensure it uses full-width template (if available).
130 $this->ensure_preview_page_template();
131
132 return $page_id;
133 }
134
135 delete_option( $option_key );
136 }
137
138 $page_id = $this->create_preview_page();
139
140 if ( $page_id > 0 ) {
141 update_option( $option_key, $page_id );
142
143 // Set full-width template right after creation.
144 $this->ensure_preview_page_template();
145
146 return $page_id;
147 }
148
149 return false;
150 }
151
152 public function add_noindex_to_preview_page( $robots ) {
153
154 if ( ! is_page() ) {
155 return $robots;
156 }
157
158 $page_id = (int) get_queried_object_id();
159 if ( $page_id !== (int) $this->get_preview_page_id() ) {
160 return $robots;
161 }
162
163 $robots['noindex'] = true;
164 $robots['nofollow'] = true;
165 $robots['noarchive'] = true;
166
167 return $robots;
168 }
169
170 /**
171 * Resolve capability used for preview / Builder access.
172 *
173 * Uses wpbc_bfb_get_manage_cap() when available, falls back to manage_options.
174 *
175 * @return string
176 */
177 protected function get_manage_cap() {
178 if ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) {
179 return wpbc_bfb_get_manage_cap();
180 }
181
182 return 'manage_options';
183 }
184
185 /**
186 * Check if current request is a BFB preview request.
187 *
188 * @return bool
189 */
190 protected function is_preview_request() {
191
192 // Quick GET check (works before main query is parsed).
193 if ( isset( $_GET['wpbc_bfb_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
194 return true;
195 }
196
197 // Fallback for places where query vars are already set.
198 $is_preview = get_query_var( 'wpbc_bfb_preview' );
199
200 return ! empty( $is_preview );
201 }
202
203 /**
204 * Hide admin toolbar on front-end preview requests.
205 *
206 * This affects ONLY the preview page loaded in the iframe.
207 */
208 public function maybe_hide_admin_bar_for_preview() {
209
210 // Do not affect wp-admin.
211 if ( is_admin() ) {
212 return;
213 }
214
215 if ( ! $this->is_preview_request() ) {
216 return;
217 }
218
219 $cap = $this->get_manage_cap();
220
221 // Optionally limit to users who can see this preview anyway.
222 if ( ! is_user_logged_in() || ! current_user_can( $cap ) ) {
223 return;
224 }
225
226 // Disable admin bar for this request only.
227 show_admin_bar( false );
228 }
229
230 public function enqueue_js_files() {
231 if ( ! is_admin() ) {
232 return;
233 }
234 // BFB preview script on the Builder admin page.
235 wp_enqueue_script(
236 'wpbc-bfb-preview',
237 wpbc_plugin_url( '/includes/page-form-builder/preview/_out/bfb-preview.js' ),
238 array( 'wpbc-bfb_builder' ),
239 WP_BK_VERSION_NUM,
240 true
241 );
242 wp_enqueue_style(
243 'wpbc-bfb-preview-mode',
244 wpbc_plugin_url( '/includes/page-form-builder/preview/_out/bfb-preview.css' ),
245 array(),
246 WP_BK_VERSION_NUM
247 );
248 }
249
250 /**
251 * Return the option key where preview page ID is stored.
252 *
253 * @return string
254 */
255 protected function get_page_option_key() {
256 return 'wpbc_bfb_preview_page_id';
257 }
258
259 /**
260 * Create the private "Booking Form Preview" page.
261 *
262 * @return int|false Page ID on success, false on failure.
263 */
264 protected function create_preview_page() {
265
266 $page_id = wpbc_bfb_activation__create_preview_page_raw();
267 return $page_id;
268 }
269
270 /**
271 * Get the preview page ID (ensures it exists).
272 *
273 * @return int|false
274 */
275 public function get_preview_page_id() {
276 $option_key = $this->get_page_option_key();
277 $page_id = (int) get_option( $option_key );
278
279 if ( $page_id > 0 ) {
280 return $page_id;
281 }
282
283 return $this->ensure_preview_page_exists();
284 }
285
286
287 /**
288 * Build transient key for storing preview snapshot.
289 *
290 * @param int $user_id Current user ID.
291 * @param string $token Random token.
292 * @param int $form_id Booking form ID.
293 *
294 * @return string
295 */
296 protected function get_transient_key( $user_id, $token, $form_id ) {
297
298 $user_id = (int) $user_id;
299 $form_id = (int) $form_id;
300
301 return 'wpbc_bfb_preview_' . $user_id . '_' . $form_id . '_' . sanitize_key( $token );
302 }
303
304 /**
305 * Try to load preview data from current request (front-end).
306 *
307 * @return array|null
308 */
309 protected function get_preview_data_from_request() {
310
311 if ( null !== $this->current_preview_data ) {
312 return $this->current_preview_data;
313 }
314
315 // Check query flag.
316 $is_preview = get_query_var( 'wpbc_bfb_preview' );
317
318 if ( empty( $is_preview ) && ! isset( $_GET['wpbc_bfb_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
319 return null;
320 }
321
322 $cap = $this->get_manage_cap();
323
324 if ( ! is_user_logged_in() || ! current_user_can( $cap ) ) {
325 return null;
326 }
327
328 $token = get_query_var( 'wpbc_bfb_preview_token' );
329 $form_id = absint( get_query_var( 'wpbc_bfb_preview_form_id' ) );
330
331 if ( empty( $token ) ) {
332 $token = isset( $_GET['wpbc_bfb_preview_token'] ) ? sanitize_text_field( wp_unslash( $_GET['wpbc_bfb_preview_token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
333 }
334
335 if ( empty( $form_id ) ) {
336 $form_id = isset( $_GET['wpbc_bfb_preview_form_id'] ) ? absint( wp_unslash( $_GET['wpbc_bfb_preview_form_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
337 }
338
339 $nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
340
341 if ( empty( $token ) || empty( $form_id ) ) {
342 return null;
343 }
344
345 if ( ! wp_verify_nonce( $nonce, 'wpbc_bfb_preview_' . $token ) ) {
346 return null;
347 }
348
349 $user_id = wpbc_get_current_user_id();
350
351 if ( $user_id <= 0 ) {
352 return null;
353 }
354
355 $transient_key = $this->get_transient_key( $user_id, $token, $form_id );
356 $data = get_transient( $transient_key );
357
358 if ( empty( $data ) ) {
359 return null;
360 }
361
362 $has_structure = ( ! empty( $data['structure'] ) && is_array( $data['structure'] ) );
363 $has_advanced = ( ! empty( $data['advanced_form'] ) || ! empty( $data['content_form'] ) );
364
365 if ( ! $has_structure && ! $has_advanced ) {
366 return null;
367 }
368
369
370 $this->current_preview_data = $data;
371
372 return $this->current_preview_data;
373 }
374
375 // -----------------------------------------------------------------------------------------------------------------
376
377 /**
378 * Ensure preview page uses a "full width" template (if the current theme provides one).
379 *
380 * WordPress stores selected templates in post meta: _wp_page_template. :contentReference[oaicite:1]{index=1}
381 *
382 * Developers can hard-force a template via:
383 * apply_filters( 'wpbc_bfb_preview_page_template', $template, $page_id )
384 *
385 * - Classic theme template value: 'templates/full-width.php'
386 * - Block theme template value: 'page-no-title' (template slug)
387 *
388 * @return void
389 */
390 public function ensure_preview_page_template() {
391
392 $page_id = (int) $this->get_preview_page_id();
393 if ( $page_id <= 0 ) {
394 return;
395 }
396
397 // If already set and valid, do nothing (fast path).
398 $current = (string) get_post_meta( $page_id, '_wp_page_template', true );
399 if ( $this->is_preview_template_value_valid( $current ) ) {
400 return;
401 }
402
403 $template = $this->detect_full_width_template_value();
404
405 /**
406 * Force/override template for preview page.
407 *
408 * Return values:
409 * - '' or 'default' => keep theme default
410 * - classic: 'templates/full-width.php'
411 * - block: 'page-no-title' (slug)
412 */
413 $template = apply_filters( 'wpbc_bfb_preview_page_template', $template, $page_id );
414
415 $template = (string) $template;
416 $template = trim( $template );
417
418 if ( '' === $template || 'default' === $template ) {
419 // Use default template.
420 delete_post_meta( $page_id, '_wp_page_template' );
421
422 return;
423 }
424
425 // Store chosen template.
426 update_post_meta( $page_id, '_wp_page_template', $template );
427 }
428
429 /**
430 * Validate existing _wp_page_template meta value for classic themes.
431 * For block themes we can’t reliably "locate" the template file here, so we accept non-empty values.
432 *
433 * @param string $template_value Existing _wp_page_template meta value.
434 *
435 * @return bool
436 */
437 protected function is_preview_template_value_valid( $template_value ) {
438
439 $template_value = (string) $template_value;
440 $template_value = trim( $template_value );
441
442 if ( '' === $template_value || 'default' === $template_value ) {
443 return false;
444 }
445
446 // Classic themes: validate that file exists in theme.
447 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
448 return true;
449 }
450
451 $located = locate_template( array( $template_value ), false, false );
452
453 return ( ! empty( $located ) );
454 }
455
456 /**
457 * Detect a full-width template value for current theme.
458 *
459 * @return string Template value for _wp_page_template, or '' if none found.
460 */
461 protected function detect_full_width_template_value() {
462
463 // Block themes: try to find a block template slug.
464 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && function_exists( 'get_block_templates' ) ) {
465 $slug = $this->detect_block_theme_full_width_slug();
466 if ( '' !== $slug ) {
467 return $slug;
468 }
469 }
470
471 // Classic themes: try registered page templates first.
472 $file = $this->detect_classic_theme_full_width_file();
473 if ( '' !== $file ) {
474 return $file;
475 }
476
477 return '';
478 }
479
480 /**
481 * Try to detect a "full width" template slug in block themes.
482 *
483 * @return string
484 */
485 protected function detect_block_theme_full_width_slug() {
486
487 $best_slug = '';
488 $best_score = 0;
489
490 $templates = get_block_templates( array(
491 'post_type' => 'page',
492 ), 'wp_template' );
493
494 if ( empty( $templates ) || ! is_array( $templates ) ) {
495 return '';
496 }
497
498 foreach ( $templates as $tpl ) {
499
500 $title = '';
501 $slug = '';
502
503 if ( is_object( $tpl ) ) {
504 $title = isset( $tpl->title ) ? (string) $tpl->title : '';
505 $slug = isset( $tpl->slug ) ? (string) $tpl->slug : '';
506 }
507
508 $score = $this->score_full_width_candidate( $title . ' ' . $slug );
509
510 if ( $score > $best_score && '' !== $slug ) {
511 $best_score = $score;
512 $best_slug = $slug;
513 }
514 }
515
516 return ( $best_score >= 60 ) ? $best_slug : '';
517 }
518
519 /**
520 * Try to detect a "full width" template file in classic themes.
521 *
522 * @return string
523 */
524 protected function detect_classic_theme_full_width_file() {
525
526 $theme = wp_get_theme();
527 $templates = $theme->get_page_templates( null, 'page' ); // array( 'Name' => 'file.php' ).
528
529 $best_file = '';
530 $best_score = 0;
531
532 if ( ! empty( $templates ) && is_array( $templates ) ) {
533 foreach ( $templates as $name => $file ) {
534 $score = $this->score_full_width_candidate( $name . ' ' . $file );
535 if ( $score > $best_score && ! empty( $file ) ) {
536 $best_score = $score;
537 $best_file = (string) $file;
538 }
539 }
540 }
541
542 // Accept strong match from registered templates.
543 if ( $best_score >= 60 && '' !== $best_file ) {
544 $located = locate_template( array( $best_file ), false, false );
545 if ( ! empty( $located ) ) {
546 return $best_file;
547 }
548 }
549
550 // Fallback: common filenames (theme-dependent).
551 $candidates = array(
552 'page-templates/full-width.php',
553 'page-templates/fullwidth.php',
554 'templates/full-width.php',
555 'templates/fullwidth.php',
556 'full-width.php',
557 'fullwidth.php',
558 'page-no-sidebar.php',
559 'page-nosidebar.php',
560 );
561
562 foreach ( $candidates as $candidate ) {
563 $located = locate_template( array( $candidate ), false, false );
564 if ( ! empty( $located ) ) {
565 return $candidate;
566 }
567 }
568
569 return '';
570 }
571
572 /**
573 * Score a candidate (template name/slug/file) as "full width".
574 *
575 * @param string $haystack Text to score.
576 *
577 * @return int
578 */
579 protected function score_full_width_candidate( $haystack ) {
580
581 $h = strtolower( (string) $haystack );
582 $s = 0;
583
584 if ( false !== strpos( $h, 'full' ) && false !== strpos( $h, 'width' ) ) {
585 $s += 100;
586 }
587 if ( false !== strpos( $h, 'no' ) && false !== strpos( $h, 'sidebar' ) ) {
588 $s += 95;
589 }
590 // My theme exception for local test server.
591 if ( ( isset( $_SERVER['HTTP_HOST'] ) && ( 'beta' === sanitize_key( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) ) && ( false !== strpos( $h, 'no-title' ) ) ) {
592 $s += 61;
593 }
594 if ( false !== strpos( $h, 'wide' ) ) {
595 $s += 60;
596 }
597 if ( false !== strpos( $h, 'canvas' ) || false !== strpos( $h, 'blank' ) ) {
598 $s += 40;
599 }
600 if ( false !== strpos( $h, 'elementor' ) && false !== strpos( $h, 'full' ) ) {
601 $s += 30;
602 }
603
604 return $s;
605 }
606 // -----------------------------------------------------------------------------------------------------------------
607
608 /**
609 * Filter "the_content" to inject the real booking form into the preview page.
610 *
611 * Note:
612 * - This is where we run the actual booking shortcode.
613 * - The theme (classic or block) wraps this content as usual.
614 *
615 * @param string $content Original page content.
616 *
617 * @return string
618 */
619 public function filter_the_content_for_preview( $content ) {
620
621 if ( ! is_page() ) {
622 return $content;
623 }
624
625 $page_id = get_the_ID();
626 $preview_page_id = $this->get_preview_page_id();
627
628 if ( $preview_page_id <= 0 || (int) $page_id !== (int) $preview_page_id ) {
629 return $content;
630 }
631
632 $preview_data = $this->get_preview_data_from_request();
633
634 if ( empty( $preview_data ) ) {
635 // No active preview snapshot, show a simple message.
636 return '<p>' . esc_html__( 'This is a booking form preview page. Please refresh the preview from the Builder.', 'booking' ) . '</p>';
637 }
638
639 // Safety: disable caching for this request.
640 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
641 if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); }
642
643 nocache_headers();
644
645 // Store preview data for this request so other hooks can access it.
646 $this->current_preview_data = $preview_data;
647
648 // Optional: expose a filter so BFB structure loader can override structure per preview.
649 // Your wpbc_bfb_get_form_structure() can apply this filter when resolving structure.
650 add_filter( 'wpbc_bfb_get_form_structure', array( $this, 'filter_form_structure_for_preview' ), 10, 2 );
651 add_filter( 'wpbc_bfb_get_form_advanced_form', array( $this, 'filter_form_advanced_form_for_preview' ), 10, 2 );
652 add_filter( 'wpbc_bfb_get_form_content_form', array( $this, 'filter_form_content_form_for_preview' ), 10, 2 );
653
654 $resource_id = WPBC_FE_Attr_Postprocessor::get_default_booking_resource_id();
655
656 $form_name = isset( $preview_data['form_name'] ) ? sanitize_text_field( wp_unslash( $preview_data['form_name'] ) ) : 'standard';
657 if ( '' === $form_name ) {
658 $form_name = 'standard';
659 }
660
661 $shortcode = '[booking resource_id="' . esc_attr( $resource_id ) . '" form_type="' . esc_attr( $form_name ) . '" form_status="preview"]';
662
663 $preview_html = do_shortcode( $shortcode );
664
665 return $this->filter_wrapped_html_for_preview_form_style( $preview_html, array(), $resource_id, $form_name );
666 }
667
668 public function filter_form_advanced_form_for_preview( $advanced_form, $form_id ) {
669
670 if ( empty( $this->current_preview_data ) ) {
671 return $advanced_form;
672 }
673
674 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
675 return $advanced_form;
676 }
677
678 if ( isset( $this->current_preview_data['advanced_form'] ) ) {
679 return (string) $this->current_preview_data['advanced_form'];
680 }
681
682 return $advanced_form;
683 }
684
685 public function filter_form_content_form_for_preview( $content_form, $form_id ) {
686
687 if ( empty( $this->current_preview_data ) ) {
688 return $content_form;
689 }
690
691 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
692 return $content_form;
693 }
694
695 if ( isset( $this->current_preview_data['content_form'] ) ) {
696 return (string) $this->current_preview_data['content_form'];
697 }
698
699 return $content_form;
700 }
701
702 /**
703 * Filter that allows BFB structure loader to receive snapshot for preview.
704 *
705 * Usage example in your loader:
706 * $structure = apply_filters( 'wpbc_bfb_get_form_structure', $structure, $form_id );
707 *
708 * @param array $structure Default structure loaded from DB.
709 * @param int $form_id Booking form ID.
710 *
711 * @return array
712 */
713 public function filter_form_structure_for_preview( $structure, $form_id ) {
714
715 if ( empty( $this->current_preview_data ) ) {
716 return $structure;
717 }
718
719 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
720 return $structure;
721 }
722
723 if ( empty( $this->current_preview_data['structure'] ) || ! is_array( $this->current_preview_data['structure'] ) ) {
724 return $structure;
725 }
726
727 return $this->current_preview_data['structure'];
728 }
729
730 /**
731 * Apply unsaved global Form Style values to the preview iframe only.
732 *
733 * @param string $wrapped_html Wrapped booking form HTML.
734 * @param array $bfb_settings BFB settings.
735 * @param int $resource_id Booking resource ID.
736 * @param string $custom_booking_form_name Form slug.
737 * @return string
738 */
739 public function filter_wrapped_html_for_preview_form_style( $wrapped_html, $bfb_settings, $resource_id, $custom_booking_form_name ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
740
741 $preview_style = $this->get_preview_form_style();
742 if ( empty( $preview_style ) ) {
743 return $wrapped_html;
744 }
745
746 $style = isset( $preview_style['booking_form_style'] ) ? wpbc_bfb_settings__sanitize_form_style( $preview_style['booking_form_style'] ) : wpbc_bfb_settings__get_default_form_style();
747 $preset = wpbc_bfb_settings__get_form_style_preset( $style );
748
749 $wrapped_html = $this->remove_classes_from_first_form_wrapper(
750 $wrapped_html,
751 array(
752 'wpbc_theme_dark_1',
753 'wpbc_bfb_form_appearance_custom',
754 )
755 );
756 $wrapped_html = $this->remove_classes_from_first_tag_with_classes(
757 $wrapped_html,
758 array( 'wpbc_bfb_form' ),
759 array(
760 'wpbc_theme_dark_1',
761 'wpbc_bfb_form_appearance_custom',
762 )
763 );
764
765 $theme_class = isset( $preset['theme_class'] ) ? sanitize_html_class( (string) $preset['theme_class'] ) : '';
766 if ( '' !== $theme_class ) {
767 $wrapped_html = WPBC_FE_Form_Style_Injector::add_class_to_first_tag_with_classes( $wrapped_html, array( 'wpbc_container', 'wpbc_form' ), $theme_class );
768 }
769
770 $css_vars = wpbc_bfb_settings__get_form_style_css_vars( $style, $preview_style );
771 if ( ! empty( $css_vars ) ) {
772 $wrapped_html = WPBC_FE_Form_Style_Injector::inject_css_vars_into_form_wrapper( $wrapped_html, $css_vars );
773 $wrapped_html = WPBC_FE_Form_Style_Injector::inject_css_vars_into_bfb_root( $wrapped_html, $css_vars );
774 }
775
776 if ( wpbc_bfb_settings__is_custom_form_style( $style ) ) {
777 $wrapped_html = WPBC_FE_Form_Style_Injector::add_class_to_first_tag_with_classes( $wrapped_html, array( 'wpbc_container', 'wpbc_form' ), 'wpbc_bfb_form_appearance_custom' );
778 $wrapped_html = WPBC_FE_Form_Style_Injector::add_class_to_first_tag_with_classes( $wrapped_html, array( 'wpbc_bfb_form' ), 'wpbc_bfb_form_appearance_custom' );
779 }
780
781 return $wrapped_html;
782 }
783
784 /**
785 * Get sanitized preview Form Style override from the current transient data.
786 *
787 * @return array
788 */
789 protected function get_preview_form_style() {
790
791 if ( empty( $this->current_preview_data['form_style'] ) || ! is_array( $this->current_preview_data['form_style'] ) ) {
792 return array();
793 }
794
795 $style = isset( $this->current_preview_data['form_style']['booking_form_style'] ) ? $this->current_preview_data['form_style']['booking_form_style'] : '';
796 $style = wpbc_bfb_settings__sanitize_form_style( $style );
797
798 $custom_options = wpbc_bfb_settings__get_custom_form_style_options( $this->current_preview_data['form_style'] );
799
800 return array_merge(
801 array(
802 'booking_form_style' => $style,
803 ),
804 $custom_options
805 );
806 }
807
808 /**
809 * Remove classes from the first outer booking form wrapper.
810 *
811 * @param string $html HTML.
812 * @param array $class_names Class names to remove.
813 * @return string
814 */
815 protected function remove_classes_from_first_form_wrapper( $html, $class_names ) {
816
817 return $this->remove_classes_from_first_tag_with_classes( $html, array( 'wpbc_container', 'wpbc_form' ), $class_names );
818 }
819
820 /**
821 * Remove classes from the first tag that has all required classes.
822 *
823 * @param string $html HTML.
824 * @param array $required_classes Required classes.
825 * @param array $class_names Class names to remove.
826 * @return string
827 */
828 protected function remove_classes_from_first_tag_with_classes( $html, $required_classes, $class_names ) {
829
830 $html = (string) $html;
831 $required_classes = is_array( $required_classes ) ? $required_classes : array();
832 $class_names = is_array( $class_names ) ? $class_names : array();
833 $remove_map = array();
834 $required_map = array();
835
836 foreach ( $required_classes as $class_name ) {
837 $class_name = sanitize_html_class( (string) $class_name );
838 if ( '' !== $class_name ) {
839 $required_map[ $class_name ] = true;
840 }
841 }
842
843 foreach ( $class_names as $class_name ) {
844 $class_name = sanitize_html_class( (string) $class_name );
845 if ( '' !== $class_name ) {
846 $remove_map[ $class_name ] = true;
847 }
848 }
849
850 if ( empty( $remove_map ) || empty( $required_map ) ) {
851 return $html;
852 }
853
854 $done = false;
855 $out = preg_replace_callback(
856 '/<div\b[^>]*\bclass\s*=\s*(["\'])(.*?)\1[^>]*>/i',
857 function ( $matches ) use ( &$done, $remove_map, $required_map ) {
858
859 $tag = $matches[0];
860 if ( $done ) {
861 return $tag;
862 }
863
864 $quote = $matches[1];
865 $classes = preg_split( '/\s+/', trim( (string) $matches[2] ) );
866 $classes = is_array( $classes ) ? $classes : array();
867
868 foreach ( $required_map as $required_class => $unused ) {
869 if ( ! in_array( $required_class, $classes, true ) ) {
870 return $tag;
871 }
872 }
873
874 $filtered = array();
875 foreach ( $classes as $class_name ) {
876 if ( '' === $class_name || isset( $remove_map[ $class_name ] ) ) {
877 continue;
878 }
879 $filtered[] = $class_name;
880 }
881
882 $done = true;
883
884 return preg_replace( '/\bclass\s*=\s*(["\'])(.*?)\1/i', 'class=' . $quote . esc_attr( implode( ' ', $filtered ) ) . $quote, $tag, 1 );
885 },
886 $html
887 );
888
889 return ( null === $out ) ? $html : $out;
890 }
891
892 /**
893 * Hide the preview page from the Pages list in admin.
894 *
895 * @param WP_Query $query Main query.
896 */
897 public function hide_preview_page_in_admin_list( $query ) {
898
899 if ( ! is_admin() || ! $query->is_main_query() ) {
900 return;
901 }
902
903 global $pagenow;
904
905 if ( 'edit.php' !== $pagenow ) {
906 return;
907 }
908
909 $post_type = $query->get( 'post_type' );
910
911 if ( 'page' !== $post_type && '' !== $post_type ) {
912 return;
913 }
914
915 $page_id = $this->get_preview_page_id();
916
917 if ( $page_id <= 0 ) {
918 return;
919 }
920
921 $not_in = (array) $query->get( 'post__not_in' );
922 $not_in[] = (int) $page_id;
923
924 $query->set( 'post__not_in', $not_in );
925 }
926
927 // FixIn: 2026-01-03 14:31.
928 /**
929 * Create a preview session: store transient snapshot and return preview URL + token.
930 *
931 * @param int $preview_form_id Preview form/resource ID.
932 * @param int $user_id Current user ID.
933 * @param array $structure Decoded BFB structure.
934 *
935 * @return array|false { preview_url, token } or false on failure.
936 */
937 public function create_preview_session( $preview_form_id, $user_id, $structure, $form_name = 'standard', $advanced_form = '', $content_form = '', $form_style = array() ) {
938
939
940 $preview_form_id = (int) $preview_form_id;
941 $user_id = (int) $user_id;
942 $structure = ( is_array( $structure ) ? $structure : array() );
943 $form_name = sanitize_text_field( (string) $form_name );
944 if ( '' === $form_name ) {
945 $form_name = 'standard';
946 }
947 $form_style = is_array( $form_style ) ? $form_style : array();
948 if ( ! empty( $form_style ) ) {
949 $style_key = isset( $form_style['booking_form_style'] ) ? $form_style['booking_form_style'] : '';
950 $sanitized_style = wpbc_bfb_settings__sanitize_form_style( $style_key );
951 $custom_style = wpbc_bfb_settings__get_custom_form_style_options( $form_style );
952 $form_style = array_merge(
953 array(
954 'booking_form_style' => $sanitized_style,
955 ),
956 $custom_style
957 );
958 }
959 if ( $preview_form_id <= 0 ) {
960 $preview_form_id = 1;
961 }
962 if ( $user_id <= 0 ) {
963 return false;
964 }
965
966 $page_id = $this->get_preview_page_id();
967 if ( ! $page_id ) {
968 return false;
969 }
970
971 $token = wp_generate_password( 12, false, false );
972 $transient_key = $this->get_transient_key( $user_id, $token, $preview_form_id );
973
974 $payload = array(
975 'user_id' => $user_id,
976 // Keep key name as-is (your preview reader expects ['form_id']).
977 // This is a *preview context resource/calendar id* (used for shortcode type="...").
978 'form_id' => $preview_form_id,
979 'form_name' => $form_name,
980 'scope' => 'preview',
981 'structure' => $structure,
982 'time' => time(),
983 'advanced_form' => (string) $advanced_form,
984 'content_form' => (string) $content_form,
985 'form_style' => $form_style,
986 );
987
988 set_transient( $transient_key, $payload, 10 * MINUTE_IN_SECONDS );
989
990 $preview_url = add_query_arg( array(
991 'wpbc_bfb_preview' => 1,
992 'wpbc_bfb_preview_token' => rawurlencode( $token ),
993 'wpbc_bfb_preview_form_id' => $preview_form_id,
994 'nonce' => wp_create_nonce( 'wpbc_bfb_preview_' . $token ),
995 ), get_permalink( $page_id ) );
996
997 return array(
998 'preview_url' => $preview_url,
999 'token' => $token,
1000 );
1001 }
1002
1003 }
1004
1005
1006 /**
1007 * Render BFB Preview Panel in the Builder admin page.
1008 *
1009 * @param int $form_id Current booking form / resource ID.
1010 */
1011 function wpbc_bfb_render_preview_panel( $form_id = 1 ) {
1012
1013 $form_id = absint( $form_id );
1014 if ( $form_id <= 0 ) {
1015 $form_id = 1;
1016 }
1017
1018 $preview_nonce = wp_create_nonce( 'wpbc_bfb_preview' );
1019 ?>
1020 <div id="wpbc_bfb__preview_panel"
1021 class="wpbc_bfb__preview_panel"
1022 data-wpbc-bfb-preview-root="1"
1023 data-form-id="<?php echo esc_attr( $form_id ); ?>"
1024 data-preview-nonce="<?php echo esc_attr( $preview_nonce ); ?>">
1025
1026 <div class="wpbc_bfb__preview_panel__toolbar">
1027 <h1 class="wpbc_settings_page_header_title wpbc_bfb_ui__elements_show_in_preview">
1028 <?php esc_html_e( 'Booking Form Preview', 'booking' ); ?>
1029 </h1>
1030 <span class="description">
1031 <?php esc_html_e( 'The preview shows the live booking form inside your site theme.', 'booking' ); ?>
1032 </span>
1033 <style type="text/css">
1034 html[data-wpbc-bfb-live-source="advanced"] .warning_description {
1035 display: block;
1036 }
1037 .warning_description {
1038 display: none;
1039 width: 700px;
1040 color: #900;
1041 width: 630px;
1042 color: #44270d;
1043 text-align: center;
1044 font-size: 14px;
1045 line-height: 2;
1046 background: #fff;
1047 padding: 10px;
1048 padding: 0.75em 2em;
1049 margin: 20px auto 0;
1050 border-radius: 8px;
1051 border: 2px solid #b97131;
1052 }
1053 .warning_description .wpbc_bfb__live_status__advanced {
1054 font-weight: 550;
1055 display: inline !important;
1056 }
1057 </style>
1058 <div class="warning_description">
1059 <?php
1060 /* translators: 1: <strong>, 2: </strong>, 3: <a ...>, 4: </a>, 5: <a ...>, 6: </a> */
1061 printf( __( '%1$sNote!%2$s This preview is generated from %3$sAdvanced (manual)%4$s, which is not synced with the Form Builder. To sync it, enable the corresponding checkbox %5$shere%6$s.', 'booking' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1062 '<strong>',
1063 '</strong>',
1064 '<a class="wpbc_bfb__live_status__advanced" href="javascript:void(0);" onclick="javascript:WPBC_BFB_Preview.show_advanced_tab({});">',
1065 '</a>',
1066 '<a href="javascript:void(0);" onclick="javascript:WPBC_BFB_Preview.show_advanced_tab({});">',
1067 '</a>'
1068 );
1069 ?>
1070 </div>
1071 </div>
1072
1073 <div class="wpbc_bfb__preview_panel__frame_wrapper">
1074 <div class="wpbc_bfb__preview_loader" data-wpbc-bfb-preview-loader="1" aria-hidden="true">
1075 <?php
1076 wpbc_bfb_spins_loading_container_mini();
1077 ?>
1078 </div>
1079
1080 <iframe
1081 id="wpbc_bfb__preview_iframe"
1082 class="wpbc_bfb__preview_iframe"
1083 data-wpbc-bfb-preview-iframe="1"
1084 src="about:blank"
1085 title="<?php esc_attr_e( 'Booking form preview', 'booking' ); ?>"
1086 loading="lazy"
1087 referrerpolicy="no-referrer-when-downgrade">
1088 </iframe>
1089 </div>
1090 </div>
1091 <?php
1092 }
1093
1094 /**
1095 * Show Loader Spin.
1096 *
1097 * @return void
1098 */
1099 function wpbc_bfb_spins_loading_container() {
1100 ?>
1101 <div class="wpbc_spins_loading_container">
1102 <div class="wpbc_booking_form_spin_loader">
1103 <div class="wpbc_spins_loader_wrapper">
1104 <div class="wpbc_spin_loader_one_new"></div>
1105 </div>
1106 </div>
1107 <span><?php echo esc_html__( 'Loading', 'booking' ); ?> ...</span>
1108 </div>
1109 <?php
1110 }
1111
1112
1113 /**
1114 * Show Mini Loader Spin.
1115 *
1116 * @return void
1117 */
1118 function wpbc_bfb_spins_loading_container_mini() {
1119 ?>
1120 <div class="wpbc_spins_loading_container wpbc_bfb_spins_loading_container">
1121 <div class="wpbc_booking_form_spin_loader">
1122 <div class="wpbc_spins_loader_wrapper">
1123 <div class="wpbc_one_spin_loader_mini2"></div>
1124 </div>
1125 </div>
1126 <span><?php esc_html_e( 'Loading', 'booking' ); ?>...</span>
1127 </div>
1128 <?php
1129 }
1130