get_page_option_key(); $page_id = (int) get_option( $option_key ); if ( $page_id > 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing if ( is_admin() && ( defined( 'DOING_AJAX' ) ) && ( DOING_AJAX ) && ( isset( $_POST['action'] ) ) && ( 'WPBC_AJX_BFB_SAVE_FORM_CONFIG' === $_POST['action'] ) ) { // Probably saving preview before making this preview, so we can check about ensure preview page. } else { return false; } $page = get_post( $page_id ); if ( $page instanceof WP_Post && 'page' === $page->post_type ) { // Ensure it uses full-width template (if available). $this->ensure_preview_page_template(); return $page_id; } delete_option( $option_key ); } $page_id = $this->create_preview_page(); if ( $page_id > 0 ) { update_option( $option_key, $page_id ); // Set full-width template right after creation. $this->ensure_preview_page_template(); return $page_id; } return false; } public function add_noindex_to_preview_page( $robots ) { if ( ! is_page() ) { return $robots; } $page_id = (int) get_queried_object_id(); if ( $page_id !== (int) $this->get_preview_page_id() ) { return $robots; } $robots['noindex'] = true; $robots['nofollow'] = true; $robots['noarchive'] = true; return $robots; } /** * Resolve capability used for preview / Builder access. * * Uses wpbc_bfb_get_manage_cap() when available, falls back to manage_options. * * @return string */ protected function get_manage_cap() { if ( function_exists( 'wpbc_bfb_get_manage_cap' ) ) { return wpbc_bfb_get_manage_cap(); } return 'manage_options'; } /** * Check if current request is a BFB preview request. * * @return bool */ protected function is_preview_request() { // Quick GET check (works before main query is parsed). if ( isset( $_GET['wpbc_bfb_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return true; } // Fallback for places where query vars are already set. $is_preview = get_query_var( 'wpbc_bfb_preview' ); return ! empty( $is_preview ); } /** * Hide admin toolbar on front-end preview requests. * * This affects ONLY the preview page loaded in the iframe. */ public function maybe_hide_admin_bar_for_preview() { // Do not affect wp-admin. if ( is_admin() ) { return; } if ( ! $this->is_preview_request() ) { return; } $cap = $this->get_manage_cap(); // Optionally limit to users who can see this preview anyway. if ( ! is_user_logged_in() || ! current_user_can( $cap ) ) { return; } // Disable admin bar for this request only. show_admin_bar( false ); } public function enqueue_js_files() { if ( ! is_admin() ) { return; } // BFB preview script on the Builder admin page. wp_enqueue_script( 'wpbc-bfb-preview', wpbc_plugin_url( '/includes/page-form-builder/preview/_out/bfb-preview.js' ), array( 'wpbc-bfb_builder' ), WP_BK_VERSION_NUM, true ); wp_enqueue_style( 'wpbc-bfb-preview-mode', wpbc_plugin_url( '/includes/page-form-builder/preview/_out/bfb-preview.css' ), array(), WP_BK_VERSION_NUM ); } /** * Return the option key where preview page ID is stored. * * @return string */ protected function get_page_option_key() { return 'wpbc_bfb_preview_page_id'; } /** * Create the private "Booking Form Preview" page. * * @return int|false Page ID on success, false on failure. */ protected function create_preview_page() { $page_id = wpbc_bfb_activation__create_preview_page_raw(); return $page_id; } /** * Get the preview page ID (ensures it exists). * * @return int|false */ public function get_preview_page_id() { $option_key = $this->get_page_option_key(); $page_id = (int) get_option( $option_key ); if ( $page_id > 0 ) { return $page_id; } return $this->ensure_preview_page_exists(); } /** * Build transient key for storing preview snapshot. * * @param int $user_id Current user ID. * @param string $token Random token. * @param int $form_id Booking form ID. * * @return string */ protected function get_transient_key( $user_id, $token, $form_id ) { $user_id = (int) $user_id; $form_id = (int) $form_id; return 'wpbc_bfb_preview_' . $user_id . '_' . $form_id . '_' . sanitize_key( $token ); } /** * Try to load preview data from current request (front-end). * * @return array|null */ protected function get_preview_data_from_request() { if ( null !== $this->current_preview_data ) { return $this->current_preview_data; } // Check query flag. $is_preview = get_query_var( 'wpbc_bfb_preview' ); if ( empty( $is_preview ) && ! isset( $_GET['wpbc_bfb_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return null; } $cap = $this->get_manage_cap(); if ( ! is_user_logged_in() || ! current_user_can( $cap ) ) { return null; } $token = get_query_var( 'wpbc_bfb_preview_token' ); $form_id = absint( get_query_var( 'wpbc_bfb_preview_form_id' ) ); if ( empty( $token ) ) { $token = isset( $_GET['wpbc_bfb_preview_token'] ) ? sanitize_text_field( wp_unslash( $_GET['wpbc_bfb_preview_token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended } if ( empty( $form_id ) ) { $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 } $nonce = isset( $_GET['nonce'] ) ? sanitize_text_field( wp_unslash( $_GET['nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $token ) || empty( $form_id ) ) { return null; } if ( ! wp_verify_nonce( $nonce, 'wpbc_bfb_preview_' . $token ) ) { return null; } $user_id = wpbc_get_current_user_id(); if ( $user_id <= 0 ) { return null; } $transient_key = $this->get_transient_key( $user_id, $token, $form_id ); $data = get_transient( $transient_key ); if ( empty( $data ) ) { return null; } $has_structure = ( ! empty( $data['structure'] ) && is_array( $data['structure'] ) ); $has_advanced = ( ! empty( $data['advanced_form'] ) || ! empty( $data['content_form'] ) ); if ( ! $has_structure && ! $has_advanced ) { return null; } $this->current_preview_data = $data; return $this->current_preview_data; } // ----------------------------------------------------------------------------------------------------------------- /** * Ensure preview page uses a "full width" template (if the current theme provides one). * * WordPress stores selected templates in post meta: _wp_page_template. :contentReference[oaicite:1]{index=1} * * Developers can hard-force a template via: * apply_filters( 'wpbc_bfb_preview_page_template', $template, $page_id ) * * - Classic theme template value: 'templates/full-width.php' * - Block theme template value: 'page-no-title' (template slug) * * @return void */ public function ensure_preview_page_template() { $page_id = (int) $this->get_preview_page_id(); if ( $page_id <= 0 ) { return; } // If already set and valid, do nothing (fast path). $current = (string) get_post_meta( $page_id, '_wp_page_template', true ); if ( $this->is_preview_template_value_valid( $current ) ) { return; } $template = $this->detect_full_width_template_value(); /** * Force/override template for preview page. * * Return values: * - '' or 'default' => keep theme default * - classic: 'templates/full-width.php' * - block: 'page-no-title' (slug) */ $template = apply_filters( 'wpbc_bfb_preview_page_template', $template, $page_id ); $template = (string) $template; $template = trim( $template ); if ( '' === $template || 'default' === $template ) { // Use default template. delete_post_meta( $page_id, '_wp_page_template' ); return; } // Store chosen template. update_post_meta( $page_id, '_wp_page_template', $template ); } /** * Validate existing _wp_page_template meta value for classic themes. * For block themes we can’t reliably "locate" the template file here, so we accept non-empty values. * * @param string $template_value Existing _wp_page_template meta value. * * @return bool */ protected function is_preview_template_value_valid( $template_value ) { $template_value = (string) $template_value; $template_value = trim( $template_value ); if ( '' === $template_value || 'default' === $template_value ) { return false; } // Classic themes: validate that file exists in theme. if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { return true; } $located = locate_template( array( $template_value ), false, false ); return ( ! empty( $located ) ); } /** * Detect a full-width template value for current theme. * * @return string Template value for _wp_page_template, or '' if none found. */ protected function detect_full_width_template_value() { // Block themes: try to find a block template slug. if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && function_exists( 'get_block_templates' ) ) { $slug = $this->detect_block_theme_full_width_slug(); if ( '' !== $slug ) { return $slug; } } // Classic themes: try registered page templates first. $file = $this->detect_classic_theme_full_width_file(); if ( '' !== $file ) { return $file; } return ''; } /** * Try to detect a "full width" template slug in block themes. * * @return string */ protected function detect_block_theme_full_width_slug() { $best_slug = ''; $best_score = 0; $templates = get_block_templates( array( 'post_type' => 'page', ), 'wp_template' ); if ( empty( $templates ) || ! is_array( $templates ) ) { return ''; } foreach ( $templates as $tpl ) { $title = ''; $slug = ''; if ( is_object( $tpl ) ) { $title = isset( $tpl->title ) ? (string) $tpl->title : ''; $slug = isset( $tpl->slug ) ? (string) $tpl->slug : ''; } $score = $this->score_full_width_candidate( $title . ' ' . $slug ); if ( $score > $best_score && '' !== $slug ) { $best_score = $score; $best_slug = $slug; } } return ( $best_score >= 60 ) ? $best_slug : ''; } /** * Try to detect a "full width" template file in classic themes. * * @return string */ protected function detect_classic_theme_full_width_file() { $theme = wp_get_theme(); $templates = $theme->get_page_templates( null, 'page' ); // array( 'Name' => 'file.php' ). $best_file = ''; $best_score = 0; if ( ! empty( $templates ) && is_array( $templates ) ) { foreach ( $templates as $name => $file ) { $score = $this->score_full_width_candidate( $name . ' ' . $file ); if ( $score > $best_score && ! empty( $file ) ) { $best_score = $score; $best_file = (string) $file; } } } // Accept strong match from registered templates. if ( $best_score >= 60 && '' !== $best_file ) { $located = locate_template( array( $best_file ), false, false ); if ( ! empty( $located ) ) { return $best_file; } } // Fallback: common filenames (theme-dependent). $candidates = array( 'page-templates/full-width.php', 'page-templates/fullwidth.php', 'templates/full-width.php', 'templates/fullwidth.php', 'full-width.php', 'fullwidth.php', 'page-no-sidebar.php', 'page-nosidebar.php', ); foreach ( $candidates as $candidate ) { $located = locate_template( array( $candidate ), false, false ); if ( ! empty( $located ) ) { return $candidate; } } return ''; } /** * Score a candidate (template name/slug/file) as "full width". * * @param string $haystack Text to score. * * @return int */ protected function score_full_width_candidate( $haystack ) { $h = strtolower( (string) $haystack ); $s = 0; if ( false !== strpos( $h, 'full' ) && false !== strpos( $h, 'width' ) ) { $s += 100; } if ( false !== strpos( $h, 'no' ) && false !== strpos( $h, 'sidebar' ) ) { $s += 95; } // My theme exception for local test server. if ( ( isset( $_SERVER['HTTP_HOST'] ) && ( 'beta' === sanitize_key( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) ) && ( false !== strpos( $h, 'no-title' ) ) ) { $s += 61; } if ( false !== strpos( $h, 'wide' ) ) { $s += 60; } if ( false !== strpos( $h, 'canvas' ) || false !== strpos( $h, 'blank' ) ) { $s += 40; } if ( false !== strpos( $h, 'elementor' ) && false !== strpos( $h, 'full' ) ) { $s += 30; } return $s; } // ----------------------------------------------------------------------------------------------------------------- /** * Filter "the_content" to inject the real booking form into the preview page. * * Note: * - This is where we run the actual booking shortcode. * - The theme (classic or block) wraps this content as usual. * * @param string $content Original page content. * * @return string */ public function filter_the_content_for_preview( $content ) { if ( ! is_page() ) { return $content; } $page_id = get_the_ID(); $preview_page_id = $this->get_preview_page_id(); if ( $preview_page_id <= 0 || (int) $page_id !== (int) $preview_page_id ) { return $content; } $preview_data = $this->get_preview_data_from_request(); if ( empty( $preview_data ) ) { // No active preview snapshot, show a simple message. return '
' . esc_html__( 'This is a booking form preview page. Please refresh the preview from the Builder.', 'booking' ) . '
'; } // Safety: disable caching for this request. // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } nocache_headers(); // Store preview data for this request so other hooks can access it. $this->current_preview_data = $preview_data; // Optional: expose a filter so BFB structure loader can override structure per preview. // Your wpbc_bfb_get_form_structure() can apply this filter when resolving structure. add_filter( 'wpbc_bfb_get_form_structure', array( $this, 'filter_form_structure_for_preview' ), 10, 2 ); add_filter( 'wpbc_bfb_get_form_advanced_form', array( $this, 'filter_form_advanced_form_for_preview' ), 10, 2 ); add_filter( 'wpbc_bfb_get_form_content_form', array( $this, 'filter_form_content_form_for_preview' ), 10, 2 ); $resource_id = WPBC_FE_Attr_Postprocessor::get_default_booking_resource_id(); $form_name = isset( $preview_data['form_name'] ) ? sanitize_text_field( wp_unslash( $preview_data['form_name'] ) ) : 'standard'; if ( '' === $form_name ) { $form_name = 'standard'; } $shortcode = '[booking resource_id="' . esc_attr( $resource_id ) . '" form_type="' . esc_attr( $form_name ) . '" form_status="preview"]'; $preview_html = do_shortcode( $shortcode ); return $this->filter_wrapped_html_for_preview_form_style( $preview_html, array(), $resource_id, $form_name ); } public function filter_form_advanced_form_for_preview( $advanced_form, $form_id ) { if ( empty( $this->current_preview_data ) ) { return $advanced_form; } if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) { return $advanced_form; } if ( isset( $this->current_preview_data['advanced_form'] ) ) { return (string) $this->current_preview_data['advanced_form']; } return $advanced_form; } public function filter_form_content_form_for_preview( $content_form, $form_id ) { if ( empty( $this->current_preview_data ) ) { return $content_form; } if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) { return $content_form; } if ( isset( $this->current_preview_data['content_form'] ) ) { return (string) $this->current_preview_data['content_form']; } return $content_form; } /** * Filter that allows BFB structure loader to receive snapshot for preview. * * Usage example in your loader: * $structure = apply_filters( 'wpbc_bfb_get_form_structure', $structure, $form_id ); * * @param array $structure Default structure loaded from DB. * @param int $form_id Booking form ID. * * @return array */ public function filter_form_structure_for_preview( $structure, $form_id ) { if ( empty( $this->current_preview_data ) ) { return $structure; } if ( (int) $form_id !== (int) $this->current_preview_data['form_id'] ) { return $structure; } if ( empty( $this->current_preview_data['structure'] ) || ! is_array( $this->current_preview_data['structure'] ) ) { return $structure; } return $this->current_preview_data['structure']; } /** * Apply unsaved global Form Style values to the preview iframe only. * * @param string $wrapped_html Wrapped booking form HTML. * @param array $bfb_settings BFB settings. * @param int $resource_id Booking resource ID. * @param string $custom_booking_form_name Form slug. * @return string */ 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 $preview_style = $this->get_preview_form_style(); if ( empty( $preview_style ) ) { return $wrapped_html; } $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(); $preset = wpbc_bfb_settings__get_form_style_preset( $style ); $wrapped_html = $this->remove_classes_from_first_form_wrapper( $wrapped_html, array( 'wpbc_theme_dark_1', 'wpbc_bfb_form_appearance_custom', ) ); $wrapped_html = $this->remove_classes_from_first_tag_with_classes( $wrapped_html, array( 'wpbc_bfb_form' ), array( 'wpbc_theme_dark_1', 'wpbc_bfb_form_appearance_custom', ) ); $theme_class = isset( $preset['theme_class'] ) ? sanitize_html_class( (string) $preset['theme_class'] ) : ''; if ( '' !== $theme_class ) { $wrapped_html = WPBC_FE_Form_Style_Injector::add_class_to_first_tag_with_classes( $wrapped_html, array( 'wpbc_container', 'wpbc_form' ), $theme_class ); } $css_vars = wpbc_bfb_settings__get_form_style_css_vars( $style, $preview_style ); if ( ! empty( $css_vars ) ) { $wrapped_html = WPBC_FE_Form_Style_Injector::inject_css_vars_into_form_wrapper( $wrapped_html, $css_vars ); $wrapped_html = WPBC_FE_Form_Style_Injector::inject_css_vars_into_bfb_root( $wrapped_html, $css_vars ); } if ( wpbc_bfb_settings__is_custom_form_style( $style ) ) { $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' ); $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' ); } return $wrapped_html; } /** * Get sanitized preview Form Style override from the current transient data. * * @return array */ protected function get_preview_form_style() { if ( empty( $this->current_preview_data['form_style'] ) || ! is_array( $this->current_preview_data['form_style'] ) ) { return array(); } $style = isset( $this->current_preview_data['form_style']['booking_form_style'] ) ? $this->current_preview_data['form_style']['booking_form_style'] : ''; $style = wpbc_bfb_settings__sanitize_form_style( $style ); $custom_options = wpbc_bfb_settings__get_custom_form_style_options( $this->current_preview_data['form_style'] ); return array_merge( array( 'booking_form_style' => $style, ), $custom_options ); } /** * Remove classes from the first outer booking form wrapper. * * @param string $html HTML. * @param array $class_names Class names to remove. * @return string */ protected function remove_classes_from_first_form_wrapper( $html, $class_names ) { return $this->remove_classes_from_first_tag_with_classes( $html, array( 'wpbc_container', 'wpbc_form' ), $class_names ); } /** * Remove classes from the first tag that has all required classes. * * @param string $html HTML. * @param array $required_classes Required classes. * @param array $class_names Class names to remove. * @return string */ protected function remove_classes_from_first_tag_with_classes( $html, $required_classes, $class_names ) { $html = (string) $html; $required_classes = is_array( $required_classes ) ? $required_classes : array(); $class_names = is_array( $class_names ) ? $class_names : array(); $remove_map = array(); $required_map = array(); foreach ( $required_classes as $class_name ) { $class_name = sanitize_html_class( (string) $class_name ); if ( '' !== $class_name ) { $required_map[ $class_name ] = true; } } foreach ( $class_names as $class_name ) { $class_name = sanitize_html_class( (string) $class_name ); if ( '' !== $class_name ) { $remove_map[ $class_name ] = true; } } if ( empty( $remove_map ) || empty( $required_map ) ) { return $html; } $done = false; $out = preg_replace_callback( '/