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

1,099 lines 40.2 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 $bfb_source = wpbc_lang( $bfb_source );
575
576 // Replace custom params in the source (legacy behavior).
577 if ( ! empty( $custom_params ) ) {
578 foreach ( $custom_params as $custom_params_key => $custom_params_value ) {
579 $bfb_source = str_replace( $custom_params_key, $custom_params_value, $bfb_source );
580 }
581 }
582
583 $bfb_source = wpbc_bf__replace_custom_html_shortcodes( $bfb_source );
584
585 $bfb_form_html = wpbc_render_booking_form_shortcodes( $bfb_source, $render_args, $req );
586 }
587
588 if ( '' !== $bfb_form_html ) {
589 return array(
590 'body_html' => $bfb_form_html,
591 'apply_after_load_filter' => ! empty( $resolved['apply_after_load_filter'] ),
592 'bfb_settings' => $bfb_settings,
593 );
594 }
595 }
596
597 // If BFB resolution succeeded but returned empty output, fall through to legacy.
598 return null;
599 }
600 }
601
602 /**
603 * Booking form post-processor (legacy compatibility).
604 *
605 * Applies legacy transformations and hooks to the combined form markup:
606 * - Inserts calendar markup (replaces [calendar] or prepends).
607 * - Replaces additional calendars via legacy filter.
608 * - Replaces [captcha] placeholder if present.
609 * - Appends change-over times.
610 *
611 * @since 11.0.x
612 */
613 class WPBC_FE_Form_Postprocessor {
614
615 /**
616 * Apply legacy postprocessing to the composed form markup.
617 *
618 * @since 11.0.x
619 *
620 * @param string $form_html Booking form body HTML.
621 * @param string $calendar_html Calendar HTML snippet.
622 * @param array $ctx {
623 * Context data used during post-processing.
624 *
625 * @type int $resource_id Booking resource ID.
626 * @type string $custom_booking_form Booking form slug/name.
627 * @type string $selected_dates_without_calendar Preselected dates (legacy string).
628 * @type int $cal_count Number of months shown.
629 * @type mixed $shortcode_param__options Shortcode options.
630 * @type wpdev_booking $legacy_instance Legacy booking instance (optional).
631 * @type string $nl Legacy separator markup.
632 * }
633 *
634 * @return string Post-processed HTML.
635 */
636 public static function apply( $form_html, $calendar_html, $ctx ) {
637
638 $form_html = (string) $form_html;
639 $calendar_html = (string) $calendar_html;
640
641 $resource_id = (int) $ctx['resource_id'];
642 $custom_booking_form = (string) $ctx['custom_booking_form'];
643 $selected_dates_without_calendar = (string) $ctx['selected_dates_without_calendar'];
644 $cal_count = (int) $ctx['cal_count'];
645 $shortcode_param__options = $ctx['shortcode_param__options'];
646 $legacy_instance = $ctx['legacy_instance'];
647 $nl = (string) $ctx['nl'];
648
649 // Insert calendar into form.
650 if ( false !== strpos( $form_html, '[calendar]' ) ) {
651 $form_html = str_replace( '[calendar]', $calendar_html, $form_html );
652 } else {
653 // FixIn: 2981-01-13 13 Jan 2981.
654 //$form_html = '<div class="booking_form_div">' . $calendar_html . '</div>' . $nl . $form_html;
655 $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;
656 }
657
658 // Replace additional calendars.
659 $form_html = apply_bk_filter(
660 'wpdev_check_for_additional_calendars_in_form',
661 $form_html,
662 $resource_id,
663 array(
664 'booking_form' => $custom_booking_form,
665 'selected_dates' => $selected_dates_without_calendar,
666 'cal_count' => $cal_count,
667 'options' => $shortcode_param__options,
668 )
669 );
670
671 // Captcha replacement.
672 $form_html = apply_filters( 'wpbc_booking_form_html__create_captcha', $form_html, $resource_id );
673
674 // Change-over times injection.
675 $form_html = apply_filters( 'wpbc_booking_form_html__update__append_change_over_times', $form_html, $resource_id );
676
677 return $form_html;
678 }
679 }
680
681 /**
682 * Booking form wrapper (legacy HTML structure).
683 *
684 * Responsible for building the outer <div> container and <form> tag, including:
685 * - ajax response containers
686 * - anchor link (bklnk)
687 * - hidden bk_type field
688 * - nonce field
689 * - "garbage" container used by legacy JS
690 *
691 * @since 11.0.x
692 */
693 class WPBC_FE_Form_Wrapper {
694
695 /**
696 * Wrap the form HTML into legacy outer container + add hidden fields + nonce + garbage.
697 *
698 * @param string $form_html
699 * @param int $resource_id
700 *
701 * @param string $form_html Post-processed booking form HTML (already contains calendar markup).
702 * @param int $resource_id Booking resource ID.
703 *
704 * @return string Wrapped HTML.
705 */
706 public static function wrap( $form_html, $resource_id ) {
707
708 $resource_id = (int) $resource_id;
709 $wpbc_nonce = wp_nonce_field( 'CALCULATE_THE_COST', ( 'wpbc_nonceCALCULATE_THE_COST' . $resource_id ), true, false );
710
711 $booking_form_is_using_bs_css = get_bk_option( 'booking_form_is_using_bs_css' );
712 $booking_form_format_type = get_bk_option( 'booking_form_format_type' );
713 $form_style_preset = function_exists( 'wpbc_bfb_settings__get_form_style_preset' )
714 ? wpbc_bfb_settings__get_form_style_preset( wpbc_bfb_settings__get_current_form_style() )
715 : array();
716 $booking_form_theme = isset( $form_style_preset['theme_class'] ) ? sanitize_html_class( $form_style_preset['theme_class'] ) : '';
717
718 $form_container_random_id = 'form_id' . ( time() * wp_rand( 0, 1000 ) );
719 $form_container_css = 'wpbc_container wpbc_form wpbc_container_booking_form ' . ( ( 'On' === wpbc_is_this_demo() ) ? ' wpbc_demo_site ' : '' ) .
720 $booking_form_theme . ( ( 'On' === $booking_form_is_using_bs_css ) ? ' wpdevelop ' : '' );
721
722 $return_form = '<div id="' . esc_attr( $form_container_random_id ) . '" class="' . esc_attr( $form_container_css ) . '" >' .
723 '<form id="booking_form' . intval( $resource_id ) . '" class="booking_form ' . esc_attr( $booking_form_format_type ) . '" method="post" action="">' .
724 '<div id="ajax_respond_insert' . intval( $resource_id ) . '" class="ajax_respond_insert" style="display:none;"></div>' .
725 '<a name="bklnk' . $resource_id . '" id="bklnk' . $resource_id . '"></a>' .
726 '<div id="booking_form_div' . $resource_id . '" class="booking_form_div">' .
727 $form_html .
728 '<input id="bk_type' . $resource_id . '" name="bk_type' . $resource_id . '" class="" type="hidden" value="' . $resource_id . '" />' .
729 '</div>' .
730 '<div id="submiting' . $resource_id . '"></div>' .
731 '<div class="form_bk_messages" id="form_bk_messages' . $resource_id . '" ></div>' .
732 $wpbc_nonce .
733 '</form>' .
734 '</div>' .
735 '<div id="booking_form_garbage' . intval( $resource_id ) . '" class="booking_form_garbage"></div>';
736
737 return $return_form;
738 }
739 }
740
741 // ---------------------------------------------------------------------------------------------------------------------
742
743 /**
744 * Inline scripts builder (legacy compatibility).
745 *
746 * Generates and appends legacy inline JavaScript required by booking form UX:
747 * - Duplicate-calendar warning in console when the same resource calendar is rendered multiple times.
748 * - Cost hint display trigger when selected dates are predefined.
749 * - Autofill for logged-in users (legacy behavior).
750 *
751 * @since 11.0.x
752 */
753 class WPBC_FE_Inline_Scripts {
754
755 /**
756 * Collect legacy inline scripts into WPBC_FE_Assets (Elementor-safe).
757 *
758 * @param int $resource_id - ID of booking resource.
759 *
760 * @return string Inline scripts HTML.
761 */
762 public static function collect( $resource_id ) {
763
764 // FixIn: 10.14.17.2.
765
766 $html = '';
767 $resource_id = (int) $resource_id;
768
769 // == Autofill (legacy conditions preserved) ==
770 $autofill_js = self::build_autofill_js_body( $resource_id );
771 if ( '' !== $autofill_js ) {
772 $assets_key = 'wpbc:autofill:' . intval( $resource_id );
773 $added = false;
774 if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
775 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key . ':ajax' );
776 $added = true;
777 } else {
778 $added = WPBC_FE_Assets::add_jq_ready_js_to_wp_script( 'wpbc_all', $autofill_js, $assets_key );
779 }
780 if ( ! $added ) {
781 $html .= WPBC_FE_Assets::add_inline_js( $autofill_js, $assets_key ); // Fallback to direct inline JS, if previosly not edded script.
782 }
783 }
784
785 return $html;
786 }
787
788 /**
789 * Build ONLY the JS BODY for legacy autofill (no <script>, no ready wrapper).
790 *
791 * Now calls the shared JS function:
792 * window.WPBC_FE.autofill_booking_form_fields( resource_id, fill_values )
793 *
794 * @param int $resource_id Booking resource ID.
795 *
796 * @return string JS body (or empty string if not applicable).
797 */
798 private static function build_autofill_js_body( $resource_id ) {
799
800 $resource_id = (int) $resource_id;
801
802 if ( WPBC_GET_Request::has_non_empty_get( 'booking_hash' ) ) {
803 return '';
804 }
805
806 $is_use_auto_fill_for_logged = get_bk_option( 'booking_is_use_autofill_4_logged_user' );
807 if ( 'On' !== $is_use_auto_fill_for_logged ) {
808 return '';
809 }
810
811 $curr_user = wpbc_get_current_user();
812 if ( empty( $curr_user ) || ( (int) $curr_user->ID <= 0 ) ) {
813 return '';
814 }
815
816 $user_nick_name = get_user_meta( $curr_user->ID, 'nickname' );
817 $user_nick_name = ( empty( $user_nick_name ) ) ? '' : $user_nick_name[0];
818
819 $fill_values = array(
820 'nickname' => (string) $user_nick_name,
821 'last_name' => (string) $curr_user->last_name,
822 'first_name' => (string) $curr_user->first_name,
823 'email' => (string) $curr_user->user_email,
824 'phone' => (string) $curr_user->phone_number,
825 'nb_enfant' => (string) $curr_user->nb_enfant,
826 'url' => (string) $curr_user->user_url,
827 );
828
829 // IMPORTANT: return body only (no <script>, no ready wrapper).
830 $js_body = 'if ( window.WPBC_FE && ( typeof window.WPBC_FE.autofill_booking_form_fields === "function" ) ) {';
831 $js_body .= 'window.WPBC_FE.autofill_booking_form_fields(' . (int) $resource_id . ', ' . wp_json_encode( $fill_values ) . ');';
832 $js_body .= '}';
833
834 return $js_body;
835 }
836 }
837
838 class WPBC_FE_Form_Style_Injector {
839
840 /**
841 * Inject CSS variables into the FIRST <div ... class="... wpbc_bfb_form ..."> tag.
842 *
843 * @param string $html
844 * @param array $css_vars Map: '--var-name' => 'value'
845 *
846 * @return string
847 */
848 public static function inject_css_vars_into_bfb_root( $html, $css_vars ) {
849
850 return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_bfb_form' ) );
851 }
852
853 /**
854 * Inject CSS variables into the FIRST outer booking form wrapper.
855 *
856 * @param string $html
857 * @param array $css_vars
858 *
859 * @return string
860 */
861 public static function inject_css_vars_into_form_wrapper( $html, $css_vars ) {
862
863 return self::inject_css_vars_into_first_tag_with_classes( $html, $css_vars, array( 'wpbc_container', 'wpbc_form' ) );
864 }
865
866 /**
867 * Add a class to the FIRST <div> with all required class tokens.
868 *
869 * @param string $html
870 * @param array $required_classes
871 * @param string $class_name
872 *
873 * @return string
874 */
875 public static function add_class_to_first_tag_with_classes( $html, $required_classes, $class_name ) {
876
877 $html = (string) $html;
878 $required_classes = is_array( $required_classes ) ? $required_classes : array();
879 $class_name = sanitize_html_class( (string) $class_name );
880
881 if ( '' === $class_name || empty( $required_classes ) ) {
882 return $html;
883 }
884
885 $pattern = self::get_first_div_with_classes_pattern( $required_classes );
886 if ( '' === $pattern ) {
887 return $html;
888 }
889
890 $done = false;
891 $result = preg_replace_callback(
892 $pattern,
893 function( $m ) use ( $required_classes, $class_name, &$done ) {
894
895 $tag = $m[0];
896
897 if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
898 return $tag;
899 }
900
901 if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', $tag, $cm ) ) {
902 return $tag;
903 }
904
905 $quote = $cm[1];
906 $classes = trim( preg_replace( '/\s+/', ' ', (string) $cm[2] ) );
907
908 if ( false === strpos( ' ' . $classes . ' ', ' ' . $class_name . ' ' ) ) {
909 $classes = trim( $classes . ' ' . $class_name );
910 }
911
912 $done = true;
913
914 return preg_replace( '/\bclass\s*=\s*(["\'])(.*?)\1/i', 'class=' . $quote . esc_attr( $classes ) . $quote, $tag, 1 );
915 },
916 $html
917 );
918
919 return ( null === $result ) ? $html : $result;
920 }
921
922 /**
923 * Build CSS vars fragment: "--a:1;--b:2;"
924 *
925 * @param array $css_vars
926 *
927 * @return string
928 */
929 private static function build_css_vars_style_fragment( $css_vars ) {
930
931 $out = '';
932
933 foreach ( $css_vars as $name => $value ) {
934
935 $name = self::sanitize_css_var_name( $name );
936 $value = self::sanitize_css_var_value( $value );
937
938 // Allow "0" but skip empty.
939 if ( '' === $name || '' === $value ) {
940 continue;
941 }
942
943 $out .= $name . ':' . $value . ';';
944 }
945
946 return $out;
947 }
948
949 private static function sanitize_css_var_name( $name ) {
950
951 $name = is_scalar( $name ) ? trim( (string) $name ) : '';
952 if ( '' === $name ) {
953 return '';
954 }
955
956 // Keep it strict: only CSS custom properties.
957 if ( ! preg_match( '/^--[a-z0-9\-_]+$/i', $name ) ) {
958 return '';
959 }
960
961 // Optional: enforce prefix to avoid abusing other vars (case-insensitive).
962 $lower = strtolower( $name );
963 if ( 0 !== strpos( $lower, '--wpbc-' ) && 0 !== strpos( $lower, '--wpbc_bfb-' ) && 0 !== strpos( $lower, '--wpbc_form-' ) ) {
964 return '';
965 }
966
967 return $name;
968 }
969
970 private static function sanitize_css_var_value( $value ) {
971
972 $value = is_scalar( $value ) ? trim( (string) $value ) : '';
973 if ( '' === $value ) {
974 return '';
975 }
976
977 /**
978 * IMPORTANT:
979 * Disallow characters that can break out of "--var:value;" into extra declarations:
980 * - ';' would start a new declaration
981 * - '{' '}' can start blocks
982 * Also strip quotes/newlines/< > to keep inline style safe.
983 */
984 $value = str_replace(
985 array( ';', '{', '}', '"', "'", '<', '>', "\n", "\r", "\0" ),
986 '',
987 $value
988 );
989
990 return trim( $value );
991 }
992
993 private static function inject_css_vars_into_first_tag_with_classes( $html, $css_vars, $required_classes ) {
994
995 $html = (string) $html;
996 $css_vars = is_array( $css_vars ) ? $css_vars : array();
997
998 if ( empty( $css_vars ) ) {
999 return $html;
1000 }
1001
1002 $style_append = self::build_css_vars_style_fragment( $css_vars );
1003 if ( '' === $style_append ) {
1004 return $html;
1005 }
1006
1007 $pattern = self::get_first_div_with_classes_pattern( $required_classes );
1008 if ( '' === $pattern ) {
1009 return $html;
1010 }
1011
1012 $done = false;
1013 $result = preg_replace_callback(
1014 $pattern,
1015 function( $m ) use ( $required_classes, $style_append, &$done ) {
1016
1017 $tag = $m[0];
1018
1019 if ( ! empty( $done ) || ! self::tag_has_required_classes( $tag, $required_classes ) ) {
1020 return $tag;
1021 }
1022
1023 // If style already exists: append.
1024 if ( preg_match( '/\bstyle\s*=\s*(["\'])(.*?)\1/i', $tag, $sm ) ) {
1025
1026 $quote = $sm[1];
1027 $existing = (string) $sm[2];
1028
1029 // Avoid double-escaping if the tag already contains entities.
1030 $existing = html_entity_decode( $existing, ENT_QUOTES, 'UTF-8' );
1031
1032 $merged = trim( $existing );
1033 if ( ( '' !== $merged ) && ( ';' !== substr( $merged, -1 ) ) ) {
1034 $merged .= ';';
1035 }
1036 $merged .= $style_append;
1037
1038 $done = true;
1039
1040 return preg_replace(
1041 '/\bstyle\s*=\s*(["\'])(.*?)\1/i',
1042 'style=' . $quote . esc_attr( $merged ) . $quote,
1043 $tag,
1044 1
1045 );
1046 }
1047
1048 // No style attr: add it.
1049 $done = true;
1050 return rtrim( $tag, '>' ) . ' style="' . esc_attr( $style_append ) . '">';
1051 },
1052 $html
1053 );
1054
1055 return ( null === $result ) ? $html : $result;
1056 }
1057
1058 private static function get_first_div_with_classes_pattern( $required_classes ) {
1059
1060 $required_classes = is_array( $required_classes ) ? $required_classes : array();
1061 $has_class = false;
1062
1063 foreach ( $required_classes as $class_name ) {
1064 $class_name = sanitize_html_class( (string) $class_name );
1065 if ( '' === $class_name ) {
1066 continue;
1067 }
1068 $has_class = true;
1069 }
1070
1071 if ( ! $has_class ) {
1072 return '';
1073 }
1074
1075 return '/<div\b[^>]*\bclass\s*=\s*(["\'])(.*?)\1[^>]*>/i';
1076 }
1077
1078 private static function tag_has_required_classes( $tag, $required_classes ) {
1079
1080 if ( ! preg_match( '/\bclass\s*=\s*(["\'])(.*?)\1/i', (string) $tag, $cm ) ) {
1081 return false;
1082 }
1083
1084 $classes = ' ' . preg_replace( '/\s+/', ' ', (string) $cm[2] ) . ' ';
1085
1086 foreach ( (array) $required_classes as $class_name ) {
1087 $class_name = sanitize_html_class( (string) $class_name );
1088 if ( '' === $class_name ) {
1089 continue;
1090 }
1091 if ( false === strpos( $classes, ' ' . $class_name . ' ' ) ) {
1092 return false;
1093 }
1094 }
1095
1096 return true;
1097 }
1098 }
1099