container + nonce + hidden fields) * - Legacy inline scripts (duplicate-calendar warning, cost hints, autofill) * * @package Booking Calendar * @since 11.0.x * @file ../includes/_front_end/class-fe-render-form-body.php */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Booking Form Body Renderer. * * Orchestrates building of the booking form HTML while preserving legacy output and * filter/action execution order. * * @since 11.0.x */ class WPBC_FE_Form_Body_Renderer { /** * Render booking form HTML (calendar + form body + wrapper + inline scripts), * preserving legacy output and hook execution order. * * Expected flow: * 1) Parse shortcode "options" into custom parameters (for BFB/simple form context). * 2) Build calendar markup OR hidden textarea with preselected dates. * 3) Resolve form body source: * - BFB source (if available) with filter override and shortcode-engine fallback. * - Legacy booking form (personal.php). * - Simple form fallback. * 4) Post-process composed markup: * - Insert calendar ([calendar] placeholder or prepend). * - Replace additional calendars via legacy filter. * - Replace [captcha] placeholder. * - Append change-over times. * 5) Wrap in legacy container + add nonce + hidden fields. * 6) Append legacy inline scripts. * * @since 11.0.x * * @param array $args { * Optional. Arguments for rendering. * * @type int $resource_id Booking resource ID. * @type string $custom_booking_form Booking form slug/name (legacy: "standard"). * @type string $selected_dates_without_calendar Preselected dates string (legacy format). * @type int $cal_count Number of months to show in calendar. * @type mixed $shortcode_param__options Shortcode options string (legacy: options="...") or array. * @type wpdev_booking $legacy_instance Legacy plugin booking object (Phase #1/compat). * } * * @return string Rendered HTML (ready to echo). */ public static function render( $args ) { $defaults = array( 'resource_id' => 1, 'custom_booking_form' => 'standard', 'form_status' => 'published', 'selected_dates_without_calendar' => '', 'cal_count' => 1, 'shortcode_param__options' => '', 'booking_hash' => '', 'legacy_instance' => null, ); $args = wp_parse_args( $args, $defaults ); $resource_id = (int) $args['resource_id']; $custom_booking_form = (string) $args['custom_booking_form']; $selected_dates_without_calendar = (string) $args['selected_dates_without_calendar']; $cal_count = (int) $args['cal_count']; $shortcode_param__options = $args['shortcode_param__options']; $booking_hash = sanitize_text_field( wp_unslash( (string) $args['booking_hash'] ) ); $legacy_instance = $args['legacy_instance']; $form_status = isset( $args['form_status'] ) ? (string) $args['form_status'] : 'published'; $nl = '
'; $custom_params = WPBC_FE_Options_Parser::parse_for_parameter__in_shortcode_options( $shortcode_param__options ); $calendar_html = WPBC_FE_Calendar_Markup::build( $resource_id, $cal_count, $shortcode_param__options, $selected_dates_without_calendar ); $source_res = WPBC_FE_Form_Source::get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash ); $form_html = $source_res['body_html']; // Preserve legacy: apply after-load filter ONLY for BFB and Simple form. if ( ! empty( $source_res['apply_after_load_filter'] ) ) { // Re-update 'Capacity Hints' | 'Steps Timeline shortcode' ! $form_html = apply_filters( 'wpbc_booking_form_content__after_load', $form_html, $resource_id, $custom_booking_form ); } // 1. Body HTML, before you inject calendar/captcha/extra calendars. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ... $form_html = apply_filters( 'wpbc_booking_form__body_html__before_postprocess', $form_html, $source_res['bfb_settings'], $resource_id, $custom_booking_form ); $form_html = WPBC_FE_Form_Postprocessor::apply( $form_html, $calendar_html, array( 'resource_id' => $resource_id, 'custom_booking_form' => $custom_booking_form, 'selected_dates_without_calendar' => $selected_dates_without_calendar, 'cal_count' => $cal_count, 'shortcode_param__options' => $shortcode_param__options, 'custom_params' => $custom_params, 'legacy_instance' => $legacy_instance, 'nl' => $nl, ) ); // Info: Hook for addons. Composed booking form HTML (calendar already inserted). Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ... $form_html = apply_filters( 'wpbc_booking_form__html__before_wrapper', $form_html, $source_res['bfb_settings'], $resource_id, $custom_booking_form ); // 2. Form wraper into $wrapped = WPBC_FE_Form_Wrapper::wrap( $form_html, $resource_id ); // 3. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ... $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__before_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form ); $wrapped .= WPBC_FE_Inline_Scripts::collect( $resource_id ); // Info: Hook for addons. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ... $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__after_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form ); return $wrapped; } } // --------------------------------------------------------------------------------------------------------------------- /** * Calendar markup builder for booking form composition. * * If no preselected dates are provided, returns full calendar markup via legacy generator. * If preselected dates are provided, returns a hidden textarea for legacy JS to consume. * * @since 11.0.x */ class WPBC_FE_Calendar_Markup { /** * Build calendar HTML (real calendar markup or hidden textarea with preselected dates). * * @param int $resource_id * @param int $cal_count * @param mixed $shortcode_param__options * @param string $selected_dates_without_calendar * * @param int $resource_id Booking resource ID. * @param int $cal_count Number of months to show. * @param mixed $shortcode_param__options Shortcode options parameter (legacy). * @param string $selected_dates_without_calendar Preselected dates (legacy string format). * * @return string Calendar HTML snippet. */ public static function build( $resource_id, $cal_count, $shortcode_param__options, $selected_dates_without_calendar ) { $resource_id = (int) $resource_id; $cal_count = (int) $cal_count; $selected_dates_without_calendar = (string) $selected_dates_without_calendar; if ( '' === $selected_dates_without_calendar ) { // Get HTML with [calendar] shortcode and Styles for calendar. Get legend html. //TODO: extract CSS Styles separately !!!! return wpbc_pre_get_calendar_html( $resource_id, $cal_count, $shortcode_param__options ); } return ''; } } /** * Return the booking form body HTML, choosing BFB source if available, else legacy. * Does NOT insert calendar, captcha, additional calendars, wrapper, nonce, etc. * * @since 11.0.x * * @param int $resource_id Booking resource ID. * @param string $custom_booking_form Booking form slug/name. * @param array $custom_params Parsed custom params from shortcode options ("{parameter ...}"). * @param wpdev_booking $legacy_instance Legacy booking object (optional). * * @return array { * Result array. * * @type string $body_html Booking form body HTML. * @type bool $apply_after_load_filter Whether to apply 'wpbc_booking_form_content__after_load'. * } */ class WPBC_FE_Form_Source { /** * Check if the booking editing, and return -> [ 'booking_id': booking_id, 'resource_id': resource_id] otherwise -> false * * @param string $booking_hash__usualy_from_get optional. - hash of the booking. If missed, then system check $_GET['booking_hash']. * * @return array|false */ public static function maybe_check_if_booking_edit( $booking_hash__usualy_from_get = '' ) { if ( empty( $booking_hash__usualy_from_get ) ) { /* phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */ $booking_hash__usualy_from_get = ( ( isset( $_GET['booking_hash'] ) ) ? sanitize_text_field( wp_unslash( $_GET['booking_hash'] ) ) : '' ); } if ( ! empty( $booking_hash__usualy_from_get ) ) { $maybe__booking_id__resource_id = wpbc_hash__get_booking_id__resource_id( $booking_hash__usualy_from_get ); if ( false !== $maybe__booking_id__resource_id ) { $booking_id = intval( $maybe__booking_id__resource_id[0] ); $resource_id = intval( $maybe__booking_id__resource_id[1] ); if ( ( $booking_id > 0 ) && ( $resource_id > 0 ) ) { return array( 'booking_id' => $booking_id, 'resource_id' => $resource_id, ); } } } return false; } /** * Get parsed form data of specific booking. * * @param int $booking_id - ID of booking. * * @return array|array[]|false */ public static function get_booking_data( $booking_id ) { global $wpdb; $separators = array( 'row' => '~', 'col' => '^', ); if ( empty( $booking_id ) ) { return false; } $sql = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}booking as bk INNER JOIN {$wpdb->prefix}bookingdates as dt ON bk.booking_id = dt.booking_id WHERE bk.booking_id = %d ORDER BY dt.booking_date ASC ", $booking_id ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $result = $wpdb->get_results( $sql ); if ( empty( $result ) ) { return false; } $return = array( 'dates' => array() ); foreach ( $result as $res ) { $return['dates'][] = $res->booking_date; } $return['form'] = $res->form; $return['type'] = $res->booking_type; $return['approved'] = $res->approved; $return['id'] = $res->booking_id; // -- Parse data from booking form ---------------------------------------------- $bktype = $res->booking_type; $parsed_form = $res->form; $parsed_form = explode( $separators['row'], $parsed_form ); $parsed_form_results = array(); foreach ( $parsed_form as $field ) { $elemnts = explode( $separators['col'], $field ); if ( count( $elemnts ) < 3 ) { continue; } $type = $elemnts[0]; $element_name = $elemnts[1]; $value = $elemnts[2]; $count_pos = strlen( (string) $bktype ); $type_name = $elemnts[1]; $type_name = str_replace( '[]', '', $type_name ); if ( intval( $bktype ) === intval( substr( $type_name, - 1 * $count_pos ) ) ) { $type_name = substr( $type_name, 0, - 1 * $count_pos ); } if ( ( 'email' === $type_name ) && ( ! isset( $email_adress ) ) ) { $email_adress = $value; } if ( 'name' === $type_name ) { $name_of_person = $value; } if ( 'checkbox' === $type ) { if ( 'true' === $value ) { $value = 'on'; } elseif ( ( empty( $value ) ) || ( 'false' === $value ) || ( 'Off' === $value ) ) { $value = ''; } } $element_name = str_replace( '[]', '', $element_name ); if ( isset( $parsed_form_results[ $element_name ] ) ) { if ( '' !== $value ) { $parsed_form_results[ $element_name ]['value'] .= ',' . $value; } } else { $parsed_form_results[ $element_name ] = array( 'value' => $value, 'type' => $type, 'element_name' => $type_name, ); } } $return['parsed_form'] = $parsed_form_results; if ( isset( $email_adress ) ) { $return['email'] = $email_adress; } if ( isset( $name_of_person ) ) { $return['name'] = $name_of_person; } return $return; } /** * Return the booking form body HTML, choosing BFB source if available, else legacy. * * @param int $resource_id * @param string $custom_booking_form * @param string $form_status 'published'|'preview' * @param array $custom_params * @param wpdev_booking $legacy_instance * @param string $booking_hash * * @return array */ public static function get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash = '' ) { $resource_id = (int) $resource_id; $custom_booking_form = (string) $custom_booking_form; $form_status = (string) $form_status; $custom_params = ( is_array( $custom_params ) ) ? $custom_params : array(); $booking_hash = sanitize_text_field( wp_unslash( (string) $booking_hash ) ); $req = array( 'resource_id' => $resource_id, 'form_slug' => $custom_booking_form, 'form_status' => $form_status, 'custom_params' => $custom_params, 'legacy_instance' => $legacy_instance, ); // Fix: 2026-02-18 16:51. $req['current_resource_id'] = $resource_id; $req['current_edit_booking'] = false; $req['current_edit_booking_id'] = false; // ============================================================================================================= // == If Booking Edit ? == // ============================================================================================================= // Maybe get the "custom booking form" and child "booking resource ID", if the booking edited. And this booking was created in custom booking form. $maybe_edited__booking_id__resource_id = self::maybe_check_if_booking_edit( $booking_hash ); if ( ! empty( $maybe_edited__booking_id__resource_id ) ) { // == Edit Booking ========================================================================================= $req['resource_id'] = $maybe_edited__booking_id__resource_id['resource_id']; $req['current_resource_id'] = $maybe_edited__booking_id__resource_id['resource_id']; $req['current_edit_booking_id'] = $maybe_edited__booking_id__resource_id['booking_id']; // Parsed booking form values. $req['current_edit_booking'] = self::get_booking_data( $maybe_edited__booking_id__resource_id['booking_id'] ); // -- Is use custom form ? --------------------------------------------------------------------------------- // Get 'custom booking form name' from URL. // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $my_booking_form = ( ( isset( $_GET['booking_form'] ) ) ? sanitize_text_field( wp_unslash( $_GET['booking_form'] ) ) : '' ); // Otherwise get 'custom booking form name' from EDIT booking details. if ( ( empty( $my_booking_form ) ) && ( ! empty( $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ] ) ) ) { $my_booking_form = $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ]['value']; } // If not '' then OVVERIDE. if ( ! empty( $my_booking_form ) ) { $req['form_slug'] = $my_booking_form; } // -- Check out dates ? ------------------------------------------------------------------------------------ if ( ( 'On' === get_bk_option( 'booking_last_checkout_day_available' ) ) && ( ! empty( $req['current_edit_booking']['dates'] ) ) ) { // Add one additional date for editing, if "last_checkout_day_available" is "On". // Dates in format: [ ... 2: 2019-03-13 00:00:00, 3: 2019-03-14 12:00:02 ]. $last_day = $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ]; $check_out = strtotime( $last_day ); $check_out = strtotime( '+1 day', $check_out ); $last_day = date_i18n( 'Y-m-d 00:00:00', strtotime( $last_day ) ); $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ] = $last_day; $req['current_edit_booking']['dates'][] = date_i18n( 'Y-m-d H:i:s', $check_out ); } // -- Check out dates ? ------------------------------------------------------------------------------------ // Check when we edit "child resource" -> need re-update ID in calendar and form elements to have parent resource // FixIn: 6.1.1.9. // FixIn: 10.10.1.2 ( ! isset( $_GET['resource_no_update'] ) ) // FixIn: 9.4.2.3. if ( ( function_exists( 'wpbc_is_this_child_resource' ) ) && ( wpbc_is_this_child_resource( $req['resource_id'] ) ) ) { $bk_parent_br_id = wpbc_get_parent_resource( $req['resource_id'] ); $new_booking_data_arr = array(); foreach ( $req['current_edit_booking']['parsed_form'] as $my_key_old => $booking_data ) { $new_booking_data_arr[ $booking_data['element_name'] . $bk_parent_br_id ] = $booking_data; } $req['current_edit_booking']['parsed_form'] = $new_booking_data_arr; $req['resource_id'] = $bk_parent_br_id; $req['current_resource_id'] = $bk_parent_br_id; } } // Update after “edit booking” resource overrides. $resource_id = (int) $req['resource_id']; $custom_booking_form = (string) $req['form_slug']; /** * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple' * 'apply_after_load_filter' : bool * 'bfb_loader_args' : array() * 'fallback_chain' : array() */ $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req ); // ----------------------------------------------------------------- // BFB DB engine (only if resolver decided it). // ----------------------------------------------------------------- if ( ( isset( $resolved['engine'] ) ) && ( 'bfb_db' === $resolved['engine'] ) && function_exists( 'wpbc_bfb_get_booking_form_pair' ) ) { $booking_form_html__arr = self::wpbc_bfb__get_booking_form_html__arr( $req, $resolved ); if ( null !== $booking_form_html__arr ) { return $booking_form_html__arr; } } // ----------------------------------------------------------------- // Legacy engine (unchanged output). // ----------------------------------------------------------------- if ( ( ! empty( $legacy_instance ) ) && ( false !== $legacy_instance->wpdev_bk_personal ) // && ( 'On' != get_bk_option( 'booking_is_use_simple_booking_form' ) ) // Important! in paid versions, if edited in "Simple mode" the compiled form, still saved as "Advanced form" content, so we need to get it from there! ) { return array( 'body_html' => $legacy_instance->wpdev_bk_personal->get_booking_form( $resource_id, $custom_booking_form, $custom_params ), 'apply_after_load_filter' => false, 'bfb_settings' => array(), ); } // ----------------------------------------------------------------- // Simple fallback. - ONLY for the Booking Calendar Free version ! // ----------------------------------------------------------------- return array( 'body_html' => wpbc_simple_form__get_booking_form__as_html( $resource_id, $custom_booking_form, $custom_params ), 'apply_after_load_filter' => true, 'bfb_settings' => array(), ); } /** * Get BFB booking form content array. Helper function. * * @param array $req - array. * @param array $resolved - array. * * @return array|null */ public static function wpbc_bfb__get_booking_form_html__arr( $req, $resolved ) { $custom_params = $req ['custom_params']; $form_status = $req ['form_status']; $custom_booking_form = $req['form_slug']; $resource_id = $req ['resource_id']; $legacy_instance = $req ['legacy_instance']; $bfb_loader_args = ( isset( $resolved['bfb_loader_args'] ) && is_array( $resolved['bfb_loader_args'] ) ) ? $resolved['bfb_loader_args'] : array(); // Keep old behavior: allow explicit form_id/status overrides from options only if present. if ( ! empty( $custom_params['bfb_form_id'] ) && empty( $bfb_loader_args['form_id'] ) ) { $bfb_loader_args['form_id'] = (int) $custom_params['bfb_form_id']; } $bfb_settings = array(); $bfb_source = trim( '' ); $settings_json = trim( '' ); $bfb_pair = wpbc_bfb_get_booking_form_pair( $bfb_loader_args ); if ( is_array( $bfb_pair ) ) { $bfb_source = isset( $bfb_pair['form'] ) ? (string) $bfb_pair['form'] : ''; $settings_json = isset( $bfb_pair['settings_json'] ) ? (string) $bfb_pair['settings_json'] : ''; } if ( '' !== $settings_json ) { $decoded = json_decode( $settings_json, true ); if ( is_array( $decoded ) ) { $bfb_settings = $decoded; } } // Allow loader to return either string OR array with settings. if ( is_array( $bfb_source ) ) { // Common possible keys (support multiple formats, future-proof). if ( ! empty( $bfb_source['settings'] ) && is_array( $bfb_source['settings'] ) ) { $bfb_settings = $bfb_source['settings']; } elseif ( ! empty( $bfb_source['settings_json'] ) && is_string( $bfb_source['settings_json'] ) ) { $decoded = json_decode( $bfb_source['settings_json'], true ); if ( is_array( $decoded ) ) { $bfb_settings = $decoded; } } // Source itself. if ( isset( $bfb_source['advanced_form'] ) ) { $bfb_source = (string) $bfb_source['advanced_form']; } elseif ( isset( $bfb_source['source'] ) ) { $bfb_source = (string) $bfb_source['source']; } else { $bfb_source = ''; } } // Optional fallback hook if you keep loader returning string for now. if ( empty( $bfb_settings ) ) { $bfb_settings = apply_filters( 'wpbc_bfb_form_settings_for_render', array(), $bfb_loader_args, $resource_id, $custom_booking_form, $custom_params ); } if ( '' !== trim( (string) $bfb_source ) ) { // Info: Hook for addons. Give addons a chance to fully handle rendering. $bfb_form_html = apply_filters( 'wpbc_bfb_render_booking_form_source', '', $bfb_source, $resource_id, $custom_booking_form, $custom_params ); if ( '' === $bfb_form_html ) { $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) ); $bfb_source = wpbc_lang( $bfb_source ); // Replace custom params in the source (legacy behavior). if ( ! empty( $custom_params ) ) { foreach ( $custom_params as $custom_params_key => $custom_params_value ) { $bfb_source = str_replace( $custom_params_key, $custom_params_value, $bfb_source ); } } $bfb_source = wpbc_bf__replace_custom_html_shortcodes( $bfb_source ); $bfb_form_html = wpbc_render_booking_form_shortcodes( $bfb_source, $render_args, $req ); } if ( '' !== $bfb_form_html ) { return array( 'body_html' => $bfb_form_html, 'apply_after_load_filter' => ! empty( $resolved['apply_after_load_filter'] ), 'bfb_settings' => $bfb_settings, ); } } // If BFB resolution succeeded but returned empty output, fall through to legacy. return null; } } /** * Booking form post-processor (legacy compatibility). * * Applies legacy transformations and hooks to the combined form markup: * - Inserts calendar markup (replaces [calendar] or prepends). * - Replaces additional calendars via legacy filter. * - Replaces [captcha] placeholder if present. * - Appends change-over times. * * @since 11.0.x */ class WPBC_FE_Form_Postprocessor { /** * Apply legacy postprocessing to the composed form markup. * * @since 11.0.x * * @param string $form_html Booking form body HTML. * @param string $calendar_html Calendar HTML snippet. * @param array $ctx { * Context data used during post-processing. * * @type int $resource_id Booking resource ID. * @type string $custom_booking_form Booking form slug/name. * @type string $selected_dates_without_calendar Preselected dates (legacy string). * @type int $cal_count Number of months shown. * @type mixed $shortcode_param__options Shortcode options. * @type wpdev_booking $legacy_instance Legacy booking instance (optional). * @type string $nl Legacy separator markup. * } * * @return string Post-processed HTML. */ public static function apply( $form_html, $calendar_html, $ctx ) { $form_html = (string) $form_html; $calendar_html = (string) $calendar_html; $resource_id = (int) $ctx['resource_id']; $custom_booking_form = (string) $ctx['custom_booking_form']; $selected_dates_without_calendar = (string) $ctx['selected_dates_without_calendar']; $cal_count = (int) $ctx['cal_count']; $shortcode_param__options = $ctx['shortcode_param__options']; $legacy_instance = $ctx['legacy_instance']; $nl = (string) $ctx['nl']; // Insert calendar into form. if ( false !== strpos( $form_html, '[calendar]' ) ) { $form_html = str_replace( '[calendar]', $calendar_html, $form_html ); } else { // FixIn: 2981-01-13 13 Jan 2981. //$form_html = '