PluginProbe
Booking Calendar / 11.8.3
Booking Calendar v11.8.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.8.3, at includes/_front_end/class-fe-render-form-body.php

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