PluginProbe
Booking Calendar / 11.3
Booking Calendar v11.3
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 10.11.2 All 203 releases
booking / includes / _front_end / class-fe-render-form-body.php

class-fe-render-form-body.php in Booking Calendar 11.3, at includes/_front_end/class-fe-render-form-body.php

981 lines 37.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Front-End Form Body Rendering
4 *
5 * - Calendar markup (or hidden selected-dates field)
6 * - Form body source resolution (BFB / legacy / simple)
7 * - Legacy post-processing hooks (additional calendars, captcha, change-over times)
8 * - Legacy wrapper (<form> container + nonce + hidden fields)
9 * - Legacy inline scripts (duplicate-calendar warning, cost hints, autofill)
10 *
11 * @package Booking Calendar
12 * @since 11.0.x
13 * @file ../includes/_front_end/class-fe-render-form-body.php
14 */
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 /**
21 * Booking Form Body Renderer.
22 *
23 * Orchestrates building of the booking form HTML while preserving legacy output and
24 * filter/action execution order.
25 *
26 * @since 11.0.x
27 */
28 class WPBC_FE_Form_Body_Renderer {
29
30 /**
31 * Render booking form HTML (calendar + form body + wrapper + inline scripts),
32 * preserving legacy output and hook execution order.
33 *
34 * Expected flow:
35 * 1) Parse shortcode "options" into custom parameters (for BFB/simple form context).
36 * 2) Build calendar markup OR hidden textarea with preselected dates.
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.
41 * 4) Post-process composed markup:
42 * - Insert calendar ([calendar] placeholder or prepend).
43 * - Replace additional calendars via legacy filter.
44 * - Replace [captcha] placeholder.
45 * - Append change-over times.
46 * 5) Wrap in legacy container + add nonce + hidden fields.
47 * 6) Append legacy inline scripts.
48 *
49 * @since 11.0.x
50 *
51 * @param array $args {
52 * Optional. Arguments for rendering.
53 *
54 * @type int $resource_id Booking resource ID.
55 * @type string $custom_booking_form Booking form slug/name (legacy: "standard").
56 * @type string $selected_dates_without_calendar Preselected dates string (legacy format).
57 * @type int $cal_count Number of months to show in calendar.
58 * @type mixed $shortcode_param__options Shortcode options string (legacy: options="...") or array.
59 * @type wpdev_booking $legacy_instance Legacy plugin booking object (Phase #1/compat).
60 * }
61 *
62 * @return string Rendered HTML (ready to echo).
63 */
64 public static function render( $args ) {
65
66 $defaults = array(
67 'resource_id' => 1,
68 'custom_booking_form' => 'standard',
69 'form_status' => 'published',
70 'selected_dates_without_calendar' => '',
71 'cal_count' => 1,
72 'shortcode_param__options' => '',
73 'booking_hash' => '',
74 'legacy_instance' => null,
75 );
76
77 $args = wp_parse_args( $args, $defaults );
78
79 $resource_id = (int) $args['resource_id'];
80 $custom_booking_form = (string) $args['custom_booking_form'];
81 $selected_dates_without_calendar = (string) $args['selected_dates_without_calendar'];
82 $cal_count = (int) $args['cal_count'];
83 $shortcode_param__options = $args['shortcode_param__options'];
84 $booking_hash = sanitize_text_field( wp_unslash( (string) $args['booking_hash'] ) );
85 $legacy_instance = $args['legacy_instance'];
86 $form_status = isset( $args['form_status'] ) ? (string) $args['form_status'] : 'published';
87
88 $nl = '<div style="clear:both;height:10px;"></div>';
89
90 $custom_params = WPBC_FE_Options_Parser::parse_for_parameter__in_shortcode_options( $shortcode_param__options );
91
92 $calendar_html = WPBC_FE_Calendar_Markup::build( $resource_id, $cal_count, $shortcode_param__options, $selected_dates_without_calendar );
93
94 $source_res = WPBC_FE_Form_Source::get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash );
95
96 $form_html = $source_res['body_html'];
97
98 // Preserve legacy: apply after-load filter ONLY for BFB and Simple form.
99 if ( ! empty( $source_res['apply_after_load_filter'] ) ) {
100 // Re-update 'Capacity Hints' | 'Steps Timeline shortcode' !
101 $form_html = apply_filters( 'wpbc_booking_form_content__after_load', $form_html, $resource_id, $custom_booking_form );
102 }
103
104
105 // 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", .... ], ...
106 $form_html = apply_filters( 'wpbc_booking_form__body_html__before_postprocess', $form_html, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
107
108 $form_html = WPBC_FE_Form_Postprocessor::apply( $form_html, $calendar_html, array(
109 'resource_id' => $resource_id,
110 'custom_booking_form' => $custom_booking_form,
111 'selected_dates_without_calendar' => $selected_dates_without_calendar,
112 'cal_count' => $cal_count,
113 'shortcode_param__options' => $shortcode_param__options,
114 'custom_params' => $custom_params,
115 'legacy_instance' => $legacy_instance,
116 'nl' => $nl,
117 ) );
118
119 // 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", .... ], ...
120 $form_html = apply_filters( 'wpbc_booking_form__html__before_wrapper', $form_html, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
121 // 2. Form wraper into <form> ... </form>
122 $wrapped = WPBC_FE_Form_Wrapper::wrap( $form_html, $resource_id );
123
124 // 3. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ...
125 $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__before_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
126
127 $wrapped .= WPBC_FE_Inline_Scripts::collect( $resource_id );
128
129 // Info: Hook for addons. Postprocess Form Conent - regarding settings - e.g.: $source_res['bfb_settings'] = [ options = [ booking_form_theme = "wpbc_theme_dark_1", .... ], ...
130 $wrapped = apply_filters( 'wpbc_booking_form__wrapped_html__after_inline_scripts', $wrapped, $source_res['bfb_settings'], $resource_id, $custom_booking_form );
131
132 return $wrapped;
133 }
134 }
135
136 // ---------------------------------------------------------------------------------------------------------------------
137
138 /**
139 * Calendar markup builder for booking form composition.
140 *
141 * If no preselected dates are provided, returns full calendar markup via legacy generator.
142 * If preselected dates are provided, returns a hidden textarea for legacy JS to consume.
143 *
144 * @since 11.0.x
145 */
146 class WPBC_FE_Calendar_Markup {
147
148 /**
149 * Build calendar HTML (real calendar markup or hidden textarea with preselected dates).
150 *
151 * @param int $resource_id
152 * @param int $cal_count
153 * @param mixed $shortcode_param__options
154 * @param string $selected_dates_without_calendar
155 *
156 * @param int $resource_id Booking resource ID.
157 * @param int $cal_count Number of months to show.
158 * @param mixed $shortcode_param__options Shortcode options parameter (legacy).
159 * @param string $selected_dates_without_calendar Preselected dates (legacy string format).
160 *
161 * @return string Calendar HTML snippet.
162 */
163 public static function build( $resource_id, $cal_count, $shortcode_param__options, $selected_dates_without_calendar ) {
164
165 $resource_id = (int) $resource_id;
166 $cal_count = (int) $cal_count;
167 $selected_dates_without_calendar = (string) $selected_dates_without_calendar;
168
169 if ( '' === $selected_dates_without_calendar ) {
170 // Get HTML with [calendar] shortcode and Styles for calendar. Get legend html. //TODO: extract CSS Styles separately !!!!
171 return wpbc_pre_get_calendar_html( $resource_id, $cal_count, $shortcode_param__options );
172 }
173
174
175 return '<textarea rows="3" cols="50" id="date_booking' . $resource_id . '" name="date_booking' . $resource_id . '" autocomplete="off" style="display:none;">' .
176 esc_textarea( $selected_dates_without_calendar ) .
177 '</textarea>';
178 }
179 }
180
181 /**
182 * Return the booking form body HTML, choosing BFB source if available, else legacy.
183 * Does NOT insert calendar, captcha, additional calendars, wrapper, nonce, etc.
184 *
185 * @since 11.0.x
186 *
187 * @param int $resource_id Booking resource ID.
188 * @param string $custom_booking_form Booking form slug/name.
189 * @param array $custom_params Parsed custom params from shortcode options ("{parameter ...}").
190 * @param wpdev_booking $legacy_instance Legacy booking object (optional).
191 *
192 * @return array {
193 * Result array.
194 *
195 * @type string $body_html Booking form body HTML.
196 * @type bool $apply_after_load_filter Whether to apply 'wpbc_booking_form_content__after_load'.
197 * }
198 */
199 class WPBC_FE_Form_Source {
200
201 /**
202 * Check if the booking editing, and return -> [ 'booking_id': booking_id, 'resource_id': resource_id] otherwise -> false
203 *
204 * @param string $booking_hash__usualy_from_get optional. - hash of the booking. If missed, then system check $_GET['booking_hash'].
205 *
206 * @return array|false
207 */
208 public static function maybe_check_if_booking_edit( $booking_hash__usualy_from_get = '' ) {
209
210 if ( empty( $booking_hash__usualy_from_get ) ) {
211 /* phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing */
212 $booking_hash__usualy_from_get = ( ( isset( $_GET['booking_hash'] ) ) ? sanitize_text_field( wp_unslash( $_GET['booking_hash'] ) ) : '' );
213 }
214
215 if ( ! empty( $booking_hash__usualy_from_get ) ) {
216
217 $maybe__booking_id__resource_id = wpbc_hash__get_booking_id__resource_id( $booking_hash__usualy_from_get );
218
219 if ( false !== $maybe__booking_id__resource_id ) {
220
221 $booking_id = intval( $maybe__booking_id__resource_id[0] );
222 $resource_id = intval( $maybe__booking_id__resource_id[1] );
223
224 if ( ( $booking_id > 0 ) && ( $resource_id > 0 ) ) {
225 return array(
226 'booking_id' => $booking_id,
227 'resource_id' => $resource_id,
228 );
229 }
230 }
231 }
232 return false;
233 }
234
235
236 /**
237 * Get parsed form data of specific booking.
238 *
239 * @param int $booking_id - ID of booking.
240 *
241 * @return array|array[]|false
242 */
243 public static function get_booking_data( $booking_id ) {
244 global $wpdb;
245
246 $separators = array(
247 'row' => '~',
248 'col' => '^',
249 );
250
251 if ( empty( $booking_id ) ) {
252 return false;
253 }
254
255 $sql = $wpdb->prepare(
256 "SELECT * FROM {$wpdb->prefix}booking as bk
257 INNER JOIN {$wpdb->prefix}bookingdates as dt
258 ON bk.booking_id = dt.booking_id
259 WHERE bk.booking_id = %d ORDER BY dt.booking_date ASC ",
260 $booking_id
261 );
262
263 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
264 $result = $wpdb->get_results( $sql );
265
266 if ( empty( $result ) ) {
267 return false;
268 }
269
270 $return = array( 'dates' => array() );
271 foreach ( $result as $res ) {
272 $return['dates'][] = $res->booking_date;
273 }
274 $return['form'] = $res->form;
275 $return['type'] = $res->booking_type;
276 $return['approved'] = $res->approved;
277 $return['id'] = $res->booking_id;
278
279 // -- Parse data from booking form ----------------------------------------------
280 $bktype = $res->booking_type;
281 $parsed_form = $res->form;
282 $parsed_form = explode( $separators['row'], $parsed_form );
283
284 $parsed_form_results = array();
285
286 foreach ( $parsed_form as $field ) {
287 $elemnts = explode( $separators['col'], $field );
288 if ( count( $elemnts ) < 3 ) {
289 continue;
290 }
291 $type = $elemnts[0];
292 $element_name = $elemnts[1];
293 $value = $elemnts[2];
294
295 $count_pos = strlen( (string) $bktype );
296
297 $type_name = $elemnts[1];
298 $type_name = str_replace( '[]', '', $type_name );
299 if ( intval( $bktype ) === intval( substr( $type_name, - 1 * $count_pos ) ) ) {
300 $type_name = substr( $type_name, 0, - 1 * $count_pos );
301 }
302
303 if ( ( 'email' === $type_name ) && ( ! isset( $email_adress ) ) ) {
304 $email_adress = $value;
305 }
306 if ( 'name' === $type_name ) {
307 $name_of_person = $value;
308 }
309 if ( 'checkbox' === $type ) {
310 if ( 'true' === $value ) {
311 $value = 'on';
312 } elseif ( ( empty( $value ) ) || ( 'false' === $value ) || ( 'Off' === $value ) ) {
313 $value = '';
314 }
315 }
316 $element_name = str_replace( '[]', '', $element_name );
317 if ( isset( $parsed_form_results[ $element_name ] ) ) {
318 if ( '' !== $value ) {
319 $parsed_form_results[ $element_name ]['value'] .= ',' . $value;
320 }
321 } else {
322 $parsed_form_results[ $element_name ] = array(
323 'value' => $value,
324 'type' => $type,
325 'element_name' => $type_name,
326 );
327 }
328 }
329 $return['parsed_form'] = $parsed_form_results;
330
331 if ( isset( $email_adress ) ) {
332 $return['email'] = $email_adress;
333 }
334 if ( isset( $name_of_person ) ) {
335 $return['name'] = $name_of_person;
336 }
337
338 return $return;
339 }
340
341
342 /**
343 * Return the booking form body HTML, choosing BFB source if available, else legacy.
344 *
345 * @param int $resource_id
346 * @param string $custom_booking_form
347 * @param string $form_status 'published'|'preview'
348 * @param array $custom_params
349 * @param wpdev_booking $legacy_instance
350 * @param string $booking_hash
351 *
352 * @return array
353 */
354 public static function get_form_body_html( $resource_id, $custom_booking_form, $form_status, $custom_params, $legacy_instance, $booking_hash = '' ) {
355
356 $resource_id = (int) $resource_id;
357 $custom_booking_form = (string) $custom_booking_form;
358 $form_status = (string) $form_status;
359 $custom_params = ( is_array( $custom_params ) ) ? $custom_params : array();
360 $booking_hash = sanitize_text_field( wp_unslash( (string) $booking_hash ) );
361
362 $req = array(
363 'resource_id' => $resource_id,
364 'form_slug' => $custom_booking_form,
365 'form_status' => $form_status,
366 'custom_params' => $custom_params,
367 'legacy_instance' => $legacy_instance,
368 );
369
370 // Fix: 2026-02-18 16:51.
371 $req['current_resource_id'] = $resource_id;
372 $req['current_edit_booking'] = false;
373 $req['current_edit_booking_id'] = false;
374 // =============================================================================================================
375 // == If Booking Edit ? ==
376 // =============================================================================================================
377 // Maybe get the "custom booking form" and child "booking resource ID", if the booking edited. And this booking was created in custom booking form.
378 $maybe_edited__booking_id__resource_id = self::maybe_check_if_booking_edit( $booking_hash );
379
380 if ( ! empty( $maybe_edited__booking_id__resource_id ) ) {
381
382 // == Edit Booking =========================================================================================
383 $req['resource_id'] = $maybe_edited__booking_id__resource_id['resource_id'];
384 $req['current_resource_id'] = $maybe_edited__booking_id__resource_id['resource_id'];
385 $req['current_edit_booking_id'] = $maybe_edited__booking_id__resource_id['booking_id'];
386
387 // Parsed booking form values.
388 $req['current_edit_booking'] = self::get_booking_data( $maybe_edited__booking_id__resource_id['booking_id'] );
389
390 // -- Is use custom form ? ---------------------------------------------------------------------------------
391
392 // Get 'custom booking form name' from URL.
393 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
394 $my_booking_form = ( ( isset( $_GET['booking_form'] ) ) ? sanitize_text_field( wp_unslash( $_GET['booking_form'] ) ) : '' );
395
396 // Otherwise get 'custom booking form name' from EDIT booking details.
397 if ( ( empty( $my_booking_form ) ) && ( ! empty( $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ] ) ) ) {
398 $my_booking_form = $req['current_edit_booking']['parsed_form'][ 'wpbc_custom_booking_form' . $req['resource_id'] ]['value'];
399 }
400
401 // If not '' then OVVERIDE.
402 if ( ! empty( $my_booking_form ) ) {
403 $req['form_slug'] = $my_booking_form;
404 }
405
406
407 // -- Check out dates ? ------------------------------------------------------------------------------------
408 if (
409 ( 'On' === get_bk_option( 'booking_last_checkout_day_available' ) ) &&
410 ( ! empty( $req['current_edit_booking']['dates'] ) )
411 ) {
412 // Add one additional date for editing, if "last_checkout_day_available" is "On".
413 // Dates in format: [ ... 2: 2019-03-13 00:00:00, 3: 2019-03-14 12:00:02 ].
414 $last_day = $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ];
415 $check_out = strtotime( $last_day );
416 $check_out = strtotime( '+1 day', $check_out );
417 $last_day = date_i18n( 'Y-m-d 00:00:00', strtotime( $last_day ) );
418
419 $req['current_edit_booking']['dates'][ ( count( $req['current_edit_booking']['dates'] ) - 1 ) ] = $last_day;
420
421 $req['current_edit_booking']['dates'][] = date_i18n( 'Y-m-d H:i:s', $check_out );
422 }
423
424 // -- Check out dates ? ------------------------------------------------------------------------------------
425 // Check when we edit "child resource" -> need re-update ID in calendar and form elements to have parent resource // FixIn: 6.1.1.9.
426 // FixIn: 10.10.1.2 ( ! isset( $_GET['resource_no_update'] ) ) // FixIn: 9.4.2.3.
427 if (
428 ( function_exists( 'wpbc_is_this_child_resource' ) ) &&
429 ( wpbc_is_this_child_resource( $req['resource_id'] ) )
430 ) {
431 $bk_parent_br_id = wpbc_get_parent_resource( $req['resource_id'] );
432
433 $new_booking_data_arr = array();
434
435 foreach ( $req['current_edit_booking']['parsed_form'] as $my_key_old => $booking_data ) {
436
437 $new_booking_data_arr[ $booking_data['element_name'] . $bk_parent_br_id ] = $booking_data;
438 }
439
440 $req['current_edit_booking']['parsed_form'] = $new_booking_data_arr;
441 $req['resource_id'] = $bk_parent_br_id;
442 $req['current_resource_id'] = $bk_parent_br_id;
443 }
444 }
445
446 // Update after “edit booking” resource overrides.
447 $resource_id = (int) $req['resource_id'];
448 $custom_booking_form = (string) $req['form_slug'];
449
450 /**
451 * $resolved = array( 'engine' : 'bfb_db'|'legacy'|'simple'
452 * 'apply_after_load_filter' : bool
453 * 'bfb_loader_args' : array()
454 * 'fallback_chain' : array()
455 */
456 $resolved = WPBC_FE_Form_Source_Resolver::resolve( $req );
457
458 // -----------------------------------------------------------------
459 // BFB DB engine (only if resolver decided it).
460 // -----------------------------------------------------------------
461 if ( ( isset( $resolved['engine'] ) ) && ( 'bfb_db' === $resolved['engine'] ) && function_exists( 'wpbc_bfb_get_booking_form_pair' ) ) {
462
463 $booking_form_html__arr = self::wpbc_bfb__get_booking_form_html__arr( $req, $resolved );
464
465 if ( null !== $booking_form_html__arr ) {
466 return $booking_form_html__arr;
467 }
468 }
469
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 }
483
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
494
495 /**
496 * Get BFB booking form content array. Helper function.
497 *
498 * @param array $req - array.
499 * @param array $resolved - array.
500 *
501 * @return array|null
502 */
503 public static function wpbc_bfb__get_booking_form_html__arr( $req, $resolved ) {
504
505 $custom_params = $req ['custom_params'];
506 $form_status = $req ['form_status'];
507 $custom_booking_form = $req['form_slug'];
508 $resource_id = $req ['resource_id'];
509 $legacy_instance = $req ['legacy_instance'];
510
511 $bfb_loader_args = ( isset( $resolved['bfb_loader_args'] ) && is_array( $resolved['bfb_loader_args'] ) ) ? $resolved['bfb_loader_args'] : array();
512
513
514 // Keep old behavior: allow explicit form_id/status overrides from options only if present.
515 if ( ! empty( $custom_params['bfb_form_id'] ) && empty( $bfb_loader_args['form_id'] ) ) {
516 $bfb_loader_args['form_id'] = (int) $custom_params['bfb_form_id'];
517 }
518
519 $bfb_settings = array();
520 $bfb_source = trim( '' );
521 $settings_json = trim( '' );
522
523 $bfb_pair = wpbc_bfb_get_booking_form_pair( $bfb_loader_args );
524
525 if ( is_array( $bfb_pair ) ) {
526 $bfb_source = isset( $bfb_pair['form'] ) ? (string) $bfb_pair['form'] : '';
527 $settings_json = isset( $bfb_pair['settings_json'] ) ? (string) $bfb_pair['settings_json'] : '';
528 }
529
530 if ( '' !== $settings_json ) {
531 $decoded = json_decode( $settings_json, true );
532 if ( is_array( $decoded ) ) {
533 $bfb_settings = $decoded;
534 }
535 }
536
537 // Allow loader to return either string OR array with settings.
538 if ( is_array( $bfb_source ) ) {
539
540 // Common possible keys (support multiple formats, future-proof).
541 if ( ! empty( $bfb_source['settings'] ) && is_array( $bfb_source['settings'] ) ) {
542 $bfb_settings = $bfb_source['settings'];
543 } elseif ( ! empty( $bfb_source['settings_json'] ) && is_string( $bfb_source['settings_json'] ) ) {
544 $decoded = json_decode( $bfb_source['settings_json'], true );
545 if ( is_array( $decoded ) ) {
546 $bfb_settings = $decoded;
547 }
548 }
549
550 // Source itself.
551 if ( isset( $bfb_source['advanced_form'] ) ) {
552 $bfb_source = (string) $bfb_source['advanced_form'];
553 } elseif ( isset( $bfb_source['source'] ) ) {
554 $bfb_source = (string) $bfb_source['source'];
555 } else {
556 $bfb_source = '';
557 }
558 }
559
560 // Optional fallback hook if you keep loader returning string for now.
561 if ( empty( $bfb_settings ) ) {
562 $bfb_settings = apply_filters( 'wpbc_bfb_form_settings_for_render', array(), $bfb_loader_args, $resource_id, $custom_booking_form, $custom_params );
563 }
564
565
566 if ( '' !== trim( (string) $bfb_source ) ) {
567
568 // Info: Hook for addons. Give addons a chance to fully handle rendering.
569 $bfb_form_html = apply_filters( 'wpbc_bfb_render_booking_form_source', '', $bfb_source, $resource_id, $custom_booking_form, $custom_params );
570
571 if ( '' === $bfb_form_html ) {
572
573 $render_args = array_merge( $custom_params, array( 'booking_type' => (int) $resource_id ) );
574
575 $bfb_source = wpbc_lang( $bfb_source );
576
577 // Replace custom params in the source (legacy behavior).
578 if ( ! empty( $custom_params ) ) {
579 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
580 $bfb_source = str_replace( $custom_params_key, $custom_params_value, $bfb_source );
581 }
582 }
583
584 $bfb_source = wpbc_bf__replace_custom_html_shortcodes( $bfb_source );
585
586 $bfb_form_html = wpbc_render_booking_form_shortcodes( $bfb_source, $render_args, $req );
587 }
588
589 if ( '' !== $bfb_form_html ) {
590 return array(
591 'body_html' => $bfb_form_html,
592 'apply_after_load_filter' => ! empty( $resolved['apply_after_load_filter'] ),
593 'bfb_settings' => $bfb_settings,
594 );
595 }
596 }
597
598 // If BFB resolution succeeded but returned empty output, fall through to legacy.
599 return null;
600 }
601 }
602
603 /**
604 * Booking form post-processor (legacy compatibility).
605 *
606 * Applies legacy transformations and hooks to the combined form markup:
607 * - Inserts calendar markup (replaces [calendar] or prepends).
608 * - Replaces additional calendars via legacy filter.
609 * - Replaces [captcha] placeholder if present.
610 * - Appends change-over times.
611 *
612 * @since 11.0.x
613 */
614 class WPBC_FE_Form_Postprocessor {
615
616 /**
617 * Apply legacy postprocessing to the composed form markup.
618 *
619 * @since 11.0.x
620 *
621 * @param string $form_html Booking form body HTML.
622 * @param string $calendar_html Calendar HTML snippet.
623 * @param array $ctx {
624 * Context data used during post-processing.
625 *
626 * @type int $resource_id Booking resource ID.
627 * @type string $custom_booking_form Booking form slug/name.
628 * @type string $selected_dates_without_calendar Preselected dates (legacy string).
629 * @type int $cal_count Number of months shown.
630 * @type mixed $shortcode_param__options Shortcode options.
631 * @type wpdev_booking $legacy_instance Legacy booking instance (optional).
632 * @type string $nl Legacy separator markup.
633 * }
634 *
635 * @return string Post-processed HTML.
636 */
637 public static function apply( $form_html, $calendar_html, $ctx ) {
638
639 $form_html = (string) $form_html;
640 $calendar_html = (string) $calendar_html;
641
642 $resource_id = (int) $ctx['resource_id'];
643 $custom_booking_form = (string) $ctx['custom_booking_form'];
644 $selected_dates_without_calendar = (string) $ctx['selected_dates_without_calendar'];
645 $cal_count = (int) $ctx['cal_count'];
646 $shortcode_param__options = $ctx['shortcode_param__options'];
647 $legacy_instance = $ctx['legacy_instance'];
648 $nl = (string) $ctx['nl'];
649
650 // Insert calendar into form.
651 if ( false !== strpos( $form_html, '[calendar]' ) ) {
652 $form_html = str_replace( '[calendar]', $calendar_html, $form_html );
653 } else {
654 // FixIn: 2981-01-13 13 Jan 2981.
655 //$form_html = '<div class="booking_form_div">' . $calendar_html . '</div>' . $nl . $form_html;
656 $form_html = '<textarea id="date_booking'.esc_attr($resource_id).'" name="date_booking'.esc_attr($resource_id).'" autocomplete="off" style="display:none;">13.01.2981</textarea>' . $nl . $form_html;
657 }
658
659 // Replace additional calendars.
660 $form_html = apply_bk_filter(
661 'wpdev_check_for_additional_calendars_in_form',
662 $form_html,
663 $resource_id,
664 array(
665 'booking_form' => $custom_booking_form,
666 'selected_dates' => $selected_dates_without_calendar,
667 'cal_count' => $cal_count,
668 'options' => $shortcode_param__options,
669 )
670 );
671
672 // Captcha replacement.
673 $form_html = apply_filters( 'wpbc_booking_form_html__create_captcha', $form_html, $resource_id );
674
675 // Change-over times injection.
676 $form_html = apply_filters( 'wpbc_booking_form_html__update__append_change_over_times', $form_html, $resource_id );
677
678 return $form_html;
679 }
680 }
681
682 /**
683 * Booking form wrapper (legacy HTML structure).
684 *
685 * Responsible for building the outer <div> container and <form> tag, including:
686 * - ajax response containers
687 * - anchor link (bklnk)
688 * - hidden bk_type field
689 * - nonce field
690 * - "garbage" container used by legacy JS
691 *
692 * @since 11.0.x
693 */
694 class WPBC_FE_Form_Wrapper {
695
696 /**
697 * Wrap the form HTML into legacy outer container + add hidden fields + nonce + garbage.
698 *
699 * @param string $form_html
700 * @param int $resource_id
701 *
702 * @param string $form_html Post-processed booking form HTML (already contains calendar markup).
703 * @param int $resource_id Booking resource ID.
704 *
705 * @return string Wrapped HTML.
706 */
707 public static function wrap( $form_html, $resource_id ) {
708
709 $resource_id = (int) $resource_id;
710 $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
711
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 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
717 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
718 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
719
720 $return_form = '<div id="' . esc_attr( $form_container_random_id ) . '" class="' . esc_attr( $form_container_css ) . '" >' .
721 '<form id="booking_form' . intval( $resource_id ) . '" class="booking_form ' . esc_attr( $booking_form_format_type ) . '" method="post" action="">' .
722 '<div id="ajax_respond_insert' . intval( $resource_id ) . '" class="ajax_respond_insert" style="display:none;"></div>' .
723 '<a name="bklnk' . $resource_id . '" id="bklnk' . $resource_id . '"></a>' .
724 '<div id="booking_form_div' . $resource_id . '" class="booking_form_div">' .
725 $form_html .
726 '<input id="bk_type' . $resource_id . '" name="bk_type' . $resource_id . '" class="" type="hidden" value="' . $resource_id . '" />' .
727 '</div>' .
728 '<div id="submiting' . $resource_id . '"></div>' .
729 '<div class="form_bk_messages" id="form_bk_messages' . $resource_id . '" ></div>' .
730 $wpbc_nonce .
731 '</form>' .
732 '</div>' .
733 '<div id="booking_form_garbage' . intval( $resource_id ) . '" class="booking_form_garbage"></div>';
734
735 return $return_form;
736 }
737 }
738
739 // ---------------------------------------------------------------------------------------------------------------------
740
741 /**
742 * Inline scripts builder (legacy compatibility).
743 *
744 * Generates and appends legacy inline JavaScript required by booking form UX:
745 * - Duplicate-calendar warning in console when the same resource calendar is rendered multiple times.
746 * - Cost hint display trigger when selected dates are predefined.
747 * - Autofill for logged-in users (legacy behavior).
748 *
749 * @since 11.0.x
750 */
751 class WPBC_FE_Inline_Scripts {
752
753 /**
754 * Collect legacy inline scripts into WPBC_FE_Assets (Elementor-safe).
755 *
756 * @param int $resource_id - ID of booking resource.
757 *
758 * @return string Inline scripts HTML.
759 */
760 public static function collect( $resource_id ) {
761
762 // FixIn: 10.14.17.2.
763
764 $html = '';
765 $resource_id = (int) $resource_id;
766
767 // == Autofill (legacy conditions preserved) ==
768 $autofill_js = self::build_autofill_js_body( $resource_id );
769 if ( '' !== $autofill_js ) {
770 $assets_key = 'wpbc:autofill:' . intval( $resource_id );
771 $added = false;
772 if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
773 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key . ':ajax' );
774 $added = true;
775 } else {
776 $added = WPBC_FE_Assets::add_jq_ready_js_to_wp_script( 'wpbc_all', $autofill_js, $assets_key );
777 }
778 if ( ! $added ) {
779 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key ); // Fallback to direct inline JS, if previosly not edded script.
780 }
781 }
782
783 return $html;
784 }
785
786 /**
787 * Build ONLY the JS BODY for legacy autofill (no <script>, no ready wrapper).
788 *
789 * Now calls the shared JS function:
790 * window.WPBC_FE.autofill_booking_form_fields( resource_id, fill_values )
791 *
792 * @param int $resource_id Booking resource ID.
793 *
794 * @return string JS body (or empty string if not applicable).
795 */
796 private static function build_autofill_js_body( $resource_id ) {
797
798 $resource_id = (int) $resource_id;
799
800 if ( WPBC_GET_Request::has_non_empty_get( 'booking_hash' ) ) {
801 return '';
802 }
803
804 $is_use_auto_fill_for_logged = get_bk_option( 'booking_is_use_autofill_4_logged_user' );
805 if ( 'On' !== $is_use_auto_fill_for_logged ) {
806 return '';
807 }
808
809 $curr_user = wpbc_get_current_user();
810 if ( empty( $curr_user ) || ( (int) $curr_user->ID <= 0 ) ) {
811 return '';
812 }
813
814 $user_nick_name = get_user_meta( $curr_user->ID, 'nickname' );
815 $user_nick_name = ( empty( $user_nick_name ) ) ? '' : $user_nick_name[0];
816
817 $fill_values = array(
818 'nickname' => (string) $user_nick_name,
819 'last_name' => (string) $curr_user->last_name,
820 'first_name' => (string) $curr_user->first_name,
821 'email' => (string) $curr_user->user_email,
822 'phone' => (string) $curr_user->phone_number,
823 'nb_enfant' => (string) $curr_user->nb_enfant,
824 'url' => (string) $curr_user->user_url,
825 );
826
827 // IMPORTANT: return body only (no <script>, no ready wrapper).
828 $js_body = 'if ( window.WPBC_FE && ( typeof window.WPBC_FE.autofill_booking_form_fields === "function" ) ) {';
829 $js_body .= 'window.WPBC_FE.autofill_booking_form_fields(' . (int) $resource_id . ', ' . wp_json_encode( $fill_values ) . ');';
830 $js_body .= '}';
831
832 return $js_body;
833 }
834 }
835
836 class WPBC_FE_Form_Style_Injector {
837
838 /**
839 * Inject CSS variables into the FIRST <div ... class="... wpbc_bfb_form ..."> tag.
840 *
841 * @param string $html
842 * @param array $css_vars Map: '--var-name' => 'value'
843 *
844 * @return string
845 */
846 public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
847
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 /**
911 * Build CSS vars fragment: "--a:1;--b:2;"
912 *
913 * @param array $css_vars
914 *
915 * @return string
916 */
917 private static function build_css_vars_style_fragment( $css_vars ) {
918
919 $out = '';
920
921 foreach ( $css_vars as $name => $value ) {
922
923 $name = self::sanitize_css_var_name( $name );
924 $value = self::sanitize_css_var_value( $value );
925
926 // Allow "0" but skip empty.
927 if ( '' === $name || '' === $value ) {
928 continue;
929 }
930
931 $out .= $name . ':' . $value . ';';
932 }
933
934 return $out;
935 }
936
937 private static function sanitize_css_var_name( $name ) {
938
939 $name = is_scalar( $name ) ? trim( (string) $name ) : '';
940 if ( '' === $name ) {
941 return '';
942 }
943
944 // Keep it strict: only CSS custom properties.
945 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
946 return '';
947 }
948
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 }
954
955 return $name;
956 }
957
958 private static function sanitize_css_var_value( $value ) {
959
960 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
961 if ( '' === $value ) {
962 return '';
963 }
964
965 /**
966 * IMPORTANT:
967 * Disallow characters that can break out of "--var:value;" into extra declarations:
968 * - ';' would start a new declaration
969 * - '{' '}' can start blocks
970 * Also strip quotes/newlines/< > to keep inline style safe.
971 */
972 $value = str_replace(
973 array( ';', '{', '}', '"', "'", '<', '>', "\n", "\r", "\0" ),
974 '',
975 $value
976 );
977
978 return trim( $value );
979 }
980 }
981