PluginProbe
Booking Calendar / 11.8.4
Booking Calendar v11.8.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
← All changes | includes/_front_end/class-fe-render-form-body.php +253 -117 10.15.611.8.4 View file →
@@ -1,12 +1,12 @@
1 1 <?php
2 2 /**
3 3 * Front-End Form Body Rendering
4 4 *
5 5 * - Calendar markup (or hidden selected-dates field)
6 - * - Form body source resolution (BFB / legacy / simple)
6 + * - Form body source resolution (BFB)
7 7 * - Legacy post-processing hooks (additional calendars, captcha, change-over times)
8 - * - Legacy wrapper (<form> container + nonce + hidden fields)
8 + * - Shared wrapper (<form> container + nonce + hidden fields)
9 9 * - Legacy inline scripts (duplicate-calendar warning, cost hints, autofill)
10 10 *
11 11 * @package Booking Calendar
12 12 * @since 11.0.x
@@ -19,9 +19,9 @@
19 19
20 20 /**
21 21 * Booking Form Body Renderer.
22 22 *
23 - * Orchestrates building of the booking form HTML while preserving legacy output and
23 + * Orchestrates building of the booking form HTML while preserving output and
24 24 * filter/action execution order.
25 25 *
26 26 * @since 11.0.x
27 27 */
@@ -34,11 +34,9 @@
34 34 * Expected flow:
35 35 * 1) Parse shortcode "options" into custom parameters (for BFB/simple form context).
36 36 * 2) Build calendar markup OR hidden textarea with preselected dates.
37 37 * 3) Resolve form body source:
38 - * - BFB source (if available) with filter override and shortcode-engine fallback.
39 - * - Legacy booking form (personal.php).
40 - * - Simple form fallback.
38 + * - BFB source with filter override and shortcode-engine fallback.
41 39 * 4) Post-process composed markup:
42 40 * - Insert calendar ([calendar] placeholder or prepend).
43 41 * - Replace additional calendars via legacy filter.
44 42 * - Replace [captcha] placeholder.
@@ -69,8 +67,9 @@
69 67 'form_status' => 'published',
70 68 'selected_dates_without_calendar' => '',
71 69 'cal_count' => 1,
72 70 'shortcode_param__options' => '',
71 + 'booking_hash' => '',
73 72 'legacy_instance' => null,
74 73 );
75 74
76 75 $args = wp_parse_args( $args, $defaults );
@@ -79,8 +78,9 @@
79 78 $custom_booking_form = (string) $args['custom_booking_form'];
80 79 $selected_dates_without_calendar = (string) $args['selected_dates_without_calendar'];
81 80 $cal_count = (int) $args['cal_count'];
82 81 $shortcode_param__options = $args['shortcode_param__options'];
82 + $booking_hash = sanitize_text_field( wp_unslash( (string) $args['booking_hash'] ) );
83 83 $legacy_instance = $args['legacy_instance'];
84 84 $form_status = isset( $args['form_status'] ) ? (string) $args['form_status'] : 'published';
85 85
86 86 $nl = '<div style="clear:both;height:10px;"></div>';
@@ -88,9 +88,9 @@
88 88 $custom_params = WPBC_FE_Options_Parser::parse_for_parameter__in_shortcode_options( $shortcode_param__options );
89 89
90 90 $calendar_html = WPBC_FE_Calendar_Markup::build( $resource_id, $cal_count, $shortcode_param__options, $selected_dates_without_calendar );
91 91
92 - $source_res = WPBC_FE_Form_Source::get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance );
92 + $source_res = WPBC_FE_Form_Source::get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash );
93 93
94 94 $form_html = $source_res['body_html'];
95 95
96 96 // Preserve legacy: apply after-load filter ONLY for BFB and Simple form.
@@ -337,9 +337,9 @@
337 337 }
338 338
339 339
340 340 /**
341 - * Return the booking form body HTML, choosing BFB source if available, else legacy.
341 + * Return the booking form body HTML from BFB storage.
342 342 *
343 343 * @param int $resource_id
344 344 * @param string $custom_booking_form
345 345 * @param string $form_status 'published'|'preview'
@@ -344,17 +344,19 @@
344 344 * @param string $custom_booking_form
345 345 * @param string $form_status 'published'|'preview'
346 346 * @param array $custom_params
347 347 * @param wpdev_booking $legacy_instance
348 + * @param string $booking_hash
348 349 *
349 350 * @return array
350 351 */
351 - public static function get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance ) {
352 + public static function get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash = '' ) {
352 353
353 354 $resource_id = (int) $resource_id;
354 355 $custom_booking_form = (string) $custom_booking_form;
355 356 $form_status = (string) $form_status;
356 357 $custom_params = ( is_array( $custom_params ) ) ? $custom_params : array();
358 + $booking_hash = sanitize_text_field( wp_unslash( (string) $booking_hash ) );
357 359
358 360 $req = array(
359 361 'resource_id' => $resource_id,
360 362 'form_slug' => $custom_booking_form,
@@ -370,9 +372,9 @@
370 372 // =============================================================================================================
371 373 // == If Booking Edit ? ==
372 374 // =============================================================================================================
373 375 // Maybe get the "custom booking form" and child "booking resource ID", if the booking edited. And this booking was created in custom booking form.
374 - $maybe_edited__booking_id__resource_id = self::maybe_check_if_booking_edit();
376 + $maybe_edited__booking_id__resource_id = self::maybe_check_if_booking_edit( $booking_hash );
375 377
376 378 if ( ! empty( $maybe_edited__booking_id__resource_id ) ) {
377 379
378 380 // == Edit Booking =========================================================================================
@@ -443,9 +445,9 @@
443 445 $resource_id = (int) $req['resource_id'];
444 446 $custom_booking_form = (string) $req['form_slug'];
445 447
446 448 /**
447 - * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple'
449 + * $resolved = array( 'engine' : 'bfb_db'|'bfb_missing'
448 450 * 'apply_after_load_filter' : bool
449 451 * 'bfb_loader_args' : array()
450 452 * 'fallback_chain' : array()
451 453 */
@@ -462,33 +464,34 @@
462 464 return $booking_form_html__arr;
463 465 }
464 466 }
465 467
466 - // -----------------------------------------------------------------
467 - // Legacy engine (unchanged output).
468 - // -----------------------------------------------------------------
469 - if ( ( ! empty( $legacy_instance ) )
470 - && ( false !== $legacy_instance->wpdev_bk_personal )
471 - // && ( '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!
472 - ) {
473 - return array(
474 - 'body_html' => $legacy_instance->wpdev_bk_personal->get_booking_form( $resource_id, $custom_booking_form, $custom_params ),
475 - 'apply_after_load_filter' => false,
476 - 'bfb_settings' => array(),
477 - );
478 - }
468 + return array(
469 + 'body_html' => self::get_missing_bfb_form_html( $resolved ),
470 + 'apply_after_load_filter' => false,
471 + 'bfb_settings' => array(),
472 + );
473 + }
474 +
475 + /**
476 + * Render a controlled empty state when no BFB form row exists.
477 + *
478 + * @param array $resolved Resolver result.
479 + *
480 + * @return string
481 + */
482 + private static function get_missing_bfb_form_html( $resolved ) {
483 +
484 + $reason = ( is_array( $resolved ) && ! empty( $resolved['reason'] ) ) ? (string) $resolved['reason'] : 'bfb_form_not_found';
485 +
486 + if ( is_admin() || current_user_can( 'manage_options' ) ) {
487 + return '<div class="wpbc_bfb_missing_form wpbc_alert_warning">' . esc_html__( 'Booking form configuration was not found. Please open the Booking Form Builder and save the form once.', 'booking' ) . ' <span class="wpbc_bfb_missing_reason">' . esc_html( $reason ) . '</span></div>';
488 + }
489 +
490 + return '<div class="wpbc_bfb_missing_form" style="display:none;"></div>';
491 + }
479 492
480 - // -----------------------------------------------------------------
481 - // Simple fallback. - ONLY for the Booking Calendar Free version !
482 - // -----------------------------------------------------------------
483 - return array(
484 - 'body_html' => wpbc_simple_form__get_booking_form__as_html( $resource_id, $custom_booking_form, $custom_params ),
485 - 'apply_after_load_filter' => true,
486 - 'bfb_settings' => array(),
487 - );
488 - }
489 493
490 -
491 494 /**
492 495 * Get BFB booking form content array. Helper function.
493 496 *
494 497 * @param array $req - array.
@@ -567,9 +570,17 @@
567 570 if ( '' === $bfb_form_html ) {
568 571
569 572 $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) );
570 573
571 - $bfb_source = wpbc_lang( $bfb_source );
574 + /*
575 + * Do not pass the complete Builder form through wpbc_lang(). Builder
576 + * labels and option values may contain legacy [lang=LOCALE] markers.
577 + * Parsing one of those inline markers as a whole-form language block
578 + * truncates everything before/after the matching field and can leave an
579 + * incomplete form on the front end. BFB multilingual forms are separate
580 + * custom forms and are selected by their form slug.
581 + */
582 + $bfb_source = (string) $bfb_source;
572 583
573 584 // Replace custom params in the source (legacy behavior).
574 585 if ( ! empty( $custom_params ) ) {
575 586 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
@@ -701,15 +712,18 @@
701 712 * @return string Wrapped HTML.
702 713 */
703 714 public static function wrap( $form_html, $resource_id ) {
704 715
705 - $resource_id = (int) $resource_id;
706 - $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
716 + $resource_id = (int) $resource_id;
717 + $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
718 +
719 + $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
720 + $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
721 + $form_style_preset = function_exists( 'wpbc_bfb_settings__get_form_style_preset' )
722 + ? wpbc_bfb_settings__get_form_style_preset( wpbc_bfb_settings__get_current_form_style() )
723 + : array();
724 + $booking_form_theme = isset( $form_style_preset['theme_class'] ) ? sanitize_html_class( $form_style_preset['theme_class'] ) : '';
707 725
708 - $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
709 - $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
710 - $booking_form_theme = get_bk_option( 'booking_form_theme' );
711 -
712 726 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
713 727 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
714 728 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
715 729
@@ -763,9 +777,15 @@
763 777 // == Autofill (legacy conditions preserved) ==
764 778 $autofill_js = self::build_autofill_js_body( $resource_id );
765 779 if ( '' !== $autofill_js ) {
766 780 $assets_key = 'wpbc:autofill:' . intval( $resource_id );
767 - $added = WPBC_FE_Assets::add_jq_ready_js_to_wp_script( 'wpbc_all', $autofill_js, $assets_key );
781 + $added = false;
782 + if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
783 + $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key . ':ajax' );
784 + $added = true;
785 + } else {
786 + $added = WPBC_FE_Assets::add_jq_ready_js_to_wp_script( 'wpbc_all', $autofill_js, $assets_key );
787 + }
768 788 if ( ! $added ) {
769 789 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key ); // Fallback to direct inline JS, if previosly not edded script.
770 790 }
771 791 }
@@ -832,72 +852,82 @@
832 852 * @param array $css_vars Map: '--var-name' => 'value'
833 853 *
834 854 * @return string
835 855 */
836 - public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
856 + public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
857 +
858 + return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_bfb_form' ) );
859 + }
860 +
861 + /**
862 + * Inject CSS variables into the FIRST outer booking form wrapper.
863 + *
864 + * @param string $html
865 + * @param array $css_vars
866 + *
867 + * @return string
868 + */
869 + public static function inject_css_vars_into_form_wrapper( $html, $css_vars ) {
870 +
871 + return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_container', 'wpbc_form' ) );
872 + }
873 +
874 + /**
875 + * Add a class to the FIRST <div> with all required class tokens.
876 + *
877 + * @param string $html
878 + * @param array $required_classes
879 + * @param string $class_name
880 + *
881 + * @return string
882 + */
883 + public static function add_class_to_first_tag_with_classes( $html, $required_classes, $class_name ) {
884 +
885 + $html = (string) $html;
886 + $required_classes = is_array( $required_classes ) ? $required_classes : array();
887 + $class_name = sanitize_html_class( (string) $class_name );
888 +
889 + if ( '' === $class_name || empty( $required_classes ) ) {
890 + return $html;
891 + }
892 +
893 + $pattern = self::get_first_div_with_classes_pattern( $required_classes );
894 + if ( '' === $pattern ) {
895 + return $html;
896 + }
897 +
898 + $done = false;
899 + $result = preg_replace_callback(
900 + $pattern,
901 + function( $m ) use ( $required_classes, $class_name, &$done ) {
902 +
903 + $tag = $m[0];
904 +
905 + if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
906 + return $tag;
907 + }
908 +
909 + if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', $tag, $cm ) ) {
910 + return $tag;
911 + }
912 +
913 + $quote = $cm[1];
914 + $classes = trim( preg_replace( '/\s+/', ' ', (string) $cm[2] ) );
915 +
916 + if ( false === strpos( ' ' . $classes . ' ', ' ' . $class_name . ' ' ) ) {
917 + $classes = trim( $classes . ' ' . $class_name );
918 + }
919 +
920 + $done = true;
921 +
922 + return preg_replace( '/\bclass\s*=\s*(["\'])(.*?)\1/i', 'class=' . $quote . esc_attr( $classes ) . $quote, $tag, 1 );
923 + },
924 + $html
925 + );
926 +
927 + return ( null === $result ) ? $html : $result;
928 + }
837 929
838 - $html = (string) $html;
839 - $css_vars = is_array( $css_vars ) ? $css_vars : array();
840 -
841 - if ( empty( $css_vars ) ) {
842 - return $html;
843 - }
844 - if ( false === strpos( $html, 'wpbc_bfb_form' ) ) {
845 - return $html;
846 - }
847 -
848 - $style_append = self::build_css_vars_style_fragment( $css_vars );
849 - if ( '' === $style_append ) {
850 - return $html;
851 - }
852 -
853 - // Match FIRST <div ... class="... wpbc_bfb_form ..."> tag.
854 - $pattern = '/<div\b[^>]*\bclass\s*=\s*(["\'])(?:(?!\1).)*\bwpbc_bfb_form\b(?:(?!\1).)*\1[^>]*>/i';
855 -
856 - $result = preg_replace_callback(
857 - $pattern,
858 - function( $m ) use ( $style_append ) {
859 -
860 - $tag = $m[0];
861 -
862 - // If style already exists: append.
863 - if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
864 -
865 - $quote = $sm[1];
866 - $existing = (string) $sm[2];
867 -
868 - // Avoid double-escaping if the tag already contains entities.
869 - $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
870 -
871 - $merged = trim( $existing );
872 - if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
873 - $merged .= ';';
874 - }
875 - $merged .= $style_append;
876 -
877 - $tag = preg_replace(
878 - '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
879 - 'style=' . $quote . esc_attr( $merged ) . $quote,
880 - $tag,
881 - 1
882 - );
883 -
884 - return $tag;
885 - }
886 -
887 - // No style attr: add it.
888 - $tag = rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
889 -
890 - return $tag;
891 - },
892 - $html,
893 - 1
894 - );
895 -
896 - // preg_replace_callback can return null on regex error.
897 - return ( null === $result ) ? $html : $result;
898 - }
899 -
900 930 /**
901 931 * Build CSS vars fragment: "--a:1;--b:2;"
902 932 *
903 933 * @param array $css_vars
@@ -903,9 +933,9 @@
903 933 * @param array $css_vars
904 934 *
905 935 * @return string
906 936 */
907 - private static function build_css_vars_style_fragment( $css_vars ) {
937 + private static function build_css_vars_style_fragment( $css_vars ) {
908 938
909 939 $out = '';
910 940
911 941 foreach ( $css_vars as $name => $value ) {
@@ -935,17 +965,17 @@
935 965 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
936 966 return '';
937 967 }
938 968
939 - // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
940 - $lower = strtolower( $name );
941 - if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) ) {
942 - return '';
943 - }
969 + // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
970 + $lower = strtolower( $name );
971 + if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) && 0 !== strpos( $lower, '--wpbc_form-' ) ) {
972 + return '';
973 + }
974 +
975 + return $name;
976 + }
944 977
945 - return $name;
946 - }
947 -
948 978 private static function sanitize_css_var_value( $value ) {
949 979
950 980 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
951 981 if ( '' === $value ) {
@@ -964,7 +994,113 @@
964 994 '',
965 995 $value
966 996 );
967 997
968 - return trim( $value );
969 - }
970 -}
998 + return trim( $value );
999 + }
1000 +
1001 + private static function inject_css_vars_into_first_tag_with_classes( $html, $css_vars, $required_classes ) {
1002 +
1003 + $html = (string) $html;
1004 + $css_vars = is_array( $css_vars ) ? $css_vars : array();
1005 +
1006 + if ( empty( $css_vars ) ) {
1007 + return $html;
1008 + }
1009 +
1010 + $style_append = self::build_css_vars_style_fragment( $css_vars );
1011 + if ( '' === $style_append ) {
1012 + return $html;
1013 + }
1014 +
1015 + $pattern = self::get_first_div_with_classes_pattern( $required_classes );
1016 + if ( '' === $pattern ) {
1017 + return $html;
1018 + }
1019 +
1020 + $done = false;
1021 + $result = preg_replace_callback(
1022 + $pattern,
1023 + function( $m ) use ( $required_classes, $style_append, &$done ) {
1024 +
1025 + $tag = $m[0];
1026 +
1027 + if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
1028 + return $tag;
1029 + }
1030 +
1031 + // If style already exists: append.
1032 + if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
1033 +
1034 + $quote = $sm[1];
1035 + $existing = (string) $sm[2];
1036 +
1037 + // Avoid double-escaping if the tag already contains entities.
1038 + $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
1039 +
1040 + $merged = trim( $existing );
1041 + if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
1042 + $merged .= ';';
1043 + }
1044 + $merged .= $style_append;
1045 +
1046 + $done = true;
1047 +
1048 + return preg_replace(
1049 + '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
1050 + 'style=' . $quote . esc_attr( $merged ) . $quote,
1051 + $tag,
1052 + 1
1053 + );
1054 + }
1055 +
1056 + // No style attr: add it.
1057 + $done = true;
1058 + return rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
1059 + },
1060 + $html
1061 + );
1062 +
1063 + return ( null === $result ) ? $html : $result;
1064 + }
1065 +
1066 + private static function get_first_div_with_classes_pattern( $required_classes ) {
1067 +
1068 + $required_classes = is_array( $required_classes ) ? $required_classes : array();
1069 + $has_class = false;
1070 +
1071 + foreach ( $required_classes as $class_name ) {
1072 + $class_name = sanitize_html_class( (string) $class_name );
1073 + if ( '' === $class_name ) {
1074 + continue;
1075 + }
1076 + $has_class = true;
1077 + }
1078 +
1079 + if ( ! $has_class ) {
1080 + return '';
1081 + }
1082 +
1083 + return '/<div\b[^>]*\bclass\s*=\s*(["\'])(.*?)\1[^>]*>/i';
1084 + }
1085 +
1086 + private static function tag_has_required_classes( $tag, $required_classes ) {
1087 +
1088 + if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', (string) $tag, $cm ) ) {
1089 + return false;
1090 + }
1091 +
1092 + $classes = ' ' . preg_replace( '/\s+/', ' ', (string) $cm[2] ) . ' ';
1093 +
1094 + foreach ( (array) $required_classes as $class_name ) {
1095 + $class_name = sanitize_html_class( (string) $class_name );
1096 + if ( '' === $class_name ) {
1097 + continue;
1098 + }
1099 + if ( false === strpos( $classes, ' ' . $class_name . ' ' ) ) {
1100 + return false;
1101 + }
1102 + }
1103 +
1104 + return true;
1105 + }
1106 +}