PluginProbe
Booking Calendar / 11.0
Booking Calendar v11.0
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 10.11.2 All 203 releases
booking / includes / page-form-builder / preview / bfb-preview.php

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

952 lines 26.7 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 '1.0.0',
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 return do_shortcode( $shortcode );
664 }
665
666 public function filter_form_advanced_form_for_preview( $advanced_form, $form_id ) {
667
668 if ( empty( $this->current_preview_data ) ) {
669 return $advanced_form;
670 }
671
672 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
673 return $advanced_form;
674 }
675
676 if ( isset( $this->current_preview_data['advanced_form'] ) ) {
677 return (string) $this->current_preview_data['advanced_form'];
678 }
679
680 return $advanced_form;
681 }
682
683 public function filter_form_content_form_for_preview( $content_form, $form_id ) {
684
685 if ( empty( $this->current_preview_data ) ) {
686 return $content_form;
687 }
688
689 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
690 return $content_form;
691 }
692
693 if ( isset( $this->current_preview_data['content_form'] ) ) {
694 return (string) $this->current_preview_data['content_form'];
695 }
696
697 return $content_form;
698 }
699
700 /**
701 * Filter that allows BFB structure loader to receive snapshot for preview.
702 *
703 * Usage example in your loader:
704 * $structure = apply_filters( 'wpbc_bfb_get_form_structure', $structure, $form_id );
705 *
706 * @param array $structure Default structure loaded from DB.
707 * @param int $form_id Booking form ID.
708 *
709 * @return array
710 */
711 public function filter_form_structure_for_preview( $structure, $form_id ) {
712
713 if ( empty( $this->current_preview_data ) ) {
714 return $structure;
715 }
716
717 if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) {
718 return $structure;
719 }
720
721 if ( empty( $this->current_preview_data['structure'] ) || ! is_array( $this->current_preview_data['structure'] ) ) {
722 return $structure;
723 }
724
725 return $this->current_preview_data['structure'];
726 }
727
728 /**
729 * Hide the preview page from the Pages list in admin.
730 *
731 * @param WP_Query $query Main query.
732 */
733 public function hide_preview_page_in_admin_list( $query ) {
734
735 if ( ! is_admin() || ! $query->is_main_query() ) {
736 return;
737 }
738
739 global $pagenow;
740
741 if ( 'edit.php' !== $pagenow ) {
742 return;
743 }
744
745 $post_type = $query->get( 'post_type' );
746
747 if ( 'page' !== $post_type && '' !== $post_type ) {
748 return;
749 }
750
751 $page_id = $this->get_preview_page_id();
752
753 if ( $page_id <= 0 ) {
754 return;
755 }
756
757 $not_in = (array) $query->get( 'post__not_in' );
758 $not_in[] = (int) $page_id;
759
760 $query->set( 'post__not_in', $not_in );
761 }
762
763 // FixIn: 2026-01-03 14:31.
764 /**
765 * Create a preview session: store transient snapshot and return preview URL + token.
766 *
767 * @param int $preview_form_id Preview form/resource ID.
768 * @param int $user_id Current user ID.
769 * @param array $structure Decoded BFB structure.
770 *
771 * @return array|false { preview_url, token } or false on failure.
772 */
773 public function create_preview_session( $preview_form_id, $user_id, $structure, $form_name = 'standard', $advanced_form = '', $content_form = '' ) {
774
775
776 $preview_form_id = (int) $preview_form_id;
777 $user_id = (int) $user_id;
778 $structure = ( is_array( $structure ) ? $structure : array() );
779 $form_name = sanitize_text_field( (string) $form_name );
780 if ( '' === $form_name ) {
781 $form_name = 'standard';
782 }
783 if ( $preview_form_id <= 0 ) {
784 $preview_form_id = 1;
785 }
786 if ( $user_id <= 0 ) {
787 return false;
788 }
789
790 $page_id = $this->get_preview_page_id();
791 if ( ! $page_id ) {
792 return false;
793 }
794
795 $token = wp_generate_password( 12, false, false );
796 $transient_key = $this->get_transient_key( $user_id, $token, $preview_form_id );
797
798 $payload = array(
799 'user_id' => $user_id,
800 // Keep key name as-is (your preview reader expects ['form_id']).
801 // This is a *preview context resource/calendar id* (used for shortcode type="...").
802 'form_id' => $preview_form_id,
803 'form_name' => $form_name,
804 'scope' => 'preview',
805 'structure' => $structure,
806 'time' => time(),
807 'advanced_form' => (string) $advanced_form,
808 'content_form' => (string) $content_form,
809 );
810
811 set_transient( $transient_key, $payload, 10 * MINUTE_IN_SECONDS );
812
813 $preview_url = add_query_arg( array(
814 'wpbc_bfb_preview' => 1,
815 'wpbc_bfb_preview_token' => rawurlencode( $token ),
816 'wpbc_bfb_preview_form_id' => $preview_form_id,
817 'nonce' => wp_create_nonce( 'wpbc_bfb_preview_' . $token ),
818 ), get_permalink( $page_id ) );
819
820 return array(
821 'preview_url' => $preview_url,
822 'token' => $token,
823 );
824 }
825
826 }
827
828
829 /**
830 * Render BFB Preview Panel in the Builder admin page.
831 *
832 * @param int $form_id Current booking form / resource ID.
833 */
834 function wpbc_bfb_render_preview_panel( $form_id = 1 ) {
835
836 $form_id = absint( $form_id );
837 if ( $form_id <= 0 ) {
838 $form_id = 1;
839 }
840
841 $preview_nonce = wp_create_nonce( 'wpbc_bfb_preview' );
842 ?>
843 <div id="wpbc_bfb__preview_panel"
844 class="wpbc_bfb__preview_panel"
845 data-wpbc-bfb-preview-root="1"
846 data-form-id="<?php echo esc_attr( $form_id ); ?>"
847 data-preview-nonce="<?php echo esc_attr( $preview_nonce ); ?>">
848
849 <div class="wpbc_bfb__preview_panel__toolbar">
850 <h1 class="wpbc_settings_page_header_title wpbc_bfb_ui__elements_show_in_preview">
851 <?php esc_html_e( 'Booking Form Preview', 'booking' ); ?>
852 </h1>
853 <span class="description">
854 <?php esc_html_e( 'The preview shows the live booking form inside your site theme.', 'booking' ); ?>
855 </span>
856 <style type="text/css">
857 html[data-wpbc-bfb-live-source="advanced"] .warning_description {
858 display: block;
859 }
860 .warning_description {
861 display: none;
862 width: 700px;
863 color: #900;
864 width: 630px;
865 color: #44270d;
866 text-align: center;
867 font-size: 14px;
868 line-height: 2;
869 background: #fff;
870 padding: 10px;
871 padding: 0.75em 2em;
872 margin: 20px auto 0;
873 border-radius: 8px;
874 border: 2px solid #b97131;
875 }
876 .warning_description .wpbc_bfb__live_status__advanced {
877 font-weight: 550;
878 display: inline !important;
879 }
880 </style>
881 <div class="warning_description">
882 <?php
883 /* translators: 1: <strong>, 2: </strong>, 3: <a ...>, 4: </a>, 5: <a ...>, 6: </a> */
884 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
885 '<strong>',
886 '</strong>',
887 '<a class="wpbc_bfb__live_status__advanced" href="javascript:void(0);" onclick="javascript:WPBC_BFB_Preview.show_advanced_tab({});">',
888 '</a>',
889 '<a href="javascript:void(0);" onclick="javascript:WPBC_BFB_Preview.show_advanced_tab({});">',
890 '</a>'
891 );
892 ?>
893 </div>
894 </div>
895
896 <div class="wpbc_bfb__preview_panel__frame_wrapper">
897 <div class="wpbc_bfb__preview_loader" data-wpbc-bfb-preview-loader="1" aria-hidden="true">
898 <?php
899 wpbc_bfb_spins_loading_container_mini();
900 ?>
901 </div>
902
903 <iframe
904 id="wpbc_bfb__preview_iframe"
905 class="wpbc_bfb__preview_iframe"
906 data-wpbc-bfb-preview-iframe="1"
907 src="about:blank"
908 title="<?php esc_attr_e( 'Booking form preview', 'booking' ); ?>"
909 loading="lazy"
910 referrerpolicy="no-referrer-when-downgrade">
911 </iframe>
912 </div>
913 </div>
914 <?php
915 }
916
917 /**
918 * Show Loader Spin.
919 *
920 * @return void
921 */
922 function wpbc_bfb_spins_loading_container() {
923 ?>
924 <div class="wpbc_spins_loading_container">
925 <div class="wpbc_booking_form_spin_loader">
926 <div class="wpbc_spins_loader_wrapper">
927 <div class="wpbc_spin_loader_one_new"></div>
928 </div>
929 </div>
930 <span><?php echo esc_html__( 'Loading', 'booking' ); ?> ...</span>
931 </div>
932 <?php
933 }
934
935
936 /**
937 * Show Mini Loader Spin.
938 *
939 * @return void
940 */
941 function wpbc_bfb_spins_loading_container_mini() {
942 ?>
943 <div class="wpbc_spins_loading_container wpbc_bfb_spins_loading_container">
944 <div class="wpbc_booking_form_spin_loader">
945 <div class="wpbc_spins_loader_wrapper">
946 <div class="wpbc_one_spin_loader_mini2"></div>
947 </div>
948 </div>
949 <span><?php esc_html_e( 'Loading', 'booking' ); ?>...</span>
950 </div>
951 <?php
952 }