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
booking / includes / _front_end / class-fe-render-form-body.php

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

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