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 +239 -113 11.311.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.
@@ -339,9 +337,9 @@
339 337 }
340 338
341 339
342 340 /**
343 - * Return the booking form body HTML, choosing BFB source if available, else legacy.
341 + * Return the booking form body HTML from BFB storage.
344 342 *
345 343 * @param int $resource_id
346 344 * @param string $custom_booking_form
347 345 * @param string $form_status 'published'|'preview'
@@ -447,9 +445,9 @@
447 445 $resource_id = (int) $req['resource_id'];
448 446 $custom_booking_form = (string) $req['form_slug'];
449 447
450 448 /**
451 - * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple'
449 + * $resolved = array( 'engine' : 'bfb_db'|'bfb_missing'
452 450 * 'apply_after_load_filter' : bool
453 451 * 'bfb_loader_args' : array()
454 452 * 'fallback_chain' : array()
455 453 */
@@ -466,33 +464,34 @@
466 464 return $booking_form_html__arr;
467 465 }
468 466 }
469 467
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 - }
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 + }
483 492
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 493
494 -
495 494 /**
496 495 * Get BFB booking form content array. Helper function.
497 496 *
498 497 * @param array $req - array.
@@ -571,9 +570,17 @@
571 570 if ( '' === $bfb_form_html ) {
572 571
573 572 $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) );
574 573
575 - $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;
576 583
577 584 // Replace custom params in the source (legacy behavior).
578 585 if ( ! empty( $custom_params ) ) {
579 586 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
@@ -705,15 +712,18 @@
705 712 * @return string Wrapped HTML.
706 713 */
707 714 public static function wrap( $form_html, $resource_id ) {
708 715
709 - $resource_id = (int) $resource_id;
710 - $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'] ) : '';
711 725
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 726 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
717 727 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
718 728 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
719 729
@@ -842,72 +852,82 @@
842 852 * @param array $css_vars Map: '--var-name' => 'value'
843 853 *
844 854 * @return string
845 855 */
846 - 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 + }
847 929
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 930 /**
911 931 * Build CSS vars fragment: "--a:1;--b:2;"
912 932 *
913 933 * @param array $css_vars
@@ -913,9 +933,9 @@
913 933 * @param array $css_vars
914 934 *
915 935 * @return string
916 936 */
917 - private static function build_css_vars_style_fragment( $css_vars ) {
937 + private static function build_css_vars_style_fragment( $css_vars ) {
918 938
919 939 $out = '';
920 940
921 941 foreach ( $css_vars as $name => $value ) {
@@ -945,17 +965,17 @@
945 965 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
946 966 return '';
947 967 }
948 968
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 - }
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 + }
954 977
955 - return $name;
956 - }
957 -
958 978 private static function sanitize_css_var_value( $value ) {
959 979
960 980 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
961 981 if ( '' === $value ) {
@@ -974,7 +994,113 @@
974 994 '',
975 995 $value
976 996 );
977 997
978 - return trim( $value );
979 - }
980 -}
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 +}