PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 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 All 205 releases
← All changes | includes/_front_end/class-fe-render-form-body.php +266 -114 11.2 → 11.9 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.
@@ -123,9 +121,35 @@
123 121
124 122 // 3. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ...
125 123 $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__before_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
126 124
127 - $wrapped .= WPBC_FE_Inline_Scripts::collect( $resource_id );
125 + /**
126 + * Filters whether the current booking form render should collect legacy
127 + * page-level inline scripts.
128 + *
129 + * Request-local administration previews initialize their returned markup
130 + * through an explicit bootstrap payload. They must not queue callbacks on
131 + * the parent administration page because the preview form can be replaced
132 + * before those callbacks run.
133 + *
134 + * @since 11.9.0
135 + *
136 + * @param bool $should_collect Whether legacy inline scripts should be collected.
137 + * @param int $resource_id Booking resource ID used by the rendered form.
138 + * @param string $custom_booking_form Booking form slug requested by the renderer.
139 + * @param string $form_status Normalized renderer status such as published or preview.
140 + */
141 + $should_collect_inline_scripts = (bool) apply_filters(
142 + 'wpbc_booking_form__should_collect_inline_scripts',
143 + true,
144 + $resource_id,
145 + $custom_booking_form,
146 + $form_status
147 + );
148 +
149 + if ( $should_collect_inline_scripts ) {
150 + $wrapped .= WPBC_FE_Inline_Scripts::collect( $resource_id );
151 + }
128 152
129 153 // Info: Hook for addons. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ...
130 154 $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__after_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
131 155
@@ -339,9 +363,9 @@
339 363 }
340 364
341 365
342 366 /**
343 - * Return the booking form body HTML, choosing BFB source if available, else legacy.
367 + * Return the booking form body HTML from BFB storage.
344 368 *
345 369 * @param int $resource_id
346 370 * @param string $custom_booking_form
347 371 * @param string $form_status 'published'|'preview'
@@ -447,9 +471,9 @@
447 471 $resource_id = (int) $req['resource_id'];
448 472 $custom_booking_form = (string) $req['form_slug'];
449 473
450 474 /**
451 - * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple'
475 + * $resolved = array( 'engine' : 'bfb_db'|'bfb_missing'
452 476 * 'apply_after_load_filter' : bool
453 477 * 'bfb_loader_args' : array()
454 478 * 'fallback_chain' : array()
455 479 */
@@ -466,33 +490,34 @@
466 490 return $booking_form_html__arr;
467 491 }
468 492 }
469 493
470 - // -----------------------------------------------------------------
471 - // Legacy engine (unchanged output).
472 - // -----------------------------------------------------------------
473 - if ( ( ! empty( $legacy_instance ) )
474 - && ( false !== $legacy_instance->wpdev_bk_personal )
475 - // && ( '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!
476 - ) {
477 - return array(
478 - 'body_html' => $legacy_instance->wpdev_bk_personal->get_booking_form( $resource_id, $custom_booking_form, $custom_params ),
479 - 'apply_after_load_filter' => false,
480 - 'bfb_settings' => array(),
481 - );
482 - }
494 + return array(
495 + 'body_html' => self::get_missing_bfb_form_html( $resolved ),
496 + 'apply_after_load_filter' => false,
497 + 'bfb_settings' => array(),
498 + );
499 + }
500 +
501 + /**
502 + * Render a controlled empty state when no BFB form row exists.
503 + *
504 + * @param array $resolved Resolver result.
505 + *
506 + * @return string
507 + */
508 + private static function get_missing_bfb_form_html( $resolved ) {
509 +
510 + $reason = ( is_array( $resolved ) && ! empty( $resolved['reason'] ) ) ? (string) $resolved['reason'] : 'bfb_form_not_found';
511 +
512 + if ( is_admin() || current_user_can( 'manage_options' ) ) {
513 + 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>';
514 + }
515 +
516 + return '<div class="wpbc_bfb_missing_form" style="display:none;"></div>';
517 + }
483 518
484 - // -----------------------------------------------------------------
485 - // Simple fallback. - ONLY for the Booking Calendar Free version !
486 - // -----------------------------------------------------------------
487 - return array(
488 - 'body_html' => wpbc_simple_form__get_booking_form__as_html( $resource_id, $custom_booking_form, $custom_params ),
489 - 'apply_after_load_filter' => true,
490 - 'bfb_settings' => array(),
491 - );
492 - }
493 519
494 -
495 520 /**
496 521 * Get BFB booking form content array. Helper function.
497 522 *
498 523 * @param array $req - array.
@@ -571,9 +596,17 @@
571 596 if ( '' === $bfb_form_html ) {
572 597
573 598 $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) );
574 599
575 - $bfb_source = wpbc_lang( $bfb_source );
600 + /*
601 + * Do not pass the complete Builder form through wpbc_lang(). Builder
602 + * labels and option values may contain legacy [lang=LOCALE] markers.
603 + * Parsing one of those inline markers as a whole-form language block
604 + * truncates everything before/after the matching field and can leave an
605 + * incomplete form on the front end. BFB multilingual forms are separate
606 + * custom forms and are selected by their form slug.
607 + */
608 + $bfb_source = (string) $bfb_source;
576 609
577 610 // Replace custom params in the source (legacy behavior).
578 611 if ( ! empty( $custom_params ) ) {
579 612 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
@@ -705,15 +738,18 @@
705 738 * @return string Wrapped HTML.
706 739 */
707 740 public static function wrap( $form_html, $resource_id ) {
708 741
709 - $resource_id = (int) $resource_id;
710 - $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
742 + $resource_id = (int) $resource_id;
743 + $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
744 +
745 + $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
746 + $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
747 + $form_style_preset = function_exists( 'wpbc_bfb_settings__get_form_style_preset' )
748 + ? wpbc_bfb_settings__get_form_style_preset( wpbc_bfb_settings__get_current_form_style() )
749 + : array();
750 + $booking_form_theme = isset( $form_style_preset['theme_class'] ) ? sanitize_html_class( $form_style_preset['theme_class'] ) : '';
711 751
712 - $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
713 - $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
714 - $booking_form_theme = get_bk_option( 'booking_form_theme' );
715 -
716 752 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
717 753 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
718 754 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
719 755
@@ -842,72 +878,82 @@
842 878 * @param array $css_vars Map: '--var-name' => 'value'
843 879 *
844 880 * @return string
845 881 */
846 - public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
882 + public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
883 +
884 + return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_bfb_form' ) );
885 + }
886 +
887 + /**
888 + * Inject CSS variables into the FIRST outer booking form wrapper.
889 + *
890 + * @param string $html
891 + * @param array $css_vars
892 + *
893 + * @return string
894 + */
895 + public static function inject_css_vars_into_form_wrapper( $html, $css_vars ) {
896 +
897 + return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_container', 'wpbc_form' ) );
898 + }
899 +
900 + /**
901 + * Add a class to the FIRST <div> with all required class tokens.
902 + *
903 + * @param string $html
904 + * @param array $required_classes
905 + * @param string $class_name
906 + *
907 + * @return string
908 + */
909 + public static function add_class_to_first_tag_with_classes( $html, $required_classes, $class_name ) {
910 +
911 + $html = (string) $html;
912 + $required_classes = is_array( $required_classes ) ? $required_classes : array();
913 + $class_name = sanitize_html_class( (string) $class_name );
914 +
915 + if ( '' === $class_name || empty( $required_classes ) ) {
916 + return $html;
917 + }
918 +
919 + $pattern = self::get_first_div_with_classes_pattern( $required_classes );
920 + if ( '' === $pattern ) {
921 + return $html;
922 + }
923 +
924 + $done = false;
925 + $result = preg_replace_callback(
926 + $pattern,
927 + function( $m ) use ( $required_classes, $class_name, &$done ) {
928 +
929 + $tag = $m[0];
930 +
931 + if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
932 + return $tag;
933 + }
934 +
935 + if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', $tag, $cm ) ) {
936 + return $tag;
937 + }
938 +
939 + $quote = $cm[1];
940 + $classes = trim( preg_replace( '/\s+/', ' ', (string) $cm[2] ) );
941 +
942 + if ( false === strpos( ' ' . $classes . ' ', ' ' . $class_name . ' ' ) ) {
943 + $classes = trim( $classes . ' ' . $class_name );
944 + }
945 +
946 + $done = true;
947 +
948 + return preg_replace( '/\bclass\s*=\s*(["\'])(.*?)\1/i', 'class=' . $quote . esc_attr( $classes ) . $quote, $tag, 1 );
949 + },
950 + $html
951 + );
952 +
953 + return ( null === $result ) ? $html : $result;
954 + }
847 955
848 - $html = (string) $html;
849 - $css_vars = is_array( $css_vars ) ? $css_vars : array();
850 -
851 - if ( empty( $css_vars ) ) {
852 - return $html;
853 - }
854 - if ( false === strpos( $html, 'wpbc_bfb_form' ) ) {
855 - return $html;
856 - }
857 -
858 - $style_append = self::build_css_vars_style_fragment( $css_vars );
859 - if ( '' === $style_append ) {
860 - return $html;
861 - }
862 -
863 - // Match FIRST <div ... class="... wpbc_bfb_form ..."> tag.
864 - $pattern = '/<div\b[^>]*\bclass\s*=\s*(["\'])(?:(?!\1).)*\bwpbc_bfb_form\b(?:(?!\1).)*\1[^>]*>/i';
865 -
866 - $result = preg_replace_callback(
867 - $pattern,
868 - function( $m ) use ( $style_append ) {
869 -
870 - $tag = $m[0];
871 -
872 - // If style already exists: append.
873 - if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
874 -
875 - $quote = $sm[1];
876 - $existing = (string) $sm[2];
877 -
878 - // Avoid double-escaping if the tag already contains entities.
879 - $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
880 -
881 - $merged = trim( $existing );
882 - if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
883 - $merged .= ';';
884 - }
885 - $merged .= $style_append;
886 -
887 - $tag = preg_replace(
888 - '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
889 - 'style=' . $quote . esc_attr( $merged ) . $quote,
890 - $tag,
891 - 1
892 - );
893 -
894 - return $tag;
895 - }
896 -
897 - // No style attr: add it.
898 - $tag = rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
899 -
900 - return $tag;
901 - },
902 - $html,
903 - 1
904 - );
905 -
906 - // preg_replace_callback can return null on regex error.
907 - return ( null === $result ) ? $html : $result;
908 - }
909 -
910 956 /**
911 957 * Build CSS vars fragment: "--a:1;--b:2;"
912 958 *
913 959 * @param array $css_vars
@@ -913,9 +959,9 @@
913 959 * @param array $css_vars
914 960 *
915 961 * @return string
916 962 */
917 - private static function build_css_vars_style_fragment( $css_vars ) {
963 + private static function build_css_vars_style_fragment( $css_vars ) {
918 964
919 965 $out = '';
920 966
921 967 foreach ( $css_vars as $name => $value ) {
@@ -945,17 +991,17 @@
945 991 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
946 992 return '';
947 993 }
948 994
949 - // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
950 - $lower = strtolower( $name );
951 - if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) ) {
952 - return '';
953 - }
995 + // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
996 + $lower = strtolower( $name );
997 + if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) && 0 !== strpos( $lower, '--wpbc_form-' ) ) {
998 + return '';
999 + }
1000 +
1001 + return $name;
1002 + }
954 1003
955 - return $name;
956 - }
957 -
958 1004 private static function sanitize_css_var_value( $value ) {
959 1005
960 1006 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
961 1007 if ( '' === $value ) {
@@ -974,7 +1020,113 @@
974 1020 '',
975 1021 $value
976 1022 );
977 1023
978 - return trim( $value );
979 - }
980 -}
1024 + return trim( $value );
1025 + }
1026 +
1027 + private static function inject_css_vars_into_first_tag_with_classes( $html, $css_vars, $required_classes ) {
1028 +
1029 + $html = (string) $html;
1030 + $css_vars = is_array( $css_vars ) ? $css_vars : array();
1031 +
1032 + if ( empty( $css_vars ) ) {
1033 + return $html;
1034 + }
1035 +
1036 + $style_append = self::build_css_vars_style_fragment( $css_vars );
1037 + if ( '' === $style_append ) {
1038 + return $html;
1039 + }
1040 +
1041 + $pattern = self::get_first_div_with_classes_pattern( $required_classes );
1042 + if ( '' === $pattern ) {
1043 + return $html;
1044 + }
1045 +
1046 + $done = false;
1047 + $result = preg_replace_callback(
1048 + $pattern,
1049 + function( $m ) use ( $required_classes, $style_append, &$done ) {
1050 +
1051 + $tag = $m[0];
1052 +
1053 + if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
1054 + return $tag;
1055 + }
1056 +
1057 + // If style already exists: append.
1058 + if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
1059 +
1060 + $quote = $sm[1];
1061 + $existing = (string) $sm[2];
1062 +
1063 + // Avoid double-escaping if the tag already contains entities.
1064 + $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
1065 +
1066 + $merged = trim( $existing );
1067 + if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
1068 + $merged .= ';';
1069 + }
1070 + $merged .= $style_append;
1071 +
1072 + $done = true;
1073 +
1074 + return preg_replace(
1075 + '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
1076 + 'style=' . $quote . esc_attr( $merged ) . $quote,
1077 + $tag,
1078 + 1
1079 + );
1080 + }
1081 +
1082 + // No style attr: add it.
1083 + $done = true;
1084 + return rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
1085 + },
1086 + $html
1087 + );
1088 +
1089 + return ( null === $result ) ? $html : $result;
1090 + }
1091 +
1092 + private static function get_first_div_with_classes_pattern( $required_classes ) {
1093 +
1094 + $required_classes = is_array( $required_classes ) ? $required_classes : array();
1095 + $has_class = false;
1096 +
1097 + foreach ( $required_classes as $class_name ) {
1098 + $class_name = sanitize_html_class( (string) $class_name );
1099 + if ( '' === $class_name ) {
1100 + continue;
1101 + }
1102 + $has_class = true;
1103 + }
1104 +
1105 + if ( ! $has_class ) {
1106 + return '';
1107 + }
1108 +
1109 + return '/<div\b[^>]*\bclass\s*=\s*(["\'])(.*?)\1[^>]*>/i';
1110 + }
1111 +
1112 + private static function tag_has_required_classes( $tag, $required_classes ) {
1113 +
1114 + if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', (string) $tag, $cm ) ) {
1115 + return false;
1116 + }
1117 +
1118 + $classes = ' ' . preg_replace( '/\s+/', ' ', (string) $cm[2] ) . ' ';
1119 +
1120 + foreach ( (array) $required_classes as $class_name ) {
1121 + $class_name = sanitize_html_class( (string) $class_name );
1122 + if ( '' === $class_name ) {
1123 + continue;
1124 + }
1125 + if ( false === strpos( $classes, ' ' . $class_name . ' ' ) ) {
1126 + return false;
1127 + }
1128 + }
1129 +
1130 + return true;
1131 + }
1132 +}